refactor: 优化后端支付与微信支付
- 优化payment/config.py、wechat_pay.py - 优化routers/payment.py、plans.py、quota.py、websocket.py - 更新main.py、.env.production Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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}分")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
Reference in New Issue
Block a user