Files
zt-closet-backend/.codebuddy/plans/接入微信虚拟支付_全终端__8cbaa9ab.md
2026-09-11 01:41:52 +08:00

149 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: 接入微信虚拟支付(全终端)
overview: 按微信官方政策把虚拟商品(会员/额度包)支付从普通 JSAPI 支付切换到小程序虚拟支付(wx.requestVirtualPayment,覆盖 Android/iOS 全终端):新增 pkg/wechat/virtualpay.go(xpay 服务器 API HMAC 签名客户端 + 事件推送验签/应答)、虚拟支付订单创建接口、道具服务器 API 同步上架、发货/退款/iOS 退款问询三类推送处理、goods/orders 扩展字段与新迁移、配置与文档同步;现有 JSAPI 支付代码保留但不再用于虚拟商品。
todos:
- id: config-wiring
content: 新增 VirtualPaySettingS 配置结构与全局变量,接线 main.go 初始化与 config.example.yaml 样例段
status: completed
- id: xpay-client
content: 实现 pkg/wechat/virtualpay.go 客户端(HMAC 签名、道具上传发布、查单退款、推送验签应答)并编写单元测试
status: completed
dependencies:
- config-wiring
- id: data-layer
content: 扩展 goods/orders 模型与常量错误码,编写 000017 迁移 SQL,并用 [subagent:code-explorer] 校对一致性
status: completed
- id: virtual-order
content: 实现道具同步 SyncVirtualGoods 与虚拟支付订单创建接口(service、dto、handler、路由)
status: completed
dependencies:
- xpay-client
- data-layer
- id: notify-callbacks
content: 实现发货/退款/iOS 退款问询推送回调处理并复用权益发放回退链路,调整退款申请分流
status: completed
dependencies:
- xpay-client
- data-layer
- id: docs-verify
content: 同步 README 与接口文档,执行 go build 与 go vet 全量验证
status: completed
dependencies:
- virtual-order
- notify-callbacks
---
## 产品概述
为微信小程序接入微信官方「虚拟支付(XPay)」能力:小程序内所有虚拟商品(会员、上传包、AI 推荐包、衣柜分享包、搭配分享包)的购买统一改走虚拟支付通道,覆盖 Android、iOS、鸿蒙、Windows 全终端(符合微信 2026-04-01 起虚拟商品全终端强制接入虚拟支付的政策要求)。现有普通微信支付(JSAPI)代码保留不删,供未来独立 App 或实物商品使用。
## 核心功能
- 配置与初始化:新增 VirtualPay 配置段(OfferID、现网/沙箱 AppKey、回调参数),服务启动时初始化虚拟支付客户端,未配置仅告警不阻断启动。
- 道具同步:实现 xpay 服务器 API 客户端(HMAC 签名),将上架商品自动上传并发布为虚拟支付道具(一次一个、轮询任务状态),回填 product_id;失败告警不阻塞服务。
- 虚拟支付下单:新增「创建虚拟支付订单」接口,本地落单(OutTradeNo 复用本地订单号、pay_channel=2、记录商品 product_id 与平台)后返回前端调起 wx.requestVirtualPayment 所需业务参数。
- 发货推送回调:处理 xpay_goods_deliver_notify(验签、幂等、校验 ProductId 与实际支付金额后流转订单并幂等发放会员/额度权益),必须按微信规范应答 ErrCode=0。
- 退款与 iOS 退款问询:处理 xpay_refund_notify 推送复用现有权益回退链路;处理 xpay_subscribe_ios_refund_query_notify(3 秒内应答建议退款/拒绝);iOS 订单退款申请接口提示走 App Store,Android 虚拟支付订单走 xpay 退款任务。
## 技术栈
- 语言与框架:Go 1.27 + Gin + GORM(复用现有项目栈)
- 新增模块:pkg/wechat/virtualpay.go(XPay 客户端,仅 Go 标准库 + 现有依赖,与现有 mini.go 风格一致)
- 数据迁移:沿用现有 golang-migrate 版本化迁移体系(pkg/migration/*.sql embed),新增 000017 版本
- 可复用基建:redisdao.TryLock、redismodel.KeyLockPayCallback、benefit_grant_tasks 幂等发放、sendNotification、slog
## 实现方案
微信虚拟支付是独立体系:无服务端下单 API(下单由前端 wx.requestVirtualPayment 内部完成),服务端职责为「道具上传发布 + 本地订单管理 + 推送回调发货/退款 + iOS 退款问询应答」。因此本次采用**双通道并存、虚拟支付旁路接入**的架构:
```mermaid
flowchart LR
A[小程序前端] -->|全终端统一| B[POST /order/virtual 创建虚拟订单]
B --> C[service.VirtualCreate<br/>本地落单 pay_channel=2]
C --> D[返回 product_id / order_no]
A -->|wx.requestVirtualPayment| E[微信虚拟支付<br/>iOS走Apple 其余走微信]
E -->|xpay_goods_deliver_notify| F[POST /xpay/notify]
F --> G[验签 幂等锁 金额校验]
G --> H[复用 finalizePaidOrder<br/>grantOrderBenefits 发放权益]
E -->|xpay_refund_notify| I[复用 settleRefundSuccess 回退权益]
E -->|xpay_subscribe_ios_refund_query_notify| J[3秒内应答 result_code]
K[道具同步 SyncVirtualGoods] -->|start_upload_goods 轮询 publish| E
```
关键决策:
1. **订单与道具标识**:goods 表加 product_id(道具 ID,空表示未发布);orders 表加 pay_channel=2(虚拟支付)、xpay_product_id、xpay_platform(android/ios,下单时前端上报,用于退款分流)、xpay_env(0 现网 1 沙箱)。发货推送以 OutTradeNo 匹配本地 order_no。
2. **金额校验**:发货推送 GoodsInfo.ActualPrice(分)必须等于订单 PayAmount,ProductId 必须等于订单 xpay_product_id,否则不流转不发货、记日志人工介入(沿用现有 PayNotify 的防篡改策略)。
3. **权益发放零改造**:虚拟支付发货完全复用 finalizePaidOrder → grantOrderBenefits → benefit_grant_tasks 幂等凭证链路;退款推送复用 settleRefundSuccess → revokeOrderBenefits 回退链路。不新增第二套发放逻辑。
4. **道具同步策略**:提供 SyncVirtualGoods(ctx) 服务方法(遍历上架且 product_id 为空或发布失败的 goods,逐个 upload → 轮询 → publish),启动时 goroutine 异步执行并告警,另暴露内部触发入口;单个商品失败不影响其他商品。
5. **推送应答**:XPay 消息推送与普通 V3 回调不同(ErrCode 格式、最多重推 15 次),必须用独立应答函数,不能复用现有 wechatCallbackSuccess。
6. **旧通道保留**:现有 JSAPI 下单/回调/退款代码不动;虚拟商品前端不再调用 POST /order 的 JSAPI 下单(保留给未来实物商品/独立 App)。
性能与可靠性:
- 回调处理热路径与现有 PayNotify 一致:Redis 幂等锁(30 秒)+ 条件状态流转 + 并发竞态兜底,无新增瓶颈。
- iOS 退款问询必须在 3 秒内应答:处理逻辑仅本地 DB 查询 + 构造应答,不做外部调用,保证时延。
- 道具同步轮询使用短超时 HTTP 客户端(复用 10s 风格),启动异步执行不阻塞服务。
## 目录结构
```
pkg/wechat/
├── virtualpay.go # [NEW] XPay 客户端:HMAC 签名、道具上传/发布/查询、query_order/refund_order、推送验签解析与 ErrCode 应答
└── virtualpay_test.go # [NEW] HMAC 签名样例、推送解析/应答单元测试(固定 appKey,不连真实微信)
pkg/setting/section.go # [MODIFY] 新增 VirtualPaySettingS 配置结构
global/setting.go # [MODIFY] 新增 VirtualPaySetting 全局变量
main.go # [MODIFY] ReadSection("VirtualPay") + setupVirtualPayClient()
configs/config.example.yaml # [MODIFY] 新增 VirtualPay 配置样例段(占位值 + 注释)
internal/service/
├── constants.go # [MODIFY] PayChannelVirtual=2、GoodsProductStatus 常量、Platform 常量
├── virtualpay.go # [NEW] SyncVirtualGoods 道具同步 + VirtualOrderCreate + 推送处理(发货/退款/iOS 退款问询)
internal/mysqlmodel/
├── goods.go # [MODIFY] 加 ProductID、ProductStatus 字段
└── order.go # [MODIFY] 加 XPayProductID、XPayPlatform、XPayEnv 字段
internal/dto/order.go # [MODIFY] 新增 VirtualOrderCreateRequest/Response
internal/routers/api/v1/order.go # [MODIFY] 新增 VirtualCreate、XPayNotify handler + XPay 应答函数
internal/routers/router.go # [MODIFY] 注册 POST /order/virtual(鉴权)与 POST /xpay/notify(公开)
pkg/errcode/order_code.go # [MODIFY] 新增虚拟支付相关错误码(道具未发布/虚拟支付未配置/iOS 退款走 App Store 等)
pkg/migration/
├── 000017_add_virtual_pay_fields.up.sql # [NEW] goods/orders 加字段与索引
└── 000017_add_virtual_pay_fields.down.sql # [NEW] 对应回滚
README.md # [MODIFY] 配置表、支付架构说明、迁移版本数
接口文档.md # [MODIFY] 虚拟支付下单接口、三类推送、前端 wx.requestVirtualPayment 调用契约
```
## 关键代码结构
```
// VirtualPaySettingS 微信虚拟支付(XPay)配置,与普通微信支付独立结算。
type VirtualPaySettingS struct {
OfferID string // 虚拟支付 OfferID(AppID)
EnvAppKey string // 现网 AppKey
SandboxAppKey string // 沙箱 AppKey
Enable bool // 是否启用虚拟支付(未开通时服务降级为不可下单)
}
// VirtualOrderCreateResponse 创建虚拟支付订单响应(前端据此调 wx.requestVirtualPayment)。
type VirtualOrderCreateResponse struct {
OrderNo string `json:"order_no"` // 业务订单号(作为 OutTradeNo)
PayAmount int64 `json:"pay_amount"` // 实付金额(分)
ProductID string `json:"product_id"` // 虚拟支付道具 ID
Quantity int64 `json:"quantity"` // 购买数量(固定 1)
Attach string `json:"attach"` // 透传信息
}
```
实现注意事项:
- XPay 签名规则 `paySig = hex(hmac_sha256(appKey, uri + "&" + signData))`,服务器 API 的 uri 为 API 路径(不含 query),post_body 必须与真实请求体完全一致;推送验签以官方「虚拟支付」消息推送文档为准,实现时不得虚构字段。
- xpay 请求域名与路径(/xpay/start_upload_goods 等)以官方文档为准;客户端初始化为全局单例 + RWMutex,风格对齐 pay.go。
- 推送应答:成功返回 `{"ErrCode":0,"ErrMsg":"success"}`(JSON)或对应 XML;非 goods_deliver/refund/ios_refund_query 的推送(complaint/coin/wxpay_callback)仅结构化记日志并应答成功。
- Env 字段解析保留(0 现网 / 1 沙箱),沙箱默认关闭(iOS 不支持沙箱)。
## Agent Extensions
### SubAgent
- **code-explorer**
- 用途:迁移 SQL 落地后,交叉核对 000017 迁移脚本与 internal/mysqlmodel 中 goods/orders 模型字段的一致性(列名、类型、默认值、索引),并复核全仓库无遗漏的 pay_channel=2 分支与错误码引用。
- 预期结果:输出逐表差异清单,确认新增字段与索引与模型定义完全一致、无旧路径引用遗漏。