Эх сурвалжийг харах

docs: 商品可选赠品功能设计稿(product-gift-selection-design)

- 新增 specs/2026-09-02-product-gift-selection-design.md:
  - 3张新表:product_gift_configs / product_gift_items / product_order_gifts
  - 与现有 product_gift_rules(赠送会员)完全独立
  - 赠品类型:商品 或 CF值,按会员等级配置,用户可多选
  - 下单时写入订单赠品记录(fulfilled=0),支付成功后发放
- 更新 PROJECT-OVERVIEW.md §4.2 电商与供应体系,记录新设计稿
E2E Test Bot 2 долоо хоног өмнө
parent
commit
65513ea822

+ 1 - 0
docs/superpowers/PROJECT-OVERVIEW.md

@@ -197,6 +197,7 @@
 | 供应商订单修复 | Phase 4 | 🟡 计划中 | `plans/2026-06-20-danshop-order-fix.md` |
 | SKU 价格单位统一分 + 规格选择响应式修复 | Phase 4 | ✅ 已实施(7 Tasks:实体/DTO/订单/购物车/迁移/管理端/小程序,5 commits) | `plans/2026-08-15-sku-price-cents-and-spec-selector.md` |
 | 库存管理(入库单+流水+盘点+预警+套餐拆库存) | Phase 4 | 🟡 设计稿 | `specs/2026-08-05-inventory-management-design.md` |
+| 商品可选赠品(按会员等级配置赠品池:赠品商品/CF值,购买时可多选) | Phase 4 | 🟡 设计稿 | `specs/2026-09-02-product-gift-selection-design.md` |
 
 ---
 

+ 383 - 0
docs/superpowers/specs/2026-09-02-product-gift-selection-design.md

@@ -0,0 +1,383 @@
+# 商品可选赠品功能设计
+
+- **日期**: 2026-09-02
+- **状态**: 设计稿
+- **关联**: 已有 `product_gift_rules` 表(赠送会员功能,本功能不复用);本功能新增 `product_gift_configs` / `product_gift_items` / `product_order_gifts` 三张表
+- **业务目标**: 运营指定某商品,配置不同会员等级下可选的赠品集合(含其他商品 或 CF值);用户购买时按自身等级从赠品池中勾选一个或多个(每项限选1次);支付成功后发放对应赠品
+
+---
+
+## 1. 需求背景
+
+### 已有功能(不复用)
+
+`product_gift_rules` 实现的是"购买→自动赠送会员等级"功能(无用户选择、无CF值、无赠品商品)。本功能是完全不同的业务场景,表结构和接口独立。
+
+### 本功能
+
+| 决策点 | 结论 |
+|--------|------|
+| 等级粒度 | 商品 × 多个等级,各配一套赠品(同一商品可为 FAMILY/PREMIUM 分别配置不同赠品池) |
+| 选择规则 | 用户可多选,每个赠品项最多选1次 |
+| 等级判定 | 以家庭创建者(creatorId)的有效会员等级为准 |
+| 赠品商品限制 | **无**(赠品商品只要启用即可选,不要求购买者等级匹配赠品商品的 `memberEligible`) |
+| CF 值赠品 | 类型 `cf` 的赠品,支付成功时调用 `PlatformPointsService.earn(buyerId, cfValue, ...)` 发放到购买者本人账户 |
+| 实物赠品 | 扣减赠品商品库存,写入 `product_order_gifts` 记录,随主商品统一发货;不产生CF值变化 |
+| 退款 | 本期不回收 CF 赠品,实物赠品库存不回滚(与现有逻辑一致) |
+| 原价购买 | 不限,积分/优惠券购买都可享受赠品 |
+
+---
+
+## 2. 数据模型
+
+### 2.1 新表定义
+
+**`product_gift_configs`** — 商品 × 会员等级 赠品配置(一商品可配多个等级)
+
+```sql
+CREATE TABLE IF NOT EXISTS product_gift_configs (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    product_id BIGINT NOT NULL COMMENT '关联products表',
+    level_code VARCHAR(50) NOT NULL COMMENT '会员等级编码(FAMILY/PREMIUM等)',
+    status TINYINT(1) DEFAULT 1 COMMENT '1=启用 0=停用',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_product_level (product_id, level_code),
+    INDEX idx_product_id (product_id),
+    INDEX idx_level_code (level_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠品配置(商品×等级)';
+```
+
+**`product_gift_items`** — 单个配置下的可选赠品项
+
+```sql
+CREATE TABLE IF NOT EXISTS product_gift_items (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    config_id BIGINT NOT NULL COMMENT '关联product_gift_configs.id',
+    gift_type VARCHAR(20) NOT NULL COMMENT 'product=赠品商品 cf=CF值',
+    gift_product_id BIGINT COMMENT '赠品商品ID(gift_type=product时必填)',
+    gift_product_name VARCHAR(200) COMMENT '赠品商品名快照',
+    gift_product_image VARCHAR(500) COMMENT '赠品商品图快照',
+    cf_value INT DEFAULT 0 COMMENT 'CF值数量(gift_type=cf时必填,单位分)',
+    sort_order INT DEFAULT 0 COMMENT '排序(值越小越靠前)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_config_id (config_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠品项';
+```
+
+**`product_order_gifts`** — 订单选中的赠品记录(**下单时写入**,支付成功后发放)
+
+```sql
+CREATE TABLE IF NOT EXISTS product_order_gifts (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    order_id BIGINT NOT NULL COMMENT '关联product_orders.id',
+    order_no VARCHAR(64) NOT NULL COMMENT '订单号快照',
+    gift_type VARCHAR(20) NOT NULL COMMENT 'product=赠品商品 cf=CF值',
+    gift_product_id BIGINT COMMENT '赠品商品ID',
+    gift_product_name VARCHAR(200) COMMENT '赠品商品名快照',
+    gift_product_image VARCHAR(500) COMMENT '赠品商品图快照',
+    cf_value INT DEFAULT 0 COMMENT 'CF值数量(gift_type=cf时)',
+    quantity INT DEFAULT 1 COMMENT '数量(默认1,可选扩展)',
+    fulfilled TINYINT(1) DEFAULT 0 COMMENT '0=已选未发放 1=已发放(支付成功置1)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_order_id (order_id),
+    INDEX idx_order_no (order_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单赠品记录';
+```
+
+### 2.2 DTO 变更
+
+**`CreateProductOrderDTO`** 新增字段:
+```java
+private List<Long> giftItemIds; // 选中的赠品项ID列表(来自 product_gift_items.id)
+```
+
+**`ProductDTO`** 新增字段:
+```java
+// 赠品配置(当前用户等级对应的配置,如无匹配则为空数组)
+private List<GiftConfigVO> giftConfigs;
+```
+
+新增内部 VO:
+```java
+@Data
+public static class GiftConfigVO {
+    private String levelCode;
+    private String levelName;
+    private Integer status;     // 1启用 0停用
+    private List<GiftItemVO> items;
+}
+
+@Data
+public static class GiftItemVO {
+    private Long id;
+    private String giftType;    // product / cf
+    private Long giftProductId;
+    private String giftProductName;
+    private String giftProductImage;
+    private Integer cfValue;
+    private Integer sortOrder;
+}
+```
+
+### 2.3 索引与约束说明
+
+- `product_gift_configs.uk_product_level`:同一商品同一等级只允许一条配置
+- `product_gift_items` 无唯一约束(一配置下可含多个赠品项)
+- `product_order_gifts` 允许重复(一个订单同一赠品可多次出现,由业务逻辑保证不重复选)
+
+---
+
+## 3. 后端实现
+
+### 3.1 新增文件
+
+| 文件 | 说明 |
+|------|------|
+| `entity/ProductGiftConfig.java` | 配置实体 |
+| `entity/ProductGiftItem.java` | 赠品项实体 |
+| `entity/ProductOrderGift.java` | 订单赠品记录实体 |
+| `mapper/ProductGiftConfigMapper.java` | Mapper |
+| `mapper/ProductGiftItemMapper.java` | Mapper |
+| `mapper/ProductOrderGiftMapper.java` | Mapper |
+| `service/ProductGiftService.java` | 赠品配置管理 + 赠品发放 |
+| `dto/ProductDTO.GiftConfigVO.java` | 内嵌 VO(已在 §2.2 定义) |
+| `dto/ProductDTO.GiftItemVO.java` | 内嵌 VO |
+| 修改 `dto/CreateProductOrderDTO.java` | 新增 `giftItemIds` 字段 |
+| 修改 `dto/ProductDTO.java` | 新增 `giftConfigs` 字段 |
+
+### 3.2 接口设计
+
+**管理端**(继承现有 `AdminProductGiftRuleController`,或新增独立 Controller):
+
+| 端点 | 说明 |
+|------|------|
+| `POST /api/admin/product-gift/save` | 保存某商品某等级的赠品配置(含赠品项列表,幂等:先删后插) |
+| `POST /api/admin/product-gift/list` | 查询某商品的全部等级赠品配置 |
+| `POST /api/admin/product-gift/delete` | 删除某商品某等级的赠品配置(status=0 或直接删除,按业务选择) |
+
+请求体(save):
+```json
+{
+  "productId": 123,
+  "levelCode": "FAMILY",
+  "status": 1,
+  "items": [
+    {"id": 1, "giftType": "product", "giftProductId": 456, "sortOrder": 0},
+    {"id": 2, "giftType": "cf", "cfValue": 1000, "sortOrder": 1}
+  ]
+}
+```
+- `id` 为 null 表示新增,有值表示更新现有项
+- 赠品商品(`giftType=product`)必须 `status=enabled`(启用状态)
+- 不校验赠品商品的 `memberEligible`(赠品不受等级限制)
+
+**商品详情**(复用现有 `/api/product/detail`,在 `ProductService.detail()` 中扩展):
+
+详情组装时查询 `product_gift_configs`(status=1),按当前用户等级过滤:
+- 若用户等级有匹配的配置 → `giftConfigs` 仅包含该等级配置
+- 若用户等级无匹配 → `giftConfigs` 为空
+- 每个配置项展开 `giftItems`
+
+### 3.3 下单流程变更
+
+`ProductOrderService.create(dto, buyerId)` 新增赠品校验:
+
+```
+1. 查询 user 的家庭 creatorId
+2. 调用 MembershipService.getMemberLevel(creatorId) 获取家庭等级
+3. 查询 product_gift_configs(productId 匹配 && levelCode 匹配 && status=1)
+4. 若存在有效配置(不强制选择,用户可选0个或多个赠品项,每项限1次):
+   - 若 dto.giftItemIds 非空:
+     - 校验每个 giftItemId 属于该配置下的 item(防越权)
+     - 校验赠品商品库存充足(giftType=product 时)
+   - 若 dto.giftItemIds 为空:无需赠品,跳过
+5. 校验通过后,**立即写入 `product_order_gifts`**(fulfilled=0),记录用户选择的赠品项快照;
+   若未选赠品,则 product_order_gifts 无记录。
+```
+
+**关键约束**:giftItemId 必须属于当前用户等级对应的配置(防其他等级越权)。
+
+### 3.4 支付成功流程(handlePaymentSuccess)
+
+在现有赠送会员逻辑之后追加赠品发放(第 706-720 行附近):
+
+```
+支付成功
+  → 查询 product_order_gifts(order_id = paidOrder.id AND fulfilled=0)
+  → 遍历每条记录:
+      if gift_type = 'cf':
+          platformPointsService.earn(buyerId, cfValue, 
+              "product_gift", orderId, "购买商品赠品: " + giftProductName)
+      if gift_type = 'product':
+          decreaseStock(giftProductId, quantity)
+          inventoryService.recordOutbound(giftProductId, null, quantity,
+              "gift", orderId, null, "商品赠品:" + orderNo)
+      → 置 fulfilled=1
+  → 异常捕获:log.error,不抛错(与现有赠送会员失败处理一致)
+```
+
+**为什么在 create 时写入、在支付成功时发放:**
+- create 时写入:确保订单持久化记录了用户选择,即便用户未支付,历史仍可追溯
+- 支付成功时发放:与主商品库存扣减时机一致,取消/退款不影响已发放赠品
+
+### 3.5 ProductGiftService 核心方法
+
+```java
+// 保存赠品配置(幂等:先删后插 items;config 行更新或插入)
+@Transactional
+public Result<Void> saveGiftConfig(Long productId, String levelCode, Integer status, List<GiftItemDTO> items)
+
+// 查询商品全部赠品配置(管理端用,返回所有等级)
+public List<GiftConfigVO> listConfigsByProduct(Long productId)
+
+// 查询某商品某等级的可用赠品项(含库存校验,供商品详情页展示)
+public List<GiftItemVO> getAvailableItems(Long productId, String levelCode)
+
+// 下单时写入订单赠品记录(create 流程调用,非事务内)
+public void recordGiftSelection(Long orderId, String orderNo, List<Long> giftItemIds)
+
+// 支付成功时发放赠品
+public void fulfillGifts(Long orderId, String orderNo, Long buyerId)
+```
+
+---
+
+## 4. 前端实现
+
+### 4.1 小程序 — 商品详情页
+
+`pages/discover-detail/product-detail/product-detail.vue`
+
+- 若 `product.giftConfigs` 非空,在价格区下方展示"购买即送"区块
+- 按家庭等级显示对应赠品配置
+- 赠品项以卡片形式展示(商品类:缩略图+名称;CF类:CF图标+数量)
+- 支持多选,底部显示已选数量
+- 提交订单时带 `giftItemIds` 字段
+
+### 4.2 小程序 — 下单页
+
+`CreateProductOrderDTO` 接收 `giftItemIds`,下单时透传。
+
+### 4.3 Web 管理端 — 商品编辑
+
+`cfc-web/src/views/admin/ProductEdit.vue`
+
+- 新增"赠品配置"区块
+- 等级 Tab:遍历 `membership_levels`(过滤 FREE),显示各等级配置
+- 每个等级下:赠品项列表(商品选择器 + CF值输入框),可增删排序
+- 保存时调 `POST /api/admin/product-gift/save`
+
+### 4.4 Web 管理端 — 赠品配置独立管理(可选后续)
+
+暂不需要独立管理页,赠品配置随商品编辑一并管理。后续如配置复杂可扩展独立页面。
+
+---
+
+## 5. 边界情况
+
+| 场景 | 行为 |
+|------|------|
+| 家庭等级为 FREE | 不展示赠品选择区(无对应配置) |
+| 赠品配置停用(status=0) | 不展示 |
+| 赠品商品库存不足 | 下单校验失败,提示"赠品商品库存不足" |
+| 赠品商品已下架 | 商品详情页不展示该赠品项 |
+| 用户选中的赠品项不属于其等级配置 | 下单校验失败,拒绝(防越权) |
+| 赠品发放异常(如库存扣减失败) | 记日志,订单支付状态不变,已发放的赠品不可逆(与退款不回收赠品一致) |
+| 取消/退款 | 不回收 CF 赠品,不回滚实物赠品库存;product_order_gifts.fulfilled 已置1则保持不变 |
+| 未选赠品(用户主动不选) | 下单正常完成,product_order_gifts 无记录 |
+| 一商品多等级配置 | 用户按自身等级只看对应配置 |
+
+---
+
+## 6. 迁移(DatabaseInitializer)
+
+```java
+// 迁移N: 创建 product_gift_configs 表(商品可选赠品配置)
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_gift_configs (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "product_id BIGINT NOT NULL, " +
+            "level_code VARCHAR(50) NOT NULL, " +
+            "status TINYINT(1) DEFAULT 1, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "UNIQUE KEY uk_product_level (product_id, level_code), " +
+            "INDEX idx_product_id (product_id), " +
+            "INDEX idx_level_code (level_code)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠品配置'");
+    log.info("已创建product_gift_configs表");
+} catch (Exception e) {
+    // 表已存在,忽略
+}
+
+// 迁移N+1: 创建 product_gift_items 表(商品赠品项)
+// 迁移N+2: 创建 product_order_gifts 表(订单赠品记录)
+```
+
+同步更新 `schema.sql`。
+
+---
+
+## 7. 验证
+
+- 后端:`mvn clean compile` 通过
+- 小程序:`npm run build:mp-weixin` 通过
+- 管理端:`npm run build` 通过
+- 人工验证:
+  - 配置 FAMILY 等级赠品 →  FAMILY 用户下单可见并可选
+  - PREMIUM 用户下单可见 PREMIUM 等级赠品(与 FAMILY 不同)
+  - 选择 CF 赠品 → 支付后购买者 CF 余额增加
+  - 选择商品赠品 → 支付后赠品库存扣减,`product_order_gifts` 有记录
+  - 赠品商品库存不足 → 下单失败
+
+---
+
+## 8. 涉及文件清单
+
+### 后端(cfc-backend)
+
+| 操作 | 文件 |
+|------|------|
+| 新增 | `entity/ProductGiftConfig.java` |
+| 新增 | `entity/ProductGiftItem.java` |
+| 新增 | `entity/ProductOrderGift.java` |
+| 新增 | `mapper/ProductGiftConfigMapper.java` |
+| 新增 | `mapper/ProductGiftItemMapper.java` |
+| 新增 | `mapper/ProductOrderGiftMapper.java` |
+| 新增 | `service/ProductGiftService.java` |
+| 修改 | `dto/CreateProductOrderDTO.java`(新增 `giftItemIds`) |
+| 修改 | `dto/ProductDTO.java`(新增 `giftConfigs` 及内嵌 VO) |
+| 修改 | `service/ProductService.java`(详情组装赠品配置) |
+| 修改 | `service/ProductOrderService.java`(create 校验赠品 + handlePaymentSuccess 发放赠品) |
+| 修改 | `controller/admin/AdminProductGiftRuleController.java` 或新增 `AdminProductGiftController.java` |
+| 修改 | `config/DatabaseInitializer.java`(三张建表迁移) |
+| 修改 | `resources/schema.sql`(三张建表 SQL) |
+
+### 小程序(cfc-frontend)
+
+| 操作 | 文件 |
+|------|------|
+| 修改 | `pages/discover-detail/product-detail/product-detail.vue`(展示赠品选择区) |
+| 修改 | 下单相关页面/组件(提交 `giftItemIds`) |
+| 修改 | `utils/api.js`(新增赠品配置查询接口封装) |
+
+### 管理端(cfc-web)
+
+| 操作 | 文件 |
+|------|------|
+| 修改 | `src/views/admin/ProductEdit.vue`(赠品配置区块) |
+| 修改 | `src/api/admin.js`(新增赠品管理接口封装) |
+
+---
+
+## 9. 与现有赠送会员功能的区分
+
+| 维度 | 赠送会员(已有) | 可选赠品(本次) |
+|------|----------------|----------------|
+| 表 | `product_gift_rules` | `product_gift_configs` + `product_gift_items` |
+| 触发 | 支付成功自动赠送 | 用户主动选择后支付成功发放 |
+| 内容 | 会员等级 | 商品 或 CF值 |
+| 选择 | 无选择,系统自动 | 用户从赠品池中多选 |
+| 会员限制 | 商品→会员等级绑定 | 配置→会员等级绑定,赠品商品无等级限制 |
+| CF值 | 不涉及 | 赠品项可为 CF 值类型 |