Procházet zdrojové kódy

docs: 套餐独立模型重写实施计划(9任务) + PROJECT-OVERVIEW 登记

Xiaogang Liao před 1 týdnem
rodič
revize
e402afd597

+ 4 - 2
docs/superpowers/PROJECT-OVERVIEW.md

@@ -1,7 +1,7 @@
 # 浠艾福 项目全景 — 阶段性需求与设计汇总
 
-**文档版本:** v2.22
-**日期:** 2026-10-25
+**文档版本:** v2.23
+**日期:** 2026-10-31
 **状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(Tasks 1-12 已完成,退款闭环实施中)+ 新用户注册引导(12 Tasks 全栈完成)+ 家庭成员关系条增强(✅ 已实施)+ TabBar 重构(✅ 4Tab + 中间⭐扇形菜单,设计已对齐实现)+ **LIFETIME 终身会员(✅ 11 Tasks 全栈完成)** + **健康数据中心(✅ 全栈完成)** + **SKU 价格单位统一分 + 规格选择响应式修复(✅ 7 Tasks 全栈完成)** + **AI健康教练人格分化与管家自助选择(✅ 11 Tasks 全栈完成)** + **优惠券全链路改为家庭维度(待实施)** + **关系三图重构(关键人拓展章鱼图/珍珠图/能力图,设计规格已完成)** + **注册零填写+信息按需补充(✅ 全栈完成:登录直接进首页 + 家庭邀请独立页真实姓名/家庭身份必填 + 功能→信息字段映射表 + 添加成员手机号冲突邀请加入)** + **健康报告归属重分配(✅ 后端接口 + 管理端页面 + 小程序入口已实现,见 `specs/2026-10-05-health-report-reassign-design.md`)**
 **指标变化曲线·指标速查·指标关联(✅ 全栈完成:后端11任务+小程序M1-M8+管理端W1-W6;HBuilderX人工验收待用户执行,见 `specs/2026-10-09-indicator-change-curve-design.md`)**
 **饮食/睡眠/运动打卡接入任务完成链路(✅ 全栈完成:实体taskId字段 + 三个Controller isDailyProgress分流回写 + RepeatTaskGenerator每日生成打卡任务 + tasks.vue跳转 + 三打卡页taskId透传 + API文档;见 `specs/2026-10-13-health-checkin-task-linkage-design.md` 与 `plans/2026-10-13-health-checkin-task-linkage.md`)**
@@ -10,6 +10,7 @@
 **限时特价(扫码闪购)(✅ 全栈完成:迁移389/390 → SpecialPriceService 7 方法 + 双控制器(公开 info 白名单 + admin 6 接口)→ 特价下单 createSpecialDealOrder + cancel/approveRefund/OrderTimeoutCancelTask 三处 releaseQuota 回滚 + 供应商改价拒绝 + 小程序商品页 sp 解析/倒计时/特价区块/checkout 透传 + cfc-web SpecialPriceManage.vue;5 个测试类 31 case 全过;见 `specs/2026-10-20-limited-time-special-price-design.md` 与 `plans/2026-10-20-limited-time-special-price.md`)**
 **后台多渠道发货(🟡 设计稿待审查:管理端发货对齐微信小程序后台四种方式——快递 logistics_type=1 / 同城配送 2(deliveryMethod 新增编码 6)/ 用户自提 4(不再跳过微信同步)/ 虚拟商品 3(支付成功自动发货 autoShipVirtualOrder,幂等 fail-open);另修正 upload_shipping_info URL 缺 /wxa 前缀;cfc-web 发货对话框按类型切换;见 `specs/2026-10-22-fulfillment-multi-mode-design.md`)**
 **CF 值体系统一收尾(F-8 清理批次,✅ 2026-10-25):三套旧 CF 账本(`family_platform_balance` / `family_members.system_points` / `user_platform_balance`)写接口全部 410 Gone(`PointsService` 5 个 + `PlatformPointsService` 6 个 + `FamilyPlatformPointsService` 7 个),只读历史保留;`system_points` 误导性「CF值」注释与 F-3b1 遗留「待 Q1 确认」注释清理完毕(F10 已定:每个完成成员各发一次);新增《CF 值获取/消耗单一事实源》规范文档;核销 W0.2「product_config 下线」条目(基线/测试库均无该表,`/api/admin/product-config/*` 已 410 Gone,无需删表);见 `specs/2026-10-25-cf-value-single-source-of-truth.md`**
+**商品套餐独立模型重写(📋 计划就绪:套餐=独立商品(product_type='bundle')非SKU + 内容清单新表 product_combo_items(迁移437, 挂套餐SKU级, 子商品SKU×数量) + 移除旧combo/bundle两机制(迁移438/439删表) + 购物车勾选子集服务端offer检测(节省额>0才提示) + 一键替换为套餐接口 + 下单仅扣套餐SKU库存;cfc-web 套餐内容编辑/移除SKU组合模式 + cfc-frontend 购物车一键替换提示/详情内含清单;9任务实施计划;见 `specs/2026-10-31-bundle-redesign-design.md` 与 `plans/2026-10-31-bundle-redesign.md`)**
 **维护:** 所有需求变更需更新本文档
 
 ---
@@ -161,6 +162,7 @@
 | 家庭推广体系重构(wxacode+家庭账户) | Phase 2 | ✅ 已完成(CommissionService + 27 个后端文件,promotion/ 6 前端页面,含推广中心/成员贡献榜/收益概览) | `specs/2026-07-14-family-promotion-redesign.md` + `plans/2026-07-22-comprehensive-implementation.md` Task 3-4 |
 | 行远页重要关系维护(通讯录导入) | Phase 1 | ✅ 已完成(action/ 5页面 2116行,含OctopusGraph+问卷+互动日志) | `specs/2026-06-14-action-page-relationship-design.md` |
 | 关系三图重构(关键人拓展章鱼图/珍珠图/能力图) | Phase 6 | 🟡 设计规格已完成(OctopusDiagram 替换为关键人拓展模型:成效对象=会员客户 + 类型bitmask(金额大/频次高/新成交) + 关键人画像(单位/部门/职务/初识场合/如何认识) + 四步法(盘点项目→找关键人→分析→拓展) + 高频词分析→≤8个拓展方向;新增3表 octopus_effect_record/octopus_key_person/octopus_effect_key_person) | `specs/2026-09-10-key-person-expansion-octopus-design.md` |
+| 商品套餐独立模型重写(combo/bundle → 套餐=独立商品) | Phase 6 | 📋 计划就绪(后端迁移437建 product_combo_items + 移除SKU级combo与bundle计价 + 购物车勾选子集offer检测/一键替换 + 下单仅扣套餐SKU库存;cfc-web 套餐内容编辑;cfc-frontend 一键替换提示) | `specs/2026-10-31-bundle-redesign-design.md` + `plans/2026-10-31-bundle-redesign.md` |
 | 家庭关系图谱(见跨维度) | — | ✅ 已实施(FamilyRelationGraph.vue 797行,10+页面使用) | 见 2.3 |
 
 ---

+ 414 - 0
docs/superpowers/plans/2026-10-31-bundle-redesign.md

@@ -0,0 +1,414 @@
+# 商品套餐独立模型重写 实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 用全新的「套餐=独立商品」模型替换现有 combo(SKU级)与 bundle(product级)两套机制:套餐商品自带 SKU 售价/库存、内容清单(子商品 SKU×数量)按套餐 SKU 挂载、购物车结算命中套餐且套餐价<子商品总和时提示「一键替换为套餐」,否则按原价。
+
+**Architecture:** 后端新增 `product_combo_items` 表(迁移437)+ `ProductComboItem` 实体/Mapper;新增纯计算 `ComboOfferCalculator`(覆盖检测+节省额>0 规则)与购物车替换接口;删除旧 combo/bundle 实体、定价计算器与全部扣子商品库存逻辑(迁移438/439 删旧表)。前端 cfc-web 管理端重写套餐内容编辑、移除 SKU 编辑器组合模式;cfc-frontend 消费端购物车以「一键替换」提示替代自动优惠。
+
+**Tech Stack:** cfc-backend(Spring Boot 2.7.18 / MyBatis-Plus / MySQL 8.0.34 / JUnit)、cfc-web(Vue2 + Element UI + jest)、cfc-frontend(uni-app + jest)。代码位于远程 `/app/cfc`(monorepo,git 分支 `cfclub`),本地 `/root/cfc` 为空壳。所有编辑经 ssh 至远程执行(本会话已验证 workflow:本地 write 文件 → scp → 远程应用)。
+
+**Spec:** `docs/superpowers/specs/2026-10-31-bundle-redesign-design.md`
+
+## Global Constraints
+
+- 新迁移编号从 **437** 起(436 已被「products 营养数据字段」占用)。
+- 线上 `product_sku_combo_items`、`product_bundle_items` 均 **0 数据**——旧表可安全 DROP(幂等迁移)。
+- 套餐价格/子商品价格均为「分」单位;比较以**下单口径结算单价**为准。
+- 替换接口数量/金额**必须后端重算**(防篡改),前端仅展示后端下发的 offer。
+- 迁移走 `DatabaseInitializer.runMigrations()` 幂等模式,同步 `schema.sql`。
+- 每次任务结束保持 `mvn compile`/`npm run build` 可通过。
+- MySQL 8.0:索引/唯一键不得含函数表达式。
+
+## Review Focus
+
+1. **套餐价 ≥ 子商品单买总和** → 不提示、按原价结算(不能出现「越组越贵」)。→ Task 5 计算器测试。
+2. **覆盖不足**(购物车某子商品数量 < 组成数量)→ 无 offer,不能部分替换。→ Task 5 计算器测试。
+3. **超量覆盖**(如可组 2 份)→ offer 替换 2 份,购物车余量保留。→ Task 5 计算器+服务测试。
+4. **套餐商品已被直接加购** → 不应再对该套装重复提示/替换套餐自身。→ Task 5 检测去重。
+5. **替换防篡改**:前端传组数不信任,后端按购物车实际重算并事务提交。→ Task 5 服务测试。
+6. **子商品 SKU 已失效/不存在** → 套餐内容保存时校验拒绝;检测时跳过脏行。→ Task 2 保存校验 + Task 5 检测防御。
+7. **多套餐候选** → 取节省额最大者;同额取先出现(确定性)。→ Task 5 计算器测试。
+
+---
+
+### Task 1: 迁移437 + ProductComboItem 实体/Mapper
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(新增 `migrate437`)
+- Modify: `cfc-backend/src/main/resources/schema.sql`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductComboItem.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductComboItemMapper.java`
+
+**Interfaces:**
+- Produces: `ProductComboItem`(字段:`Long id`、`Long comboSkuId`、`Long childProductId`、`Long childSkuId`、`Integer quantity`、`Date createdAt`,`@TableName("product_combo_items")`);`ProductComboItemMapper extends BaseMapper<ProductComboItem>`;表 `product_combo_items`(UK `(combo_sku_id, child_sku_id)`)。
+
+- [ ] **Step 1: 写实体与 Mapper**
+
+`ProductComboItem.java` 字段与上表一致(MyBatis-Plus 注解风格参照 `ProductSkuComboItem.java` 删除前的写法);`ProductComboItemMapper.java` 空接口继承 `BaseMapper<ProductComboItem>`。
+
+- [ ] **Step 2: 在 DatabaseInitializer 新增 `migrate437`**
+
+```java
+jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `product_combo_items` ("
+    + "`id` BIGINT NOT NULL AUTO_INCREMENT, "
+    + "`combo_sku_id` BIGINT NOT NULL COMMENT '套餐SKU id(内容挂SKU级)', "
+    + "`child_product_id` BIGINT NOT NULL, "
+    + "`child_sku_id` BIGINT NOT NULL COMMENT '子商品SKU(精确到规格)', "
+    + "`quantity` INT NOT NULL DEFAULT 1, "
+    + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
+    + "PRIMARY KEY (`id`), "
+    + "UNIQUE KEY `uk_combo_sku_child` (`combo_sku_id`, `child_sku_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+```
+在迁移入口(`runMigrations` 中)注册调用,与现有迁移同样的 try/catch + log 幂等模式。
+
+- [ ] **Step 3: schema.sql 同步 `product_combo_items` 建表语句**
+
+- [ ] **Step 4: 编译验证**
+
+Run: `cd /app/cfc/cfc-backend && mvn -q compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /app/cfc && git add cfc-backend/src && git commit -m "feat(backend): 新增套餐内容表 product_combo_items(迁移437)与实体/Mapper"
+```
+
+---
+
+### Task 2: 套餐内容管理接口 save/listComboItems(含校验)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ComboService.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java`(新增 `POST /api/admin/inventory/combo/items`、`POST /api/admin/inventory/combo/items/list`)
+- Create: `cfc-backend/src/test/java/.../ComboServiceTest.java`(放实际测试目录)
+
+**Interfaces:**
+- Consumes: Task 1 的 `ProductComboItemMapper`、`ProductSku` 实体与 `product_skus` 查询。
+- Produces: `ComboService.saveComboItems(Long comboSkuId, List<ComboItemInput> items)`、`ComboService.listComboItems(Long comboSkuId)`;`ComboItemInput` = `{Long childProductId, Long childSkuId, Integer quantity}`。
+
+- [ ] **Step 1: 写失败测试 `ComboServiceTest`**
+
+用 mock 的 `ProductComboItemMapper`/`ProductSkuMapper`(复用项目现有单测 mock 风格,参照 `ProductSkuComboServiceTest` 删除前的写法)断言:
+- `saveComboItems` 先删后插:对同一 `comboSkuId` 先 `delete` 再逐条 `insert`;
+- `quantity == null || quantity <= 0` → 抛 `IllegalArgumentException`;
+- `childSkuId == null` → 抛 `IllegalArgumentException`(SKU 级必填);
+- `comboSkuId` 对应 SKU 不存在 → 抛 `IllegalArgumentException`;
+- `listComboItems` 按 comboSkuId 查询并回填 `childProductName`/`childProductImage`/`childSkuSpecs`(按 `child_product_id`+`child_sku_id` 联查 product_skus/products,mock 返回值)。
+
+- [ ] **Step 2: 运行确认失败**
+
+Run: `mvn test -Dtest=ComboServiceTest`
+Expected: FAIL(ComboService 不存在/方法未定义)
+
+- [ ] **Step 3: 实现 `ComboService`**
+
+先校验:`comboSkuId` 非空且 `product_skus` 存在;逐条校验 `childSkuId != null`、`quantity > 0`、`child_sku_id` 存在且属于 `child_product_id`。通过后 `delete` 旧行 + 批量 `insert`。`listComboItems` 回填展示字段。
+
+- [ ] **Step 4: 在 AdminInventoryController 新增两个端点**
+
+`POST /api/admin/inventory/combo/items`(body: `{comboSkuId, items:[]}`)、`POST /api/admin/inventory/combo/items/list`(body: `{comboSkuId}`),返回 `Result`。沿用现有 `ParamUtils`/`Result` 风格。
+
+- [ ] **Step 5: 商品详情挂载套餐清单**
+
+`ProductService.detail`:当商品 `product_type='bundle'` 时,按该商品 SKU 列表调 `ComboService.listComboItems` 附带 `comboItems` 清单(含子商品名/图/规格/数量)到详情响应,供消费端「内含清单」展示。
+
+- [ ] **Step 6: 运行通过**
+
+Run: `mvn test -Dtest=ComboServiceTest`
+Expected: PASS
+
+- [ ] **Step 7: 编译 + Commit**
+
+Run: `mvn -q compile`
+Expected: BUILD SUCCESS
+Commit: `feat(backend): 套餐内容保存/列表接口(combo/items)含子SKU校验与详情清单挂载`
+
+---
+
+### Task 3: 删除 SKU 级 combo(后端代码 + 迁移438 删表)
+
+**Files:**
+- Delete: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductSkuComboItem.java`、`mapper/ProductSkuComboItemMapper.java`、`ProductSkuComboServiceTest`、`ProductOrderComboStockTest`(如存在)
+- Modify: `entity/ProductSku.java`(删 `comboItems` 字段)、`service/ProductSkuService.java`(删 combo 方法)、`service/CartService.java`(删 combo 摘要)、`service/ProductOrderService.java`(删 combo 行处理/deductComboStock/restockComboOrder 及其调用)、`controller/product/ProductController.java`(删 detail 中 comboItems 组装)、`controller/admin/AdminProductPickerController.java`(删 combo 相关)、`config/DatabaseInitializer.java`(新增 `migrate438` drop `product_sku_combo_items`)
+
+**Interfaces:**
+- Produces: 无残留 SKU 级 combo 引用(`mvn compile` 通过);旧表被 DROP。
+
+- [ ] **Step 1: 删除 combo 实体/Mapper/旧测试**
+
+`git rm` 上述 Delete 文件。
+
+- [ ] **Step 2: 清除引用**
+
+按编译错误逐个清除:`ProductSku.comboItems`、`ProductSkuService` 的 `fillComboItems/validateComboItems/saveComboItems/buildComboSummary/hasComboItems`、`CartService` specDesc 组合摘要、`ProductOrderService` combo 行特殊处理与 `deductComboStock`/`restockComboOrder`(含第 1082/1380/1830/2050 行附近调用)、`ProductController.detail` comboItems 组装、`AdminProductPickerController` combo 引用。
+
+- [ ] **Step 3: 新增 `migrate438` 删旧表**
+
+`DROP TABLE IF EXISTS product_sku_combo_items`,注册进 `runMigrations`(幂等)。
+
+- [ ] **Step 4: 编译验证**
+
+Run: `cd /app/cfc/cfc-backend && mvn -q compile`
+Expected: BUILD SUCCESS(无 combo 残留引用)
+
+- [ ] **Step 5: Commit**
+
+Commit: `refactor(backend): 移除SKU级组合机制(实体/库存扣减回补/迁移438删表)`
+
+---
+
+### Task 4: 删除 bundle 机制(后端代码 + 迁移439 删表)
+
+**Files:**
+- Delete: `pricing/BundlePricingCalculator.java`、`pricing/BundleDef.java`、`pricing/BundleChild.java`、`pricing/BundleHit.java`、`pricing/BundleDiscountResult.java`、`pricing/BundlePricingService.java`、`entity/ProductBundleItem.java`、`mapper/ProductBundleItemMapper.java`(**保留** `pricing/LineItem.java` 供 Task 5 复用)
+- Modify: `dto/CartListResultDTO.java`(删 `bundlePromo`,后续 Task 5 加 `comboOffer`)、`dto/CartItemDTO.java`(删 bundleMatch/bundleInfo 相关)、`service/CartService.java`(删 `attachBundleMatches/computeBundlePromo/toBundlePromo/buildBundleInfo` 及 import)、`service/ProductOrderService.java`(删 `checkBundleStock` 3 处调用与 `bundleDiscount` 应用)、`service/InventoryService.java`(删 `saveBundleItems/listBundleItems/checkBundleStock` 及 bundle 详情组装)、`controller/admin/AdminInventoryController.java`(删 `/bundle/items`、`/bundle/items/list`、`/bundle/check-stock` 端点)、`controller/MarketController.java`/`controller/PackagePaymentController.java`(删 bundle 引用)、`config/DatabaseInitializer.java`(新增 `migrate439` drop `product_bundle_items`)
+
+**Interfaces:**
+- Produces: 无 bundle 残留(`mvn compile` 通过);`pricing/LineItem` 保留。
+
+- [ ] **Step 1: 删除 bundle 实体/定价包/旧端点**
+
+按上表 `git rm`/删除文件与控制器端点。
+
+- [ ] **Step 2: 清除引用至编译通过**
+
+按编译错误逐个清除:CartService/ProductOrderService/InventoryService/控制器/DTO 中所有 bundle 引用(`checkBundleStock`、`bundleDiscount`、`bundlePromo`、`bundleMatch`、`bundleInfo`、`BundlePricingService` 注入等)。
+
+- [ ] **Step 3: 新增 `migrate439` 删旧表**
+
+`DROP TABLE IF EXISTS product_bundle_items`,注册进 `runMigrations`。
+
+- [ ] **Step 4: 编译验证**
+
+Run: `cd /app/cfc/cfc-backend && mvn -q compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+Commit: `refactor(backend): 移除旧bundle自动计价机制(定价计算器/库存联动/迁移439删表),保留LineItem`
+
+---
+
+### Task 5: 购物车套餐 offer 检测 + 一键替换接口
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/pricing/ComboOfferDef.java`、`pricing/ComboOfferResult.java`、`pricing/ComboOfferCalculator.java`
+- Create: `cfc-backend/src/test/java/.../ComboOfferCalculatorTest.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ComboOfferService.java`(装载定义 + 购物车接入)
+- Modify: `service/CartService.java`(新增 `computeComboOffer`/`replaceCartWithCombo`,移除 Task 4 后残留的 bundle 引用)、`controller/cart/CartController.java`(新增 `POST /api/cart/combo-offer` 与 `POST /api/cart/replace-combo`)
+- Create: `cfc-backend/src/test/java/.../CartReplaceComboTest.java`
+
+**Interfaces:**
+- Consumes: Task 1 `ProductComboItemMapper`、Task 4 保留的 `pricing/LineItem`。
+- Produces: `ComboOfferDef{Long comboSkuId, String comboName, Integer comboPrice, List<ComboChild> children}`;`ComboChild{Long childProductId, Long childSkuId, Integer quantity}`;`ComboOfferResult{boolean active, Long comboSkuId, String comboName, int groups, int originalTotal, int comboPrice, int saving}`;`ComboOfferCalculator.compute(List<LineItem> lines, List<ComboOfferDef> defs) -> ComboOfferResult`;`CartService.computeComboOffer(Long userId, List<Long> checkedCartItemIds) -> ComboOfferResult`;`CartService.replaceCartWithCombo(Long userId, List<Long> checkedCartItemIds, Long comboSkuId)`;`POST /api/cart/combo-offer {checkedCartItemIds:Long[]} -> Result<ComboOfferResult>`;`POST /api/cart/replace-combo {checkedCartItemIds:Long[], comboSkuId} -> Result<Void>`。
+
+> **勾选态感知**:购物车行无服务端 checked 字段(勾选是前端本地态,见 cart.vue `item.checked`),而 offer 必须按**勾选结算子集**判定——因此检测以独立端点 `combo-offer` 承载:前端把勾选记录 id(`CartItemDTO.id`=购物车主键)传给后端,后端按该子集重算(服务端权威),不再烘焙进 `CartListResultDTO`。
+
+- [ ] **Step 1: 写失败测试 `ComboOfferCalculatorTest`(纯计算,覆盖 Review Focus 1/2/3/7)**
+
+断言(lines 用 `(productId, skuId, quantity, unitPrice 分)` 构造):
+1. 恰好覆盖:A×2+B×1,套餐X(comboPrice=15000)组成 A×2+B×1,A 单价5000、B 单价6000 → 命中,`saving=5000`(原价16000-套餐15000),`groups=1`;
+2. 套餐价≥总和:comboPrice=16000 → `active=false`(原价结算,Review Focus 1);
+3. 覆盖不足:A×1+B×1 → `active=false`(Review Focus 2);
+4. 超量覆盖:A×4+B×2 → `groups=2`,`originalTotal=32000`,套餐价 15000×2 → `saving=10000`,原价=16000×2(Review Focus 3);
+5. 多套餐候选:同时命中套餐X(省5000)与套餐Y(省3000) → 返回 X(节省最大);两套餐同额 → 返回先出现者(Review Focus 7);
+6. 脏行防御:child 的 childSkuId 不在 lines 中 → 不命中该套餐。
+
+- [ ] **Step 2: 运行确认失败**
+
+Run: `mvn test -Dtest=ComboOfferCalculatorTest`
+Expected: FAIL(类不存在)
+
+- [ ] **Step 3: 实现 `ComboOfferCalculator`**
+
+签名:`public ComboOfferResult compute(List<LineItem> lines, List<ComboOfferDef> defs)`。算法:按 `(productId, skuId)` 聚合 lines 为池;对每个 def 计算可覆盖组数 `groups = min over children(floor(haveQty/needQty))`(haveQty 严格按 child_sku_id 匹配,缺失即 0);`originalTotal = sum(组内 child 实际消耗数量 × 对应 line 单价 × groups)`;`saving = originalTotal - comboPrice×groups`;`saving > 0` 且 `groups > 0` 才候选;取 saving 最大(同额取 defs 先出现);返回单条 offer(多套餐只报最优一个,符合设计)。同一套餐重复(comboSkuId 相同)只报一次。
+
+- [ ] **Step 4: 运行通过**
+
+Run: `mvn test -Dtest=ComboOfferCalculatorTest`
+Expected: PASS(6 项全绿)
+
+- [ ] **Step 5: 实现 `ComboOfferService` 装载与购物车接入**
+
+`ComboOfferService.computeForLines(List<LineItem> lines)`:查询 `product_combo_items` 按 `combo_sku_id` 聚合 → 取套餐 SKU(`product_skus.price`/`label`)组装 `ComboOfferDef`(脏行跳过)→ 调 Calculator。`CartService.computeComboOffer(userId, checkedCartItemIds)`:按 id 加载购物车记录(仅限该 userId,越权 id 忽略/报错)→ 构建 lines(按 `(productId, skuId)` 聚合、单位分单价)→ 调 computeForLines。`getCartList` **不再返回任何套餐 offer**(列表与勾选态解耦,避免「按全车算的优惠」误导)。
+
+- [ ] **Step 6: 写失败测试 `CartReplaceComboTest`(服务层,mock mapper,Review Focus 4/5)**
+
+断言 `CartService.computeComboOffer(userId, checkedCartItemIds)` 仅按勾选记录计算(未勾选行不参与、saving>0 才 active);`replaceCartWithCombo(userId, checkedCartItemIds, comboSkuId)`:
+1. 后端按 checked 行的 `(productId, skuId)` 重算组数(忽略前端传入组数)——购物车 A×4+B×2 传 99 份 → 只替换 2 组;
+2. 替换后:子商品行数量相应扣减(A 剩 0、B 剩 0),新增套餐 SKU 行 quantity=2;
+3. 余量保留:A×5+B×2 → A 余 1,B 余 0,套餐行 2;
+4. 套餐商品已在购物车(同 product 已在库)→ 返回错误或合并数量而非重复套装(设计决策:与现有 add 行为一致,合并数量);
+5. 事务性:任一步失败整体回滚(mock mapper 抛异常断言无部分写入)。
+第一次运行确认 FAIL(方法未定义)。
+
+- [ ] **Step 7: 实现 `CartController POST /api/cart/replace-combo` 与 `CartService.replaceCartWithCombo`**
+
+事务(`@Transactional`):后端重算组数 → 逐行扣减购物车子商品数量(不足则整体失败回滚)→ 删除数量为 0 的行 → upsert 套餐 SKU 行(存在则加数量)。
+
+- [ ] **Step 8: 运行通过**
+
+Run: `mvn test -Dtest=CartReplaceComboTest`
+Expected: PASS(5 项全绿)
+
+- [ ] **Step 9: 全量后端测试 + 编译 + Commit**
+
+Run: `cd /app/cfc/cfc-backend && mvn test -Dmaven.test.skip=false 2>&1 | tail -20`
+Expected: 全绿(旧 combo/bundle 测试已删,其余回归通过)
+Commit: `feat(backend): 购物车套餐offer检测(勾选子集/节省额>0)与一键替换接口`
+
+---
+
+### Task 6: 套餐商品下单只扣套餐 SKU 库存
+
+**Files:**
+- Modify: `service/ProductOrderService.java`(确认套餐商品走普通 SKU 下单路径;清理 Task 4 后遗留的 bundle/combo 分支)
+- Create: `cfc-backend/src/test/java/.../ComboOrderStockTest.java`
+
+**Interfaces:**
+- Consumes: Task 3/4 移除后 ProductOrderService 已无 combo/bundle 库存逻辑。
+- Produces: 套餐商品(product_type='bundle')下单按普通商品:校验套餐 SKU 库存 → 支付扣减 → 取消/失败回补,均只作用于套餐 SKU。
+
+- [ ] **Step 1: 写失败测试 `ComboOrderStockTest`**
+
+断言(mock):
+1. 下单套餐商品(product_type='bundle')→ 库存校验走套餐 SKU `product_skus.stock`;
+2. 支付成功后套餐 SKU 库存扣减 quantity,**子商品 SKU 库存不变**(Review 关键);
+3. 取消/失败回补只加套餐 SKU 库存。
+若 ProductOrderService 仍有 bundle/combo 分支(如还调 checkBundleStock 或子商品扣减)→ 测试失败。
+
+- [ ] **Step 2: 运行确认失败**
+
+Run: `mvn test -Dtest=ComboOrderStockTest`
+Expected: FAIL
+
+- [ ] **Step 3: 实现**
+
+确保 ProductOrderService 对 `product_type='bundle'` 无特殊库存分支(走普通 SKU 流);删除任何残留 `checkBundleStock`/子商品扣减。
+
+- [ ] **Step 4: 运行通过 + 编译 + Commit**
+
+Run: `mvn test -Dtest=ComboOrderStockTest` → PASS;`mvn -q compile` → SUCCESS
+Commit: `feat(backend): 套餐下单仅扣套餐SKU库存(校验/扣减/回补)`
+
+---
+
+### Task 7: cfc-web 管理端(套餐内容编辑 + 移除 SKU 组合模式)
+
+**Files:**
+- Modify: `cfc-web/src/api/admin.js`(`saveBundleItems`/`getBundleItems` → `saveComboItems`/`listComboItems` 调用新端点)
+- Modify: `cfc-web/src/views/admin/ProductEdit.vue`(bundle 区块改为「套餐内容编辑」:选子商品 SKU 规格 + 数量;产品当前 SKU id 作 comboSkuId)
+- Modify: `cfc-web/src/views/admin/components/ProductSkuEditor.vue`(**移除「商品组合」模式**:radio 只留「规格变体」,删 comboItems/ProductPicker 组合块相关逻辑与样式)
+- Modify: `cfc-web/src/__tests__/components/ProductSkuEditor.mode.test.js`(既有 mode 测试追加断言:radio 仅有「规格变体」一项)
+
+**Interfaces:**
+- Consumes: Task 2 后端端点 `POST /api/admin/inventory/combo/items`、`/combo/items/list`。
+
+- [ ] **Step 1: 写失败测试(ProductSkuEditor 无组合模式)**
+
+沿用既有 `src/__tests__/components/ProductSkuEditor.mode.test.js`,追加断言:挂载后打开对话框,`.el-radio-button` 文本**不含「商品组合」且不含「关联商品」**,仅含「规格变体」。运行确认 FAIL(当前仍有组合)。
+
+- [ ] **Step 2: api/admin.js 替换接口**
+
+`saveComboItems(comboSkuId, items)` → `POST /api/admin/inventory/combo/items`;`listComboItems(comboSkuId)` → `POST /api/admin/inventory/combo/items/list`。
+
+- [ ] **Step 3: ProductSkuEditor 移除组合模式**
+
+删除「商品组合」radio-button、combo 模板块、`comboItems`/`openProductPicker`/组合保存分支、相关样式(保留「规格变体」)。`ProductPicker` 组件保留(Task 7 套餐内容编辑复用其子商品/SKU 选择能力)。
+
+- [ ] **Step 4: ProductEdit.vue 套餐内容编辑**
+
+`productType==='bundle'` 时:加载套餐 SKU 列表(`getProductSkuList`),取首个/指定 SKU 的 id 作 `comboSkuId`;「套餐内容编辑」用 ProductPicker 多选子商品 SKU + 数量;保存调 `saveComboItems(list[0].id, items)`,加载调 `listComboItems`。
+
+- [ ] **Step 5: 运行测试**
+
+Run: `cd /app/cfc/cfc-web && npx jest`
+Expected: 全绿(既有 90 → 更新后全过,含新断言)
+
+- [ ] **Step 6: 构建 + Commit**
+
+Run: `npm run build`
+Expected: DONE Build complete
+Commit: `feat(admin): 套餐内容编辑改挂商品级、SKU编辑器移除组合模式`
+
+---
+
+### Task 8: cfc-frontend 消费端(购物车「一键替换为套餐」)
+
+**Files:**
+- Modify: `cfc-frontend/utils/bundle-pricing.js`(重写为 comboOffer 展示辅助:`comboOfferOf(offer)`/`formatComboOfferText(offer)` 纯展示,删除自动扣减逻辑)
+- Modify: `cfc-frontend/pages/shop/cart/cart.vue`(删「已享套餐价」自动区块与 `applyBundleDiscount` 扣减;勾选态变化→防抖调 `/api/cart/combo-offer` 取勾选子集 offer;有 offer 时提示「这 N 件组成套餐「XX」,套餐价 ¥Y(省 ¥Z)」+「一键替换为套餐」按钮 → `replace-combo` → 刷新购物车)
+- Modify: `cfc-frontend/pages/shop/checkout/checkout.vue`(移除 bundlePromo 自动优惠处理)
+- Modify: `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue`(套餐商品详情展示「内含清单」:用详情接口 `comboItems` 渲染子商品名/规格/数量)
+- Modify: `cfc-frontend/utils/api.js`(新增 `queryComboOffer`、`replaceCartWithCombo`)
+- Modify: `cfc-frontend/__tests__/utils/bundle-pricing.test.js`(适配 comboOffer)
+
+**Interfaces:**
+- Consumes: Task 5 的 `POST /api/cart/combo-offer`(勾选子集 offer)与 `POST /api/cart/replace-combo`;Task 2 详情接口的 `comboItems` 清单。
+
+- [ ] **Step 1: 写失败测试(bundle-pricing → comboOffer 辅助)**
+
+改写 `__tests__/utils/bundle-pricing.test.js`:`comboOfferOf({active:true,...})` 返回 offer;`active=false/缺失` 返回 null;总价**不**再被自动扣减(纯展示)。运行确认 FAIL(旧逻辑仍在)。
+
+- [ ] **Step 2: 实现 utils 与 api**
+
+`utils/api.js` 加 `queryComboOffer(payload)`(POST /cart/combo-offer)与 `replaceCartWithCombo(payload)`(POST /cart/replace-combo);`utils/bundle-pricing.js` 重写为 comboOffer 展示辅助。
+
+- [ ] **Step 3: cart.vue 改造**
+
+删自动优惠区块与扣减;勾选态变化(checked 集合变更)时防抖调 `queryComboOffer({checkedCartItemIds})`(ids 取勾选行 `item.id`,空集不调);返回 `active` 时渲染提示「这 N 件可换成套餐「XX」(省 ¥Z)」+「一键替换为套餐」按钮 → `replaceCartWithCombo({checkedCartItemIds, comboSkuId})` → 成功后刷新购物车列表与勾选态(替换后重查 offer 应为 inactive)。
+
+- [ ] **Step 4: checkout.vue 移除 bundlePromo 处理**
+
+- [ ] **Step 5: 运行测试 + 构建 + Commit**
+
+Run: `cd /app/cfc/cfc-frontend && npx jest`(如项目脚本)→ 全绿;`npm run build`(或项目构建脚本)→ 成功
+Commit: `feat(consumer): 购物车套餐「一键替换为套餐」提示(替代自动优惠)`
+
+---
+
+### Task 9: 全量回归、构建与部署
+
+**Files:**
+- Modify: 无(部署流程操作)
+
+**Interfaces:**
+- Consumes: Task 1-8 全部产物。
+
+- [ ] **Step 1: 后端全量回归**
+
+Run: `cd /app/cfc/cfc-backend && mvn test 2>&1 | tail -20`
+Expected: 全绿;`mvn clean package -Dmaven.test.skip=true -Dmaven.compile.fork=true` 打包成功。
+
+- [ ] **Step 2: 前端双端回归与构建**
+
+Run: `cd /app/cfc/cfc-web && npx jest && npm run build`;`cd /app/cfc/cfc-frontend && npx jest && npm run build`(按项目实际脚本)
+Expected: 全绿 + 构建成功。
+
+- [ ] **Step 3: 部署**
+
+按 AGENTS.md:git 状态确认干净 → 备份 `/app/pro/cfc` → 后端 `cp jar + systemctl restart cfc-backend` → 前端 `cfc-web/dist` rsync 至 `/app/pro/cfc/frontend/` → 消费端 cfc-frontend 构建产物按本项目部署方式(如 miniprogram/H5)部署 → 验证健康端点与页面。
+
+- [ ] **Step 4: 生产验证**
+
+后端 `systemctl status` active;`curl POST /api/cart/list` 200 且无 `bundlePromo`/`bundleMatch`;`curl POST /api/cart/combo-offer`(空 ids)返回正常结构;日志无 ERROR。管理端商品编辑套餐商品可见「套餐内容编辑」。数据库核对 `product_combo_items` 表存在、旧表已删。
+
+- [ ] **Step 5: git 收尾**
+
+提交部署相关变更(版本号冲突取远程),推送 `cfclub`,更新台账 `.superpowers/sdd/…`。
+
+---
+
+## Review Focus 归属自查
+
+| 关注点 | 测试归属 |
+|---|---|
+| 套餐价≥总和→取原价 | Task 5.1 用例2 |
+| 覆盖不足→无offer | Task 5.1 用例3 |
+| 超量→替换N份余量保留 | Task 5.1 用例4 + Task 5.6 用例3 |
+| 套餐已加购不重复提示 | Task 5 检测去重(计算器单 def 唯一 + 服务去重) |
+| 替换防篡改 | Task 5.6 用例1/5 |
+| 子SKU失效 | Task 2 保存校验 + Task 5.1 用例6 |
+| 多套餐取最大 | Task 5.1 用例5 |