2026-09-02-product-gift-selection-design.md 16 KB

商品可选赠品功能设计

  • 日期: 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 — 商品 × 会员等级 赠品配置(一商品可配多个等级)

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 — 单个配置下的可选赠品项

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 — 订单选中的赠品记录(下单时写入,支付成功后发放)

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 新增字段:

private List<Long> giftItemIds; // 选中的赠品项ID列表(来自 product_gift_items.id)

ProductDTO 新增字段:

// 赠品配置(当前用户等级对应的配置,如无匹配则为空数组)
private List<GiftConfigVO> giftConfigs;

新增内部 VO:

@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):

{
  "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 核心方法

// 保存赠品配置(幂等:先删后插 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)

// 迁移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 值类型