docs/plans/2026-07-10-pay-v2-integration.md(checkbox),本页为阅读版 · pay v2 契约读自 design/pay-v2 分支代码(非文档口径)· 已并入授权管理独立屏三 tab 实现(原型评审通过 13b7274)· Task 8 文档同步已收尾jiu 两侧的 pay v1 对接已经完整上线,本任务不是从零接入,而是契约升级(v1 → v2):
backend/internal/service/pay.go 已有 PayService 四职责:签名下单 CreatePurchase / webhook 入账 HandleCallback→settle(FOR UPDATE + out_trade_no 幂等 + 金额核对)/ 续期 entitle(max(现到期,now)+时长)/ 60s 查单兜底 reconcileOnce;表 license_purchases、配置 PAY_BASE_URL/PAY_SECRET/PAY_RETURN_URL、路由 POST /api/v1/pay/callback 全部就位purchase_card.dart 已有三态状态机(选套餐 → launchUrl 拉起收银台 → 3s 轮询 + 30s 心跳兜底),iOS hideExternalPurchaseUi 合规开关、promo 限购、到期横幅入口都在web/checkout.njk:167 直读下单响应的 d.pay_url 跳收银台——兼容约束的来源零改动沿用HMAC 签名算法(util.PaySign:base64(HMAC-SHA256(secret, "jiu\n ts\n nonce\n rawBody"))、±5min 窗)、out_trade_no 幂等键、entitle 续期语义、路由挂载、PAY_* 配置链、iOS 合规开关、轮询/心跳。
| 项 | v1(现实现) | v2(本计划目标) |
|---|---|---|
| 下单 | POST /api/v1/orders{product_id, biz_system, biz_ref, return_url, client_type} | POST /api/v2/orders {sku, method, biz_system, biz_ref, return_url};sku 直接用 biz_code(productID 查询+10min 缓存整段删除);无 client_type |
| 下单响应 | {pay_url, out_trade_no, amount("2999.00"), subject} | {order_no, session:{render_type, payload, expires_at?}};alipay 落 redirect + payload.url;不回传金额 |
| 查单 | GET /api/v1/orders/:otn,四态 | GET /api/v2/orders/:no(无鉴权,见风险节),{order_no, status, subject, amount_minor, currency, paid_at?},八态 created|pending|paid|canceled|expired|refunding|partially_refunded|refunded;不回传 biz_ref |
| webhook | 扁平 payload,amount 元串,有 trade_no | +event_type(当前仅 payment.succeeded)+ X-Pay-Event 头(不参与签名);amount_minor int64 + currency;无 trade_no;重投固定 60s 无退避无死信;成功判定 = 200 且 body 含 "SUCCESS"(大小写不敏感,现回包 {"code":"SUCCESS"} 天然满足) |
| 签名 | 完全一致四段 \n 拼接 HMAC-SHA256 → base64 标准编码,util.PaySign/PaySignVerify 零改动;pay 侧无 nonce 去重,jiu 侧自建(D3) | |
| 金额 | string 元 | amount_minor int64(CNY=分)+ currency 大写码,全链路禁 float |
| retry / cancel | 无 | retry:409 currency_mismatch=需换方式新建单(jiu 单渠道不接,D6);cancel:恒 200 {"data":{"canceled":bool}},仅 pending 翻转成功(jiu 接,防单堆积) |
qr render_type 仅文档口径无代码实现——体验增强项而非缺失:alipay PC 收银台页面本身含二维码,桌面扫码今天即可用,qr 内嵌只是省一次浏览器跳转seedPlans 仅 alipay_sandbox 开关下执行,生产套餐种子需部署时确认BIZ_JIU_SECRET / BIZ_JIU_CALLBACK_URLamount_minor bigint + currency varchar(8);旧 amount 列保留只读,启动 Go 回填存量(复用 toCents,参照 backfillPinyin),观察一版后另行 DROP(同定价消歧惯例)/license;移动 /me/license),三 tab:授权信息 / 在线购买续费(iOS 合规态整 tab 隐藏)/ 订单管理。「继续支付」=打开下单时存储的 pay_url(不接 retry,维持 D6);「授权续期至」=settle 回写 renewed_to;模型加 pay_url/renewed_to 两列BackfillPurchaseAmountMinor("2999.00"→299900+CNY,幂等只处理 amount_minor=0 行)挂 main.go;schema.sql / testutil SQLite 建表同步。测试:回填正确、已有值不覆盖PurchaseResult 改 {out_trade_no, render_type, payload, amount_minor, currency, subject, POST /api/v1/license/purchase/:otn/cancel(admin,license 组=LicenseGuard 豁免区)→ pay cancel → canceled==true 才标 failed(false=已付竞态,保持 pending 等入账)。测试:成功/竞态/跨店 404yuanFromMinor(299900→"2,999.00")落 core/utils/money.dart;repository +cancelPurchase。测试:fromJson/格式化GET /api/v1/license/purchases:分页/状态筛选/summary(累计·笔数·待支付),仅管理员,跨店隔离;pending 单回 pay_url 供继续支付;LEFT JOIN users 取下单人/license+侧边栏项+settings 摘除授权子导航;订单 tab 桌面 DsTable+抽屉 / 移动 MCard 流+sheet;继续支付/取消订单/重新购买切购买 tab;settings golden 更新 + 新屏 golden ×3 + fidelity 注册docs/pay支付对接开发指南.html v2 化(标注 v1 断代);db-schema.html 四新列;CONTRACT 台账回填;用户手册补「授权管理/订单管理」节(两侧同步)执行顺序:后端链 1→2→3→4→5→9 与前端链 6→7→10 交错推进,Task 8 收尾。
go build/vet/test ./... + flutter analyze/test + check_ds_code.mjs + check-l1-sync.mjs 全绿后停在本地提交态,todo 标 done 等验收docs/plans/2026-07-10-pay-v2-integration.md + 本 HTML 落 docs/plans/pay-v2-integration.html 登记 index.html