|
|
@@ -0,0 +1,247 @@
|
|
|
+# 小程序虚拟支付接入设计
|
|
|
+
|
|
|
+**日期:** 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_goods`、`query_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`
|
|
|
+
|
|
|
+```sql
|
|
|
+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 签名算法(官方参考脚本同款)
|
|
|
+
|
|
|
+```java
|
|
|
+// 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`
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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_goods`(`POST /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:` 块新增:
|
|
|
+
|
|
|
+```yaml
|
|
|
+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` 分支共存:
|
|
|
+
|
|
|
+```js
|
|
|
+// 判断虚拟支付可用性(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/*`(测评购买/申请页) | 下单 → 拿 `VirtualPayParams` → `requestVirtualPayment`;支付结果页轮询订单状态 |
|
|
|
+| `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`)
|
|
|
+- 代币充值体系(如需钱包余额功能再评估)
|