# New API 支付系统集成计划(简化版) ## 概述 将 New API 作为 qwen-client 和 qwen-agent 的支付后端。qwen-agent 只做代理转发,不维护配额和交易数据。 **创建日期**: 2026-02-25 **更新日期**: 2026-02-25 **状态**: 待审核 **复杂度**: 中 --- ## 核心设计 ### 设计原则 1. **New API 是数据源**: 所有配额、交易、订单数据由 New API 维护 2. **qwen-agent 是代理层**: 只转发请求,不存储支付相关数据 3. **Session 同步登录**: 用户登录 qwen-agent 时,同步登录 New API 并存储 session 4. **Cookie 转发**: 代理请求时转发用户的 session cookie 实现 New API 鉴权 ### 架构图 ``` ┌─────────────┐ ┌─────────────────┐ ┌─────────────┐ │ qwen-client │ ───► │ qwen-agent │ ───► │ New API │ │ (前端) │ │ (代理 + Session)│ │ (支付后端) │ └─────────────┘ └─────────────────┘ └─────────────┘ │ ▼ 存储用户对应的 New API Session Cookie ``` ### 支付方式支持 | 支付方式 | New API 接口 | 说明 | |---------|-------------|------| | 易支付 | `/api/user/pay` | 通用支付方式(微信/支付宝等) | | Stripe | `/api/user/stripe/pay` | 国际信用卡支付 | | Creem | `/api/user/creem/pay` | 新支付方式,支持套餐 | --- ## 数据库设计 ### 只需新增一个字段 ```sql -- 在 agent_user 表添加 New API Session ALTER TABLE agent_user ADD COLUMN new_api_session VARCHAR(500); ALTER TABLE agent_user ADD COLUMN new_api_user_id INTEGER; ``` **不需要新建表**,所有配额、交易、订单数据都在 New API 中。 --- ## 核心流程 ### 1. 用户登录同步 ``` 用户登录 qwen-client │ ▼ qwen-agent 验证本地用户 │ ▼ qwen-agent 用相同凭证调用 New API /api/user/login │ ├─ 成功 → 存储 new_api_session 到 agent_user 表 │ └─ 失败 → 如果是用户不存在,先在 New API 注册再登录 │ ▼ 返回 qwen-agent 的 token 给前端 ``` ### 2. 支付/充值流程 ``` 用户点击充值 │ ▼ qwen-agent 读取用户的 new_api_session │ ▼ 代理请求到 New API (带 session cookie) │ ▼ 返回支付链接给前端 │ ▼ 用户完成支付 (New API 直接处理) │ ▼ 前端跳转到 New API 的用户中心查看结果 ``` ### 3. 配额检查 ``` 用户发起聊天请求 │ ▼ qwen-agent 调用 New API /api/user/self 获取实时配额 │ ├─ 配额不足 → 返回 402 错误 + New API 充值链接 │ └─ 配额充足 → 处理请求 │ ▼ (可选) 调用 New API 扣减配额 ``` --- ## API 设计 ### qwen-agent 新增接口 | 端点 | 方法 | 描述 | 代理的 New API 接口 | |------|------|------|---------------------| | `/api/v1/payment/info` | GET | 获取充值配置(含套餐) | `/api/user/topup/info` | | `/api/v1/payment/plans` | GET | 获取订阅套餐列表 | `/api/subscription/plans` | | `/api/v1/payment/create` | POST | 创建支付订单(易支付) | `/api/user/pay` | | `/api/v1/payment/stripe` | POST | 创建 Stripe 支付 | `/api/user/stripe/pay` | | `/api/v1/payment/creem` | POST | 创建 Creem 支付(套餐) | `/api/user/creem/pay` | | `/api/v1/payment/amount` | POST | 计算实际支付金额 | `/api/user/amount` | | `/api/v1/quota` | GET | 查询用户配额 | `/api/user/self` | | `/api/v1/topup/history` | GET | 充值记录 | `/api/user/topup/self` | 所有接口都是代理转发,使用存储的 new_api_session。 --- ## New API 接口详情 ### 1. 获取充值配置 `/api/user/topup/info` (GET) **鉴权**: 需要登录(Cookie) **响应示例**: ```json { "success": true, "message": "", "data": { "enable_online_topup": true, "enable_stripe_topup": true, "enable_creem_topup": true, "creem_products": [ { "productId": "prod_xxx", "name": "基础套餐", "price": 9.99, "currency": "USD", "quota": 500000 } ], "pay_methods": [ {"name": "微信支付", "type": "wxpay", "color": "#07C160", "min_topup": "1"}, {"name": "支付宝", "type": "alipay", "color": "#1677FF", "min_topup": "1"} ], "min_topup": 1, "stripe_min_topup": 1, "amount_options": [100, 500, 1000, 5000], "discount": { "100": 1.0, "500": 0.95, "1000": 0.9, "5000": 0.85 } } } ``` **字段说明**: - `enable_online_topup`: 是否启用易支付 - `enable_stripe_topup`: 是否启用 Stripe 支付 - `enable_creem_topup`: 是否启用 Creem 支付 - `creem_products`: Creem 套餐列表(**这是套餐列表的数据来源**) - `pay_methods`: 支持的支付方式列表 - `min_topup`: 最小充值额度 - `amount_options`: 预设充值额度选项 - `discount`: 折扣配置(额度 -> 折扣率) ### 2. 获取订阅套餐列表 `/api/subscription/plans` (GET) **鉴权**: 需要登录(Cookie) **响应示例**: ```json { "success": true, "data": [ { "plan": { "id": 1, "title": "月度订阅", "subtitle": "适合轻度用户", "price_amount": 9.99, "currency": "USD", "duration_unit": "month", "duration_value": 1, "total_amount": 500000, "enabled": true, "sort_order": 100, "stripe_price_id": "price_xxx", "creem_product_id": "prod_xxx", "max_purchase_per_user": 0, "upgrade_group": "", "quota_reset_period": "monthly" } } ] } ``` **字段说明**: - `title`: 套餐名称 - `price_amount`: 价格 - `duration_unit`: 周期单位(month/year/custom) - `duration_value`: 周期数值 - `total_amount`: 包含的配额 - `stripe_price_id`: Stripe 价格 ID - `creem_product_id`: Creem 产品 ID ### 3. 创建易支付订单 `/api/user/pay` (POST) **请求**: ```json { "amount": 1000, "payment_method": "wxpay" } ``` **响应**: ```json { "message": "success", "data": {...}, "url": "https://pay.example.com/submit.php?..." } ``` ### 4. 创建 Creem 支付 `/api/user/creem/pay` (POST) **请求**: ```json { "product_id": "prod_xxx", "payment_method": "creem" } ``` **响应**: ```json { "message": "success", "data": { "checkout_url": "https://checkout.creem.io/...", "order_id": "ref_xxx" } } ``` ### 5. 获取用户信息(含配额)`/api/user/self` (GET) **鉴权**: 需要登录(Cookie) **响应示例**: ```json { "success": true, "data": { "id": 1, "username": "user", "email": "user@example.com", "quota": 500000, "used_quota": 10000, "group": "default" } } ``` ### 6. 用户登录 `/api/user/login` (POST) **请求**: ```json { "username": "user", "password": "password" } ``` **响应**: - 返回 Set-Cookie header 包含 session - 用户信息 JSON --- ## 实施步骤 ### Step 1: 添加环境变量配置 在 `utils/settings.py` 添加 New API 配置: ```python from pydantic_settings import BaseSettings class Settings(BaseSettings): # ... 现有配置 ... # New API 配置 NEW_API_BASE_URL: str = "https://new-api.example.com" NEW_API_TIMEOUT: int = 30 class Config: env_file = ".env" ``` ### Step 2: 创建 New API 代理工具类 创建 `utils/new_api_proxy.py`: ```python import httpx from typing import Optional, Any from utils.settings import Settings settings = Settings() class NewAPIProxy: """New API 代理工具类""" def __init__(self): self.base_url = settings.NEW_API_BASE_URL self.timeout = settings.NEW_API_TIMEOUT async def login(self, username: str, password: str) -> Optional[str]: """登录 New API 并返回 session cookie""" async with httpx.AsyncClient() as client: resp = await client.post( f"{self.base_url}/api/user/login", json={"username": username, "password": password}, timeout=self.timeout ) if resp.status_code == 200 and resp.json().get("success"): # 提取 session cookie cookies = resp.cookies.get("session") return cookies return None async def register(self, username: str, password: str, email: str) -> bool: """在 New API 注册用户""" async with httpx.AsyncClient() as client: resp = await client.post( f"{self.base_url}/api/user/register", json={"username": username, "password": password, "email": email}, timeout=self.timeout ) return resp.status_code == 200 async def proxy_get( self, endpoint: str, session_cookie: str ) -> dict: """代理 GET 请求到 New API""" async with httpx.AsyncClient() as client: resp = await client.get( f"{self.base_url}{endpoint}", cookies={"session": session_cookie}, timeout=self.timeout ) return resp.json() async def proxy_post( self, endpoint: str, data: dict, session_cookie: str ) -> dict: """代理 POST 请求到 New API""" async with httpx.AsyncClient() as client: resp = await client.post( f"{self.base_url}{endpoint}", json=data, cookies={"session": session_cookie}, timeout=self.timeout ) return resp.json() async def get_user_info(self, session_cookie: str) -> Optional[dict]: """获取用户信息(含配额)""" try: result = await self.proxy_get("/api/user/self", session_cookie) if result.get("success"): return result.get("data") except: pass return None async def check_quota(self, session_cookie: str) -> tuple[bool, int]: """检查用户配额""" user_info = await self.get_user_info(session_cookie) if user_info: quota = user_info.get("quota", 0) return quota > 0, quota # 降级策略:无法获取配额时允许使用 return True, -1 # 全局实例 new_api_proxy = NewAPIProxy() ``` ### Step 3: 修改登录逻辑 修改 `routes/bot_manager.py` 中的登录函数: ```python from utils.new_api_proxy import new_api_proxy async def login_user(username: str, password: str): # 1. 验证本地用户 user = await verify_local_user(username, password) # 2. 同步登录 New API new_api_session = await new_api_proxy.login(username, password) # 3. 如果登录失败,尝试注册 if not new_api_session: await new_api_proxy.register(username, password, user.email) new_api_session = await new_api_proxy.login(username, password) # 4. 存储 session if new_api_session: await db.execute( "UPDATE agent_user SET new_api_session = $1 WHERE id = $2", new_api_session, user.id ) # 5. 返回本地 token return generate_local_token(user) ``` ### Step 4: 创建支付代理路由 创建 `routes/payment.py`: ```python from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import Optional from utils.new_api_proxy import new_api_proxy from utils.auth import get_current_user router = APIRouter(prefix="/api/v1", tags=["payment"]) class CreatePaymentRequest(BaseModel): amount: int payment_method: str class CreemPaymentRequest(BaseModel): product_id: str payment_method: str = "creem" class AmountRequest(BaseModel): amount: int @router.get("/payment/info") async def get_payment_info(user = Depends(get_current_user)): """获取充值配置(含套餐列表)""" if not user.new_api_session: raise HTTPException(status_code=401, detail="未同步 New API session") return await new_api_proxy.proxy_get("/api/user/topup/info", user.new_api_session) @router.get("/payment/plans") async def get_subscription_plans(user = Depends(get_current_user)): """获取订阅套餐列表""" if not user.new_api_session: raise HTTPException(status_code=401, detail="未同步 New API session") return await new_api_proxy.proxy_get("/api/subscription/plans", user.new_api_session) @router.post("/payment/create") async def create_payment(req: CreatePaymentRequest, user = Depends(get_current_user)): """创建支付订单(易支付)""" if not user.new_api_session: raise HTTPException(status_code=401, detail="未同步 New API session") return await new_api_proxy.proxy_post( "/api/user/pay", {"amount": req.amount, "payment_method": req.payment_method}, user.new_api_session ) @router.post("/payment/stripe") async def create_stripe_payment(req: AmountRequest, user = Depends(get_current_user)): """创建 Stripe 支付""" if not user.new_api_session: raise HTTPException(status_code=401, detail="未同步 New API session") return await new_api_proxy.proxy_post( "/api/user/stripe/pay", {"amount": req.amount}, user.new_api_session ) @router.post("/payment/creem") async def create_creem_payment(req: CreemPaymentRequest, user = Depends(get_current_user)): """创建 Creem 支付(套餐)""" if not user.new_api_session: raise HTTPException(status_code=401, detail="未同步 New API session") return await new_api_proxy.proxy_post( "/api/user/creem/pay", {"product_id": req.product_id, "payment_method": req.payment_method}, user.new_api_session ) @router.post("/payment/amount") async def get_payment_amount(req: AmountRequest, user = Depends(get_current_user)): """计算实际支付金额""" if not user.new_api_session: raise HTTPException(status_code=401, detail="未同步 New API session") return await new_api_proxy.proxy_post( "/api/user/amount", {"amount": req.amount}, user.new_api_session ) @router.get("/quota") async def get_quota(user = Depends(get_current_user)): """获取用户配额""" if not user.new_api_session: raise HTTPException(status_code=401, detail="未同步 New API session") user_info = await new_api_proxy.get_user_info(user.new_api_session) if user_info: return { "quota": user_info.get("quota", 0), "used_quota": user_info.get("used_quota", 0) } raise HTTPException(status_code=500, detail="获取配额失败") @router.get("/topup/history") async def get_topup_history(user = Depends(get_current_user)): """获取充值记录""" if not user.new_api_session: raise HTTPException(status_code=401, detail="未同步 New API session") return await new_api_proxy.proxy_get("/api/user/topup/self", user.new_api_session) ``` ### Step 5: 在 Chat 路由添加配额检查 在 `routes/chat.py` 添加配额检查中间件: ```python from utils.new_api_proxy import new_api_proxy async def check_quota_before_chat(user) -> tuple[bool, int]: """聊天前检查配额""" if not user.new_api_session: # 没有 session,允许通过(可能是非支付用户) return True, -1 try: has_quota, quota = await new_api_proxy.check_quota(user.new_api_session) return has_quota, quota except Exception as e: # 降级策略:New API 不可用时允许使用 logger.warning(f"配额检查失败: {e}") return True, -1 # 在 chat 处理函数中调用 async def handle_chat(request, user): has_quota, quota = await check_quota_before_chat(user) if not has_quota: raise HTTPException( status_code=402, detail={ "error": "quota_exceeded", "message": "配额不足,请充值", "topup_url": f"{settings.NEW_API_BASE_URL}/console/topup" } ) # ... 继续处理聊天请求 ... ``` ### Step 6: 注册路由 在 `fastapi_app.py` 中注册新路由: ```python from routes.payment import router as payment_router app.include_router(payment_router) ``` ### Step 7: 数据库迁移 ```sql -- 在 agent_user 表添加 New API Session ALTER TABLE agent_user ADD COLUMN new_api_session VARCHAR(500); ALTER TABLE agent_user ADD COLUMN new_api_user_id INTEGER; ``` --- ## 环境变量 ```bash # .env NEW_API_BASE_URL=https://new-api.example.com NEW_API_TIMEOUT=30 ``` --- ## 前端集成指南 ### 1. 充值页面流程 ```typescript // 1. 获取充值配置 const paymentInfo = await fetch('/api/v1/payment/info'); // 2. 根据返回的支付方式展示选项 const { pay_methods, creem_products, amount_options, discount } = paymentInfo.data; // 3. 用户选择套餐或输入金额 // - 如果选择 Creem 套餐:调用 /api/v1/payment/creem // - 如果选择自定义金额:调用 /api/v1/payment/create // 4. 获取支付链接并跳转 const result = await fetch('/api/v1/payment/creem', { method: 'POST', body: JSON.stringify({ product_id: 'prod_xxx' }) }); // 5. 跳转到支付页面 window.location.href = result.data.checkout_url; ``` ### 2. 显示配额余额 ```typescript // 获取用户配额 const quota = await fetch('/api/v1/quota'); // 显示格式化后的配额 // quota.quota - 当前可用配额 // quota.used_quota - 已使用配额 ``` ### 3. 配额不足处理 ```typescript // 当聊天返回 402 错误时 if (error.status === 402) { const { topup_url } = error.detail; // 显示充值提示 // 提供"去充值"按钮,跳转到 topup_url } ``` --- ## 用户体验流程 ### 充值流程(易支付) ``` 1. 用户点击"充值"按钮 2. 前端调用 GET /api/v1/payment/info 获取配置 3. 展示支付方式和预设金额选项 4. 用户选择金额和支付方式 5. 前端调用 POST /api/v1/payment/create 6. 获取支付链接,跳转到支付页面 7. 用户完成支付 8. 支付成功后跳转回前端 ``` ### 充值流程(Creem 套餐) ``` 1. 用户点击"购买套餐" 2. 前端调用 GET /api/v1/payment/info 获取 creem_products 3. 展示套餐列表(名称、价格、配额) 4. 用户选择套餐 5. 前端调用 POST /api/v1/payment/creem 6. 获取 checkout_url,跳转到 Creem 支付页面 7. 用户完成支付 8. 自动跳转回前端 ``` ### 配额检查 ``` 1. 用户发送聊天消息 2. qwen-agent 检查配额 3. 如果配额充足:正常处理 4. 如果配额不足:返回 402 + 充值链接 5. 前端显示"配额不足,请充值"提示 ``` --- ## 总结 简化方案的核心是 **不重复存储数据**,让 New API 作为唯一的支付数据源。qwen-agent 只负责: 1. 用户登录时同步 New API session 2. 代理转发支付相关请求 3. 实时查询配额进行检查 ### 套餐列表获取方式 套餐列表有两个来源: 1. **Creem 产品列表**(推荐) - 接口:`/api/user/topup/info` → `creem_products` 字段 - 适用于一次性购买配额包 2. **订阅套餐列表** - 接口:`/api/subscription/plans` - 适用于周期性订阅 ### 与原方案对比 | 项目 | 原方案 | 简化方案 | |------|--------|----------| | 新建数据库表 | 4 个 | 0 个 | | 新增字段 | 多个 | 2 个 | | 本地配额管理 | 需要 | 不需要 | | Webhook 处理 | 需要 | 不需要 | | 交易记录 | 本地存储 | New API 管理 | | 实施周期 | 2-3 周 | 3-5 天 | ### 文件变更清单 | 文件 | 操作 | 说明 | |------|------|------| | `utils/settings.py` | 修改 | 添加 New API 环境变量 | | `utils/new_api_proxy.py` | 新建 | New API 代理工具类 | | `routes/payment.py` | 新建 | 支付代理路由 | | `routes/bot_manager.py` | 修改 | 登录逻辑添加 session 同步 | | `routes/chat.py` | 修改 | 添加配额检查 | | `fastapi_app.py` | 修改 | 注册新路由 | | 数据库迁移 | 执行 | 添加 new_api_session 字段 | ### 依赖添加 ```bash poetry add httpx ```