瀏覽代碼

docs(superpowers): 商品套餐管理功能设计文档(建表/管理端配置/小程序展示)

Xiaogang Liao 1 月之前
父節點
當前提交
29b6f6edfe
共有 1 個文件被更改,包括 138 次插入0 次删除
  1. 138 0
      docs/superpowers/specs/2026-08-15-product-bundle-design.md

+ 138 - 0
docs/superpowers/specs/2026-08-15-product-bundle-design.md

@@ -0,0 +1,138 @@
+# 商品套餐管理功能设计文档
+
+- 日期: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<BundleItemVO> 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
+
+- 商品类型下拉新增选项:`<el-option label="套餐" value="bundle" />`
+- 当 `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)