Переглянути джерело

docs: 虚拟支付实施计划 + PROJECT-OVERVIEW 索引更新

asus 1 місяць тому
батько
коміт
fce30f6658

+ 21 - 3
docs/superpowers/PROJECT-OVERVIEW.md

@@ -1,8 +1,8 @@
 # 浠艾福 项目全景 — 阶段性需求与设计汇总
 
-**文档版本:** v2.2
-**日期:** 2026-07-30
-**状态:** 已确认(v2.1 Phase 2-4 全栈完成)
+**文档版本:** v2.3
+**日期:** 2026-08-02
+**状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(设计中)
 **维护:** 所有需求变更需更新本文档
 
 ---
@@ -17,6 +17,7 @@
 | **Phase 3** | 2026-Q3 | 五维增强 | 富维度子维度拆分、文章发布系统、辈分重构 | ✅ 已完成 |
 | **Phase 4** | 2026-Q4 | 平台扩展 | 供应商体系、健康7维模型、成长档案 | ✅ 已完成 |
 | **Phase 5** | 2026-Q3 | 主动健康六阶模型 | 微启动+即时反馈+智能预警+身份重塑+家庭互动+社区扩散 | ✅ 已完成(P0-P3 全栈交付) |
+| **Phase 6** | 2026-08 | 虚拟支付合规化 | 小程序虚拟支付接入(测评/会员/订阅)、道具映射、发货/退款闭环 | 🟡 设计中 |
 
 ---
 
@@ -81,6 +82,20 @@
 
 ---
 
+### 2.5 小程序虚拟支付合规接入 ⭐ P0
+
+**阶段:** Phase 6(2026-08 启动)
+**状态:** 🟡 设计完成 + 实施计划就绪(`plans/2026-08-02-virtual-payment-implementation.md`)
+**背景:** 小程序内测评订单/会员/订阅此前为假支付或假定已支付,需接入官方「小程序虚拟支付」能力(`wx.requestVirtualPayment`),合规收款
+
+| 功能 | 状态 | 关联文档 |
+|------|:----:|---------|
+| 虚拟支付服务层(VirtualPayService + 签名/发货/退款) | 🟡 设计中 | `specs/2026-08-02-virtual-payment-design.md` |
+| 道具映射表 virtual_goods_config + Web 管理端 CRUD | 🟡 设计中 | `specs/2026-08-02-virtual-payment-design.md` |
+| 三类订单接入(测评/会员/订阅)| 🟡 设计中 | `specs/2026-08-02-virtual-payment-design.md` |
+
+---
+
 ## 三、维度专属功能
 
 ### 3.1 🌏 身(Body / 土)— 健康管理
@@ -313,6 +328,7 @@
 | `2026-07-10-certainty-ppt-design.md` | 🔄 已废弃(一次性PPT设计,无代码实现) | 确定性叙事PPT |
 | `2026-07-30-service-role-apply-design.md` | ✅ 已实施 | 服务角色统一申请(规划师/营养师/管家/服务商/文章管理员/活动提供商) |
 | `2026-07-31-compare-table-mobile-redesign.md` | 🟡 设计稿 | 四大饮水方案对比 — 移动端轮播式重设计 |
+| `2026-08-02-virtual-payment-design.md` | 🟡 设计中 | 小程序虚拟支付合规接入(测评/会员/订阅)|
 
 ### 实施计划(plans/)
 
@@ -367,6 +383,7 @@
 | `2026-07-24-cfc-web-responsive-implementation.md` | ✅ 已实施(commit 3ee7289e) | 管理后台手机自适应 |
 | `2026-07-28-membership-price-tier-update.md` | ✅ 全栈完成(FREE/FAMILY/PREMIUM 三等级,7 次提交) | 会员等级价格体系更新 |
 | `2026-07-31-family-members-redesign.md` | 🟡 计划中 | 家庭成员页面重新设计 |
+| `2026-08-02-virtual-payment-implementation.md` | 🟡 计划就绪 | 小程序虚拟支付接入实施计划(12 Tasks:建表/签名/推送/三链路改造/前端/管理端) |
 
 ### 计划与设计文档(specs/)
 
@@ -377,6 +394,7 @@
 | `2026-07-24-cfc-web-responsive-design.md` | ✅ 已实施 | 管理后台手机自适应设计 |
 | `2026-07-30-service-role-apply-design.md` | ✅ 已实施 | 服务角色统一申请设计 |
 | `2026-07-22-active-health-six-stage-design.md` | 🟡 当前 | 主动健康六阶模型改进总设计 |
+| `2026-08-02-virtual-payment-design.md` | 🟡 当前 | 小程序虚拟支付合规接入设计 |
 
 ---
 

+ 770 - 0
docs/superpowers/plans/2026-08-02-virtual-payment-implementation.md

@@ -0,0 +1,770 @@
+# 小程序虚拟支付接入实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement plan. Steps use checkbox (`- [ ]`) syntax.
+
+**Goal:** 将测评订单(`assessment_orders`,前缀 `A`)、会员订单(`payment_orders`,前缀 `ORD`)、订阅订单(`member_subscription_order`,前缀 `SUB`)三条链路从假支付/手动回调/假定已付,改为微信官方**小程序虚拟支付(道具直购模式)**:下单返回支付参数 → 前端 `wx.requestVirtualPayment` 拉起支付 → 发货推送(`xpay_goods_deliver_notify`)标记已付并解锁权益。
+
+**架构:** 后端新增虚拟支付基础设施(`VirtualPayService` / `VirtualPayController` / `virtual_goods_config` 映射表)→ 三条业务链路改造(下单返回 `VirtualPayParamsDTO`,保持 pending;发货推送按前缀路由解锁)→ 前端三个购买页接 `wx.requestVirtualPayment` → Web 管理端新增道具映射 CRUD。
+
+**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus (Java 8) / uni-app Vue 2 小程序 / Vue 2 + Element UI 管理端 / MySQL 8.0
+
+**参考:**
+- 设计文档 `docs/superpowers/specs/2026-08-02-virtual-payment-design.md`(已确认,实施唯一依据)
+- 官方文档 https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html
+- 计划格式参考 `docs/superpowers/plans/2026-07-28-membership-price-tier-update.md`
+
+**外部依赖(不阻塞开发):**
+- 微信商户后台道具(ProductId)尚未配置 → 映射表先建,上线前配置
+- mp.weixin.qq.com 服务器推送 URL 需指向 `/api/internal/virtual-pay/notify`(联调时配置,默认明文模式)
+
+---
+
+## 关键约束(AGENTS.md 强制)
+
+- 接口统一 `@PostMapping`;响应统一 `Result<T>`;DI 用 `@Resource`
+- **推送回调端点返回 `{"ErrCode":0,"ErrMsg":"success"}` 格式 Map,不用 `Result`**(微信协议要求)
+- 新增类前 grep 检查 Bean 命名冲突;新增路由后跑路由重复检查
+- 数据库迁移唯一入口 `DatabaseInitializer.runMigrations()`,迁移后必须同步 `schema.sql`
+- HMAC 签名用 JDK 原生 `javax.crypto.Mac`(项目无 commons-codec 依赖,零新依赖)
+- 前端禁止可选链 `?.`、禁止 CSS Grid、禁止 `:key` 表达式
+
+## 文件结构
+
+### 后端(cfc-backend)
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `config/DatabaseInitializer.java` | 修改 | 迁移 121:建 `virtual_goods_config` 表(幂等) |
+| `src/main/resources/schema.sql` | 修改 | 同步追加 `virtual_goods_config` CREATE TABLE |
+| `src/main/resources/application.yml` | 修改 | wechat 块加 `appkey` + `virtual-pay-env` |
+| `config/application.yml` | 修改 | 模板同步(占位符) |
+| `config/application-prod.yml` | 修改 | 生产覆盖同步 |
+| `entity/VirtualGoodsConfig.java` | 新增 | 道具映射实体(`@Data @TableName @TableId(AUTO)`) |
+| `mapper/VirtualGoodsConfigMapper.java` | 新增 | `@Mapper extends BaseMapper<VirtualGoodsConfig>` |
+| `service/VirtualGoodsConfigService.java` | 新增 | 映射查询(启用态)+ 管理端 CRUD |
+| `controller/admin/VirtualGoodsConfigAdminController.java` | 新增 | 管理端 CRUD 接口(`/api/admin/virtual-goods/*`) |
+| `service/VirtualPayService.java` | 新增 | paySig 签名、下单参数生成、发货/退款推送处理、query_order 兜底 |
+| `controller/VirtualPayController.java` | 新增 | `POST /api/internal/virtual-pay/notify` + `ios-refund-query` |
+| `dto/VirtualPayParamsDTO.java` | 新增 | 支付参数返回体(设计文档 §5.2) |
+| `task/VirtualPayTimeoutTask.java` | 新增 | `@Scheduled`:超时 pending 关单 + query_order 兜底 |
+| `service/AssessmentOrderService.java` | 修改 | `payOrder()` 改返回支付参数;抽出预约确认公共方法;补 appointmentId 关联 |
+| `controller/DanAssessmentController.java` | 修改 | `/order/pay` 返回 `Result<VirtualPayParamsDTO>` |
+| `service/MembershipService.java` | 修改 | 下单后生成支付参数;修正 L382 硬编码 `"FAMILY"` |
+| `controller/MembershipController.java` | 修改 | 下单接口返回支付参数;保留 `/notify` 作 testMode/兜底 |
+| `service/MemberSubscriptionService.java` | 修改 | 拆分 `subscribe()`:建 pending 单 + 提取 `activateSubscription()` |
+| `controller/subscription/SubscriptionController.java` | 修改 | `/create` 返回支付参数;testMode 分支保留 |
+
+### 前端(cfc-frontend)
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `utils/api.js` | 修改 | 新增 `createVirtualPayOrder` 封装 + `getVirtualPaymentEnv()` helper |
+| `pages/assessment/purchase.vue`(及测评购买入口) | 修改 | 接虚拟支付流程,修复自跳转 bug(L270-272) |
+| `pages/membership/upgrade.vue` | 修改 | `doUpgrade`/`doRenew`(L336-344/L358-366)假成功改虚拟支付 |
+| `pages/membership/plans.vue` | 修改 | 订阅购买接虚拟支付(入口 `purchasePlan` L128 不动) |
+| `pages/shop/payment/payment.vue` | **不动** | 实物商品 v3 路径保留 |
+
+### 管理端(cfc-web)
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `src/views/admin/VirtualGoodsConfig.vue` | 新增 | 道具映射 CRUD(照抄 `KnowledgeTag.vue`) |
+| `src/api/virtualGoods.js` | 新增 | API 模块(`import request` + 命名导出) |
+| `src/router/index.js` | 修改 | children 加路由(参照 knowledge-tags L329-333,`perm` 须在 permissions.js 存在) |
+| `src/views/Layout.vue` | 修改 | `menuItems`(L165+)加菜单项(系统配置分组 L268-275) |
+
+---
+
+## Task 1: 建表迁移 `virtual_goods_config`(迁移 121)
+
+### 现状
+- `DatabaseInitializer.java` 的 `runMigrations()` 起始于 L112,最新迁移 `// 迁移120` 在 **L7277**(文件末尾区域)
+- `schema.sql` 共 3686 行,最新表 `recommendation_logs`(L3669),含 UNIQUE KEY 的参照表 `butler_assignments`(L2432)
+
+- [ ] **Step 1: DatabaseInitializer 追加迁移 121**
+
+在 `// 迁移120` 块(L7277-7283)之后追加(照抄项目建表范式:`try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS ...") } catch (Exception e) {}` 幂等):
+
+```java
+// 迁移121: 创建小程序虚拟支付道具映射表
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS 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)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小程序虚拟支付道具映射'");
+    log.info("已创建virtual_goods_config表");
+} catch (Exception e) {
+    log.warn("创建virtual_goods_config表失败: {}", e.getMessage());
+}
+```
+
+- [ ] **Step 2: 同步 schema.sql**
+
+在 `schema.sql` 末尾(L3686 后)追加同构 `CREATE TABLE IF NOT EXISTS virtual_goods_config (...)`(同上 SQL,含 UNIQUE KEY + ENGINE 子句)。
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql
+git commit -m "feat: 迁移121 创建virtual_goods_config道具映射表
+测评/会员/订阅虚拟支付道具映射,UNIQUE(goods_type,biz_key),幂等"
+```
+
+---
+
+## Task 2: 配置 `wechat.appkey` + `virtual-pay-env`
+
+### 现状
+- 主配置 `application.yml` wechat 块 L67-83:有 `appid/secret/mch-id/mch-key/api-v3-key/mch-serial-no/private-key-path/notify-url/test-mode` 等,**无 `appkey`、无 `virtual-pay-env`**
+- `config/application.yml` wechat 块 L43-56(模板占位符)、`config/application-prod.yml` wechat 块 L31-32(仅 `test-mode: false`)
+- 环境变量插值风格:`${WECHAT_APPKEY:}`(带默认空值)
+
+- [ ] **Step 1: 主配置 `src/main/resources/application.yml` wechat 块(L78 `test-mode` 后)追加**
+
+```yaml
+  appkey: ${WECHAT_APPKEY:}                # 虚拟支付 AppKey(商户后台基本配置获取,现网/沙箱分开)
+  virtual-pay-env: ${WECHAT_VIRTUAL_PAY_ENV:0}   # 0=现网 1=沙箱
+```
+
+- [ ] **Step 2: `config/application.yml` 同步(模板占位符)**
+
+同样位置追加 `appkey: ${WECHAT_APPKEY:}` 与 `virtual-pay-env: ${WECHAT_VIRTUAL_PAY_ENV:0}`。
+
+- [ ] **Step 3: `config/application-prod.yml` 同步**
+
+wechat 块追加 `appkey: ${WECHAT_APPKEY:}`(生产用环境变量注入真实值)。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/resources/application.yml cfc-backend/config/application.yml cfc-backend/config/application-prod.yml
+git commit -m "feat: 新增wechat.appkey与virtual-pay-env配置
+小程序虚拟支付paySig签名密钥与环境(0现网/1沙箱)"
+```
+
+---
+
+## Task 3: `VirtualGoodsConfig` 实体/Mapper/Service + 管理端 CRUD 接口
+
+### 现状(照抄范式)
+- 实体范式:`entity/SchemaVersion.java` —— `@Data @TableName("表名") @TableId(type = IdType.AUTO)`,不加列注解(全局 `map-underscore-to-camel-case: true`)
+- Mapper 范式:`mapper/SchemaVersionMapper.java` —— `@Mapper interface XxxMapper extends BaseMapper<Xxx>`
+- 管理端控制器范式:`controller/admin/` 子包,`@RequestAttribute("role")` 手动校验 admin
+
+- [ ] **Step 1: 实体 `entity/VirtualGoodsConfig.java`**
+
+```java
+@Data
+@TableName("virtual_goods_config")
+public class VirtualGoodsConfig {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String goodsType;    // ASSESSMENT_PACKAGE / MEMBERSHIP / SUBSCRIPTION
+    private String bizKey;       // 业务SKU标识
+    private String productId;    // 微信商户后台道具ID
+    private String goodsName;
+    private Integer status;      // 1=启用 0=停用
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}
+```
+
+- [ ] **Step 2: Mapper `mapper/VirtualGoodsConfigMapper.java`**
+
+```java
+@Mapper
+public interface VirtualGoodsConfigMapper extends BaseMapper<VirtualGoodsConfig> {
+}
+```
+
+- [ ] **Step 3: Service `service/VirtualGoodsConfigService.java`**
+
+方法集(继承 `ServiceImpl<VirtualGoodsConfigMapper, VirtualGoodsConfig>`):
+- `VirtualGoodsConfig getEnabled(String goodsType, String bizKey)` —— `status=1` + 按 `(goods_type, biz_key)` 查;查不到返回 null(调用方转 46001)
+- `List<VirtualGoodsConfig> list(String goodsType)` —— 管理端列表(可按类型筛选)
+- `save/update/delete` —— 管理端 CRUD(删除建议软删置 status=0 或直接删,简单配置类参照 `admin.js` 中 ZodiacConfigs 先例)
+
+- [ ] **Step 4: 管理端控制器 `controller/admin/VirtualGoodsConfigAdminController.java`**
+
+`@RequestMapping("/api/admin/virtual-goods")`,全 `@PostMapping`:
+- `POST /list`(body: goodsType 可选)→ `Result<List<VirtualGoodsConfig>>`
+- `POST /save`(body: 实体字段,id 空=新增/非空=更新)→ `Result<Boolean>`
+- `POST /delete`(body: id)→ `Result<Boolean>`
+- 控制器内 `@RequestAttribute("role")` 校验 `"admin"`(照现有 admin 控制器)
+
+- [ ] **Step 5: 编译验证 + 冲突检查**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+grep -rn "class VirtualGoodsConfig" cfc-backend/src --include=*.java   # 确认无重名 Bean
+```
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/VirtualGoodsConfig.java cfc-backend/src/main/java/com/etotem/cfc/mapper/VirtualGoodsConfigMapper.java cfc-backend/src/main/java/com/etotem/cfc/service/VirtualGoodsConfigService.java cfc-backend/src/main/java/com/etotem/cfc/controller/admin/VirtualGoodsConfigAdminController.java
+git commit -m "feat: 虚拟支付道具映射 实体/Mapper/Service/管理端CRUD接口"
+```
+
+---
+
+## Task 4: `VirtualPayService`(签名 + 参数生成 + 推送处理)+ `VirtualPayParamsDTO`
+
+### 签名要点(官方规范 + 踩坑验证)
+- `paySig = to_hex(hmac_sha256(appKey, uri + "&" + signData))`,`uri` 固定 `"requestVirtualPayment"`
+- `signData` = `orderInfo` + `extInfo` 按官方规则拼接;**signData 中不得包含 `platform` 字段**(否则 -15005)
+- 道具直购需**两个签名**:`paySig`(订单参数签名,后端用 appKey 算)+ `signature`(用户身份签名,用 sessionKey 算 HMAC-SHA256,**直接字符串编码,不先 base64 解码 sessionKey**)
+- 错误码:`1001` 参数错误、`-15005` 签名无效
+- `mode` 固定 `"short_series_goods"`;`offerId` 传微信平台道具 ID(即 `product_id`,非业务 productId);`env` 必须与 offerId 道具版本对应
+- ⚠️ 以上 `signData` 精确拼接串在实现时**对照官方 Python 签名脚本做断言单测**(文档示例输出 `c37809f27c...`),联调再以沙箱实测为准
+
+- [ ] **Step 1: DTO `dto/VirtualPayParamsDTO.java`**
+
+```java
+@Data
+public class VirtualPayParamsDTO {
+    private Map<String, Object> orderInfo;   // {mch_id, appid, out_trade_no, total_fee, product_info, attach, mode, offerId, ...}
+    private Map<String, Object> extInfo;     // 空对象即可
+    private String sign;                     // paySig(appKey 签名)
+    private String signature;                // 用户身份签名(sessionKey 签名,道具直购必需)
+    private Integer env;                     // 0=现网 1=沙箱(配置 wechat.virtual-pay-env)
+    private String signType;                 // "HMAC-SHA256"
+}
+```
+
+- [ ] **Step 2: HMAC-SHA256 工具(JDK 原生,零新依赖)**
+
+在 `VirtualPayService` 内(或 `common/` 新增 `HmacSignUtil`):
+
+```java
+private static String hmacSha256Hex(String key, String data) throws Exception {
+    Mac mac = Mac.getInstance("HmacSHA256");
+    mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
+    byte[] raw = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+    StringBuilder sb = new StringBuilder();
+    for (byte b : raw) sb.append(String.format("%02x", b));
+    return sb.toString();
+}
+```
+
+- [ ] **Step 3: `generatePayParams()` 核心方法**
+
+```java
+public VirtualPayParamsDTO generatePayParams(VirtualGoodsConfig config, String outTradeNo,
+        Integer totalFee, String productInfo, String attach, String sessionKey) {
+    // 1. orderInfo 组装(mch_id/appid 从 wechat 配置取;total_fee 服务端计算传入;mode="short_series_goods";offerId=config.productId)
+    // 2. signData 按官方规则拼接(不含 platform)
+    // 3. paySig = hmacSha256Hex(appKey, "requestVirtualPayment&" + signData)
+    // 4. signature = hmacSha256Hex(sessionKey, <官方定义的拼接串>)  // 直接字符串编码
+    // 5. 组装 DTO 返回
+}
+```
+
+`appid` 复用现有 `wechat.appid`(`wx5ba8038ef16fb245`);`mch_id` 为**虚拟支付商户号**(非现有 v3 商户号,从配置或联调确认);`sessionKey` 从用户登录态取(`wx.login → jscode2session` 时后端已保存,需确认存储位置——UserService 或 Redis;若未保存需在登录流程补充)。
+
+- [ ] **Step 4: 发货推送处理 `handleDeliverNotify()`**
+
+```java
+public Map<String, Object> handleDeliverNotify(Map<String, Object> payload) {
+    // 1. 校验 payload 格式(OutTradeNo / TransactionId / 可选金额)
+    // 2. 按 OutTradeNo 前缀路由:A→assessment  ORD→membership  SUB→subscription
+    //    (参照 PaymentService.handleWechatNotify() L306-348 前缀路由模式)
+    // 3. 分别调对应 Service 的"标记已付+解锁"方法(Task 6/7/8 实现),幂等
+    // 4. 返回 {"ErrCode":0,"ErrMsg":"success"}
+}
+```
+
+- [ ] **Step 5: 退款推送处理 `handleRefundNotify()`**
+
+按前缀路由 → 调对应订单 Service 标记 `refunded`(`AssessmentOrderService.refundOrder` 思路 / 订阅逻辑退款)。返回 `{"ErrCode":0,"ErrMsg":"success"}`。
+
+- [ ] **Step 6: 签名单测 `VirtualPayServiceTest`**
+
+对照官方 Python 脚本的固定输入输出做断言(`tests/` 分层策略下放 unit 层,参照 `tests/AGENTS.md`):
+
+```java
+@Test
+public void paySig_matchesOfficialExample() {
+    // 用官方文档示例的 appKey/signData 断言 hmacSha256Hex 输出 == 官方值
+}
+```
+
+- [ ] **Step 7: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/VirtualPayParamsDTO.java cfc-backend/src/main/java/com/etotem/cfc/service/VirtualPayService.java
+git commit -m "feat: VirtualPayService paySig签名/参数生成/发货退款推送处理
+JDK原生HMAC-SHA256零新依赖,双签名(paySig+signature),前缀路由"
+```
+
+---
+
+## Task 5: `VirtualPayController`(推送接收端点)
+
+### 现状
+- `/api/internal/` 已在 `JwtInterceptor.PUBLIC_PATHS`(L39)**前缀公开**,无需改拦截器
+- `FamilyAccessInterceptor` 默认 logging 模式、匿名请求放行,不会拦截
+- 回调端点范式:`ProductOrderController.notify()` L120-155(`getReader()` 读原始 body)
+- 注意:**返回 `Map`(微信协议 `{"ErrCode":0,"ErrMsg":"success"}`),不用 `Result`**
+
+- [ ] **Step 1: `controller/VirtualPayController.java`**
+
+```java
+@RestController
+@RequestMapping("/api/internal/virtual-pay")
+public class VirtualPayController {
+
+    @Resource
+    private VirtualPayService virtualPayService;
+
+    /** 微信虚拟支付发货/退款等消息推送 */
+    @PostMapping("/notify")
+    public Map<String, Object> notify(HttpServletRequest request) {
+        // 读原始 body(getReader()),解析 JSON → virtualPayService.handleDeliverNotify / handleRefundNotify
+        // 解析失败/未知事件 → 记日志 + 返回 {"ErrCode":0,"ErrMsg":"success"}(微信重试最多15次,避免死循环)
+    }
+
+    /** iOS App Store 退款问询应答(3秒内应答;3次未应答默认"不确定") */
+    @PostMapping("/ios-refund-query")
+    public Map<String, Object> iosRefundQuery(HttpServletRequest request) {
+        // 校验 payload → 查订阅状态返回 {"IsAgree": true/false, "Reason": "..."}(按官方问询应答协议)
+        // 未接入 Apple 服务端通知时先返回协议规定的最低响应
+    }
+}
+```
+
+- [ ] **Step 2: 编译验证 + 路由重复检查**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u
+```
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/VirtualPayController.java
+git commit -m "feat: VirtualPayController 接收虚拟支付消息推送
+/api/internal/virtual-pay/notify(公开) + ios-refund-query,返回微信ErrCode协议"
+```
+
+---
+
+## Task 6: 测评订单链路改造(后端)
+
+### 现状(勘察结论)
+- `AssessmentOrderService.payOrder()`(L149-176):**假支付**——直接置 paid + 内联预约确认(L161-173,`appointment.status 0→1`),不设 transactionId、不结佣金
+- `paySuccess()`(L62-78):**已存在零调用**——置 paid + transactionId + `commissionService.settle`,**无预约确认**
+- `createOrder()`(L35-56):订单号前缀 `A`(L58-60 `generateOrderNo()`);金额 `totalPrice/discountAmount` **由前端传参,后端不校验**(安全缺口,需服务端重算)
+- 链路断点:`AssessmentOrder.appointmentId` 字段存在(L29)但 `createOrder()` **从不赋值**,预约确认依赖它
+- 服务端计价参照:`PackagePurchaseService.getPackagePrice()`(L50-121)——`task_template_packages` 基础价 + `guide_packages` 指导师定价 + 邀请码关联价
+- Controller:`DanAssessmentController` `/order/pay`(L273-279)现返回 `Result<String>`,body 为 `Map{orderNo, payType}`
+- 报告解锁:`TeacherService.createAssessmentResult()`(L159-205)L196-202 按 appointmentId 查订单,`paid → results_ready`(`markResultsReady()` L121-129);家长 `getPendingOrdersByFamily()`(L141-147)按 `status IN (paid, results_ready)` 过滤可见
+
+- [ ] **Step 1: 抽出预约确认公共方法**
+
+在 `AssessmentAppointmentService`(或 `AssessmentOrderService`)新增 `confirmAppointmentByOrderNo(String orderNo)`:按 orderNo 查订单 → `getByAppointmentId` 语义查预约 → `status 0→1`(复用 `confirmAppointment()` L54-63 的校验逻辑或 payOrder L161-173 内联逻辑),失败仅 warn 不阻断。
+
+- [ ] **Step 2: `payOrder()` 改为生成支付参数(保持 pending)**
+
+```java
+public VirtualPayParamsDTO payOrder(String orderNo, String payType, String sessionKey) {
+    // 1. 查订单 + 校验 pending(原逻辑保留)
+    // 2. 查 virtual_goods_config:goodsType=ASSESSMENT_PACKAGE, bizKey=订单packageId
+    //    → 查不到/停用 → 抛 BusinessException(46001, "该商品暂未开放购买")
+    // 3. 服务端重算金额:total_fee = PackagePurchaseService.getPackagePrice(packageId, guideId, inviteCode)(服务端计价,不信任前端)
+    //    → 校验 total_fee >= 100(1元)
+    // 4. 组装 attach = "assessment:" + packageId;orderInfo.product_info = packageName
+    // 5. 调 VirtualPayService.generatePayParams(...) 返回 DTO
+    // 6. 订单保持 pending(不置 paid!)
+}
+```
+
+- [ ] **Step 3: 建立 appointmentId 关联**
+
+在 `/order/create` 或 `/order/pay` 请求中接收前端传的 `appointmentId` 并回填订单(修复链路断点);若无则发货推送时按 familyId+childId 最近待确认预约关联(二选一,优先前端传参)。
+
+- [ ] **Step 4: 发货推送解锁(复用 `paySuccess()` 并补预约确认)**
+
+`VirtualPayService.handleDeliverNotify()` 的 `A` 前缀分支调用(建议在 `AssessmentOrderService` 新增 `handleVirtualPaySuccess(orderNo, transactionId)`):
+1. 幂等:已 paid 直接返回 true
+2. `paySuccess(orderNo, transactionId, "virtual")` 基础上:置 `payType="virtual"`(沿用 payType 字段)、写 `transactionId`
+3. 调 Step 1 的预约确认公共方法
+4. 佣金结算(`paySuccess` 已含 `commissionService.settle`)
+
+- [ ] **Step 5: Controller `/order/pay` 返回类型改造**
+
+`DanAssessmentController` L273-279:返回 `Result<VirtualPayParamsDTO>`(body 增加 `sessionKey` 来源说明:后端从登录态取,不由前端传)。testMode 分支:`wechat.test-mode=true` 时保持原"直接标记已付"逻辑(mock)。
+
+- [ ] **Step 6: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 7: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java cfc-backend/src/main/java/com/etotem/cfc/controller/DanAssessmentController.java cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java
+git commit -m "feat: 测评订单改虚拟支付
+payOrder返回VirtualPayParams保持pending,服务端计价,发货推送markPaid+预约确认+佣金"
+```
+
+---
+
+## Task 7: 会员订单链路改造(后端)
+
+### 现状(勘察结论)
+- `MembershipService.createOrder()`(L226-320):非 trial 单**已是 pending**(L300),订单号 `"ORD"+timestamp`(L557-559),金额 `sys_config`/`membership_levels` 表 + 兜底常量(L236-274,单位分)
+- `processPaymentCallback(orderNo, transactionId, payMethod)`(L325-411):幂等(L341-343),标记已付 → 优惠券核销 → 插 `family_memberships` → **L382 硬编码升级为 `"FAMILY"`**(PREMIUM 单也会升 FAMILY,属既有 bug)→ `member_upgrade_record` → 佣金
+- **发货推送直接复用 `processPaymentCallback()` 即可完成全部开通**,改造量最小
+- Controller:`/api/membership/orders`(L199-215)返回 `Result<PaymentOrderDTO>`;手动回调 `/api/membership/notify`(L258-266)保留作 testMode/兜底
+- ⚠️ `PaymentOrder` 实体**无 `userId` 字段**(只有 familyId);无 product 字段,attach 走映射表反查
+
+- [ ] **Step 1: 下单接口生成支付参数**
+
+`MembershipService` 新增 `payOrder(PaymentOrder order, String sessionKey)`(或改造 `createOrder` 尾部):
+1. 查 `virtual_goods_config`:goodsType=MEMBERSHIP, bizKey=order.levelCode → 查不到抛 46001
+2. `total_fee = order.amount`(服务端已算,无需重算;校验 ≥100)
+3. attach = `"membership:" + levelCode`;product_info = 等级展示名
+4. 调 `VirtualPayService.generatePayParams(...)`
+5. 订单保持 pending(现状已是)
+
+- [ ] **Step 2: 修正 L382 硬编码 `"FAMILY"`**
+
+`processPaymentCallback` L381-385:`order.getMemberLevel()` 或 `order.getLevelCode()` 替换硬编码 `"FAMILY"`(顺带修复 PREMIUM 单升级错等级的既有 bug;注意 `member_upgrade_record.fromLevel/toLevel` 同步)。
+
+- [ ] **Step 3: 发货推送解锁**
+
+`VirtualPayService.handleDeliverNotify()` 的 `ORD` 分支 → 直接调 `membershipService.processPaymentCallback(orderNo, transactionId, "virtual")`(幂等已内置)。
+
+- [ ] **Step 4: Controller `/api/membership/orders` 返回 `VirtualPayParamsDTO`**
+
+`MembershipController` L199-215:改为 `Result<Object>`(兼容返回 `VirtualPayParamsDTO`);保留 `/api/membership/notify`(L258-266)作 testMode/手动兜底。trial 单不变(直接 activateTrialMembership,无支付)。
+
+- [ ] **Step 5: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java
+git commit -m "feat: 会员订单改虚拟支付
+createOrder后返回VirtualPayParams,发货推送复用processPaymentCallback,修正等级硬编码FAMILY"
+```
+
+---
+
+## Task 8: 订阅订单链路改造(后端)
+
+### 现状(勘察结论)
+- `MemberSubscriptionService.subscribe()`(L88-153):**一体方法**——算过期时间 → 置旧订阅 expired → 插 `member_subscription` → **插订单直接写 `"paid"`**(L114-124,假定已支付)→ 分配管家 `butlerService.assignMemberToButler`(L130,L290-318)→ 受益日志(L133-135)→ 佣金 + 分享收益(L137-150)
+- 金额 L1=36500 / L2=131400 **硬编码在 SubscriptionController**(L75-93 plans、L105-111 create);`subscribe()` 的 amount 由 Controller 传入
+- testMode 分支在 **Controller**(`/create` L115-123),非 testMode 走 v3 `PaymentService.createWechatPrepay`,失败回退直接 subscribe(L138)
+- `MemberSubscriptionOrder`:status 字段 schema 默认 `pending`(支持 pending/paid/cancelled/refunded),**实体/表结构无需改**
+- 订阅**无回调端点**,需新建(Task 5 已建 `/api/internal/virtual-pay/notify`)
+
+- [ ] **Step 1: 拆分 `subscribe()`**
+
+- 新方法 `MemberSubscription createPendingOrder(Long familyId, String level, Integer amount, String paymentType)`:只建 pending 订单 + 返回(含 orderNo),**不开通权益**
+- 提取开通段为新方法 `activateSubscription(Long familyId, String level, Integer amount, String paymentType, String transactionId, String orderNo)`:内容 = 原 subscribe() L102-150(置旧订阅 expired → 插 subscription → 订单置 paid + transactionId → 分配管家 → 受益日志 → 佣金分享收益)
+- `subscribe()` 保留为 `createPendingOrder` + `activateSubscription` 的组合(供 testMode/兼容旧调用)
+
+- [ ] **Step 2: Controller `/api/subscription/create` 改返回支付参数**
+
+`SubscriptionController` L98-143:
+- testMode=true:保持原逻辑(直接 `subscribe()` 标已付,返回 `{orderNo, amount, testMode:true}`)
+- testMode=false:`createPendingOrder` 建 pending 单 → 查 `virtual_goods_config`(goodsType=SUBSCRIPTION, bizKey=level)→ 校验金额 ≥100 → `VirtualPayService.generatePayParams(...)` → 返回 `Result<VirtualPayParamsDTO>`;attach=`"subscription:"+level`。**不再走 v3 createWechatPrepay,不再失败回退直接开通**
+- `/api/subscription/subscribe`(L148-157,完全无支付)→ 保留但标注仅 testMode 使用(或加 testMode 门禁)
+
+- [ ] **Step 3: 发货推送解锁**
+
+`VirtualPayService.handleDeliverNotify()` 的 `SUB` 分支 → `memberSubscriptionService.activateSubscription(familyId, level, amount, "virtual", transactionId, orderNo)`(familyId/level/amount 从订单查回)。
+
+- [ ] **Step 4: 退款推送(逻辑退款自动处理)**
+
+`handleRefundNotify()` 的 `SUB` 分支 → 置对应 `member_subscription` 过期 + 订单 `refunded`(iOS 用户 App Store 退款场景)。
+
+- [ ] **Step 5: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java
+git commit -m "feat: 订阅订单改虚拟支付
+subscribe拆分为createPendingOrder+activateSubscription,下单返回VirtualPayParams"
+```
+
+---
+
+## Task 9: 定时任务兜底(query_order + 超时关单)
+
+### 现状(范式)
+- `@EnableScheduling` 在 `CfcApplication.java` L12;任务类放 `com.etotem.cfc.task` 包,`@Component` + `@Scheduled`
+- 参照:`OrderTimeoutCancelTask`(fixedRate=60000 + LambdaQueryWrapper + `@Value("${...:30}")`)
+
+- [ ] **Step 1: 新建 `task/VirtualPayTimeoutTask.java`**
+
+```java
+@Component
+public class VirtualPayTimeoutTask {
+    // @Scheduled(fixedRate = 60000) 每分钟
+    // 1. 扫描三张订单表 status=pending 且 created_at < now-30min(配置 virtual-pay.payment-timeout-minutes:30)
+    // 2. 调 VirtualPayService.queryOrder(outTradeNo) 确认实际支付状态(兜底 success 回调丢失)
+    //    → 已支付则触发对应解锁逻辑(幂等)
+    //    → 未支付则置 status=cancelled(测评/会员),订阅置 cancelled
+    // 3. query_order 调微信接口需 appKey 签名,参照 generatePayParams 同款签名逻辑
+}
+```
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/task/VirtualPayTimeoutTask.java
+git commit -m "feat: 虚拟支付超时兜底任务
+pending订单query_order确认+超时关单,防success回调丢失"
+```
+
+---
+
+## Task 10: 前端改造(utils/api.js + 三个购买页)
+
+### 现状(勘察结论)
+- `utils/api.js`:`request()` 基座 L25-76;`payAssessmentOrder(orderNo, payType)` L835-837(**零页面调用,仅测试**);`createOrder(levelCode, paymentType, period)` L304-310(upgrade.vue 用);`getOrderStatus(orderNo)` L790-792(零调用,可复用于有限轮询)
+- `config.js` L24-45:`uni.getAccountInfoSync().miniProgram.envVersion` 映射 develop/trial/release → API 地址
+- `upgrade.vue`:`doUpgrade()` L336-344 / `doRenew()` L358-366 —— `createOrder → showToast('开通成功') → loadData` 假成功
+- `plans.vue` `purchasePlan` L128-130:跳 upgrade 页
+- `purchase.vue` L256 调 `createPackageOrder`(package 订单,非 assessment 订单);L270-272 **跳转到自身**的自跳转 bug;测评订单前端入口缺失(`payAssessmentOrder` 无调用者)
+- 实物 `payment.vue` L237-267:`uni.requestPayment` 成功后调后端确认接口 → toast → redirectTo(**无轮询**,模式可沿用)
+
+- [ ] **Step 1: `utils/api.js` 新增封装**
+
+```js
+// 创建虚拟支付订单(返回 VirtualPayParamsDTO)
+export const createVirtualPayOrder = (orderNo, goodsType) => {
+  return request('/api/payment/virtual/create', 'POST', { orderNo, goodsType })
+}
+
+// 虚拟支付环境映射(复用 config.js 同一 envVersion 来源)
+export const getVirtualPaymentEnv = () => {
+  try {
+    var info = uni.getAccountInfoSync()
+    var env = info.miniProgram && info.miniProgram.envVersion
+    if (env === 'develop') return 2   // 测试环境
+    if (env === 'trial') return 1     // 沙箱
+    return 0                          // release 现网
+  } catch (e) {
+    return 0
+  }
+}
+
+// 虚拟支付拉起(封装 wx.requestVirtualPayment)
+export const requestVirtualPayment = (params, callbacks) => {
+  wx.requestVirtualPayment({
+    orderInfo: params.orderInfo,
+    extInfo: params.extInfo,
+    signature: params.signature,   // 用户身份签名(后端返回)
+    mode: 'short_series_goods',
+    env: params.env !== undefined ? params.env : getVirtualPaymentEnv(),
+    success: function(res) { if (callbacks && callbacks.success) callbacks.success(res) },
+    fail: function(err) { if (callbacks && callbacks.fail) callbacks.fail(err) }
+  })
+}
+```
+
+> 说明:`sign`(paySig)由后端在 `orderInfo` 构造时按官方规则使用(官方 `wx.requestVirtualPayment` 参数以联调实测为准,`paySig` 的放置位置在联调时对照官方工具确认——`signData` 传参与支付参数结构对齐)。页面不直接拼签名。
+
+- [ ] **Step 2: `pages/membership/upgrade.vue` 改造 `doUpgrade`/`doRenew`**
+
+```js
+doUpgrade: function() {
+  var self = this
+  createOrder(self.selectedLevel, 'pay', self.selectedPeriod).then(function(res) {
+    var orderNo = res.data && res.data.orderNo
+    if (!orderNo) { uni.showToast({ title: '下单失败', icon: 'none' }); return }
+    return createVirtualPayOrder(orderNo, 'MEMBERSHIP')   // 后端返回 VirtualPayParamsDTO
+  }).then(function(params) {
+    requestVirtualPayment(params, {
+      success: function() {
+        uni.showToast({ title: '支付成功,开通中…', icon: 'none' })
+        self.pollOrderStatus()   // 有限轮询订单状态(复用 getOrderStatus,最多 N 次)
+      },
+      fail: function() { uni.showToast({ title: '支付未完成', icon: 'none' }) }
+    })
+  }).catch(function(e) {
+    if (e.code === 46001) uni.showToast({ title: '该商品暂未开放购买', icon: 'none' })
+    else uni.showToast({ title: e.message || '开通失败', icon: 'none' })
+  })
+},
+```
+
+`doRenew` 同构替换。新增 `pollOrderStatus()`:`getOrderStatus(orderNo)` 每 2s 查一次,最多 5 次,`paid` 则 `loadData()` + toast"开通成功"。
+
+- [ ] **Step 3: `pages/membership/plans.vue` 订阅流程**
+
+订阅档位购买(L1/L2)接 `/api/subscription/create` → `createVirtualPayOrder(orderNo, 'SUBSCRIPTION')` → `requestVirtualPayment`;testMode 响应 `testMode:true` 时沿用旧直接成功逻辑。
+
+- [ ] **Step 4: 测评购买入口**
+
+- 确认并接通 `payAssessmentOrder`(api.js L835)调用链:下单(`/api/dan-assessment/order/create`)→ 支付(`/order/pay` 返回 `VirtualPayParamsDTO`,直接前端拉起,无需再走 `createVirtualPayOrder`——后端接口已内联生成支付参数)
+- 修复 `purchase.vue` L270-272 自跳转 bug(改为创建订单后进入支付拉起)
+- 前端 `totalPrice` 计算(purchase.vue L198-220)仅作展示,实际金额以服务端重算为准
+- 支付成功后:`requestVirtualPayment` success 提示"支付成功" + 轮询订单状态(复用 `getOrderStatus`)→ 跳转测评预约页
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/pages/membership/upgrade.vue cfc-frontend/pages/membership/plans.vue cfc-frontend/pages/assessment/purchase.vue
+git commit -m "feat: 前端接入小程序虚拟支付
+api.js新增createVirtualPayOrder/requestVirtualPayment/getVirtualPaymentEnv,三个购买页假支付改真支付"
+```
+
+---
+
+## Task 11: Web 管理端道具映射 CRUD
+
+### 现状(勘察结论)
+- 标准 CRUD 模板:`cfc-web/src/views/admin/KnowledgeTag.vue`(184 行,表格+搜索+弹窗+增删改三件套 `dialogVisible/isEdit/form`)
+- 路由注册:`router/index.js` children(参照 L329-333 knowledge-tags 格式),`meta.perm` 必须存在于 `utils/permissions.js`
+- 菜单注册:`Layout.vue` `menuItems`(L165+),系统配置分组 L268-275
+- API 模块:新文件 `src/api/virtualGoods.js`(`import request from '@/utils/request'` + 命名导出,参照 `api/dimension.js`)
+
+- [ ] **Step 1: `src/api/virtualGoods.js`**
+
+```js
+import request from '@/utils/request'
+
+export function getVirtualGoodsList(data) {
+  return request({ url: '/api/admin/virtual-goods/list', method: 'post', data })
+}
+export function saveVirtualGoods(data) {
+  return request({ url: '/api/admin/virtual-goods/save', method: 'post', data })
+}
+export function deleteVirtualGoods(data) {
+  return request({ url: '/api/admin/virtual-goods/delete', method: 'post', data })
+}
+```
+
+- [ ] **Step 2: `src/views/admin/VirtualGoodsConfig.vue`**
+
+照抄 `KnowledgeTag.vue` 结构,替换为:
+- 筛选:goods_type 下拉(ASSESSMENT_PACKAGE / MEMBERSHIP / SUBSCRIPTION)+ 状态
+- 表格列:id / goods_type / biz_key / product_id / goods_name / status(开关)/ create_time / update_time
+- 表单弹窗:goods_type(select)、biz_key、product_id、goods_name、status(switch)
+- 提示文案:product_id 需与微信商户后台道具 ID 一致;上线前必须配置
+
+- [ ] **Step 3: 路由 + 菜单 + 权限注册**
+
+- `router/index.js` children 加:`{ path: 'virtual-goods-config', name: 'VirtualGoodsConfig', component: () => import('@/views/admin/VirtualGoodsConfig.vue'), meta: { title: '虚拟支付道具', perm: 'system:config' } }`
+- `Layout.vue` menuItems 系统配置组(L268-275)加:`{ path: '/virtual-goods-config', label: '虚拟支付道具', icon: 'el-icon-goods', perm: 'system:config' }`
+- 确认 `system:config` 已存在于 `utils/permissions.js`(若没有则新增或改用现有 admin 权限串)
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-web/src/api/virtualGoods.js cfc-web/src/views/admin/VirtualGoodsConfig.vue cfc-web/src/router/index.js cfc-web/src/views/Layout.vue
+git commit -m "feat: 管理端虚拟支付道具映射CRUD页面
+VirtualGoodsConfig.vue照KnowledgeTag模板,路由/菜单/权限注册"
+```
+
+---
+
+## Task 12: 全量验证 + 文档同步
+
+- [ ] **Step 1: 后端编译 + 冲突检查**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+grep -rn "class VirtualPay\|class VirtualGoods" cfc-backend/src/main/java --include=*.java | sort   # 无重名
+grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u   # 无路由重复
+```
+
+- [ ] **Step 2: 测试执行**
+
+- 签名单测通过(Task 4 Step 6)
+- 推送幂等测试:重复发货推送同一 OutTradeNo 只解锁一次
+- testMode 回归:`wechat.test-mode=true` 下测评/订阅走 mock 直付
+- 实物商品 v3 支付回归:`payment.vue` / `PaymentService` / `/api/payment/notify` 不受影响
+
+- [ ] **Step 3: 更新 `docs/superpowers/PROJECT-OVERVIEW.md`**
+
+按 AGENTS.md 约定,本计划文件创建后更新 PROJECT-OVERVIEW.md 的 plans/ 索引条目(版本号、状态、文件链接)。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add docs/superpowers/PROJECT-OVERVIEW.md docs/superpowers/plans/2026-08-02-virtual-payment-implementation.md
+git commit -m "docs: 虚拟支付实施计划 + PROJECT-OVERVIEW 索引更新"
+```
+
+---
+
+## 依赖与执行顺序
+
+```
+Task 1 (建表) ──┐
+Task 2 (配置) ──┼──→ Task 3 (映射Service) ──→ Task 4 (VirtualPayService) ──→ Task 5 (Controller)
+Task 3 (CRUD) ─┘                                          │
+                                                          ▼
+                        ┌──────────────┬──────────────────┴──────────────┐
+                        ▼              ▼                                 ▼
+                Task 6 (测评)   Task 7 (会员)                     Task 8 (订阅)
+                        └──────────────┴──────────────────┬──────────────┘
+                                                          ▼
+                                                Task 9 (定时任务兜底)
+                                                          ▼
+                                            Task 10 (前端) ── 依赖 Task 6/7/8 接口
+                                            Task 11 (管理端) ── 依赖 Task 3 接口
+                                                          ▼
+                                                Task 12 (全量验证)
+```
+
+可并行:Task 1/2 可并行;Task 6/7/8 依赖 Task 4,彼此独立可并行;Task 10/11 依赖各自后端接口,可并行;Task 12 收尾。
+
+## 联调前置清单(上线前需人工完成,不阻塞开发)
+
+- [ ] 微信商户后台配置道具(ProductId)并发布至现网,映射表按 product_id 填入
+- [ ] mp.weixin.qq.com 配置服务器推送 URL → `https://cfc.bianwoyou.com.cn/api/internal/virtual-pay/notify`(确认明文/加密模式)
+- [ ] 配置 `WECHAT_APPKEY`(现网与沙箱分开)
+- [ ] iOS 真机验证(沙箱不支持 iOS):iOS 15+ / 微信 8.0.68+
+- [ ] 沙箱联调:env=1 全链路;对照官方签名工具核对 paySig/signature
+
+## 自检清单
+
+- [ ] `virtual_goods_config` 表迁移 121 幂等可重复执行;schema.sql 已同步
+- [ ] `wechat.appkey`/`wechat.virtual-pay-env` 三处 yml 已配置
+- [ ] `VirtualPayService` 签名单测对照官方输出通过;JDK 原生 HMAC 零新依赖
+- [ ] `/api/internal/virtual-pay/notify` 挂载成功(JwtInterceptor 已公开,无需改)
+- [ ] 测评:下单 → 返回 VirtualPayParams → 支付 → 发货推送 → paid + 预约确认 + 佣金
+- [ ] 会员:下单 → VirtualPayParams → 推送 → processPaymentCallback 开通(等级已修正)
+- [ ] 订阅:下单建 pending → VirtualPayParams → 推送 → activateSubscription 开通 + 分配管家
+- [ ] 退款:`xpay_refund_notify` 更新订单状态;iOS 问询端点可应答
+- [ ] 道具未配置/停用 → 46001 提示不可购买
+- [ ] 幂等:重复发货推送不重复解锁;成功回调丢失由定时任务 query_order 兜底
+- [ ] 前端三个购买页无假支付残留;`payment.vue` 实物 v3 未动
+- [ ] 管理端 VirtualGoodsConfig.vue CRUD 可用
+- [ ] `mvn clean compile` 通过;无路由重复;无 Bean 命名冲突
+- [ ] `docs/superpowers/PROJECT-OVERVIEW.md` 已同步