Просмотр исходного кода

docs: 商品赠送会员实施计划

iwt 3 месяцев назад
Родитель
Сommit
53e12e769b
1 измененных файлов с 626 добавлено и 0 удалено
  1. 626 0
      docs/superpowers/plans/2026-08-03-product-gift-membership.md

+ 626 - 0
docs/superpowers/plans/2026-08-03-product-gift-membership.md

@@ -0,0 +1,626 @@
+# 实施计划:商品原价购买赠送会员
+
+- **日期**: 2026-08-03
+- **依据**: `docs/superpowers/specs/2026-08-03-product-gift-membership-design.md`(已确认方案 B)
+- **状态**: 待实施
+- **验证基线**: 后端 `/bwydata/maven/bin/mvn clean compile`;后端打包 `/bwydata/maven/bin/mvn clean package -DskipTests`;小程序 `/sc-data/cfc/cfc-frontend` 下 `npm run build:mp-weixin`;管理端 `/sc-data/cfc/cfc-web` 下 `npm run build`
+
+## 0. 总览
+
+运营给指定商品配置"原价购买送 XX 级别会员"。支付成功后,若订单为原价(无积分/无券/无折扣)且商品存在启用中的赠送规则,则给**家庭创建者**赠送该级别会员(已有同级或更高级有效会员则跳过)。
+
+- 规则表 `product_gift_rules` 与 `assessment_products` 同构(一商品一启用规则,`status` 可停用)。
+- 触发收口点:`ProductOrderService.handlePaymentSuccess`(现有"测评额度发放"逻辑之后追加,失败仅记日志不阻塞支付)。
+- 赠送核心:`MembershipService.grantMembershipByGift`,仿 `processPaymentCallback`,跳过佣金。
+
+## 1. 关键参考代码(实施前必读)
+
+| 参考 | 路径 | 用途 |
+|------|------|------|
+| 建表迁移模式 | `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` 迁移58 assessment_products 块(~4902 行) | 新增迁移100 的模板 |
+| 扩展实体模式 | `cfc-backend/src/main/java/com/etotem/cfc/entity/AssessmentProduct.java` | ProductGiftRule 实体模板 |
+| 扩展服务模式 | `cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentProductService.java` | saveOrUpdate = 先删后插 |
+| 管理端控制器模式 | `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPurchaseFieldController.java` | 管理接口模板 |
+| 赠送核心参考 | `cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java` `processPaymentCallback`(334-427 行)、`upgradeToFamily`(428-458 行)、`getMemberLevel`(475-490 行) | grantMembershipByGift 模板 |
+| 触发注入点 | `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java` `handlePaymentSuccess`(287-367 行,"测评额度"块 ~355-367 行之后) | 追加赠送逻辑 |
+| 详情 DTO | `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java` | 加 giftLevelCode/giftLevelName |
+| 详情组装 | `cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java` `detail(Long id)`(89-98 行) | 附带赠送规则 |
+| 小程序详情页 | `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue`(17-19 行价格区) | 加赠送徽标 |
+| 管理端表单 | `cfc-web/src/views/admin/ProductEdit.vue` `handleSave`(502-570 行)、api 引入(212 行) | 加赠送区块 |
+| 管理端 API 封装 | `cfc-web/src/api/admin.js` `saveProductPurchaseFields`(596-601 行) | 加规则 API |
+
+**项目约定(必须遵守)**:
+- 依赖注入一律 `@Resource`(JDK javax.annotation),不用构造器注入。
+- 实体继承 `Serializable`,`@TableName` + `@TableId(type = IdType.AUTO)` + `@Data`。
+- 迁移编号递增:本计划新增为 **迁移100**(当前最大为迁移99)。
+- 测试目录存在 ~166 个**预先存在**失败(BeanCreation/NoClassDefFound,与本次改动无关)。项目部署验证以 `mvn clean package -DskipTests` 为准;单步验证用 `mvn clean compile`。**不要**尝试修复预存测试失败,也不要把本次功能写进现有失败测试套件。
+- 提交信息风格 `feat: 商品赠送会员-xxx`;若 auto-bump 机器人已扫入改动则直接推送即可。
+
+---
+
+## 任务 1:数据库迁移 + schema.sql 同步
+
+### 1.1 修改 `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+
+在 `runMigrations()` 中**最后一个迁移块(迁移99)之后、方法闭合 `}` 之前**追加:
+
+```java
+        // 迁移100: 创建 product_gift_rules 表(商品赠送会员规则)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_gift_rules (" +
+                "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)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠送会员规则'");
+            log.info("创建product_gift_rules表成功");
+        } catch (Exception e) {
+            log.warn("创建product_gift_rules表失败: {}", e.getMessage());
+        }
+```
+
+### 1.2 同步 `cfc-backend/src/main/resources/schema.sql`
+
+在 `assessment_products` 建表语句之后追加同样的 `CREATE TABLE IF NOT EXISTS product_gift_rules (...)` 语句。
+
+**验证**: `/bwydata/maven/bin/mvn clean compile`(在 `/sc-data/cfc/cfc-backend`)退出码 0。
+**提交**: `feat: 商品赠送会员-建表迁移`
+
+---
+
+## 任务 2:实体 + Mapper
+
+### 2.1 新建 `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftRule.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_rules")
+public class ProductGiftRule implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long productId;
+
+    private String levelCode;   // 赠送会员等级编码(FAMILY/PREMIUM等)
+
+    private Integer status;     // 1=启用 0=停用
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}
+```
+
+### 2.2 新建 `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftRuleMapper.java`
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ProductGiftRule;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ProductGiftRuleMapper extends BaseMapper<ProductGiftRule> {
+}
+```
+
+**验证**: `mvn clean compile` 退出码 0。
+**提交**: `feat: 商品赠送会员-实体与Mapper`
+
+---
+
+## 任务 3:ProductGiftRuleService
+
+新建 `cfc-backend/src/main/java/com/etotem/cfc/service/ProductGiftRuleService.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.entity.MembershipLevel;
+import com.etotem.cfc.entity.ProductGiftRule;
+import com.etotem.cfc.mapper.MembershipLevelMapper;
+import com.etotem.cfc.mapper.ProductGiftRuleMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class ProductGiftRuleService {
+
+    @Resource
+    private ProductGiftRuleMapper mapper;
+
+    @Resource
+    private MembershipLevelMapper levelMapper;
+
+    /** 查询商品启用的赠送规则(一商品最多一条启用) */
+    public ProductGiftRule getActiveByProductId(Long productId) {
+        return mapper.selectOne(new LambdaQueryWrapper<ProductGiftRule>()
+                .eq(ProductGiftRule::getProductId, productId)
+                .eq(ProductGiftRule::getStatus, 1)
+                .last("LIMIT 1"));
+    }
+
+    /** 查询商品全部规则(管理端回显) */
+    public List<ProductGiftRule> listByProduct(Long productId) {
+        return mapper.selectList(new LambdaQueryWrapper<ProductGiftRule>()
+                .eq(ProductGiftRule::getProductId, productId));
+    }
+
+    /** 等级编码 → 等级名(不存在则原样返回) */
+    public String getLevelName(String levelCode) {
+        if (levelCode == null) {
+            return null;
+        }
+        MembershipLevel level = levelMapper.selectOne(new LambdaQueryWrapper<MembershipLevel>()
+                .eq(MembershipLevel::getLevelCode, levelCode));
+        return level != null ? level.getLevelName() : levelCode;
+    }
+
+    /**
+     * 保存赠送规则:先删后插(一商品一启用规则)。
+     * status=1 且 levelCode 有效 → 启用;否则视为停用(清空规则)。
+     */
+    @Transactional
+    public Result<Void> saveRule(Long productId, Integer status, String levelCode) {
+        if (productId == null || status == null) {
+            return Result.error("参数不完整");
+        }
+        boolean enable = (status == 1);
+        if (enable) {
+            if (levelCode == null || levelCode.isEmpty()) {
+                return Result.error("请选择赠送等级");
+            }
+            MembershipLevel level = levelMapper.selectOne(new LambdaQueryWrapper<MembershipLevel>()
+                    .eq(MembershipLevel::getLevelCode, levelCode));
+            if (level == null || "FREE".equals(level.getLevelCode())) {
+                return Result.error("无效的赠送等级");
+            }
+        }
+        mapper.delete(new LambdaQueryWrapper<ProductGiftRule>()
+                .eq(ProductGiftRule::getProductId, productId));
+        if (enable) {
+            ProductGiftRule rule = new ProductGiftRule();
+            rule.setProductId(productId);
+            rule.setLevelCode(levelCode);
+            rule.setStatus(1);
+            rule.setCreatedAt(new Date());
+            rule.setUpdatedAt(new Date());
+            mapper.insert(rule);
+        }
+        return Result.success(null);
+    }
+}
+```
+
+**验证**: `mvn clean compile` 退出码 0(确认 `MembershipLevelMapper` 存在,路径 `com.etotem.cfc.mapper`)。
+**提交**: `feat: 商品赠送会员-规则服务`
+
+---
+
+## 任务 4:管理端规则接口
+
+新建 `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductGiftRuleController.java`:
+
+```java
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ProductGiftRule;
+import com.etotem.cfc.service.ProductGiftRuleService;
+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-rule")
+public class AdminProductGiftRuleController {
+
+    @Resource
+    private ProductGiftRuleService giftRuleService;
+
+    @PostMapping("/save")
+    public Result<Void> save(@RequestBody Map<String, Object> params) {
+        Long productId = Long.parseLong(params.get("productId").toString());
+        Integer status = Integer.parseInt(params.get("status").toString());
+        Object levelCodeObj = params.get("levelCode");
+        String levelCode = levelCodeObj != null ? levelCodeObj.toString() : null;
+        return giftRuleService.saveRule(productId, status, levelCode);
+    }
+
+    @PostMapping("/list")
+    public Result<List<ProductGiftRule>> list(@RequestBody Map<String, Object> params) {
+        Long productId = Long.parseLong(params.get("productId").toString());
+        return Result.success(giftRuleService.listByProduct(productId));
+    }
+}
+```
+
+**验证**: `mvn clean compile` 退出码 0。
+**提交**: `feat: 商品赠送会员-管理端接口`
+
+---
+
+## 任务 5:grantMembershipByGift
+
+### 5.1 接口声明:`cfc-backend/src/main/java/com/etotem/cfc/service/MembershipServiceInterface.java`
+
+在 `/** 应用会员折扣... */ Integer applyMemberDiscount(...)` 之后追加:
+
+```java
+    /**
+     * 商品赠送会员:原价购买赠送指定级别会员给家庭创建者。
+     * 返回 true=已赠送/升级,false=跳过(已有同级/更高级、或参数异常)。
+     */
+    boolean grantMembershipByGift(Long familyId, Long userId, String levelCode,
+                                  String sourceOrderNo, String transactionId);
+```
+
+### 5.2 实现:`cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java`
+
+在 `applyMemberDiscount` 方法之后追加:
+
+```java
+    /**
+     * 商品赠送会员:赠送对象=家庭创建者;已有同级/更高级有效会员则跳过。
+     * 有效期=365天;写入 family_memberships + 升级记录(upgrade_type='gift');跳过佣金。
+     */
+    @Override
+    public boolean grantMembershipByGift(Long familyId, Long userId, String levelCode,
+                                         String sourceOrderNo, String transactionId) {
+        if (familyId == null || userId == null || levelCode == null || levelCode.isEmpty()) {
+            return false;
+        }
+        // 等级编码校验:需存在于 membership_levels 且非 FREE
+        MembershipLevel giftLevel = levelMapper.selectOne(new LambdaQueryWrapper<MembershipLevel>()
+                .eq(MembershipLevel::getLevelCode, levelCode));
+        if (giftLevel == null || "FREE".equals(giftLevel.getLevelCode())) {
+            return false;
+        }
+        // 目标用户=家庭创建者
+        Family family = familyMapper.selectById(familyId);
+        if (family == null || family.getCreatorId() == null) {
+            return false;
+        }
+        Long adminUserId = family.getCreatorId();
+        // 防御:购买者必须属于该家庭(赠送仅对家庭创建者生效)
+        User buyer = userMapper.selectById(userId);
+        if (buyer == null || !familyId.equals(buyer.getFamilyId())) {
+            return false;
+        }
+        User adminUser = userMapper.selectById(adminUserId);
+        if (adminUser == null) {
+            return false;
+        }
+        // 前置校验:当前有效等级低于赠送等级才送(无会员/过期=FREE 视为最低)
+        String currentLevel = getMemberLevel(adminUserId);
+        Long currentLevelId = null;
+        if (!"FREE".equals(currentLevel) && currentLevel != null) {
+            MembershipLevel current = levelMapper.selectOne(new LambdaQueryWrapper<MembershipLevel>()
+                    .eq(MembershipLevel::getLevelCode, currentLevel));
+            currentLevelId = current != null ? current.getId() : null;
+        }
+        if (currentLevelId != null && currentLevelId >= giftLevel.getId()) {
+            return false; // 已有同级或更高级有效会员,跳过
+        }
+        // 有效期:按级别年费周期(默认365天)
+        Date expireTime = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000);
+        // 写订阅记录
+        FamilyMembership membership = new FamilyMembership();
+        membership.setFamilyId(familyId);
+        membership.setLevelCode(levelCode);
+        membership.setStartDate(new Date());
+        membership.setEndDate(expireTime);
+        membership.setPaymentMethod("gift");
+        membership.setPaymentStatus("paid");
+        membership.setOrderNo(sourceOrderNo);
+        membership.setTransactionId(transactionId);
+        membership.setAmount(0);
+        membershipMapper.insert(membership);
+        // 升级用户
+        String fromLevel = adminUser.getMemberLevel() != null ? adminUser.getMemberLevel() : "FREE";
+        adminUser.setMemberLevel(levelCode);
+        adminUser.setMemberExpireTime(expireTime);
+        adminUser.setUpdatedAt(new Date());
+        userMapper.updateById(adminUser);
+        // 升级记录(upgradeType='gift')
+        MemberUpgradeRecord record = new MemberUpgradeRecord();
+        record.setUserId(adminUserId);
+        record.setFromLevel(fromLevel);
+        record.setToLevel(levelCode);
+        record.setUpgradeType("gift");
+        record.setExpireTime(expireTime);
+        record.setCreatedAt(new Date());
+        memberUpgradeRecordMapper.insert(record);
+        return true;
+    }
+```
+
+**注意**:
+- `MembershipLevel`、`Family`、`FamilyMembership`、`User`、`MemberUpgradeRecord` 均已在本文件 import(`processPaymentCallback` 已在用),无需新增 import;`LambdaQueryWrapper` 已引入。
+- 若 `MembershipService` 未实现 `MembershipServiceInterface` 的全部新方法会编译失败——本任务两步必须一起完成。
+
+**验证**: `mvn clean compile` 退出码 0。
+**提交**: `feat: 商品赠送会员-赠送核心逻辑`
+
+---
+
+## 任务 6:支付成功触发赠送
+
+修改 `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java`:
+
+### 6.1 新增注入(在 `private AssessmentProductService assessmentProductService;` 之后)
+
+```java
+    @Resource
+    private ProductGiftRuleService productGiftRuleService;
+
+    @Resource
+    private MembershipService membershipService;
+```
+
+新增 import:`com.etotem.cfc.entity.ProductGiftRule`(若用局部变量类型)。
+
+### 6.2 在 `handlePaymentSuccess` 中"测评商品发放额度"块之后、`return Result.success("支付成功");` 之前追加
+
+```java
+        // 商品赠送会员:原价购买且配置赠送规则时,支付成功后自动赠送会员
+        try {
+            if (paidProduct != null) {
+                ProductGiftRule giftRule = productGiftRuleService.getActiveByProductId(paidProduct.getId());
+                if (giftRule != null) {
+                    // 原价判定:无积分抵扣、无优惠券、无折扣
+                    boolean fullPrice = (order.getPointsUsed() == null || order.getPointsUsed() == 0)
+                            && order.getCouponId() == null
+                            && (order.getDiscountAmount() == null || order.getDiscountAmount() == 0);
+                    if (fullPrice && order.getFamilyId() != null) {
+                        boolean granted = membershipService.grantMembershipByGift(
+                                order.getFamilyId(), order.getBuyerId(), giftRule.getLevelCode(),
+                                order.getOrderNo(), order.getTransactionId());
+                        log.info("商品订单{}赠送会员{}结果={}", orderNo, giftRule.getLevelCode(), granted);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("商品赠送会员失败: orderNo={}", orderNo, e);
+            // 不阻塞支付流程
+        }
+```
+
+**验证**: `mvn clean compile` 退出码 0。
+**提交**: `feat: 商品赠送会员-支付成功触发赠送`
+
+---
+
+## 任务 7:商品详情附带赠送规则
+
+### 7.1 `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java`
+
+在 `private String guideScope;` 之后追加:
+
+```java
+    // 商品赠送会员扩展信息
+    private String giftLevelCode;
+    private String giftLevelName;
+```
+
+### 7.2 `cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java`
+
+- 新增注入(`private UserMapper userMapper;` 之后):
+
+```java
+    @Resource
+    private ProductGiftRuleService productGiftRuleService;
+```
+
+- `detail(Long id)` 改为在 `return Result.success(ProductDTO.from(product));` 前附加规则:
+
+```java
+        ProductDTO dto = ProductDTO.from(product);
+        ProductGiftRule giftRule = productGiftRuleService.getActiveByProductId(id);
+        if (giftRule != null) {
+            dto.setGiftLevelCode(giftRule.getLevelCode());
+            dto.setGiftLevelName(productGiftRuleService.getLevelName(giftRule.getLevelCode()));
+        }
+        return Result.success(dto);
+```
+
+- 新增 import:`com.etotem.cfc.entity.ProductGiftRule`。
+
+**验证**: `mvn clean compile` 退出码 0。
+**提交**: `feat: 商品赠送会员-详情附带规则`
+
+---
+
+## 任务 8:小程序详情页赠送徽标
+
+修改 `/sc-data/cfc/cfc-frontend/pages/discover-detail/product-detail/product-detail.vue`:
+
+### 8.1 在价格区(17-19 行 `<view class="price-row">` 块之后)追加
+
+```html
+        <view v-if="product.giftLevelName" class="gift-tag">购买即送 {{ product.giftLevelName }} 会员</view>
+```
+
+### 8.2 在 `<style scoped>` 中追加样式
+
+```css
+.gift-tag {
+  display: inline-block;
+  margin-top: 12rpx;
+  padding: 6rpx 16rpx;
+  font-size: 22rpx;
+  color: #b8860b;
+  background: #fff7e6;
+  border: 1rpx solid #f0d9a8;
+  border-radius: 8rpx;
+}
+```
+
+(颜色/字号可按页面现有视觉微调,但必须保持徽标位于价格下方。)
+
+**验证**: 在 `/sc-data/cfc/cfc-frontend` 执行 `npm run build:mp-weixin` 退出码 0。
+**提交**: `feat: 商品赠送会员-小程序详情徽标`
+
+---
+
+## 任务 9:管理端 API 封装
+
+修改 `/sc-data/cfc/cfc-web/src/api/admin.js`,在 `saveProductPurchaseFields`(596-601 行)之后追加:
+
+```javascript
+export function saveProductGiftRule(productId, status, levelCode) {
+  return request({
+    url: '/api/admin/product-gift-rule/save',
+    method: 'post',
+    data: { productId, status, levelCode }
+  })
+}
+
+export function listProductGiftRules(productId) {
+  return request({
+    url: '/api/admin/product-gift-rule/list',
+    method: 'post',
+    data: { productId }
+  })
+}
+
+export function listMembershipLevels() {
+  return request({
+    url: '/api/membership/levels',
+    method: 'post'
+  })
+}
+```
+
+**验证**: 无需单独构建(依赖 admin.js 语法,任务 10 一并构建验证)。
+**提交**: `feat: 商品赠送会员-管理端API封装`
+
+---
+
+## 任务 10:管理端 ProductEdit.vue 赠送会员区块
+
+修改 `/sc-data/cfc/cfc-web/src/views/admin/ProductEdit.vue`:
+
+### 10.1 引入 API(212 行 `import ... from '@/api/admin'` 中追加)
+
+```javascript
+import { ..., saveProductGiftRule, listProductGiftRules, listMembershipLevels } from '@/api/admin'
+```
+
+### 10.2 data 中新增状态(`form` 附近)
+
+```javascript
+        giftRuleEnabled: false,
+        giftRuleLevel: '',
+        membershipLevels: [],   // 可选赠送等级(过滤 FREE)
+```
+
+### 10.3 模板:在购买字段/配送等区块(表单末尾附近)追加 el-form-item
+
+```html
+          <el-form-item label="赠送会员">
+            <el-switch v-model="giftRuleEnabled" active-text="启用" inactive-text="停用" />
+            <el-select v-if="giftRuleEnabled" v-model="giftRuleLevel" placeholder="选择赠送等级" style="margin-left: 12px; width: 200px">
+              <el-option v-for="lv in membershipLevels" :key="lv.levelCode" :label="lv.levelName" :value="lv.levelCode" />
+            </el-select>
+          </el-form-item>
+```
+
+### 10.4 加载回显:在编辑态数据加载成功处(`this.form = {...}` 之后)追加
+
+```javascript
+        // 赠送会员规则回显
+        try {
+          const lvRes = await listMembershipLevels()
+          if (lvRes.code === 200 && Array.isArray(lvRes.data)) {
+            this.membershipLevels = lvRes.data.filter(l => l.levelCode !== 'FREE')
+          }
+          const ruleRes = await listProductGiftRules(this.form.id)
+          if (ruleRes.code === 200 && Array.isArray(ruleRes.data) && ruleRes.data.length > 0) {
+            const active = ruleRes.data.find(r => r.status === 1)
+            if (active) {
+              this.giftRuleEnabled = true
+              this.giftRuleLevel = active.levelCode
+            }
+          }
+        } catch (e) {
+          console.warn('加载赠送会员规则失败', e)
+        }
+```
+
+(若该编辑态加载是同步流程而非 async 方法,则将上述包成独立 async 方法并在加载后调用。)
+
+### 10.5 保存:在 `handleSave` 中 `saveProductPurchaseFields` 之后追加
+
+```javascript
+            // Save gift membership rule
+            if (productId) {
+              try {
+                await saveProductGiftRule(productId, this.giftRuleEnabled ? 1 : 0,
+                  this.giftRuleEnabled ? this.giftRuleLevel : '')
+              } catch (e) {
+                console.warn('保存赠送会员规则失败', e)
+              }
+            }
+```
+
+**验证**: 在 `/sc-data/cfc/cfc-web` 执行 `npm run build` 退出码 0。
+**提交**: `feat: 商品赠送会员-管理端配置区块`
+
+---
+
+## 任务 11:全量验证与部署
+
+1. 后端:在 `/sc-data/cfc/cfc-backend` 执行 `/bwydata/maven/bin/mvn clean package -DskipTests`,确认 BUILD SUCCESS。
+2. 小程序:在 `/sc-data/cfc/cfc-frontend` 执行 `npm run build:mp-weixin`,退出码 0。
+3. 管理端:在 `/sc-data/cfc/cfc-web` 执行 `npm run build`,退出码 0。
+4. 检查 `git status`,确认无遗漏文件;推送(若 auto-bump 未自动推送)。
+
+**提交**: 如有未提交改动 → `feat: 商品赠送会员-全量验证`
+
+---
+
+## 任务 12:手工冒烟验证(可选,需要部署后)
+
+部署到测试环境后验证:
+1. 管理端商品编辑页:启用赠送会员 → 选择 PREMIUM → 保存 → 重新进入回显正确;停用 → 保存 → 回显为空。
+2. 小程序详情页:配置了规则的商品显示"购买即送 XX 会员"徽标;未配置的不显示。
+3. 原价购买支付成功后,家庭创建者 member_level 升级、member_expire_time=+365 天、family_memberships 与 member_upgrade_records(type='gift')有记录。
+4. 用积分/优惠券购买:不赠送。
+5. 家庭已有同级/更高级有效会员时购买:不降级、不赠送。
+
+---
+
+## 范围外(本期不做)
+
+- 退款回收会员
+- 多规则同时启用(当前一商品一启用规则)
+- 赠送记录独立展示页(可查 family_memberships / member_upgrade_records)
+- 会员时长累计/顺延(直接按 365 天重设 expireTime,与现有 upgradeToFamily 行为一致)