2026-08-15-sku-linked-product-selector-design.md 11 KB

SKU 关联商品选择器设计

优先级: P1 状态: 设计稿 日期: 2026-08-15

1. 概述

1.1 背景

当前 SKU 模型是"规格变体":一个商品下的多个 SKU 表示同款商品的不同属性(颜色、尺码等),每个 SKU 有自己的价格、库存、图片。

新需求将 SKU 改为"关联商品选择器":商品 A 的 SKU 可以指向另一个独立商品 B,用户在商品 A 页面通过"规格"方式选择商品 B,查看商品 B 的信息并购买商品 B。

1.2 核心概念

  • 父商品(Parent Product):展示 SKU 选择器的商品页面
  • 关联商品(Linked Product):SKU 指向的目标商品,选中后购买此商品
  • Linked SKU:linked_product_id IS NOT NULL 的新模式 SKU
  • Legacy SKU:linked_product_id IS NULL 的旧模式 SKU(规格变体,保持兼容)

1.3 决策记录

决策 结论
父商品本身可购买吗? 可以,不选 SKU 直接买父商品
价格/库存/图片来源? 从 linked_product 读取,库存为 0 不显示选项
选项标签来源? 默认取 linked_product.name,可自定义覆盖
下单时买什么? 买 linked_product(productId = linked.id)
管理端选商品限制? 只能选同一分类的商品
可重复指向同一商品? 不可以
存量 SKU 处理? 不迁移,原样保留
specs 字段? 废弃,新 SKU 不填

2. 数据模型

2.1 product_skus 表变更

product_skus 表新增两列,旧列(specs/price/stock/image 等)保留但新模式 SKU 不填:

新增列 类型 说明
linked_product_id BIGINT NULL 关联的商品 ID,null=Legacy SKU
label VARCHAR(100) NULL 自定义显示标签,null 时取 linked_product.name

2.2 行为规则

条件 模式 行为
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 不返回

2.3 迁移

无需迁移。新增列默认 NULL,所有存量 SKU 的 linked_product_id = null,保持旧行为。


3. 后端 API

3.1 新增接口

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
  }
}

3.2 修改接口

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 不返回。

3.3 下单/购物车流程

阶段 有 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

4. 管理端(ProductSkuEditor.vue)

4.1 UI 变更

当前: 弹窗表单(specs 输入框 + 价格 + 库存 + 图片 + 启用)

新的 SKU 编辑弹窗(Linked SKU 模式):

┌─ 添加SKU ───────────────────────────┐
│                                      │
│  显示标签: [豪华版         ]         │
│                                      │
│  关联商品: [选择商品...]            │
│  ┌─ 已选: 豪华版净水机 ──┐          │
│  │ [图片] 名称 | ¥5999   │          │
│  │ 库存: 20              │          │
│  └────────────────────────┘          │
│                                      │
│  [启用]                              │
│                                      │
│       [取消]          [保存]         │
└──────────────────────────────────────┘

点击"选择商品"弹出商品选择器:

┌─ 选择关联商品 ─────────────────────────┐
│                                        │
│  分类: [全部 ▼]   搜索: [净水    ] 🔍  │
│                                        │
│  ┌─ 商品列表 ──────────────────────┐   │
│  │ ○ 豪华版净水机  ¥5999  库存20   │   │
│  │ ○ 标准版净水机  ¥3999  库存50   │   │
│  │ ○ 滤芯套装      ¥299   库存200  │   │
│  └──────────────────────────────────┘   │
│                                        │
│  共 3 条                       1/1 页  │
│                                        │
│          [取消]    [确认选择]           │
└──────────────────────────────────────────┘

4.2 规则

  • 只能选择同一分类的商品(categoryId = 父商品.categoryId)
  • 排除当前商品自身(不能关联自己)
  • 已关联过的商品不可重复选择(同一父商品下不可重复)
  • 支持关键字搜索(商品名模糊匹配)
  • 支持分页
  • 商品列表中显示已选中的商品

4.3 兼容性

  • 已有 Legacy SKU 的编辑弹窗保持原样(specs/price/stock 表单)
  • 新 SKU 创建时,linkedProductId 不为空时隐藏 specs/price/stock 等旧字段
  • 编辑时根据 linkedProductId 是否为空决定显示哪种表单

5. 小程序详情页(product-detail.vue)

5.1 规格选择流程

加载阶段:

  1. 页面加载调用 specMap 获取 SKU 列表
  2. 对每个 isLinked = true 的 option,缓存 linked 商品信息到 linkedProductMap[skuId]
  3. 旧模式 SKU(isLinked = false)保持原逻辑

选中 linked SKU 时:

  1. 在规格选择区下方展示 linked 商品简介卡片

    ┌─────────────────────────────────┐
    │  ┌─────┐                        │
    │  │     │  豪华版净水机          │
    │  │图片 │  ¥5,999                │
    │  │     │  库存 20               │
    │  └─────┘  ─────────────────    │
    │          内置五级滤芯,有效去除   │
    │          余氯、重金属、细菌...   │
    │          [查看详情 →]            │
    └─────────────────────────────────┘
    
  2. 价格显示切换为 linked 商品价格

  3. 库存显示切换为 linked 商品库存

  4. "查看详情"可跳转到 linked 商品详情页

未选规格时:

  • 显示父商品 A 的信息
  • 按钮始终可点(买 A)

5.2 购物车/下单

  • 有 linked SKU 时:productId = linked.id,skuId = sku.id
  • 无 linked SKU 时:productId = 父商品.id,skuId = 无
  • 无 SKU 时:productId = 父商品.id

6. 错误处理与边界情况

情况 处理
linked 商品库存为 0 该 option 不显示
linked 商品已下架/删除 该 option 不显示
同一商品被多个父商品关联 正常,无限制
商品关联自己 管理端排除当前商品,API 层面加校验
循环关联(A→B→A→B...) 不处理深层引用;父商品页面上的简介卡片仅展示 1 层(不加载 linked 的自己的 SKU);跳转到 linked 商品详情页后,该页正常展示其自身的 SKU 选择器
自定义 label 为空 回退用 linked.name
Legacy SKU(无 linked_product_id) 保持原样,不触发新逻辑

7. 实施范围

7.1 后端

任务 文件 改动
实体新增字段 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 列

7.2 管理端(cfc-web)

任务 文件 改动
商品选择器组件 新增 ProductPicker.vue 分类筛选 + 搜索 + 分页 + 单选
SKU 编辑弹窗改造 ProductSkuEditor.vue 按 linkedProductId 切换两种表单模式
API 新增 admin.js 新增 linkedProductList

7.3 小程序(cfc-frontend)

任务 文件 改动
规格选择扩展 product-detail.vue linked SKU 选中时展示简介卡片
价格/库存切换 product-detail.vue 切换为 linked 商品的价格/库存
下单转发 product-detail.vue 有 linked SKU 时 productId 传 linked.id
API 调用 api.js 已有 productSpecGroups 不变,解析新字段

8. 不涉及

  • 不改 schema.sql(新列通过迁移添加)
  • 不改 ProductOrderService 的库存校验逻辑(linked 商品用自己的库存)
  • 不改 ProductSkuDTO(仅创建/更新时用)
  • 不改旧 SKU 的任何行为
  • 不做数据迁移