|
|
@@ -0,0 +1,314 @@
|
|
|
+# 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`
|
|
|
+
|
|
|
+**用途:** 管理端关联商品选择器(分页、分类筛选、关键字搜索、排除当前商品)
|
|
|
+
|
|
|
+**请求:**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "productId": 123,
|
|
|
+ "categoryId": 5,
|
|
|
+ "keyword": "净水",
|
|
|
+ "page": 1,
|
|
|
+ "pageSize": 20
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**返回:**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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` 等旧字段不需要传。
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "productId": 123,
|
|
|
+ "label": "豪华版",
|
|
|
+ "linkedProductId": 456
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### `POST /api/admin/product/sku/update`
|
|
|
+
|
|
|
+同 create,支持更新 `label` 和 `linkedProductId`。
|
|
|
+
|
|
|
+#### `POST /api/product/spec/map`
|
|
|
+
|
|
|
+**返回扩展:** 每个 option 增加 linked 商品信息。
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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...) | 不处理深层引用;linked 商品详情页不展示其自身的 SKU(或仅展示 1 层) |
|
|
|
+| 自定义 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 的任何行为
|
|
|
+- 不做数据迁移
|