first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
@@ -0,0 +1,149 @@
---
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 分支与错误码引用。
- 预期结果:输出逐表差异清单,确认新增字段与索引与模型定义完全一致、无旧路径引用遗漏。