|
|
@@ -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 值类型 |
|