|
|
@@ -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 行为一致)
|