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

11 KiB
Raw Permalink Blame History

name, overview, todos
name overview todos
接入微信虚拟支付(全终端) 按微信官方政策把虚拟商品(会员/额度包)支付从普通 JSAPI 支付切换到小程序虚拟支付(wx.requestVirtualPayment,覆盖 Android/iOS 全终端):新增 pkg/wechat/virtualpay.go(xpay 服务器 API HMAC 签名客户端 + 事件推送验签/应答)、虚拟支付订单创建接口、道具服务器 API 同步上架、发货/退款/iOS 退款问询三类推送处理、goods/orders 扩展字段与新迁移、配置与文档同步;现有 JSAPI 支付代码保留但不再用于虚拟商品。
id content status
config-wiring 新增 VirtualPaySettingS 配置结构与全局变量,接线 main.go 初始化与 config.example.yaml 样例段 completed
id content status dependencies
xpay-client 实现 pkg/wechat/virtualpay.go 客户端(HMAC 签名、道具上传发布、查单退款、推送验签应答)并编写单元测试 completed
config-wiring
id content status
data-layer 扩展 goods/orders 模型与常量错误码,编写 000017 迁移 SQL,并用 [subagent:code-explorer] 校对一致性 completed
id content status dependencies
virtual-order 实现道具同步 SyncVirtualGoods 与虚拟支付订单创建接口(service、dto、handler、路由) completed
xpay-client
data-layer
id content status dependencies
notify-callbacks 实现发货/退款/iOS 退款问询推送回调处理并复用权益发放回退链路,调整退款申请分流 completed
xpay-client
data-layer
id content status dependencies
docs-verify 同步 README 与接口文档,执行 go build 与 go vet 全量验证 completed
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 退款问询应答」。因此本次采用双通道并存、虚拟支付旁路接入的架构:

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 分支与错误码引用。
  • 预期结果:输出逐表差异清单,确认新增字段与索引与模型定义完全一致、无旧路径引用遗漏。