2026-08-15-product-bundle-design.md 6.9 KB

商品套餐管理功能设计文档

  • 日期: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

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 增加套餐组成字段

// 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.selectListbundleProductId = 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)