# 实施计划:商品原价购买赠送会员 - **日期**: 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 { } ``` **验证**: `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() .eq(ProductGiftRule::getProductId, productId) .eq(ProductGiftRule::getStatus, 1) .last("LIMIT 1")); } /** 查询商品全部规则(管理端回显) */ public List listByProduct(Long productId) { return mapper.selectList(new LambdaQueryWrapper() .eq(ProductGiftRule::getProductId, productId)); } /** 等级编码 → 等级名(不存在则原样返回) */ public String getLevelName(String levelCode) { if (levelCode == null) { return null; } MembershipLevel level = levelMapper.selectOne(new LambdaQueryWrapper() .eq(MembershipLevel::getLevelCode, levelCode)); return level != null ? level.getLevelName() : levelCode; } /** * 保存赠送规则:先删后插(一商品一启用规则)。 * status=1 且 levelCode 有效 → 启用;否则视为停用(清空规则)。 */ @Transactional public Result 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() .eq(MembershipLevel::getLevelCode, levelCode)); if (level == null || "FREE".equals(level.getLevelCode())) { return Result.error("无效的赠送等级"); } } mapper.delete(new LambdaQueryWrapper() .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 save(@RequestBody Map 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(@RequestBody Map 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() .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() .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 行 `` 块之后)追加 ```html 购买即送 {{ product.giftLevelName }} 会员 ``` ### 8.2 在 `