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运营给指定商品配置"原价购买送 XX 级别会员"。支付成功后,若订单为原价(无积分/无券/无折扣)且商品存在启用中的赠送规则,则给家庭创建者赠送该级别会员(已有同级或更高级有效会员则跳过)。
product_gift_rules 与 assessment_products 同构(一商品一启用规则,status 可停用)。ProductOrderService.handlePaymentSuccess(现有"测评额度发放"逻辑之后追加,失败仅记日志不阻塞支付)。MembershipService.grantMembershipByGift,仿 processPaymentCallback,跳过佣金。| 参考 | 路径 | 用途 |
|---|---|---|
| 建表迁移模式 | 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。mvn clean package -DskipTests 为准;单步验证用 mvn clean compile。不要尝试修复预存测试失败,也不要把本次功能写进现有失败测试套件。feat: 商品赠送会员-xxx;若 auto-bump 机器人已扫入改动则直接推送即可。cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java在 runMigrations() 中最后一个迁移块(迁移99)之后、方法闭合 } 之前追加:
// 迁移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());
}
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: 商品赠送会员-建表迁移
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductGiftRule.javapackage 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;
}
cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductGiftRuleMapper.javapackage 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
新建 cfc-backend/src/main/java/com/etotem/cfc/service/ProductGiftRuleService.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: 商品赠送会员-规则服务
新建 cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductGiftRuleController.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: 商品赠送会员-管理端接口
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipServiceInterface.java在 /** 应用会员折扣... */ Integer applyMemberDiscount(...) 之后追加:
/**
* 商品赠送会员:原价购买赠送指定级别会员给家庭创建者。
* 返回 true=已赠送/升级,false=跳过(已有同级/更高级、或参数异常)。
*/
boolean grantMembershipByGift(Long familyId, Long userId, String levelCode,
String sourceOrderNo, String transactionId);
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java在 applyMemberDiscount 方法之后追加:
/**
* 商品赠送会员:赠送对象=家庭创建者;已有同级/更高级有效会员则跳过。
* 有效期=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: 商品赠送会员-赠送核心逻辑
修改 cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java:
private AssessmentProductService assessmentProductService; 之后) @Resource
private ProductGiftRuleService productGiftRuleService;
@Resource
private MembershipService membershipService;
新增 import:com.etotem.cfc.entity.ProductGiftRule(若用局部变量类型)。
handlePaymentSuccess 中"测评商品发放额度"块之后、return Result.success("支付成功"); 之前追加 // 商品赠送会员:原价购买且配置赠送规则时,支付成功后自动赠送会员
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: 商品赠送会员-支付成功触发赠送
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java在 private String guideScope; 之后追加:
// 商品赠送会员扩展信息
private String giftLevelCode;
private String giftLevelName;
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java新增注入(private UserMapper userMapper; 之后):
@Resource
private ProductGiftRuleService productGiftRuleService;
detail(Long id) 改为在 return Result.success(ProductDTO.from(product)); 前附加规则:
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: 商品赠送会员-详情附带规则
修改 /sc-data/cfc/cfc-frontend/pages/discover-detail/product-detail/product-detail.vue:
<view class="price-row"> 块之后)追加 <view v-if="product.giftLevelName" class="gift-tag">购买即送 {{ product.giftLevelName }} 会员</view>
<style scoped> 中追加样式.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: 商品赠送会员-小程序详情徽标
修改 /sc-data/cfc/cfc-web/src/api/admin.js,在 saveProductPurchaseFields(596-601 行)之后追加:
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封装
修改 /sc-data/cfc/cfc-web/src/views/admin/ProductEdit.vue:
import ... from '@/api/admin' 中追加)import { ..., saveProductGiftRule, listProductGiftRules, listMembershipLevels } from '@/api/admin'
form 附近) giftRuleEnabled: false,
giftRuleLevel: '',
membershipLevels: [], // 可选赠送等级(过滤 FREE)
<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>
this.form = {...} 之后)追加 // 赠送会员规则回显
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 方法并在加载后调用。)
handleSave 中 saveProductPurchaseFields 之后追加 // 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: 商品赠送会员-管理端配置区块
/sc-data/cfc/cfc-backend 执行 /bwydata/maven/bin/mvn clean package -DskipTests,确认 BUILD SUCCESS。/sc-data/cfc/cfc-frontend 执行 npm run build:mp-weixin,退出码 0。/sc-data/cfc/cfc-web 执行 npm run build,退出码 0。git status,确认无遗漏文件;推送(若 auto-bump 未自动推送)。提交: 如有未提交改动 → feat: 商品赠送会员-全量验证
部署到测试环境后验证: