# 商品套餐管理功能设计文档 - 日期:2026-08-15 - 状态:已批准 - 相关模块:cfc-backend / cfc-web / cfc-frontend ## 1. 需求概述 实现商品套餐(组合)管理功能: 1. **套餐包含一批产品,每个产品可指定数量**(如:套餐 = 产品A×3 + 产品B×2) 2. **套餐出库自动完成所有套餐内产品的出库** —— 通过订单支付路径自动实现(`ProductOrderService` 已有穿透扣减逻辑,本设计不修改) 3. **套餐会在商品描述下显示具体的产品及数量** —— 小程序商品详情页在描述下方展示「套餐包含」区块 已确认的范围决策: | 决策 | 值 | |------|-----| | 使用端 | 管理端(cfc-web)配置套餐组成 + 小程序(cfc-frontend)展示套餐组成 | | 建模方式 | 套餐本身是商品(`productType = 'bundle'`),延续现有半成品设计 | | 出库路径 | 只靠订单支付自动完成(已实现,不修改手动出库) | | 实施策略 | 方案A:延续现有 `ProductBundleItem` 骨架,补全缺口 | ## 2. 现状分析(探索结论) 项目已有套餐功能的**半成品骨架**,但存在关键缺口: | 已有 | 缺口 | |------|------| | `ProductBundleItem` 实体 + `ProductBundleItemMapper` | ❌ `product_bundle_items` 表从未建过(schema.sql 与 DatabaseInitializer 均无),代码引用必然报错 | | `AdminInventoryController` 3 个接口(配置/查询/检查库存) | | | `InventoryService.saveBundleItems/listBundleItems/checkBundleStock` | ❌ `ProductService.detail()` 不返回套餐组成 | | 订单支付回调已实现套餐穿透扣子商品库存(`ProductOrderService` 582 行) | ❌ 管理端 `ProductEdit.vue` 商品类型无「套餐」选项 | | `admin.js` 已封装 3 个 API | ❌ 小程序商品详情页无套餐展示 | | `Product.productType` 字段(VARCHAR,无 ENUM 约束) | | ## 3. 数据层设计 ### 3.1 建表 `product_bundle_items` ```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='套餐商品组成明细'; ``` - **迁移入口**:`DatabaseInitializer.runMigrations()` 新增迁移(编号按现有最大编号 245b 递增为 246) - **同步 schema.sql**:添加 CREATE TABLE IF NOT EXISTS 到 schema.sql(完整快照) - **幂等**:`CREATE TABLE IF NOT EXISTS` + try-catch 忽略已存在异常 ### 3.2 ProductDTO 增加套餐组成字段 ```java // ProductDTO 新增 private List bundleItems; // 仅 productType=bundle 时非空 @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; } ``` ### 3.3 ProductService.detail() 填充套餐组成 `ProductService.detail(Long id)` 在返回 dto 前,若 `productType == "bundle"`: 1. `bundleItemMapper.selectList`(`bundleProductId = id`)查询组成 2. 遍历每项:查询子商品 `Product`(名称/封面),有 `childSkuId` 时查询 `ProductSku`(specs) 3. 填充到 `dto.bundleItems` 依赖注入:`ProductService` 新增 `@Resource ProductBundleItemMapper bundleItemMapper`(需检查字段名与现有注入无冲突)。 ## 4. 管理端配置 UI(cfc-web) ### 4.1 ProductEdit.vue - 商品类型下拉新增选项:`` - 当 `form.productType === 'bundle'` 时显示「套餐组成」配置区: - 多行子商品项:`商品搜索选择` + `数量(el-input-number,min=1)` + `删除` 按钮 - 「添加子商品」按钮追加一行 - 支持可选 SKU 选择(`getProductSkuList` 拉取) - 编辑回填:详情接口返回 bundleItems 后渲染 / 或编辑时调用 `getBundleItems(id)` - 保存流程:保存商品(create/update)成功拿到 `product.id` 后,若 productType=bundle 调用 `saveBundleItems(id, items)`;productType≠bundle 时若有旧配置可调用 `saveBundleItems` 传空数组清除 > 注:现有 `AdminInventoryController.saveBundleItems` 是「覆盖式」保存(先 delete 再 insert),无需前端额外处理 diff。 ### 4.2 子商品搜索 复用现有 `getProductList({keyword, page, size})`(已封装在 admin.js)。免责声明:若子商品需区分 SKU,选择商品后调用 `getProductSkuList`(同样已有封装)。 ## 5. 小程序展示(cfc-frontend) ### 5.1 product-detail.vue(`pages/discover-detail/product-detail/`) - 详情数据加载后,若 `product.productType === 'bundle' && product.bundleItems` 非空: - 在「商品描述」区块(`.desc-section`)**下方**新增「套餐包含」区块: - 标题「套餐包含」 - 每行:子商品缩略图 + 名称 + (规格,若有)+ 「×N」数量(强调色) - 遵循小程序限制: - 禁止可选链 `?.` → 用 `&&` 判断 - 禁止 `:key` 表达式 → 用 `getItemKey(item)` 方法 - 图片 URL 走现有 `getImageUrl()` 处理(公开路径游客可见) - 样式沿用现有商品详情页视觉风格(intro-section/desc-section 相邻样式) ## 6. 错误处理与边界 | 场景 | 处理 | |------|------| | `product_bundle_items` 表不存在(生产库未迁移) | 启动时 DatabaseInitializer 自动建表;若运行时缺表报错,按 AGENTS.md 迁移流程补迁移 | | detail() 时子商品已被删除 | `productMapper.selectById` 返回 null → 跳过该项,不报错 | | SKU 已被删除 | 同上,specs 置空 | | 套餐商品无组成 | bundleItems 为空列表 → 小程序不渲染「套餐包含」区块 | | 重复保存套餐组成 | saveBundleItems 覆盖式(先删后插),天然幂等 | ## 7. 测试策略 - 后端:`mvn clean compile` 编译验证;`detail()` 对 bundle 商品返回 bundleItems 的手工验证(Postman/curl:创建 bundle 商品 → 配置组成 → 查详情) - 管理端:ProductEdit.vue 选择「套餐」类型 → 添加子商品 → 保存 → 重新编辑回填验证 - 小程序:语法校验(不打包,遵循 AGENTS.md),人工在微信开发者工具验证展示 ## 8. 不做的事(YAGNI) - ❌ 修改手动出库(`createManualOutbound`)——出库只走订单支付 - ❌ 套餐内商品再嵌套餐(只支持直接关联 product) - ❌ 套餐价格计算/折扣——价格仍由 products 表 price 控制 - ❌ 独立套餐表/实体——套餐即商品(productType=bundle)