实现商品套餐(组合)管理功能:
ProductOrderService 已有穿透扣减逻辑,本设计不修改)已确认的范围决策:
| 决策 | 值 |
|---|---|
| 使用端 | 管理端(cfc-web)配置套餐组成 + 小程序(cfc-frontend)展示套餐组成 |
| 建模方式 | 套餐本身是商品(productType = 'bundle'),延续现有半成品设计 |
| 出库路径 | 只靠订单支付自动完成(已实现,不修改手动出库) |
| 实施策略 | 方案A:延续现有 ProductBundleItem 骨架,补全缺口 |
项目已有套餐功能的半成品骨架,但存在关键缺口:
| 已有 | 缺口 |
|---|---|
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 约束) |
product_bundle_itemsCREATE 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)CREATE TABLE IF NOT EXISTS + try-catch 忽略已存在异常// 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;
}
ProductService.detail(Long id) 在返回 dto 前,若 productType == "bundle":
bundleItemMapper.selectList(bundleProductId = id)查询组成Product(名称/封面),有 childSkuId 时查询 ProductSku(specs)dto.bundleItems依赖注入:ProductService 新增 @Resource ProductBundleItemMapper bundleItemMapper(需检查字段名与现有注入无冲突)。
<el-option label="套餐" value="bundle" />form.productType === 'bundle' 时显示「套餐组成」配置区:
商品搜索选择 + 数量(el-input-number,min=1) + 删除 按钮getProductSkuList 拉取)getBundleItems(id)product.id 后,若 productType=bundle 调用 saveBundleItems(id, items);productType≠bundle 时若有旧配置可调用 saveBundleItems 传空数组清除注:现有
AdminInventoryController.saveBundleItems是「覆盖式」保存(先 delete 再 insert),无需前端额外处理 diff。
复用现有 getProductList({keyword, page, size})(已封装在 admin.js)。免责声明:若子商品需区分 SKU,选择商品后调用 getProductSkuList(同样已有封装)。
pages/discover-detail/product-detail/)product.productType === 'bundle' && product.bundleItems 非空:
.desc-section)下方新增「套餐包含」区块:?. → 用 && 判断:key 表达式 → 用 getItemKey(item) 方法getImageUrl() 处理(公开路径游客可见)| 场景 | 处理 |
|---|---|
product_bundle_items 表不存在(生产库未迁移) |
启动时 DatabaseInitializer 自动建表;若运行时缺表报错,按 AGENTS.md 迁移流程补迁移 |
| detail() 时子商品已被删除 | productMapper.selectById 返回 null → 跳过该项,不报错 |
| SKU 已被删除 | 同上,specs 置空 |
| 套餐商品无组成 | bundleItems 为空列表 → 小程序不渲染「套餐包含」区块 |
| 重复保存套餐组成 | saveBundleItems 覆盖式(先删后插),天然幂等 |
mvn clean compile 编译验证;detail() 对 bundle 商品返回 bundleItems 的手工验证(Postman/curl:创建 bundle 商品 → 配置组成 → 查详情)createManualOutbound)——出库只走订单支付