Browse Source

docs: 商品可选赠品功能实施计划

- 新增 plans/2026-09-02-product-gift-selection.md(9 Tasks)
- 涵盖:数据库迁移、实体/Mapper、ProductGiftService、管理端接口、
  下单校验+写记录、支付成功发放、商品详情装配、小程序前端、管理端前端
E2E Test Bot 2 weeks ago
parent
commit
3df284a0f8
1 changed files with 1354 additions and 0 deletions
  1. 1354 0
      docs/superpowers/plans/2026-09-02-product-gift-selection.md

+ 1354 - 0
docs/superpowers/plans/2026-09-02-product-gift-selection.md

@@ -0,0 +1,1354 @@
+# 商品可选赠品功能 实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 实现商品可选赠品功能:运营为商品配置不同会员等级的赠品池(含赠品商品 或 CF值),用户购买时按自身等级从赠品池中选0个或多个,支付成功后发放对应赠品。
+
+**Architecture:** 新增三张表(`product_gift_configs` / `product_gift_items` / `product_order_gifts`),新增 `ProductGiftService` 承载赠品配置与发放逻辑;在现有下单(`ProductOrderService.create`)和支付成功(`handlePaymentSuccess`)两个节点注入赠品校验与发放;商品详情接口附带赠品配置;管理端和小程序前端分别实现配置与选择 UI。
+
+**Tech Stack:** Java 8 / Spring Boot 2.7.18 / MyBatis-Plus / Vue 2 + uni-app(小程序)/ Vue 2 + Element UI(管理端)
+
+**Spec:** `docs/superpowers/specs/2026-09-02-product-gift-selection-design.md`
+
+## Global Constraints
+
+- 统一 `@PostMapping`,禁止 `@GetMapping/@PutMapping/@DeleteMapping`
+- DI 使用 `@Resource`,字段名必须与类型默认 Bean Name 一致
+- 响应统一 `Result<T>`(code/message/data),`Result.error(String)` 返回 code=500
+- 数据库迁移在 `DatabaseInitializer.runMigrations()` 中执行,幂等(try-catch 包裹);同步更新 `schema.sql`
+- 小程序禁止可选链 `?.`(用 `&&` 替代)、禁止 CSS Grid、禁止 `:key` 表达式(用方法调用代替)
+- 所有迁移编号递增,当前最大编号见 `DatabaseInitializer.runMigrations()` 末尾搜索 `// 迁移`
+
+---
+
+## File Map
+
+| 操作 | 文件 |
+|------|------|
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftConfig.java` |
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftItem.java` |
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrderGift.java` |
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftConfigMapper.java` |
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftItemMapper.java` |
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductOrderGiftMapper.java` |
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/service/ProductGiftService.java` |
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductGiftController.java` |
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductGiftItemDTO.java` |
+| 新增 | `cfc-backend/src/main/java/com/etotem/cfc/dto/GiftConfigVO.java` |
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java` |
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java` |
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java` |
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java` |
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` |
+| 修改 | `cfc-backend/src/main/resources/schema.sql` |
+| 修改 | `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue` |
+| 修改 | `cfc-web/src/views/admin/ProductEdit.vue` |
+| 修改 | `cfc-web/src/api/admin.js` |
+
+---
+
+### Task 1: 数据库迁移(三张新表)
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+- Modify: `cfc-backend/src/main/resources/schema.sql`
+
+**Interfaces:**
+- Consumes: existing `ensureColumn` method pattern (line ~4099 in DatabaseInitializer)
+- Produces: three new tables available for use by subsequent tasks
+
+- [ ] **Step 1: 查找最新迁移编号**
+
+在 `DatabaseInitializer.java` 中搜索最后一个 `// 迁移` 注释:
+```bash
+grep -n "// 迁移" cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java | tail -5
+```
+记录最大编号 N(预计是 `迁移278` 或更高)。
+
+- [ ] **Step 2: 在 DatabaseInitializer 末尾添加三个建表迁移**
+
+在 `runMigrations()` 方法末尾(`}` 之前)追加:
+```java
+// 迁移N+1: 创建 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 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='商品赠品配置(商品×等级)'");
+    log.info("已创建product_gift_configs表");
+} catch (Exception e) {
+    // 表已存在,忽略
+}
+
+// 迁移N+2: 创建 product_gift_items 表(商品赠品项)
+try {
+    jdbcTemplate.execute("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='商品赠品项'");
+    log.info("已创建product_gift_items表");
+} catch (Exception e) {
+    // 表已存在,忽略
+}
+
+// 迁移N+3: 创建 product_order_gifts 表(订单赠品记录)
+try {
+    jdbcTemplate.execute("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=已发放', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_order_id (order_id), " +
+            "INDEX idx_order_no (order_no)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单赠品记录'");
+    log.info("已创建product_order_gifts表");
+} catch (Exception e) {
+    // 表已存在,忽略
+}
+```
+
+- [ ] **Step 3: 同步更新 schema.sql**
+
+在 `schema.sql` 中,找到 `product_gift_rules` 建表语句(约第4301行),在其**之后**追加以下三张表的建表语句(保持与迁移代码完全一致):
+```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='商品赠品配置(商品×等级)';
+
+-- 商品赠品项
+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='商品赠品项';
+
+-- 订单赠品记录
+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=已发放',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_order_id (order_id),
+    INDEX idx_order_no (order_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单赠品记录';
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+预期:BUILD SUCCESS,无编译错误。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git add cfc-backend/src/main/resources/schema.sql
+git commit -m "feat(product-gift): 建三张赠品相关表迁移(product_gift_configs/items/order_gifts)"
+```
+
+---
+
+### Task 2: 实体类与 Mapper 接口(三个实体 + 三个 Mapper)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftConfig.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftItem.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrderGift.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftConfigMapper.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftItemMapper.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductOrderGiftMapper.java`
+
+**Interfaces:**
+- Consumes: table names and column definitions from Task 1
+- Produces: three entity classes and three Mapper interfaces available for service layer injection
+
+- [ ] **Step 1: 创建 ProductGiftConfig.java**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("product_gift_configs")
+public class ProductGiftConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long productId;
+
+    private String levelCode;
+
+    private Integer status;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}
+```
+
+- [ ] **Step 2: 创建 ProductGiftItem.java**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("product_gift_items")
+public class ProductGiftItem implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long configId;
+
+    private String giftType;
+
+    private Long giftProductId;
+
+    private String giftProductName;
+
+    private String giftProductImage;
+
+    private Integer cfValue;
+
+    private Integer sortOrder;
+
+    private Date createdAt;
+}
+```
+
+- [ ] **Step 3: 创建 ProductOrderGift.java**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("product_order_gifts")
+public class ProductOrderGift implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long orderId;
+
+    private String orderNo;
+
+    private String giftType;
+
+    private Long giftProductId;
+
+    private String giftProductName;
+
+    private String giftProductImage;
+
+    private Integer cfValue;
+
+    private Integer quantity;
+
+    private Integer fulfilled;
+
+    private Date createdAt;
+}
+```
+
+- [ ] **Step 4: 创建三个 Mapper 接口(每个都与 ProductGiftRuleMapper 同构)**
+
+`ProductGiftConfigMapper.java`:
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ProductGiftConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ProductGiftConfigMapper extends BaseMapper<ProductGiftConfig> {
+}
+```
+
+`ProductGiftItemMapper.java`:
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ProductGiftItem;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ProductGiftItemMapper extends BaseMapper<ProductGiftItem> {
+}
+```
+
+`ProductOrderGiftMapper.java`:
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ProductOrderGift;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ProductOrderGiftMapper extends BaseMapper<ProductOrderGift> {
+}
+```
+
+- [ ] **Step 5: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+预期:BUILD SUCCESS。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftConfig.java
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftItem.java
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrderGift.java
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftConfigMapper.java
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftItemMapper.java
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductOrderGiftMapper.java
+git commit -m "feat(product-gift): 新增三张赠品表实体类与Mapper接口"
+```
+
+---
+
+### Task 3: ProductGiftService(配置 CRUD + 查询)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ProductGiftService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductGiftItemDTO.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/GiftConfigVO.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java`(在末尾新增 `giftConfigs` 字段及内嵌类)
+
+**Interfaces:**
+- Consumes: `ProductGiftConfigMapper`, `ProductGiftItemMapper`, `MembershipLevelMapper`, `ProductMapper`
+- Produces: `saveGiftConfig(productId, levelCode, status, items) → Result<Void>`
+  `listConfigsByProduct(productId) → List<GiftConfigVO>`
+  `getActiveItemById(itemId) → ProductGiftItem or null`
+  `getAvailableItems(productId, levelCode) → List<GiftConfigVO>`(按等级过滤且只返回启用配置)
+
+- [ ] **Step 1: 创建 ProductGiftItemDTO.java**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class ProductGiftItemDTO {
+    private Long id;           // null=新增,有值=更新
+    private String giftType;   // "product" or "cf"
+    private Long giftProductId;
+    private Integer cfValue;
+    private Integer sortOrder;
+}
+```
+
+- [ ] **Step 2: 创建 GiftConfigVO.java**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class GiftConfigVO {
+    private Long id;
+    private String levelCode;
+    private String levelName;
+    private Integer status;
+    private List<GiftItemVO> items;
+
+    @Data
+    public static class GiftItemVO {
+        private Long id;
+        private String giftType;
+        private Long giftProductId;
+        private String giftProductName;
+        private String giftProductImage;
+        private Integer cfValue;
+        private Integer sortOrder;
+    }
+}
+```
+
+- [ ] **Step 3: 创建 ProductGiftService.java**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.GiftConfigVO;
+import com.etotem.cfc.dto.ProductGiftItemDTO;
+import com.etotem.cfc.entity.MemberLevel;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.entity.ProductGiftConfig;
+import com.etotem.cfc.entity.ProductGiftItem;
+import com.etotem.cfc.mapper.MemberLevelMapper;
+import com.etotem.cfc.mapper.ProductGiftConfigMapper;
+import com.etotem.cfc.mapper.ProductGiftItemMapper;
+import com.etotem.cfc.mapper.ProductMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class ProductGiftService {
+
+    @Resource
+    private ProductGiftConfigMapper configMapper;
+    @Resource
+    private ProductGiftItemMapper itemMapper;
+    @Resource
+    private ProductMapper productMapper;
+    @Resource
+    private MemberLevelMapper levelMapper;
+
+    /** 保存赠品配置(幂等:先删后插) */
+    @Transactional
+    public Result<Void> saveGiftConfig(Long productId, String levelCode, Integer status, List<ProductGiftItemDTO> items) {
+        if (productId == null || levelCode == null || status == null) {
+            return Result.error("参数不完整");
+        }
+        // 校验等级存在且非 FREE
+        MemberLevel level = levelMapper.selectOne(new LambdaQueryWrapper<MemberLevel>()
+                .eq(MemberLevel::getLevelCode, levelCode));
+        if (level == null || "FREE".equals(level.getLevelCode())) {
+            return Result.error("无效的赠送等级");
+        }
+        // 校验赠品商品(giftType=product)存在且启用
+        if (items != null) {
+            for (ProductGiftItemDTO it : items) {
+                if ("product".equals(it.getGiftType()) && it.getGiftProductId() != null) {
+                    Product giftProduct = productMapper.selectById(it.getGiftProductId());
+                    if (giftProduct == null || !"on_shelf".equals(giftProduct.getStatus())) {
+                        return Result.error("赠品商品不存在或未上架");
+                    }
+                }
+            }
+        }
+        // 先删后插(整个 config + 其下所有 items)
+        configMapper.delete(new LambdaQueryWrapper<ProductGiftConfig>()
+                .eq(ProductGiftConfig::getProductId, productId)
+                .eq(ProductGiftConfig::getLevelCode, levelCode));
+        if (status == 1) {
+            ProductGiftConfig config = new ProductGiftConfig();
+            config.setProductId(productId);
+            config.setLevelCode(levelCode);
+            config.setStatus(1);
+            config.setCreatedAt(new Date());
+            config.setUpdatedAt(new Date());
+            configMapper.insert(config);
+            if (items != null) {
+                for (ProductGiftItemDTO it : items) {
+                    ProductGiftItem item = new ProductGiftItem();
+                    item.setConfigId(config.getId());
+                    item.setGiftType(it.getGiftType());
+                    item.setGiftProductId(it.getGiftProductId());
+                    item.setCfValue(it.getCfValue() != null ? it.getCfValue() : 0);
+                    item.setSortOrder(it.getSortOrder() != null ? it.getSortOrder() : 0);
+                    // 快照商品名称和图
+                    if (it.getGiftProductId() != null) {
+                        Product gp = productMapper.selectById(it.getGiftProductId());
+                        if (gp != null) {
+                            item.setGiftProductName(gp.getName());
+                            item.setGiftProductImage(gp.getCoverImage());
+                        }
+                    }
+                    item.setCreatedAt(new Date());
+                    itemMapper.insert(item);
+                }
+            }
+        }
+        return Result.success(null);
+    }
+
+    /** 查询商品全部等级赠品配置(管理端用) */
+    public List<GiftConfigVO> listConfigsByProduct(Long productId) {
+        List<ProductGiftConfig> configs = configMapper.selectList(
+                new LambdaQueryWrapper<ProductGiftConfig>()
+                        .eq(ProductGiftConfig::getProductId, productId)
+                        .orderByDesc(ProductGiftConfig::getStatus));
+        return toVOList(configs);
+    }
+
+    /** 查询某商品某等级下启用的赠品项(含库存充足校验,供商品详情展示) */
+    public List<GiftConfigVO> getActiveConfigsByProductAndLevel(Long productId, String levelCode) {
+        ProductGiftConfig config = configMapper.selectOne(new LambdaQueryWrapper<ProductGiftConfig>()
+                .eq(ProductGiftConfig::getProductId, productId)
+                .eq(ProductGiftConfig::getLevelCode, levelCode)
+                .eq(ProductGiftConfig::getStatus, 1)
+                .last("LIMIT 1"));
+        if (config == null) {
+            return Collections.emptyList();
+        }
+        List<ProductGiftItem> items = itemMapper.selectList(
+                new LambdaQueryWrapper<ProductGiftItem>()
+                        .eq(ProductGiftItem::getConfigId, config.getId())
+                        .orderByAsc(ProductGiftItem::getSortOrder));
+        GiftConfigVO vo = new GiftConfigVO();
+        vo.setId(config.getId());
+        vo.setLevelCode(config.getLevelCode());
+        MemberLevel level = levelMapper.selectOne(new LambdaQueryWrapper<MemberLevel>()
+                .eq(MemberLevel::getLevelCode, levelCode));
+        vo.setLevelName(level != null ? level.getLevelName() : levelCode);
+        vo.setStatus(config.getStatus());
+        List<GiftConfigVO.GiftItemVO> itemVOs = new ArrayList<>();
+        for (ProductGiftItem it : items) {
+            GiftConfigVO.GiftItemVO itemVO = new GiftConfigVO.GiftItemVO();
+            itemVO.setId(it.getId());
+            itemVO.setGiftType(it.getGiftType());
+            itemVO.setGiftProductId(it.getGiftProductId());
+            itemVO.setGiftProductName(it.getGiftProductName());
+            itemVO.setGiftProductImage(it.getGiftProductImage());
+            itemVO.setCfValue(it.getCfValue() != null ? it.getCfValue() : 0);
+            itemVO.setSortOrder(it.getSortOrder());
+            // 库存充足校验:实物赠品必须库存>0
+            if ("product".equals(it.getGiftType()) && it.getGiftProductId() != null) {
+                Product gp = productMapper.selectById(it.getGiftProductId());
+                if (gp == null || gp.getStock() == null || gp.getStock() <= 0) {
+                    continue; // 库存不足则不展示
+                }
+            }
+            itemVOs.add(itemVO);
+        }
+        vo.setItems(itemVOs);
+        return Collections.singletonList(vo);
+    }
+
+    /** 根据 ID 查询赠品项(含 config 归属校验) */
+    public ProductGiftItem getActiveItemById(Long itemId) {
+        return itemMapper.selectById(itemId);
+    }
+
+    private List<GiftConfigVO> toVOList(List<ProductGiftConfig> configs) {
+        List<GiftConfigVO> result = new ArrayList<>();
+        for (ProductGiftConfig cfg : configs) {
+            GiftConfigVO vo = new GiftConfigVO();
+            vo.setId(cfg.getId());
+            vo.setLevelCode(cfg.getLevelCode());
+            MemberLevel level = levelMapper.selectOne(new LambdaQueryWrapper<MemberLevel>()
+                    .eq(MemberLevel::getLevelCode, cfg.getLevelCode()));
+            vo.setLevelName(level != null ? level.getLevelName() : cfg.getLevelCode());
+            vo.setStatus(cfg.getStatus());
+            List<ProductGiftItem> items = itemMapper.selectList(
+                    new LambdaQueryWrapper<ProductGiftItem>()
+                            .eq(ProductGiftItem::getConfigId, cfg.getId())
+                            .orderByAsc(ProductGiftItem::getSortOrder));
+            List<GiftConfigVO.GiftItemVO> itemVOs = new ArrayList<>();
+            for (ProductGiftItem it : items) {
+                GiftConfigVO.GiftItemVO itemVO = new GiftConfigVO.GiftItemVO();
+                itemVO.setId(it.getId());
+                itemVO.setGiftType(it.getGiftType());
+                itemVO.setGiftProductId(it.getGiftProductId());
+                itemVO.setGiftProductName(it.getGiftProductName());
+                itemVO.setGiftProductImage(it.getGiftProductImage());
+                itemVO.setCfValue(it.getCfValue() != null ? it.getCfValue() : 0);
+                itemVO.setSortOrder(it.getSortOrder());
+                itemVOs.add(itemVO);
+            }
+            vo.setItems(itemVOs);
+            result.add(vo);
+        }
+        return result;
+    }
+}
+```
+
+注意:项目中会员等级实体名为 `MembershipLevel`(`entity/MembershipLevel.java`),不是 `MemberLevel`。请将上述代码中所有 `MemberLevel` 替换为 `MembershipLevel`,`MemberLevelMapper` 替换为 `MembershipLevelMapper`,字段 `levelCode` / `levelName` 对应 `getLevelCode()` / `getLevelName()`。
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+预期:BUILD SUCCESS。如有编译错误,根据错误信息调整字段名和 import。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductGiftService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/ProductGiftItemDTO.java
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/GiftConfigVO.java
+git commit -m "feat(product-gift): 新增 ProductGiftService(配置保存/查询逻辑)"
+```
+
+---
+
+### Task 4: Admin Controller(赠品配置管理接口)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductGiftController.java`
+
+**Interfaces:**
+- Consumes: `ProductGiftService.saveGiftConfig`, `ProductGiftService.listConfigsByProduct`
+- Produces: `POST /api/admin/product-gift/save`, `POST /api/admin/product-gift/list`
+
+- [ ] **Step 1: 创建 AdminProductGiftController.java**
+
+```java
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.GiftConfigVO;
+import com.etotem.cfc.dto.ProductGiftItemDTO;
+import com.etotem.cfc.service.ProductGiftService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/product-gift")
+public class AdminProductGiftController {
+
+    @Resource
+    private ProductGiftService giftService;
+
+    /** 保存某商品某等级的赠品配置(幂等:先删后插) */
+    @PostMapping("/save")
+    public Result<Void> save(@RequestBody Map<String, Object> params) {
+        Long productId = Long.parseLong(params.get("productId").toString());
+        String levelCode = params.get("levelCode").toString();
+        Integer status = Integer.parseInt(params.get("status").toString());
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> itemsMap = (List<Map<String, Object>>) params.get("items");
+        List<ProductGiftItemDTO> items = null;
+        if (itemsMap != null) {
+            items = new java.util.ArrayList<>();
+            for (Map<String, Object> m : itemsMap) {
+                ProductGiftItemDTO dto = new ProductGiftItemDTO();
+                Object id = m.get("id");
+                dto.setId(id != null ? Long.parseLong(id.toString()) : null);
+                dto.setGiftType((String) m.get("giftType"));
+                Object gpId = m.get("giftProductId");
+                dto.setGiftProductId(gpId != null ? Long.parseLong(gpId.toString()) : null);
+                Object cf = m.get("cfValue");
+                dto.setCfValue(cf != null ? Integer.parseInt(cf.toString()) : 0);
+                Object sort = m.get("sortOrder");
+                dto.setSortOrder(sort != null ? Integer.parseInt(sort.toString()) : 0);
+                items.add(dto);
+            }
+        }
+        return giftService.saveGiftConfig(productId, levelCode, status, items);
+    }
+
+    /** 查询某商品的全部等级赠品配置(管理端回显) */
+    @PostMapping("/list")
+    public Result<List<GiftConfigVO>> list(@RequestBody Map<String, Object> params) {
+        Long productId = Long.parseLong(params.get("productId").toString());
+        return Result.success(giftService.listConfigsByProduct(productId));
+    }
+}
+```
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+预期:BUILD SUCCESS。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductGiftController.java
+git commit -m "feat(product-gift): 新增管理端赠品配置接口(/api/admin/product-gift/save + /list)"
+```
+
+---
+
+### Task 5: ProductOrderService 修改(create 校验赠品 + 写入订单赠品记录)
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrderGift.java`(已由 Task 2 创建,无需额外修改)
+
+**Interfaces:**
+- Consumes: `ProductGiftService.getActiveItemById`, `ProductGiftService.getActiveConfigsByProductAndLevel`
+- Consumes: `MembershipService.getMemberLevel(userId)` — 返回家庭创建者等级字符串
+- Consumes: `ProductOrderMapper.insert(order)` result (order.getId())
+- Produces: `product_order_gifts` 表中记录 `order_id`, `order_no`, `gift_type`, `gift_product_id`, `gift_product_name`, `gift_product_image`, `cf_value`, `fulfilled=0`
+
+- [ ] **Step 1: CreateProductOrderDTO 新增字段**
+
+在 `CreateProductOrderDTO.java` 末尾(`purchaseInfo` 字段之后)追加:
+```java
+    private List<Long> giftItemIds; // 选中的赠品项ID列表(来自 product_gift_items.id)
+
+    public List<Long> getGiftItemIds() { return giftItemIds; }
+    public void setGiftItemIds(List<Long> giftItemIds) { this.giftItemIds = giftItemIds; }
+```
+
+- [ ] **Step 2: ProductOrderService 注入依赖**
+
+在 `ProductOrderService.java` 的 `@Resource` 字段区域,追加:
+```java
+    @Resource
+    private ProductGiftService productGiftService;
+```
+
+- [ ] **Step 3: 在 create 方法末尾(insert 之后)追加赠品逻辑**
+
+在 `orderMapper.insert(order)` 之后(约第250行)、`return Result.success(...)` 之前,插入:
+```java
+        // ===== 赠品校验与记录 =====
+        if (dto.getGiftItemIds() != null && !dto.getGiftItemIds().isEmpty()) {
+            // 获取家庭创建者等级
+            Long familyId = order.getFamilyId();
+            String userLevel = null;
+            if (familyId != null) {
+                // 复用 MembershipService 中已有的 getMemberLevel 或 getCurrentLevel 逻辑
+                // 根据 MembershipService.getCurrentLevel(familyId) 返回的 MembershipLevelDTO.getLevelCode()
+                // 这里调用 membershipService.getCurrentLevel(familyId) 的 levelCode
+                userLevel = membershipService.getCurrentLevel(familyId) != null
+                        ? membershipService.getCurrentLevel(familyId).getLevelCode()
+                        : null;
+            }
+            if (userLevel != null && !"FREE".equals(userLevel)) {
+                // 查询该商品该等级下启用的赠品项(含库存校验)
+                List<GiftConfigVO> activeConfigs = productGiftService.getActiveConfigsByProductAndLevel(
+                        product.getId(), userLevel);
+                if (!activeConfigs.isEmpty()) {
+                    Set<Long> allowedItemIds = activeConfigs.stream()
+                            .flatMap(c -> c.getItems().stream())
+                            .map(GiftConfigVO.GiftItemVO::getId)
+                            .collect(Collectors.toSet());
+                    for (Long itemId : dto.getGiftItemIds()) {
+                        if (!allowedItemIds.contains(itemId)) {
+                            return Result.error("赠品项不存在或超出权限");
+                        }
+                        ProductGiftItem giftItem = productGiftService.getActiveItemById(itemId);
+                        if (giftItem == null) {
+                            return Result.error("赠品项不存在");
+                        }
+                        ProductOrderGift orderGift = new ProductOrderGift();
+                        orderGift.setOrderId(order.getId());
+                        orderGift.setOrderNo(order.getOrderNo());
+                        orderGift.setGiftType(giftItem.getGiftType());
+                        orderGift.setGiftProductId(giftItem.getGiftProductId());
+                        orderGift.setGiftProductName(giftItem.getGiftProductName());
+                        orderGift.setGiftProductImage(giftItem.getGiftProductImage());
+                        orderGift.setCfValue(giftItem.getCfValue() != null ? giftItem.getCfValue() : 0);
+                        orderGift.setQuantity(1);
+                        orderGift.setFulfilled(0);
+                        orderGift.setCreatedAt(new Date());
+                        productOrderGiftMapper.insert(orderGift);
+                    }
+                } else {
+                    return Result.error("您所在等级暂无该商品赠品,请联系客服");
+                }
+            } else {
+                return Result.error("您暂无会员等级,无法享受赠品");
+            }
+        }
+```
+
+注意:需要在文件顶部 import 中追加:
+```java
+import com.etotem.cfc.dto.GiftConfigVO;
+import com.etotem.cfc.entity.ProductGiftItem;
+import com.etotem.cfc.entity.ProductOrderGift;
+import com.etotem.cfc.mapper.ProductOrderGiftMapper;
+import com.etotem.cfc.service.ProductGiftService;
+import com.etotem.cfc.service.MembershipService;
+import java.util.Set;
+```
+
+并在 `@Resource` 区域追加:
+```java
+    @Resource
+    private ProductOrderGiftMapper productOrderGiftMapper;
+    @Resource
+    private MembershipService membershipService;
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+根据编译错误修正字段名/方法名(`MembershipService.getCurrentLevel` 的返回类型需确认,如返回 `MembershipLevelDTO` 则字段名可能是 `getLevelCode()`,如返回 `MembershipLevel` 则直接调用 `getLevelCode()`)。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
+git commit -m "feat(product-gift): create 流程注入赠品校验与 product_order_gifts 记录"
+```
+
+---
+
+### Task 6: ProductOrderService.handlePaymentSuccess 追加赠品发放
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java`
+
+**Interfaces:**
+- Consumes: `productOrderGiftMapper.selectList(fulfilled=0)`, `platformPointsService.earn()`, `productService.decreaseStock()`, `inventoryService.recordOutbound()`
+- Produces: `product_order_gifts.fulfilled=1` after successful fulfillment
+
+- [ ] **Step 1: 定位 handlePaymentSuccess 中现有赠品会员逻辑位置**
+
+在 `ProductOrderService.java` 中搜索:
+```java
+ProductGiftRule giftRule = productGiftRuleService.getActiveByProductId
+```
+该行约在第706行。在其**之后**(约第718行 `log.info("商品订单...")` 之后)插入赠品发放逻辑。
+
+- [ ] **Step 2: 插入赠品发放代码**
+
+```java
+        // ===== 发放订单赠品(CF值 + 实物库存扣减) =====
+        try {
+            List<ProductOrderGift> pendingGifts = productOrderGiftMapper.selectList(
+                    new LambdaQueryWrapper<ProductOrderGift>()
+                            .eq(ProductOrderGift::getOrderId, order.getId())
+                            .eq(ProductOrderGift::getFulfilled, 0));
+            for (ProductOrderGift gift : pendingGifts) {
+                if ("cf".equals(gift.getGiftType()) && gift.getCfValue() != null && gift.getCfValue() > 0) {
+                    platformPointsService.earn(order.getBuyerId(), gift.getCfValue(),
+                            "product_gift", order.getId(),
+                            "购买商品赠品: " + (gift.getGiftProductName() != null ? gift.getGiftProductName() : "CF值"));
+                } else if ("product".equals(gift.getGiftType()) && gift.getGiftProductId() != null) {
+                    boolean stockOk = productService.decreaseStock(gift.getGiftProductId(), gift.getQuantity() != null ? gift.getQuantity() : 1);
+                    if (!stockOk) {
+                        log.error("赠品库存扣减失败: orderNo={}, giftProductId={}", orderNo, gift.getGiftProductId());
+                    } else {
+                        inventoryService.recordOutbound(gift.getGiftProductId(), null,
+                                gift.getQuantity() != null ? gift.getQuantity() : 1,
+                                "gift", order.getId(), null, "商品赠品:" + orderNo);
+                    }
+                }
+                gift.setFulfilled(1);
+                gift.setUpdatedAt(new Date());
+                productOrderGiftMapper.updateById(gift);
+            }
+        } catch (Exception e) {
+            log.error("订单{}赠品发放异常: {}", orderNo, e.getMessage());
+        }
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+预期:BUILD SUCCESS。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
+git commit -m "feat(product-gift): handlePaymentSuccess 追加赠品发放逻辑"
+```
+
+---
+
+### Task 7: ProductService.detail 附带赠品配置
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java`
+
+**Interfaces:**
+- Consumes: `ProductGiftService.getActiveConfigsByProductAndLevel(productId, userLevel)`
+- Produces: `ProductDTO.giftConfigs` 字段(当前用户等级对应配置列表)
+
+- [ ] **Step 1: ProductDTO 新增 giftConfigs 字段**
+
+在 `ProductDTO.java` 末尾 `giftLevelName` 字段之后追加:
+```java
+    // 商品可选赠品配置(当前用户等级对应,空数组=无赠品)
+    private List<GiftConfigVO> giftConfigs;
+
+    // 内嵌 GiftConfigVO(与 dto/GiftConfigVO 同构,避免额外 import)
+    @Data
+    public static class GiftConfigVO {
+        private Long id;
+        private String levelCode;
+        private String levelName;
+        private Integer status;
+        private List<GiftItemVO> items;
+
+        @Data
+        public static class GiftItemVO {
+            private Long id;
+            private String giftType;
+            private Long giftProductId;
+            private String giftProductName;
+            private String giftProductImage;
+            private Integer cfValue;
+            private Integer sortOrder;
+        }
+    }
+```
+
+注意:`@Data` 注解来自 `lombok.Data`,`ProductDTO` 已有 `import lombok.Data`,无需新增 import。
+
+- [ ] **Step 2: ProductService.detail 组装赠品配置**
+
+在 `ProductService.java` 的 `detail` 方法中,现有赠品会员逻辑(约第163行 `ProductGiftRule giftRule = ...` 块)**之后**插入:
+```java
+        // 可选赠品配置(当前用户等级)
+        try {
+            Long currentUserId = ...; // 从 JWT 上下文获取,参考现有 detail 方法签名
+            // detail(Long id) 无 userId 参数,需改为 detail(Long id, Long userId)
+            // 参考现有调用方式:ProductController.detail(@PathVariable 或 @RequestParam)
+            // 实际调用链:ProductController.detail(@PathVariable Long id) → 调用 productMapper.selectById 等
+            // 改为从 @RequestAttribute("userId") 获取当前用户
+        } catch (Exception e) {
+            log.debug("获取赠品配置失败: {}", e.getMessage());
+        }
+```
+
+**重要:`ProductService.detail(Long id)` 目前无 userId 参数。** 需要:
+1. 修改 `ProductService.detail` 方法签名:`public Result<ProductDTO> detail(Long id, Long userId)`
+2. 修改 `ProductController.detail` 调用:从 `@RequestAttribute("userId") Long userId` 获取
+3. 在 detail 方法中组装赠品配置:
+
+```java
+        if (userId != null) {
+            // 获取用户家庭创建者等级
+            Long familyId = userMapper.selectById(userId).getFamilyId();
+            String userLevel = null;
+            if (familyId != null) {
+                MembershipLevelDTO currentLevel = membershipService.getCurrentLevel(familyId);
+                if (currentLevel != null) {
+                    userLevel = currentLevel.getLevelCode();
+                }
+            }
+            if (userLevel != null && !"FREE".equals(userLevel)) {
+                List<GiftConfigVO> activeConfigs = productGiftService.getActiveConfigsByProductAndLevel(id, userLevel);
+                dto.setGiftConfigs(activeConfigs);
+            }
+        }
+```
+
+需要先确认 `MembershipService.getCurrentLevel` 返回类型。查看现有代码:
+```bash
+grep -n "getCurrentLevel\|MembershipLevelDTO" cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java | head -10
+```
+若返回 `MembershipLevelDTO`,则字段为 `getLevelCode()`;若返回 `MembershipLevel`,则直接 `getLevelCode()`。
+
+- [ ] **Step 3: 修改 ProductController.detail 传入 userId**
+
+查看 `ProductController.java` 的 detail 方法,将:
+```java
+public Result<ProductDTO> detail(@PathVariable Long id)
+```
+改为:
+```java
+public Result<ProductDTO> detail(@RequestParam("id") Long id,
+                                  @RequestAttribute(value = "userId", required = false) Long userId)
+```
+并在方法体中传递 `userId` 给 `productService.detail(id, userId)`。
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+预期:BUILD SUCCESS。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java
+git commit -m "feat(product-gift): 商品详情附带当前用户等级赠品配置"
+```
+
+---
+
+### Task 8: 小程序前端 — 商品详情页展示赠品选择
+
+**Files:**
+- Modify: `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue`
+- Modify: `cfc-frontend/utils/api.js`(或对应 API 封装文件)
+
+**Interfaces:**
+- Consumes: `POST /api/product/detail`(现有接口,已返回 `giftConfigs`)
+- Produces: 商品详情页展示"购买即送"区块,支持多选赠品,提交订单时透传 `giftItemIds`
+
+- [ ] **Step 1: 确认 API 封装位置**
+
+搜索现有商品详情 API 调用:
+```bash
+grep -rn "product/detail\|product-detail\|api.product" cfc-frontend/pages/discover-detail/ | head -5
+```
+找到对应的 API 封装文件(通常在 `utils/api.js` 或页面内联)。
+
+- [ ] **Step 2: 在商品详情页添加赠品区块**
+
+在 `product-detail.vue` 中,价格区下方(`<view class="price">` 块之后)插入:
+```vue
+        <!-- 赠品选择区 -->
+        <view class="gift-section" v-if="product.giftConfigs && product.giftConfigs.length > 0">
+            <view class="gift-title">购买即送</view>
+            <view class="gift-items">
+                <view 
+                    class="gift-item" 
+                    :class="{ selected: selectedGiftIds.indexOf(item.id) > -1 }"
+                    v-for="config in product.giftConfigs" 
+                    v-for="item in config.items"
+                    :key="getItemKey(item)"
+                    @click="toggleGift(item)"
+                >
+                    <image 
+                        v-if="item.giftType === 'product' && item.giftProductImage" 
+                        :src="item.giftProductImage" 
+                        class="gift-image"
+                        mode="aspectFill"
+                    />
+                    <view v-else class="gift-cf-icon">💎</view>
+                    <text class="gift-name">{{ item.giftType === 'product' ? item.giftProductName : item.cfValue + ' CF值' }}</text>
+                    <view class="gift-check" v-if="selectedGiftIds.indexOf(item.id) > -1">✓</view>
+                </view>
+            </view>
+            <view class="gift-footer" v-if="selectedGiftIds.length > 0">
+                已选 {{ selectedGiftIds.length }} 件赠品
+            </view>
+        </view>
+```
+
+注意:小程序禁止 `:key` 表达式,需改用方法调用:
+```vue
+:key="getItemKey(item)"
+```
+并在 `methods` 中添加:
+```js
+getItemKey: function(item) {
+    return item.id;
+}
+```
+
+- [ ] **Step 3: 添加 data 和 methods**
+
+在 `data()` 中追加:
+```js
+selectedGiftIds: []
+```
+
+在 `methods` 中追加:
+```js
+toggleGift: function(item) {
+    var ids = this.selectedGiftIds;
+    var idx = ids.indexOf(item.id);
+    if (idx > -1) {
+        ids.splice(idx, 1);
+    } else {
+        ids.push(item.id);
+    }
+},
+```
+
+- [ ] **Step 4: 下单时透传 giftItemIds**
+
+找到提交订单的函数(通常在"立即购买"或"加入购物车"按钮的点击事件中),在构建下单参数时追加:
+```js
+var params = {
+    productId: this.productId,
+    quantity: this.quantity,
+    // ... 其他字段
+    giftItemIds: this.selectedGiftIds.length > 0 ? this.selectedGiftIds : undefined
+};
+```
+
+- [ ] **Step 5: 编译验证**
+
+```bash
+cd cfc-frontend && npm run build:mp-weixin
+```
+预期:构建成功,无语法错误。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-frontend/pages/discover-detail/product-detail/product-detail.vue
+git commit -m "feat(product-gift): 商品详情页展示赠品选择区,下单透传 giftItemIds"
+```
+
+---
+
+### Task 9: Web 管理端 — 商品编辑页新增赠品配置区块
+
+**Files:**
+- Modify: `cfc-web/src/views/admin/ProductEdit.vue`
+- Modify: `cfc-web/src/api/admin.js`
+
+**Interfaces:**
+- Consumes: `POST /api/admin/product-gift/save`,`POST /api/admin/product-gift/list`,`GET /api/membership/levels`(或复用现有接口)
+- Produces: 商品编辑页"赠品配置"区块,支持按等级配置赠品项
+
+- [ ] **Step 1: 在 admin.js 中新增 API 封装**
+
+在 `cfc-web/src/api/admin.js` 末尾追加:
+```js
+// ===== 商品赠品配置 =====
+export const saveProductGift = (data) => request('/api/admin/product-gift/save', 'POST', data)
+export const getProductGiftList = (data) => request('/api/admin/product-gift/list', 'POST', data)
+```
+
+- [ ] **Step 2: ProductEdit.vue 新增赠品配置区块**
+
+在 `ProductEdit.vue` 中,找到商品基础信息区块末尾,追加:
+```vue
+        <!-- 赠品配置区块 -->
+        <el-divider content-position="left">赠品配置</el-divider>
+        <el-form-item label="赠品">
+            <el-table :data="giftConfigs" border size="mini">
+                <el-table-column label="会员等级" width="120">
+                    <template slot-scope="scope">
+                        <span>{{ scope.row.levelName }}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="赠品项">
+                    <template slot-scope="scope">
+                        <div v-for="item in scope.row.items" :key="item.id" style="margin-bottom:4px">
+                            <span v-if="item.giftType === 'product'">{{ item.giftProductName }}</span>
+                            <span v-else>{{ item.cfValue }} CF值</span>
+                        </div>
+                        <span v-if="!scope.row.items || scope.row.items.length === 0">未配置</span>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <el-button size="mini" @click="showGiftDialog = true" style="margin-top:8px">
+                配置赠品
+            </el-button>
+        </el-form-item>
+
+        <!-- 赠品配置弹窗 -->
+        <el-dialog title="配置赠品" :visible.sync="showGiftDialog" width="700px">
+            <el-tabs v-model="activeGiftTab">
+                <el-tab-pane 
+                    v-for="level in membershipLevels" 
+                    :key="level.levelCode" 
+                    :label="level.levelName" 
+                    :name="level.levelCode"
+                >
+                    <div v-for="(item, index) in getGiftItems(level.levelCode)" :key="item.id || index" style="margin-bottom:8px; display:flex; align-items:center">
+                        <el-select v-model="item.giftType" size="small" style="width:100px; margin-right:8px">
+                            <el-option label="赠品商品" value="product" />
+                            <el-option label="CF值" value="cf" />
+                        </el-select>
+                        <el-select v-if="item.giftType === 'product'" v-model="item.giftProductId" size="small" placeholder="选择商品" style="flex:1; margin-right:8px">
+                            <el-option v-for="p in onShelfProducts" :key="p.id" :label="p.name" :value="p.id" />
+                        </el-select>
+                        <el-input v-if="item.giftType === 'cf'" v-model.number="item.cfValue" size="small" placeholder="CF值数量" style="width:120px; margin-right:8px" type="number" />
+                        <el-button size="small" type="text" @click="removeGiftItem(level.levelCode, index)">删除</el-button>
+                    </div>
+                    <el-button size="small" @click="addGiftItem(level.levelCode)">+ 添加赠品</el-button>
+                </el-tab-pane>
+            </el-tabs>
+            <div slot="footer">
+                <el-button size="small" @click="showGiftDialog = false">取消</el-button>
+                <el-button size="small" type="primary" @click="saveGiftConfig">保存</el-button>
+            </div>
+        </el-dialog>
+```
+
+- [ ] **Step 3: 添加 data 字段和 methods**
+
+在 `data()` 中追加:
+```js
+showGiftDialog: false,
+activeGiftTab: '',
+membershipLevels: [],    // 从 /api/membership/levels 获取,过滤 FREE
+giftConfigs: [],         // 已配置的赠品(加载商品详情时获取)
+giftItemsMap: {},        // { [levelCode]: [{ giftType, giftProductId, cfValue, sortOrder }] }
+onShelfProducts: [],     // 已上架商品列表(用于赠品商品选择)
+```
+
+在 `methods` 中追加:
+```js
+async loadGiftConfigs() {
+    if (!this.productId) return
+    const res = await getProductGiftList({ productId: this.productId })
+    if (res.code === 200) {
+        this.giftConfigs = res.data || []
+        this.giftItemsMap = {}
+        for (const cfg of this.giftConfigs) {
+            this.giftItemsMap[cfg.levelCode] = cfg.items || []
+        }
+    }
+},
+async loadMembershipLevels() {
+    // 复用现有会员等级接口,过滤 FREE
+    const res = await getMembershipLevels()
+    if (res.code === 200) {
+        this.membershipLevels = (res.data || []).filter(l => l.levelCode !== 'FREE')
+        if (this.membershipLevels.length > 0 && !this.activeGiftTab) {
+            this.activeGiftTab = this.membershipLevels[0].levelCode
+        }
+    }
+},
+async loadOnShelfProducts() {
+    const res = await getProductList({ status: 'on_shelf', size: 200 })
+    if (res.code === 200) {
+        this.onShelfProducts = res.data.records || res.data || []
+    }
+},
+getGiftItems(levelCode) {
+    return this.giftItemsMap[levelCode] || []
+},
+addGiftItem(levelCode) {
+    if (!this.giftItemsMap[levelCode]) {
+        this.giftItemsMap[levelCode] = []
+    }
+    this.giftItemsMap[levelCode].push({ giftType: 'product', giftProductId: null, cfValue: 0, sortOrder: 0 })
+},
+removeGiftItem(levelCode, index) {
+    this.giftItemsMap[levelCode].splice(index, 1)
+},
+async saveGiftConfig() {
+    if (!this.productId) return
+    for (const level of this.membershipLevels) {
+        const items = this.giftItemsMap[level.levelCode] || []
+        const itemsDTO = items.map((it, idx) => ({
+            giftType: it.giftType,
+            giftProductId: it.giftType === 'product' ? it.giftProductId : null,
+            cfValue: it.giftType === 'cf' ? it.cfValue : 0,
+            sortOrder: idx
+        }))
+        const res = await saveProductGift({
+            productId: this.productId,
+            levelCode: level.levelCode,
+            status: 1,
+            items: itemsDTO
+        })
+        if (res.code !== 200) {
+            this.$message.error('保存' + level.levelName + '赠品失败: ' + res.message)
+            return
+        }
+    }
+    this.$message.success('赠品配置已保存')
+    this.showGiftDialog = false
+    await this.loadGiftConfigs()
+},
+```
+
+在 `mounted` 或商品加载回调中调用:
+```js
+this.loadMembershipLevels()
+this.loadOnShelfProducts()
+// 当 productId 变化时(编辑已有商品):
+this.loadGiftConfigs()
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd cfc-web && npm run build
+```
+预期:构建成功,无语法错误。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-web/src/views/admin/ProductEdit.vue
+git add cfc-web/src/api/admin.js
+git commit -m "feat(product-gift): 管理端商品编辑页新增赠品配置区块"
+```
+
+---
+
+## Self-Review Checklist
+
+| 检查项 | 结论 |
+|--------|------|
+| 三张新表 DDL 完整 | ✅ Task 1 已覆盖 |
+| 实体类字段与 DDL 对齐 | ✅ Task 2 已覆盖 |
+| 赠品配置保存幂等(先删后插) | ✅ Task 3 `saveGiftConfig` 已实现 |
+| 管理端 save + list 接口 | ✅ Task 4 已覆盖 |
+| 下单时赠品校验(防越权) | ✅ Task 5 已覆盖 |
+| 支付成功后 CF 发放 + 实物库存扣减 | ✅ Task 6 已覆盖 |
+| 商品详情返回赠品配置 | ✅ Task 7 已覆盖 |
+| 小程序赠品选择 UI | ✅ Task 8 已覆盖 |
+| 管理端赠品配置 UI | ✅ Task 9 已覆盖 |
+| 不修改现有 `product_gift_rules` | ✅ 独立新表,不触碰旧逻辑 |
+| 赠品商品不受等级限制 | ✅ 仅校验商品 `status=on_shelf`,不校验 `memberEligible` |
+| CF 赠品发给购买者本人 | ✅ `platformPointsService.earn(buyerId, ...)` |