# 商品套餐管理功能 实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 完成商品套餐(组合)管理:套餐商品可配置子商品组成(商品+SKU+数量),套餐出库由订单支付自动穿透完成(已实现,不改),套餐商品在管理端可配置组成、在小程序商品详情页描述下方展示「套餐包含」明细。 **架构:** 延续现有半成品骨架(`ProductBundleItem` 实体/Mapper + `AdminInventoryController` 3 个 bundle 接口 + `ProductOrderService` 订单支付穿透出库)。本次补全 4 个缺口:① `DatabaseInitializer` 建 `product_bundle_items` 表 + 同步 schema.sql;② `ProductService.detail()` 对 `productType=bundle` 商品填充套餐组成到 `ProductDTO.bundleItems`;③ 管理端 `ProductEdit.vue` 增加「套餐」类型选项 + 套餐组成配置区;④ 小程序 `product-detail.vue` 描述下方展示「套餐包含」区块。 **技术栈:** Spring Boot 2.7.18 + MyBatis-Plus(Java 8)/ Vue 2 Options API + Element UI(cfc-web)/ uni-app Vue 2(cfc-frontend) **规格来源:** `docs/superpowers/specs/2026-08-15-product-bundle-design.md` **关键既有代码位置(半成品,勿重复实现):** - `cfc-backend/.../entity/ProductBundleItem.java`(已存在,勿修改) - `cfc-backend/.../mapper/ProductBundleItemMapper.java`(已存在,BaseMapper,勿修改) - `cfc-backend/.../service/InventoryService.java` 中 `saveBundleItems`(452行)/ `listBundleItems`(470行)/ `checkBundleStock`(479行)(已存在,勿修改) - `cfc-backend/.../controller/admin/AdminInventoryController.java` 中 `/bundle/items`、`/bundle/items/list`、`/bundle/check-stock` 接口(已存在,勿修改) - `cfc-backend/.../service/ProductOrderService.java` 582 行套餐穿透出库(已存在,勿修改) - `cfc-web/src/api/admin.js` 中 `saveBundleItems`/`getBundleItems`/`checkBundleStock` 封装(1870-1890 行,已存在,勿修改) --- ## 文件清单 | 操作 | 文件 | 职责 | |------|------|------| | 修改 | `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` | 迁移246:创建 `product_bundle_items` 表 | | 修改 | `cfc-backend/src/main/resources/schema.sql` | 追加 `CREATE TABLE IF NOT EXISTS product_bundle_items` 快照 | | 修改 | `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java` | 新增 `bundleItems` 字段 + `BundleItemVO` 内部类 | | 修改 | `cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java` | 注入 bundleItemMapper/productSkuMapper;`detail()` 填充套餐组成 | | 修改 | `cfc-web/src/views/admin/ProductEdit.vue` | 商品类型加「套餐」+ 套餐组成配置区 | | 修改 | `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue` | 描述下方展示「套餐包含」区块 | --- ### 任务 1:后端迁移 — 创建 `product_bundle_items` 表 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(runMigrations() 内 245b 之后、方法闭合括号 `\t}`(9159行)之前插入) - 修改:`cfc-backend/src/main/resources/schema.sql`(文件末尾追加) - [ ] **步骤 1:在 DatabaseInitializer.runMigrations() 末尾(迁移245b 块之后、方法闭合 `\t}` 之前)添加迁移246** ```java // 迁移246: 创建 product_bundle_items 表(套餐商品组成明细 BUNDLE-001) try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_bundle_items (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "bundle_product_id BIGINT NOT NULL COMMENT '套餐商品ID', " + "child_product_id BIGINT NOT NULL COMMENT '子商品ID', " + "child_sku_id BIGINT DEFAULT NULL COMMENT '子SKU ID(为空时直接扣 product.stock)', " + "quantity INT NOT NULL DEFAULT 1 COMMENT '子商品数量', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "INDEX idx_bundle (bundle_product_id), " + "INDEX idx_child (child_product_id)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐商品组成明细'"); log.info("已创建product_bundle_items表"); } catch (Exception e) { // 表已存在,忽略错误 } ``` - [ ] **步骤 2:在 schema.sql 末尾追加建表快照** ```sql CREATE TABLE IF NOT EXISTS product_bundle_items ( id BIGINT AUTO_INCREMENT PRIMARY KEY, bundle_product_id BIGINT NOT NULL COMMENT '套餐商品ID', child_product_id BIGINT NOT NULL COMMENT '子商品ID', child_sku_id BIGINT DEFAULT NULL COMMENT '子SKU ID(为空时直接扣 product.stock)', quantity INT NOT NULL DEFAULT 1 COMMENT '子商品数量', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_bundle (bundle_product_id), INDEX idx_child (child_product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐商品组成明细'; ``` - [ ] **步骤 3:Commit** ```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(backend): 迁移246创建product_bundle_items表(套餐商品组成明细)" ``` --- ### 任务 2:后端 — ProductDTO 增加套餐组成字段 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java` - [ ] **步骤 1:在 ProductDTO 类中(`deliveryConfig` 字段之后、`createdAt` 之前)新增字段与内部类** ```java // 套餐商品组成(仅 productType=bundle 时非空) private List bundleItems; @Data public static class BundleItemVO { private Long childProductId; private String childProductName; private String coverImage; private Long childSkuId; private String specs; // SKU 规格描述(无 SKU 时为空) private Integer quantity; } ``` - [ ] **步骤 2:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java git commit -m "feat(backend): ProductDTO新增bundleItems套餐组成字段" ``` --- ### 任务 3:后端 — ProductService.detail() 填充套餐组成 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java`(依赖注入区 32-51 行 + `detail()` 方法 121-142 行) - [ ] **步骤 1:新增依赖注入(在 `productDeliveryConfigService` 注入之后)** ```java @Resource private ProductBundleItemMapper bundleItemMapper; @Resource private ProductSkuMapper productSkuMapper; ``` 需在文件顶部追加 import(现有 import 区 13-16 行附近): ```java import com.etotem.cfc.entity.ProductBundleItem; import com.etotem.cfc.entity.ProductSku; import com.etotem.cfc.mapper.ProductBundleItemMapper; import com.etotem.cfc.mapper.ProductSkuMapper; ``` - [ ] **步骤 2:修改 `detail()` 方法 — 在 `ProductDTO dto = ProductDTO.from(product);` 之后、`ProductGiftRule` 逻辑之前插入套餐组成填充** ```java // 套餐商品:填充套餐组成(子商品名称/封面/SKU规格/数量) if ("bundle".equals(product.getProductType())) { List items = bundleItemMapper.selectList( new LambdaQueryWrapper() .eq(ProductBundleItem::getBundleProductId, id)); List bundleItems = new ArrayList<>(); for (ProductBundleItem bi : items) { Product child = productMapper.selectById(bi.getChildProductId()); if (child == null) continue; // 子商品已删除则跳过 ProductDTO.BundleItemVO vo = new ProductDTO.BundleItemVO(); vo.setChildProductId(child.getId()); vo.setChildProductName(child.getName()); vo.setCoverImage(child.getCoverImage()); vo.setChildSkuId(bi.getChildSkuId()); vo.setQuantity(bi.getQuantity()); if (bi.getChildSkuId() != null) { ProductSku sku = productSkuMapper.selectById(bi.getChildSkuId()); if (sku != null) vo.setSpecs(sku.getSpecs()); } bundleItems.add(vo); } dto.setBundleItems(bundleItems); } ``` - [ ] **步骤 3:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS,无编译错误(若报 Bean 注入找不到 ProductSkuMapper,确认 `mapper/ProductSkuMapper.java` 存在——已存在) - [ ] **步骤 4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java git commit -m "feat(backend): ProductService.detail返回套餐商品组成" ``` --- ### 任务 4:管理端 — ProductEdit.vue 套餐类型 + 套餐组成配置区 **文件:** - 修改:`cfc-web/src/views/admin/ProductEdit.vue` **前提说明:** `saveBundleItems`/`getBundleItems` 已在 `@/api/admin` 封装(1870-1890 行);`getProductSkuList(productId)` 已封装(607 行);`getProductList({keyword,page,size})` 已封装。子商品搜索复用 `getProductList`,SKU 拉取复用 `getProductSkuList`。 - [ ] **步骤 1:商品类型下拉增加「套餐」选项** 在 `ProductEdit.vue` 模板 22 行 `` 之后追加: ```html ``` - [ ] **步骤 2:新增套餐组成配置区模板 — 在测评商品扩展信息 ``(54 行「关联成长档案」divider 之前)之后、`` divider 之前插入** ```html ``` - [ ] **步骤 3:import 区(296 行)追加 4 个 API** 现有 import 行结尾 `...saveDeliveryOnlineSlots } from '@/api/admin'` 修改为追加: ```js import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields, getMySupplySystem, saveProductGiftRule, listProductGiftRules, listMembershipLevels, getGrowthArchiveProduct, saveGrowthArchiveProduct, listDeliveryServicePersons, saveDeliveryServicePersons, listDeliveryOnlineSlots, saveDeliveryOnlineSlots, getProductList, getProductSkuList, saveBundleItems, getBundleItems } from '@/api/admin' ``` - [ ] **步骤 4:data() 新增状态 — 在 `bundleItems: []` 定义到 data 返回对象(`sortOrder: 0` 之后,338 行 `imageList: []` 之前或之后均可)** ```js bundleItems: [], // 套餐组成: [{childProductId, childProductId, childSkuId, quantity, productOptions:[], skuOptions:[]}] ``` - [ ] **步骤 5:methods 新增 5 个方法 — 在 `loadProduct` 方法之后插入** ```js addBundleItem() { this.bundleItems.push({ childProductId: null, childSkuId: null, quantity: 1, productOptions: [], skuOptions: [] }) }, removeBundleItem(idx) { this.bundleItems.splice(idx, 1) }, async searchBundleProduct(query, idx) { if (!query || query.length < 1) return try { const res = await getProductList({ keyword: query, page: 1, size: 20 }) if (res.code === 200 && res.data && Array.isArray(res.data.records)) { this.bundleItems[idx].productOptions = res.data.records } } catch (e) { /* ignore */ } }, async onBundleProductChange(productId, idx) { const item = this.bundleItems[idx] item.childSkuId = null item.skuOptions = [] if (!productId) return try { const res = await getProductSkuList(productId) if (res.code === 200 && Array.isArray(res.data)) { item.skuOptions = res.data } } catch (e) { /* ignore */ } }, buildBundleItemPayload() { return this.bundleItems .filter(b => b.childProductId) .map(b => ({ childProductId: b.childProductId, childSkuId: b.childSkuId || null, quantity: b.quantity || 1 })) }, async loadBundleItems(productId) { try { const res = await getBundleItems(productId) if (res.code === 200 && Array.isArray(res.data)) { this.bundleItems = res.data.map(b => ({ childProductId: b.childProductId, childSkuId: b.childSkuId || null, quantity: b.quantity || 1, productOptions: [], skuOptions: [] })) // 回填 SKU 选项(为有 skuId 的行拉取规格列表供标题展示) this.bundleItems.forEach((item, idx) => { if (item.childProductId) this.onBundleProductChange(item.childProductId, idx) }) } } catch (e) { /* ignore */ } } ``` - [ ] **步骤 6:loadProduct() 中回填套餐组成 — 在 `this.loadDeliveryConfig(p.id)`(495 行)之后追加** ```js // Load bundle items config(套餐商品) if (p.productType === 'bundle') { this.loadBundleItems(p.id) } ``` - [ ] **步骤 7:handleSave() 保存套餐组成 — 在「保存线上服务配置失败」catch 块(805-807 行)之后、`this.$message.success(...)`(809 行)之前追加** ```js // Save bundle items config(套餐商品) if (productId) { try { const bundlePayload = this.buildBundleItemPayload() await saveBundleItems(productId, bundlePayload) } catch (e) { this.$message.error('保存套餐组成失败') console.warn('保存套餐组成失败', e) } } ``` > 注:`saveBundleItems` 为覆盖式保存(先 delete 后 insert),非套餐商品传空数组即可清除旧配置;此处仅对 bundle 商品调用,非 bundle 商品不受影响。 - [ ] **步骤 8:语法校验 + Commit** 运行:`node --check` 语法校验思路(复制 `