优先级: P1 状态: 设计稿 日期: 2026-08-15
当前 SKU 模型是"规格变体":一个商品下的多个 SKU 表示同款商品的不同属性(颜色、尺码等),每个 SKU 有自己的价格、库存、图片。
新需求将 SKU 改为"关联商品选择器":商品 A 的 SKU 可以指向另一个独立商品 B,用户在商品 A 页面通过"规格"方式选择商品 B,查看商品 B 的信息并购买商品 B。
linked_product_id IS NOT NULL 的新模式 SKUlinked_product_id IS NULL 的旧模式 SKU(规格变体,保持兼容)| 决策 | 结论 |
|---|---|
| 父商品本身可购买吗? | 可以,不选 SKU 直接买父商品 |
| 价格/库存/图片来源? | 从 linked_product 读取,库存为 0 不显示选项 |
| 选项标签来源? | 默认取 linked_product.name,可自定义覆盖 |
| 下单时买什么? | 买 linked_product(productId = linked.id) |
| 管理端选商品限制? | 只能选同一分类的商品 |
| 可重复指向同一商品? | 不可以 |
| 存量 SKU 处理? | 不迁移,原样保留 |
specs 字段? |
废弃,新 SKU 不填 |
product_skus 表新增两列,旧列(specs/price/stock/image 等)保留但新模式 SKU 不填:
| 新增列 | 类型 | 说明 |
|---|---|---|
linked_product_id |
BIGINT NULL |
关联的商品 ID,null=Legacy SKU |
label |
VARCHAR(100) NULL |
自定义显示标签,null 时取 linked_product.name |
| 条件 | 模式 | 行为 |
|---|---|---|
linked_product_id IS NOT NULL |
Linked SKU | 价格/库存/图片/简介从 linked_product 读取;specs/price/stock/image/purchasePrice/originalPrice/minQuantity 等旧列忽略 |
linked_product_id IS NULL |
Legacy SKU | 保持原有规格变体行为不变 |
linked.stock = 0 |
隐藏 | 该 option 不返回给前端(不显示) |
linked_product deleted/disabled |
隐藏 | 该 option 不返回 |
无需迁移。新增列默认 NULL,所有存量 SKU 的 linked_product_id = null,保持旧行为。
POST /api/admin/product/linked/list用途: 管理端关联商品选择器(分页、分类筛选、关键字搜索、排除当前商品)
请求:
{
"productId": 123,
"categoryId": 5,
"keyword": "净水",
"page": 1,
"pageSize": 20
}
返回:
{
"code": 200,
"data": {
"list": [{
"id": 456,
"name": "豪华版净水机",
"price": 5999,
"stock": 20,
"image": "https://...",
"categoryId": 5,
"categoryName": "净水设备"
}],
"total": 1,
"page": 1,
"pageSize": 20
}
}
POST /api/admin/product/sku/create接受: 新增 { ..., label, linkedProductId }。当 linkedProductId 不为空时,specs/price/stock/image 等旧字段不需要传。
{
"productId": 123,
"label": "豪华版",
"linkedProductId": 456
}
POST /api/admin/product/sku/update同 create,支持更新 label 和 linkedProductId。
POST /api/product/spec/map返回扩展: 每个 option 增加 linked 商品信息。
{
"options": [{
"id": 123,
"groupId": 1,
"name": "豪华版",
"skuId": 123,
"linkedProductId": 456,
"linked": {
"id": 456,
"name": "豪华版净水机",
"price": 5999,
"stock": 20,
"image": "https://...",
"brief": "内置五级滤芯..."
},
"isLinked": true
}]
}
过滤规则: linked_product_id IS NOT NULL 且 linked.stock = 0 或 linked.enabled = 0 或 linked.deleted = 1 时,该 option 不返回。
| 阶段 | 有 linked SKU | 无 linked SKU | 无 SKU |
|---|---|---|---|
| 加入购物车 | productId = linked.id,skuId = sku.id |
productId = 父商品.id,skuId = sku.id |
productId = 父商品.id |
| 下单 | productId = linked.id,skuId = sku.id |
productId = 父商品.id,skuId = sku.id |
productId = 父商品.id |
| 价格 | linked.price | sku.price | product.price |
| 库存 | linked.stock | sku.stock | product.stock |
当前: 弹窗表单(specs 输入框 + 价格 + 库存 + 图片 + 启用)
新的 SKU 编辑弹窗(Linked SKU 模式):
┌─ 添加SKU ───────────────────────────┐
│ │
│ 显示标签: [豪华版 ] │
│ │
│ 关联商品: [选择商品...] │
│ ┌─ 已选: 豪华版净水机 ──┐ │
│ │ [图片] 名称 | ¥5999 │ │
│ │ 库存: 20 │ │
│ └────────────────────────┘ │
│ │
│ [启用] │
│ │
│ [取消] [保存] │
└──────────────────────────────────────┘
点击"选择商品"弹出商品选择器:
┌─ 选择关联商品 ─────────────────────────┐
│ │
│ 分类: [全部 ▼] 搜索: [净水 ] 🔍 │
│ │
│ ┌─ 商品列表 ──────────────────────┐ │
│ │ ○ 豪华版净水机 ¥5999 库存20 │ │
│ │ ○ 标准版净水机 ¥3999 库存50 │ │
│ │ ○ 滤芯套装 ¥299 库存200 │ │
│ └──────────────────────────────────┘ │
│ │
│ 共 3 条 1/1 页 │
│ │
│ [取消] [确认选择] │
└──────────────────────────────────────────┘
categoryId = 父商品.categoryId)linkedProductId 不为空时隐藏 specs/price/stock 等旧字段linkedProductId 是否为空决定显示哪种表单加载阶段:
specMap 获取 SKU 列表isLinked = true 的 option,缓存 linked 商品信息到 linkedProductMap[skuId]isLinked = false)保持原逻辑选中 linked SKU 时:
在规格选择区下方展示 linked 商品简介卡片
┌─────────────────────────────────┐
│ ┌─────┐ │
│ │ │ 豪华版净水机 │
│ │图片 │ ¥5,999 │
│ │ │ 库存 20 │
│ └─────┘ ───────────────── │
│ 内置五级滤芯,有效去除 │
│ 余氯、重金属、细菌... │
│ [查看详情 →] │
└─────────────────────────────────┘
价格显示切换为 linked 商品价格
库存显示切换为 linked 商品库存
"查看详情"可跳转到 linked 商品详情页
未选规格时:
productId = linked.id,skuId = sku.idproductId = 父商品.id,skuId = 无productId = 父商品.id| 情况 | 处理 |
|---|---|
| linked 商品库存为 0 | 该 option 不显示 |
| linked 商品已下架/删除 | 该 option 不显示 |
| 同一商品被多个父商品关联 | 正常,无限制 |
| 商品关联自己 | 管理端排除当前商品,API 层面加校验 |
| 循环关联(A→B→A→B...) | 不处理深层引用;父商品页面上的简介卡片仅展示 1 层(不加载 linked 的自己的 SKU);跳转到 linked 商品详情页后,该页正常展示其自身的 SKU 选择器 |
| 自定义 label 为空 | 回退用 linked.name |
| Legacy SKU(无 linked_product_id) | 保持原样,不触发新逻辑 |
| 任务 | 文件 | 改动 |
|---|---|---|
| 实体新增字段 | ProductSku.java |
新增 linkedProductId、label |
| 新增商品选择器接口 | ProductController.java / AdminProductController.java |
POST /api/admin/product/linked/list |
| 修改 specMap | ProductController.java |
option 扩展 linked 摘要 |
| 修改 SKU 创建/更新 | ProductSkuService.java |
支持 linkedProductId |
| 购物车 linked 转发 | CartService.java |
cart.getSkuId() 时检查 linked |
| 下单 linked 转发 | ProductOrderService.java |
下单时识别 linked 目标 |
| 迁移 | DatabaseInitializer.java |
新增 linked_product_id 和 label 列 |
| 任务 | 文件 | 改动 |
|---|---|---|
| 商品选择器组件 | 新增 ProductPicker.vue |
分类筛选 + 搜索 + 分页 + 单选 |
| SKU 编辑弹窗改造 | ProductSkuEditor.vue |
按 linkedProductId 切换两种表单模式 |
| API 新增 | admin.js |
新增 linkedProductList |
| 任务 | 文件 | 改动 |
|---|---|---|
| 规格选择扩展 | product-detail.vue |
linked SKU 选中时展示简介卡片 |
| 价格/库存切换 | product-detail.vue |
切换为 linked 商品的价格/库存 |
| 下单转发 | product-detail.vue |
有 linked SKU 时 productId 传 linked.id |
| API 调用 | api.js |
已有 productSpecGroups 不变,解析新字段 |