# 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...) | 不处理深层引用;父商品页面上的简介卡片仅展示 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 的任何行为 - 不做数据迁移