From 44b405d647fe5b785d0370fe5814b48dba40e2a3 Mon Sep 17 00:00:00 2001 From: iammm0 Date: Wed, 11 Feb 2026 16:06:15 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=94=AF=E4=BB=98=E4=B8=8E=E5=BE=AE=E4=BF=A1=E6=94=AF?= =?UTF-8?q?=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化payment/config.py、wechat_pay.py - 优化routers/payment.py、plans.py、quota.py、websocket.py - 更新main.py、.env.production Co-authored-by: Cursor --- api/.env.production | 17 ++++++ api/main.py | 14 ++++- api/payment/config.py | 31 +++++++++-- api/payment/wechat_pay.py | 108 ++++++++++++++++++++++++++++++++------ api/routers/payment.py | 96 ++++++++++++++++++++++++++++----- api/routers/plans.py | 36 +++++++++++-- api/routers/quota.py | 8 ++- api/routers/websocket.py | 2 +- 8 files changed, 274 insertions(+), 38 deletions(-) diff --git a/api/.env.production b/api/.env.production index 7be6c03..ba07f2d 100644 --- a/api/.env.production +++ b/api/.env.production @@ -48,6 +48,15 @@ TENCENT_SMS_TEMPLATE_ID=2592163 # 并根据实际模板参数数量设置此值 TENCENT_SMS_TEMPLATE_PARAM_COUNT=1 +# ============================================================================= +# ASR Provider 选择 +# ============================================================================= +# ASR Provider: whisper(默认,本地 faster-whisper)| tencent(腾讯云一句话识别) +ASR_PROVIDER=whisper + +# ============================================================================= +# Whisper ASR 配置(ASR_PROVIDER=whisper 时使用) +# ============================================================================= # CPU 环境(推荐 small + int8) ASR_MODEL_SIZE=small ASR_DEVICE=cpu @@ -58,6 +67,14 @@ ASR_COMPUTE_TYPE=int8 # ASR_DEVICE=cuda # ASR_COMPUTE_TYPE=float16 +# ============================================================================= +# 腾讯云 ASR 配置(ASR_PROVIDER=tencent 时使用) +# ============================================================================= +# 腾讯云 API 密钥(与短信服务共用,或单独配置语音服务专用密钥) +TENCENT_SECRET_ID= +TENCENT_SECRET_KEY= +# TENCENT_ASR_APP_ID= + WECHAT_PAY_APP_ID=wx1df508452e06cfb8 WECHAT_PAY_MCH_ID=1662979099 WECHAT_PAY_API_V3_KEY=xjvGSJLGJAJfjgskfjslafjsajsdjals diff --git a/api/main.py b/api/main.py index e0e2e4c..97bf64f 100644 --- a/api/main.py +++ b/api/main.py @@ -109,7 +109,7 @@ async def startup_event(): # 检查并预加载 ASR 模型(在后台线程执行,避免阻塞启动) try: - from services.asr_service import asr_service + from services import asr_service asr_ready = await asyncio.to_thread(asr_service.ensure_ready) if asr_ready: logger.info("ASR 模型已就绪(本地 Whisper)") @@ -118,6 +118,18 @@ async def startup_event(): except Exception as e: logger.warning(f"ASR 初始化检查失败: {e}") + # 预初始化微信支付客户端(在后台线程执行,避免首次下单时 _ensure_client 占满 25s 导致超时) + try: + def _init_wechat_pay_client(): + from routers.payment import get_payment_service + svc = get_payment_service() + if svc.is_method_available("wechat"): + _ = svc.wechat_client # 触发 _ensure_client() + await asyncio.to_thread(_init_wechat_pay_client) + logger.info("微信支付客户端已预初始化") + except Exception as e: + logger.warning(f"微信支付预初始化失败(首次下单时再初始化): {e}") + @app.on_event("shutdown") async def shutdown_event(): diff --git a/api/payment/config.py b/api/payment/config.py index 58f4061..bc31a42 100644 --- a/api/payment/config.py +++ b/api/payment/config.py @@ -20,6 +20,10 @@ class WeChatPayConfig: private_key: str = "" # 商户 API 私钥内容(PEM 字符串,可放环境变量,与 private_key_path 二选一) cert_serial_no: str = "" # 商户证书序列号 notify_url: str = "" # 支付结果回调地址 + # 平台公钥模式(二选一):当环境无法访问 api.mch.weixin.qq.com 时使用,无需拉取平台证书 + platform_public_key: str = "" # 微信支付平台公钥 PEM 内容(与 platform_public_key_path 二选一) + platform_public_key_path: str = "" # 平台公钥文件路径 + platform_public_key_id: str = "" # 微信支付平台公钥 ID(与 platform_public_key 同时配置) @property def is_configured(self) -> bool: @@ -29,6 +33,12 @@ class WeChatPayConfig: has_key, self.cert_serial_no, self.notify_url ]) + @property + def use_platform_public_key(self) -> bool: + """是否使用平台公钥模式(无需访问微信拉取证书)""" + has_pub = bool(self.platform_public_key.strip()) or bool(self.platform_public_key_path.strip()) + return has_pub and bool(self.platform_public_key_id.strip()) + @dataclass class AlipayConfig: @@ -58,19 +68,28 @@ class PaymentConfig: @classmethod def from_env(cls) -> "PaymentConfig": """从环境变量加载配置""" - # 微信私钥:优先使用 WECHAT_PAY_PRIVATE_KEY(PEM 内容),否则用 WECHAT_PAY_PRIVATE_KEY_PATH(文件路径) + # 微信私钥:优先使用 WECHAT_PAY_PRIVATE_KEY_PATH(文件),避免 .env 中长 PEM 的转义问题;否则用 WECHAT_PAY_PRIVATE_KEY + wechat_private_key_path = os.getenv("WECHAT_PAY_PRIVATE_KEY_PATH", "").strip() wechat_private_key = os.getenv("WECHAT_PAY_PRIVATE_KEY", "").strip() - if wechat_private_key and "\\n" in wechat_private_key: + # 去除可能被解析进来的引号与 BOM + if wechat_private_key: + wechat_private_key = wechat_private_key.strip('"').strip("'").lstrip("\ufeff") wechat_private_key = wechat_private_key.replace("\\n", "\n") + wechat_platform_pub = os.getenv("WECHAT_PAY_PLATFORM_PUBLIC_KEY", "").strip() + if wechat_platform_pub and "\\n" in wechat_platform_pub: + wechat_platform_pub = wechat_platform_pub.replace("\\n", "\n") config = cls( wechat=WeChatPayConfig( app_id=os.getenv("WECHAT_PAY_APP_ID", ""), mch_id=os.getenv("WECHAT_PAY_MCH_ID", ""), api_v3_key=os.getenv("WECHAT_PAY_API_V3_KEY", ""), - private_key_path=os.getenv("WECHAT_PAY_PRIVATE_KEY_PATH", ""), - private_key=wechat_private_key, + private_key_path=wechat_private_key_path, + private_key=wechat_private_key if not wechat_private_key_path else "", # 有路径时不再用 env 内容 cert_serial_no=os.getenv("WECHAT_PAY_CERT_SERIAL_NO", ""), notify_url=os.getenv("WECHAT_PAY_NOTIFY_URL", ""), + platform_public_key=wechat_platform_pub, + platform_public_key_path=os.getenv("WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH", "").strip(), + platform_public_key_id=os.getenv("WECHAT_PAY_PLATFORM_PUBLIC_KEY_ID", "").strip(), ), alipay=AlipayConfig( app_id=os.getenv("ALIPAY_APP_ID", ""), @@ -84,7 +103,9 @@ class PaymentConfig: # 日志输出配置状态 if config.wechat.is_configured: - logger.info(f"微信支付配置已加载: APP_ID={config.wechat.app_id}, MCH_ID={config.wechat.mch_id}") + mode = "平台公钥模式" if config.wechat.use_platform_public_key else "平台证书模式" + key_src = "WECHAT_PAY_PRIVATE_KEY(环境变量)" if config.wechat.private_key.strip() else "WECHAT_PAY_PRIVATE_KEY_PATH(文件)" + logger.info(f"微信支付配置已加载: APP_ID={config.wechat.app_id}, MCH_ID={config.wechat.mch_id}, 模式={mode}, 商户私钥={key_src}") else: logger.warning("微信支付配置不完整,微信支付将不可用") diff --git a/api/payment/wechat_pay.py b/api/payment/wechat_pay.py index 651b491..39c79ba 100644 --- a/api/payment/wechat_pay.py +++ b/api/payment/wechat_pay.py @@ -8,6 +8,15 @@ import os import time from typing import Optional, Dict, Any +# #region agent log +def _debug_log(location: str, message: str, data: dict, hypothesis_id: str): + try: + with open("/Users/zuckxu/PycharmProjects/life-echo/.cursor/debug.log", "a", encoding="utf-8") as f: + f.write(json.dumps({"id": f"log_{int(time.time()*1000)}", "timestamp": int(time.time() * 1000), "location": location, "message": message, "data": data, "hypothesisId": hypothesis_id}, ensure_ascii=False) + "\n") + except Exception: + pass +# #endregion + from .config import WeChatPayConfig from .schemas import PaymentResult, NotifyResult, PaymentStatus from .exceptions import PaymentConfigError, PaymentCreateError, PaymentNotifyError, PaymentQueryError @@ -17,18 +26,40 @@ logger = logging.getLogger(__name__) def _normalize_pem_key(key: str) -> str: """ - 规范化 PEM 私钥字符串,便于 wechatpayv3 正确解析。 + 规范化 PEM 私钥/公钥字符串,便于 wechatpayv3 正确解析。 + - 去除 BOM、首尾引号 - 环境变量中常将换行写成字面量 \\n,需转为真实换行 - 统一为 \\n 换行,去除 \\r """ if not key or not key.strip(): return key - key = key.strip() + key = key.strip().lstrip("\ufeff").strip('"').strip("'") key = key.replace("\\n", "\n") key = key.replace("\r\n", "\n").replace("\r", "\n") return key +def _resolve_key_path(key_path: str) -> str: + """解析私钥/公钥文件路径:相对路径时优先相对于 api 目录,避免受当前工作目录影响。""" + if not key_path or not key_path.strip(): + return key_path + key_path = key_path.strip() + if os.path.isabs(key_path): + return key_path + # 相对路径:先相对于当前工作目录,若文件不存在则相对于 api 目录(本包所在目录的上一级) + abs_cwd = os.path.abspath(key_path) + if os.path.isfile(abs_cwd): + return abs_cwd + try: + api_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + abs_api = os.path.normpath(os.path.join(api_dir, key_path)) + if os.path.isfile(abs_api): + return abs_api + except Exception: + pass + return abs_cwd + + class WeChatPayClient: """微信支付客户端""" @@ -36,6 +67,10 @@ class WeChatPayClient: self._config = config self._client = None + def ensure_client(self): + """供外部在后台线程中提前触发初始化,避免首次下单时在预支付超时窗口内做耗时 init。""" + self._ensure_client() + def _ensure_client(self): """确保微信支付客户端已初始化""" if not self._config.is_configured: @@ -43,20 +78,20 @@ class WeChatPayClient: if self._client is None: try: + # #region agent log + _debug_log("payment/wechat_pay.py:_ensure_client", "wechat_ensure_client_enter", {}, "H_init") + # #endregion from wechatpayv3 import WeChatPay, WeChatPayType - # 商户私钥:优先用配置中的 PEM 字符串,否则从文件读取 - if self._config.private_key and self._config.private_key.strip(): - private_key = self._config.private_key.strip() - else: - key_path = self._config.private_key_path.strip() - if not key_path: - raise PaymentConfigError("未配置微信支付商户私钥:请设置 WECHAT_PAY_PRIVATE_KEY 或 WECHAT_PAY_PRIVATE_KEY_PATH") - # 若为相对路径,相对于当前工作目录解析(建议使用绝对路径或与 main.py 同级的路径) - if not os.path.isabs(key_path): - key_path = os.path.abspath(key_path) + # 商户私钥:优先用 WECHAT_PAY_PRIVATE_KEY_PATH 从文件读取(推荐),否则用 WECHAT_PAY_PRIVATE_KEY + if self._config.private_key_path and self._config.private_key_path.strip(): + key_path = _resolve_key_path(self._config.private_key_path) with open(key_path, "r", encoding="utf-8") as f: private_key = f.read() + elif self._config.private_key and self._config.private_key.strip(): + private_key = self._config.private_key.strip() + else: + raise PaymentConfigError("未配置微信支付商户私钥:请设置 WECHAT_PAY_PRIVATE_KEY_PATH(推荐)或 WECHAT_PAY_PRIVATE_KEY") # 统一 PEM 格式:换行符规范化、字面量 \n 转真实换行(环境变量或部分编辑器会写成 \n) private_key = _normalize_pem_key(private_key) @@ -65,8 +100,12 @@ class WeChatPayClient: "微信支付商户私钥格式错误:需为 PEM 格式(以 -----BEGIN PRIVATE KEY----- 开头)。" "若使用 WECHAT_PAY_PRIVATE_KEY_PATH,请确认文件内容正确;若用 WECHAT_PAY_PRIVATE_KEY,请确认环境变量为完整 PEM。" ) + # #region agent log + _debug_log("payment/wechat_pay.py:_ensure_client", "wechat_ensure_client_key_done", {}, "H_init") + # #endregion - self._client = WeChatPay( + # 平台公钥模式:无需访问 api.mch.weixin.qq.com 拉取证书,适合无法直连微信的环境 + kwargs = dict( wechatpay_type=WeChatPayType.APP, mchid=self._config.mch_id, private_key=private_key, @@ -75,14 +114,42 @@ class WeChatPayClient: apiv3_key=self._config.api_v3_key, notify_url=self._config.notify_url, ) + if self._config.use_platform_public_key: + if self._config.platform_public_key_path and self._config.platform_public_key_path.strip(): + key_path = _resolve_key_path(self._config.platform_public_key_path) + with open(key_path, "r", encoding="utf-8") as f: + platform_pub_key = _normalize_pem_key(f.read()) + elif self._config.platform_public_key and self._config.platform_public_key.strip(): + platform_pub_key = _normalize_pem_key(self._config.platform_public_key.strip()) + else: + raise PaymentConfigError("平台公钥模式需设置 WECHAT_PAY_PLATFORM_PUBLIC_KEY 或 WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH") + if not platform_pub_key or "-----BEGIN" not in platform_pub_key: + raise PaymentConfigError("微信支付平台公钥格式错误,需为 PEM 格式") + kwargs["public_key"] = platform_pub_key + kwargs["public_key_id"] = self._config.platform_public_key_id.strip() + else: + kwargs["timeout"] = (10, 25) # 证书模式:拉取平台证书时的超时 + self._client = WeChatPay(**kwargs) + # #region agent log + _debug_log("payment/wechat_pay.py:_ensure_client", "wechat_ensure_client_sdk_done", {}, "H_init") + # #endregion logger.info("微信支付客户端初始化成功") except FileNotFoundError: + key_path = _resolve_key_path(self._config.private_key_path) if self._config.private_key_path else self._config.private_key_path raise PaymentConfigError( - f"微信支付商户私钥文件不存在: {self._config.private_key_path}(当前工作目录: {os.getcwd()})" + f"微信支付商户私钥文件不存在: {key_path}(已尝试相对路径与 api 目录,当前工作目录: {os.getcwd()})" ) except PaymentConfigError: raise except Exception as e: + msg = str(e).strip() + if "No wechatpay platform certificate" in msg or "platform certificate" in msg.lower(): + raise PaymentConfigError( + "无法获取微信支付平台证书(当前环境可能无法访问 api.mch.weixin.qq.com)。" + "请改用平台公钥模式:在商户平台「API安全」中获取微信支付公钥与公钥ID,并设置环境变量 " + "WECHAT_PAY_PLATFORM_PUBLIC_KEY(或 WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH)与 " + "WECHAT_PAY_PLATFORM_PUBLIC_KEY_ID。" + ) raise PaymentConfigError( f"微信支付客户端初始化失败: {e}。" "若使用文件路径,请确认文件为 PEM 格式且路径正确;或改用 WECHAT_PAY_PRIVATE_KEY 直接配置私钥内容。" @@ -105,15 +172,26 @@ class WeChatPayClient: Returns: PaymentResult 包含调起支付所需的参数 """ + # #region agent log + _debug_log("payment/wechat_pay.py:create_app_order", "wechat_create_app_order_enter", {"out_trade_no": out_trade_no}, "H3") + # #endregion self._ensure_client() - + # #region agent log + _debug_log("payment/wechat_pay.py:create_app_order", "wechat_client_ready", {"out_trade_no": out_trade_no}, "H3") + # #endregion try: + # #region agent log + _debug_log("payment/wechat_pay.py:create_app_order", "wechat_pay_request_start", {"out_trade_no": out_trade_no}, "H1,H4") + # #endregion code, message = self._client.pay( description=description, out_trade_no=out_trade_no, amount={"total": total_amount, "currency": "CNY"}, pay_type=self._get_pay_type(), ) + # #region agent log + _debug_log("payment/wechat_pay.py:create_app_order", "wechat_pay_request_done", {"out_trade_no": out_trade_no, "code": code}, "H1,H4") + # #endregion result = json.loads(message) diff --git a/api/routers/payment.py b/api/routers/payment.py index 452bbcb..2f03ee7 100644 --- a/api/routers/payment.py +++ b/api/routers/payment.py @@ -3,11 +3,23 @@ 处理订单创建、支付回调、订单状态查询 """ import asyncio +import json +import time +import traceback import uuid import logging from datetime import timedelta from typing import List +# #region agent log +def _debug_log(location: str, message: str, data: dict, hypothesis_id: str): + try: + with open("/Users/zuckxu/PycharmProjects/life-echo/.cursor/debug.log", "a", encoding="utf-8") as f: + f.write(json.dumps({"id": f"log_{int(time.time()*1000)}", "timestamp": int(time.time() * 1000), "location": location, "message": message, "data": data, "hypothesisId": hypothesis_id}, ensure_ascii=False) + "\n") + except Exception: + pass +# #endregion + from fastapi import APIRouter, Depends, Request, HTTPException from fastapi.responses import PlainTextResponse from sqlalchemy import select @@ -24,7 +36,7 @@ from payment.schemas import ( OrderListResponse, ) from payment.exceptions import PaymentError -from routers.plans import get_plan_by_type, AVAILABLE_PLANS +from routers.plans import get_plan_by_type, get_plans_for_api logger = logging.getLogger(__name__) @@ -51,6 +63,7 @@ SUBSCRIPTION_DURATION_DAYS = { "pro": 365, # Pro 版一年 "pro_plus": 365, # Pro+ 版一年 "premium": 365, # 兼容旧高级版 + "test": 365, # 一分钱测试版(仅开发环境) } @@ -74,9 +87,9 @@ async def create_order( - plan_id: 套餐 ID(如 premium) - payment_method: 支付方式(wechat / alipay) """ - # 验证套餐 + # 验证套餐(含 ENABLE_TEST_PLAN 时的「一分钱测试版」) plan = None - for p in AVAILABLE_PLANS: + for p in get_plans_for_api(): if p.id == request.plan_id: plan = p break @@ -127,20 +140,79 @@ async def create_order( await db.flush() # 调用支付服务创建预支付订单(同步的微信/支付宝 SDK 会发 HTTP,放到线程池避免阻塞事件循环导致超时) + # 预支付超时时间(秒),便于区分「后端超时」与「客户端超时」 + PREPAY_TIMEOUT_SEC = 25 + # 微信客户端首次初始化可能较慢(读密钥、构造 SDK、或 SDK 内部建连),多 worker 时 startup 预初始化可能未在本进程执行,故在下单前先确保本进程内已初始化 + WECHAT_INIT_TIMEOUT_SEC = 35 + + # #region agent log + _debug_log("routers/payment.py:create_order", "create_order_prepay_start", {"order_no": order_no, "payment_method": request.payment_method, "plan_id": request.plan_id}, "H2,H3,H5") + # #endregion + if request.payment_method == "wechat": + try: + await asyncio.wait_for( + asyncio.to_thread(payment_service.wechat_client.ensure_client), + timeout=WECHAT_INIT_TIMEOUT_SEC, + ) + except asyncio.TimeoutError: + order.status = "failed" + await db.flush() + logger.error("微信支付客户端初始化超时(%s 秒), order_no=%s", WECHAT_INIT_TIMEOUT_SEC, order_no) + raise HTTPException(status_code=504, detail="微信支付初始化超时,请稍后重试。") + except Exception as e: + # 未配置或初始化失败(如私钥错误),直接返回 503 + order.status = "failed" + await db.flush() + logger.exception("微信支付客户端初始化失败: %s", e) + raise HTTPException(status_code=503, detail=f"微信支付暂不可用: {e!s}") try: - payment_result = await asyncio.to_thread( - payment_service.create_payment, - request.payment_method, - order_no, - amount_fen, - f"往事拾遗 - {plan.display_name}", + payment_result = await asyncio.wait_for( + asyncio.to_thread( + payment_service.create_payment, + request.payment_method, + order_no, + amount_fen, + f"往事拾遗 - {plan.display_name}", + ), + timeout=PREPAY_TIMEOUT_SEC, ) - except PaymentError as e: - # 支付下单失败,更新订单状态 + except asyncio.TimeoutError: + # #region agent log + _debug_log("routers/payment.py:create_order", "create_order_prepay_timeout", {"order_no": order_no, "payment_method": request.payment_method}, "H1,H2,H3,H4") + # #endregion order.status = "failed" await db.flush() - logger.error(f"创建支付订单失败: {e.message}") + logger.error( + "创建支付订单超时: 预支付请求在 %s 秒内未返回, plan_id=%s, payment_method=%s, order_no=%s", + PREPAY_TIMEOUT_SEC, + request.plan_id, + request.payment_method, + order_no, + ) + raise HTTPException( + status_code=504, + detail="创建预支付超时,请检查网络或稍后重试。若为微信支付,请确认商户配置与网络可达微信服务器。", + ) + except PaymentError as e: + order.status = "failed" + await db.flush() + logger.error("创建支付订单失败(PaymentError): %s", e.message) raise HTTPException(status_code=500, detail=f"创建支付订单失败: {e.message}") + except Exception as e: + order.status = "failed" + await db.flush() + full_tb = traceback.format_exc() + logger.exception( + "创建支付订单异常: %s, plan_id=%s, payment_method=%s, order_no=%s\n%s", + e, + request.plan_id, + request.payment_method, + order_no, + full_tb, + ) + # 返回详尽错误信息便于客户端 debug(类型 + 消息) + detail_msg = f"创建支付订单异常: {type(e).__name__}: {e!s}" + raise HTTPException(status_code=500, detail=detail_msg) logger.info(f"订单创建成功: {order_no}, 方式: {request.payment_method}, 金额: {amount_fen}分") diff --git a/api/routers/plans.py b/api/routers/plans.py index 19adb44..56871d4 100644 --- a/api/routers/plans.py +++ b/api/routers/plans.py @@ -1,6 +1,7 @@ """ 订阅计划相关 API 路由 """ +import os from fastapi import APIRouter, Depends from pydantic import BaseModel from typing import List, Optional @@ -94,11 +95,40 @@ AVAILABLE_PLANS = [ ) ] +# 一分钱测试套餐:仅当 ENABLE_TEST_PLAN=1 时开放,用于开发环境反复测试支付 +ENABLE_TEST_PLAN = os.getenv("ENABLE_TEST_PLAN", "").lower() in ("1", "true", "yes") + +TEST_PLAN = PlanResponse( + id="test", + name="test", + display_name="一分钱测试版", + price=0.01, + currency="CNY", + features=[ + "无限对话", + "无限章节整理", + "仅用于开发环境测试支付" + ], + max_conversations=None, + max_chapters=None, + max_words=None, + is_popular=False, +) + + +def get_plans_for_api() -> List[PlanResponse]: + """返回对外暴露的套餐列表(含测试套餐当且仅当 ENABLE_TEST_PLAN 开启)。""" + if ENABLE_TEST_PLAN: + return AVAILABLE_PLANS + [TEST_PLAN] + return list(AVAILABLE_PLANS) + def get_plan_by_type(subscription_type: str) -> Optional[PlanResponse]: - """根据订阅类型获取计划信息。旧字段 premium 按 pro 展示。""" + """根据订阅类型获取计划信息。旧字段 premium 按 pro 展示;test 仅当 ENABLE_TEST_PLAN 时有效。""" if subscription_type == "premium": subscription_type = "pro" + if subscription_type == "test": + return TEST_PLAN if ENABLE_TEST_PLAN else AVAILABLE_PLANS[0] for plan in AVAILABLE_PLANS: if plan.id == subscription_type: return plan @@ -108,9 +138,9 @@ def get_plan_by_type(subscription_type: str) -> Optional[PlanResponse]: @router.get("", response_model=List[PlanResponse]) async def get_plans(): """ - 获取所有可用的订阅计划 + 获取所有可用的订阅计划(开发环境 ENABLE_TEST_PLAN=1 时包含「一分钱测试版」)。 """ - return AVAILABLE_PLANS + return get_plans_for_api() @router.get("/current", response_model=CurrentPlanResponse) diff --git a/api/routers/quota.py b/api/routers/quota.py index 845d283..f2362f8 100644 --- a/api/routers/quota.py +++ b/api/routers/quota.py @@ -53,7 +53,13 @@ PLAN_QUOTAS = { "max_conversations": None, "max_chapters": None, "max_words": None - } + }, + # 一分钱测试版(仅开发环境 ENABLE_TEST_PLAN=1):无限对话、无限章节 + "test": { + "max_conversations": None, + "max_chapters": None, + "max_words": None + }, } diff --git a/api/routers/websocket.py b/api/routers/websocket.py index 3e124fb..33d1593 100644 --- a/api/routers/websocket.py +++ b/api/routers/websocket.py @@ -19,7 +19,7 @@ from database.models import Conversation, Segment from database.models import User as UserModel from services.auth_service import verify_token from services.memoir_state_service import get_or_create_state -from services.asr_service import asr_service +from services import asr_service from fastapi import HTTPException, status logger = logging.getLogger(__name__)