2026-08-02-virtual-payment-design.md 15 KB

小程序虚拟支付接入设计

日期: 2026-08-02 状态: 设计稿 v1 优先级: P0(合规要求) 范围: 测评订单 + 会员 + 订阅(道具直购模式)


1. 背景与合规要求

微信官方《虚拟支付业务运营指南》:小程序内提供的虚拟商品(虚拟代币、解锁功能、订阅内容、付费功能、打赏、虚拟礼物等),购买和支付均需接入小程序虚拟支付,不得引导至 app、公众号、H5、个人号、网站完成支付。该能力已支持全终端(Android/鸿蒙/Windows → 微信支付,iOS → Apple 支付)。

本项目现状(勘察结论):

  • 全项目无任何 wx.requestVirtualPayment / xpay 调用
  • 5 套订单体系中有 3 套不合规:
    • assessment_orders(测评订单,前缀 A):假支付——payOrder() 直接标记已付,无预付/无回调
    • payment_orders(会员,前缀 ORD):手动回调,无真实支付
    • member_subscription_order(L1/L2 订阅,前缀 SUB):假定外部已支付
  • 商城实物商品走 v3 JSAPI uni.requestPayment(合规,本次不动)
  • 项目微信消息推送(message push)接收端点——虚拟支付发货推送需从零建
  • wechat.appkey(虚拟支付 paySig 签名必需)配置缺失

豁免说明: 线上问诊/法律咨询属豁免范畴,本项目不涉及。

2. 已确认决策

决策项 结论
接入范围 测评订单 + 会员 + 订阅(商城虚拟商品、任务模板包本期不做)
虚拟支付模式 道具直购(每个服务对应一个 ProductId,支付即解锁)
道具映射存储 DB 配置表 virtual_goods_config + Web 管理端 CRUD
微信侧状态 虚拟支付已开通、小程序简称已配置、可联调
支付金额 均 ≥1 元(满足 iOS 最低 1 元要求,无 0 元旁路)
道具配置状态 尚未在微信后台建道具 → 开发不阻塞,映射表先建,上线前配置

3. 总体架构

┌───────────────────────────── cfc-frontend (uni-app) ─────────────────────────────┐
│  测评购买页 / 会员升级页 / 订阅页                                                  │
│      │  ① 创建待支付订单 (业务下单接口,status=pending)                            │
│      ▼                                                                           │
│  后端下单接口 ──► 生成虚拟支付参数 {orderInfo, extInfo, sign, env}                │
│      │  ② 前端 wx.requestVirtualPayment(orderInfo, extInfo, sign)                │
│      ▼  ③ 用户支付成功(success 回调可能丢失)                                     │
│  微信客户端 ──► 平台路由: Android/鸿蒙/Windows→微信支付  iOS→Apple 支付            │
└─────────────────────────────┴────────────────────────────────────────────────────┘
                              │  ④ xpay_goods_deliver_notify 发货推送(消息推送机制)
                              ▼
┌───────────────────────────── cfc-backend ────────────────────────────────────────┐
│  [新] VirtualPayController   POST /api/internal/virtual-pay/notify ← 接收推送      │
│  [新] VirtualPayService      paySig签名 / 发货推送处理 / 发货通知 / 订单查询       │
│  [新] VirtualGoodsConfig     道具映射表(DB + Web管理端)                          │
│        │  ⑤ 按订单前缀路由:  A→测评解锁  ORD→会员开通  SUB→订阅开通                │
│        ▼                                                                         │
│  AssessmentOrderService  /  MembershipService  /  MemberSubscriptionService       │
│        │  ⑥ 兜底: notify_provide_goods 通知发货完成                               │
└───────────────────────────────────────────────────────────────────────────────────┘

新增组件清单:

组件 职责 位置
VirtualPayService paySig 签名计算、发货推送处理、notify_provide_goodsquery_order cfc-backend/.../service/VirtualPayService.java
VirtualPayController 接收 xpay 消息推送(公开端点) cfc-backend/.../controller/VirtualPayController.java
VirtualGoodsConfig 道具映射实体 + Mapper cfc-backend/.../entity/ + mapper/
virtual_goods_config 业务SKU↔ProductId 映射 schema.sql + DatabaseInitializer
VirtualPayParamsDTO 支付参数返回体 cfc-backend/.../dto/
Web 管理页 道具映射 CRUD cfc-web/src/views/ + api/admin.js
前端改造 测评/会员/订阅支付调用改为虚拟支付 payment.vue、测评/会员/订阅购买页、utils/api.js

不改动:现有 v3 微信支付(PaymentService / ProductOrderService / payment.vue 实物支付分支)——实物商品继续走原链路。

4. 道具映射表 virtual_goods_config

CREATE TABLE virtual_goods_config (
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    goods_type  VARCHAR(32)  NOT NULL COMMENT 'ASSESSMENT_PACKAGE / MEMBERSHIP / SUBSCRIPTION',
    biz_key     VARCHAR(64)  NOT NULL COMMENT '业务SKU标识: 测评套餐ID / 会员等级code / 订阅档位code',
    product_id  VARCHAR(64)  NOT NULL COMMENT '微信商户后台道具ID',
    goods_name  VARCHAR(128) NOT NULL COMMENT '道具名称',
    status      TINYINT      NOT NULL DEFAULT 1 COMMENT '1=启用 0=停用',
    create_time DATETIME     DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_goods_type_biz (goods_type, biz_key)
) COMMENT '小程序虚拟支付道具映射';
  • 唯一约束: (goods_type, biz_key)
  • 停用/未配置处理: pay() 接口查不到启用映射 → 返回错误码 46001(道具未配置),前端提示"该商品暂未开放购买"
  • 迁移: DatabaseInitializer.runMigrations() 新增建表迁移(幂等),同步 schema.sql

Web 管理端: 新增 VirtualGoodsConfig.vue(admin 菜单"虚拟支付道具"),支持按 goods_type 筛选 + CRUD + 启用/停用。

5. 支付签名与下单

5.1 签名算法(官方参考脚本同款)

// paySig = to_hex(hmac_sha256(appKey, uri + "&" + signData))
// uri 固定为 "requestVirtualPayment"(wx.requestVirtualPayment 场景)
// signData = orderInfo + extInfo 按官方规则拼接(实现时对照官方 Python 脚本输出做断言单测)
String paySig = HmacUtils.hmacSha256Hex(appKey, "requestVirtualPayment&" + signData);

5.2 后端下单返回体 VirtualPayParamsDTO

{
  "orderInfo": {
    "mch_id": "虚拟支付商户号",
    "appid": "wx5ba8038ef16fb245",
    "out_trade_no": "A20260802xxx",
    "total_fee": 9900,
    "product_info": "五维测评·基础版",
    "attach": "assessment:12"
  },
  "extInfo": {},
  "sign": "paySig值",
  "env": 0,
  "signType": "HMAC-SHA256"
}
  • total_fee 单位:分,以服务端计算为准(防篡改)
  • attach 透传业务信息(goodsType:bizKey),便于对账
  • env:0=现网 1=沙箱,由配置 wechat.virtual-pay-env 控制

5.3 三个业务下单接口改造

业务 现有接口 改造方式
测评 POST /api/dan-assessment/order/pay AssessmentOrderService.payOrder() 不再直接标记已付,改为生成 VirtualPayParams 返回(订单保持 pending)
会员 POST /api/membership/orders MembershipService.createOrder() 后新增 pay 流程:生成 VirtualPayParams 返回
订阅 POST /api/subscription/create(JSON body 下单)/ /auto-renew MemberSubscriptionService 改为生成 VirtualPayParams 返回(不再假定已支付)

统一行为: 下单接口先校验 virtual_goods_config 映射存在且启用;校验金额 ≥ 1 元;返回支付参数后订单状态保持 pending只有收到发货推送才置 paid

6. 发货推送处理(结果确认核心)

6.1 接收端点

  • 路径: POST /api/internal/virtual-pay/notify(挂 JwtInterceptor 已有公开前缀 /api/internal/,无需新增豁免)
  • 需在 mp.weixin.qq.com 配置: 服务器推送 URL 指向该端点(生产域名 cfc.bianwoyou.com.cn)。若后台要求加密模式,配置 EncodingAESKey 并实现加解密;设计默认明文 + 格式校验,联调时按后台实际配置确认。

6.2 推送类型处理

Event 处理 响应
xpay_goods_deliver_notify OutTradeNo 前缀路由:A→测评 ORD→会员 SUB→订阅;标记已付 + 解锁权益(测评确认预约/会员插入 FamilyMembership/订阅开通),记录 transaction_id、paid_at {"ErrCode":0,"ErrMsg":"success"}
xpay_refund_notify 标记退款状态(iOS 用户 App Store 退款同样走此推送) {"ErrCode":0,"ErrMsg":"success"}
xpay_coin_pay_notify / 其他 记日志忽略(道具直购不涉及) {"ErrCode":0,"ErrMsg":"success"}

幂等:OutTradeNo 查订单,已 paid 的重复推送直接返回成功。微信推送最多重试 15 次(间隔 2/4/8/16...秒)。

6.3 发货完成通知

  • 正常路径:发货推送返回成功即视为发货完成,无需额外调用
  • 兜底:notify_provide_goodsPOST /xpay/notify_provide_goods)仅在推送确认异常时由对账任务触发

7. 退款与异常处理

场景 处理
Android/微信支付退款 业务退款接口改造:调 xpay/refund_order 启动退款 → 轮询 query_order 确认 → xpay_refund_notify 更新状态
iOS/Apple 退款 开发者不能主动退款。用户 App Store 申请 → 平台推 xpay_subscribe_ios_refund_query_notify 问询(3 秒内应答;3 次未应答默认"不确定"交 Apple 裁决)→ 退款成功走 xpay_refund_notify需实现问询应答端点 POST /api/internal/virtual-pay/ios-refund-query
success 回调丢失 前端支付结果页轮询后端订单状态(沿用现有轮询模式)+ 后端定时任务调 query_order 兜底(扩展 OrderTimeoutCancelTask 思路)
道具未配置 返回 46001,前端提示不可购买
金额篡改 total_fee 服务端重新计算校验

8. 配置变更

application.yml wechat: 块新增:

wechat:
  appkey: ${WECHAT_APPKEY:}                # 虚拟支付 AppKey(商户后台基本配置获取,现网/沙箱分开)
  virtual-pay-env: ${WECHAT_VIRTUAL_PAY_ENV:0}   # 0=现网 1=沙箱

同步更新 config/application.yml(模板)与 config/application-prod.yml(生产覆盖)。

9. 前端改动

9.1 虚拟支付调用封装(utils/api.js

新增 requestVirtualPayment(params) 封装,内部调 wx.requestVirtualPayment,与现有 uni.requestPayment 分支共存:

// 判断虚拟支付可用性(iOS 需 8.0.68+ / iOS 15+,Android 需基础库 2.19.2+)
wx.requestVirtualPayment({
  orderInfo: params.orderInfo,
  extInfo: params.extInfo,
  success(res) { /* 仅提示,最终以服务端订单状态为准 */ },
  fail(err) { /* 用户取消/失败,回订单页 */ }
})

9.2 页面改造

页面 改动
pages/assessment/*(测评购买/申请页) 下单 → 拿 VirtualPayParamsrequestVirtualPayment;支付结果页轮询订单状态
pages/membership/upgrade.vue 同样流程替换"直接开通成功"假逻辑
pages/membership/plans.vue(订阅) 同样流程
pages/shop/payment/payment.vue 不动(实物商品 v3 路径保留);若未来商城虚拟商品接入,在此加分支

10. 测试策略

  1. 签名单测: VirtualPayService 签名算法对照官方 Python 脚本固定输出断言(文档示例 c37809f27c...
  2. 沙箱联调: env=1 先验证完整链路(iOS 不支持沙箱,iOS 用例走真机现网)
  3. test-mode 兼容: wechat.test-mode: true 下 mock 直接标记已付,跳过虚拟支付(保留现有模拟逻辑)
  4. 推送幂等测试: 重复推送同一 OutTradeNo 只处理一次
  5. 回归: 现有实物商品 v3 支付(payment.vue / PaymentService)不受影响
  6. 编译验证: mvn clean compile;路由重复检查:grep -rn '@Mapping' ... | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u

11. 验收标准

  • 测评订单:创建 → 返回虚拟支付参数 → 前端拉起支付 → 发货推送 → 订单 paid → 测评预约确认、报告解锁
  • 会员订单:同样链路 → FamilyMembership 开通、用户升级 FAMILY
  • 订阅订单:同样链路 → MemberSubscription 开通、分配专属管家
  • 退款:Android 走 refund_order;iOS 问询应答端点可响应;退款推送更新订单状态
  • 道具未配置 → 46001 提示不可购买;停用道具同样拦截
  • 幂等:重复发货推送不重复解锁
  • 实物商品 v3 支付回归通过
  • virtual_goods_config 表迁移幂等可重复执行;schema.sql 已同步
  • Web 管理端道具映射 CRUD 可用
  • mvn clean compile 通过;无路由重复;无 Bean 命名冲突

12. 风险与依赖

说明
微信后台道具配置 上线前必须在商户后台建道具并发布至现网,映射表才能生效(开发不阻塞)
消息推送 URL 配置 需 mp.weixin.qq.com 配置服务器推送 URL;加密模式需联调确认
iOS 真机验证 沙箱不支持 iOS,需真机现网验证(最低 1 元、iOS 15+、微信 8.0.68+)
服务费率 iOS 端 17%(2026 年腾讯技术服务费限时减免 → 实际 12%),结算周期 Apple 月结 45-60 天
平台处罚 不接入虚拟支付的虚拟商品将被平台管控,本改造即合规响应

13. 未覆盖范围(后续迭代)

  • 商城虚拟商品(product_orders 非实物,如测评额度商品)→ 接入同一 VirtualPayService 链路
  • 任务模板包(package_orders
  • 代币充值体系(如需钱包余额功能再评估)