# 商品可选赠品功能设计 - **日期**: 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 giftItemIds; // 选中的赠品项ID列表(来自 product_gift_items.id) ``` **`ProductDTO`** 新增字段: ```java // 赠品配置(当前用户等级对应的配置,如无匹配则为空数组) private List giftConfigs; ``` 新增内部 VO: ```java @Data public static class GiftConfigVO { private String levelCode; private String levelName; private Integer status; // 1启用 0停用 private List 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 saveGiftConfig(Long productId, String levelCode, Integer status, List items) // 查询商品全部赠品配置(管理端用,返回所有等级) public List listConfigsByProduct(Long productId) // 查询某商品某等级的可用赠品项(含库存校验,供商品详情页展示) public List getAvailableItems(Long productId, String levelCode) // 下单时写入订单赠品记录(create 流程调用,非事务内) public void recordGiftSelection(Long orderId, String orderNo, List 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 值类型 |