Bläddra i källkod

feat(family-cf): 完成优惠券兑换闭环 + 兑换记录表

- FamilyPlatformPointsService.exchangeCouponByCf 完整实现:
  校验优惠券 → 家庭/单券日限次检查 → 扣家庭池CF → 发券 → 落记录
- 新增 FamilyPlatformExchangeRecord 实体/Mapper
- schema.sql + 迁移227 建 family_platform_exchange_record 表
- 兑换记录表含 userId/familyId/couponId/cfCost/status/redeemCode
- npm run build + mvn compile 通过
iwt 1 månad sedan
förälder
incheckning
846e8263f4

+ 24 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -8704,6 +8704,30 @@ private void runMigration100() {
 		}
 		ensureColumn("products", "points_multiplier", "DECIMAL(4,2) NOT NULL DEFAULT 1.00 COMMENT '平台积分倍数,1元=1积分×倍数'");
 		log.info("已添加products.points_multiplier列");
+		// 迁移227: 创建 family_platform_exchange_record 表(家庭CF值兑换记录)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS family_platform_exchange_record (" +
+				"  id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"  user_id BIGINT NOT NULL COMMENT '操作用户ID', " +
+				"  family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+				"  coupon_id BIGINT NOT NULL COMMENT '关联优惠券ID', " +
+				"  coupon_name VARCHAR(200) COMMENT '优惠券名称', " +
+				"  cf_cost INT NOT NULL COMMENT '消耗CF值数量', " +
+				"  status VARCHAR(20) NOT NULL DEFAULT 'completed' COMMENT 'completed/failed', " +
+				"  redeem_code VARCHAR(32) COMMENT '兑换码', " +
+				"  remark VARCHAR(255) COMMENT '备注', " +
+				"  created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"  INDEX idx_family_time (family_id, created_at), " +
+				"  INDEX idx_user (user_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭CF值兑换记录'"
+			);
+			log.info("已创建family_platform_exchange_record表");
+		} catch (Exception e) {
+			log.warn("创建family_platform_exchange_record表失败: " + e.getMessage());
+		}
+
 
 	}
 }

+ 45 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyPlatformExchangeRecord.java

@@ -0,0 +1,45 @@
+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("family_platform_exchange_record")
+public class FamilyPlatformExchangeRecord implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 操作用户ID */
+    private Long userId;
+
+    /** 家庭ID */
+    private Long familyId;
+
+    /** 关联优惠券ID */
+    private Long couponId;
+
+    /** 优惠券名称 */
+    private String couponName;
+
+    /** 消耗CF值数量 */
+    private Integer cfCost;
+
+    /** 状态: pending/completed/failed */
+    private String status;
+
+    /** 兑换码 */
+    private String redeemCode;
+
+    /** 备注 */
+    private String remark;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyPlatformExchangeRecordMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyPlatformExchangeRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FamilyPlatformExchangeRecordMapper extends BaseMapper<FamilyPlatformExchangeRecord> {
+}

+ 85 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java

@@ -13,6 +13,11 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.Date;
+import com.etotem.cfc.service.CouponService;
+import com.etotem.cfc.service.SysConfigService;
+import com.etotem.cfc.mapper.FamilyPlatformExchangeRecordMapper;
+import com.etotem.cfc.entity.FamilyPlatformExchangeRecord;
+import java.util.Calendar;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -37,9 +42,18 @@ public class FamilyPlatformPointsService {
     @Resource
     private FamilyPlatformBalanceLogMapper logMapper;
 
+    @Resource
+    private SysConfigService sysConfigService;
+
+    @Resource
+    private CouponService couponService;
+
     @Resource
     private CouponMapper couponMapper;
 
+    @Resource
+    private FamilyPlatformExchangeRecordMapper exchangeRecordMapper;
+
     /** 获取或创建家庭的积分池记录(悲观锁) */
     @Transactional
     public FamilyPlatformBalance getOrCreate(Long familyId) {
@@ -191,22 +205,89 @@ public class FamilyPlatformPointsService {
     }
 
     /**
-     * 用家庭CF值兑换优惠券(扣减家庭池,不扣个人积分)
+     * 用家庭CF值兑换优惠券:校验 → 扣家庭池 → 发券 → 落记录
      */
     @Transactional
     public void exchangeCouponByCf(Long familyId, Long couponId, Long refUserId) {
+        // 1. 校验优惠券
         Coupon coupon = couponMapper.selectById(couponId);
         if (coupon == null) {
             throw new RuntimeException("优惠券不存在");
         }
         Integer pointsPrice = coupon.getPointsPrice();
         if (pointsPrice == null || pointsPrice <= 0) {
-            throw new RuntimeException("该优惠券不支持积分兑换");
+            throw new RuntimeException("该优惠券不支持CF值兑换");
+        }
+        Date now = new Date();
+        if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
+            throw new RuntimeException("优惠券尚未开始");
         }
-        // 扣除家庭池CF值
+        if (coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
+            throw new RuntimeException("优惠券已过期");
+        }
+
+        // 2. 家庭级日限次(cf_exchange_daily_limit,默认5)
+        int dailyLimit = 5;
+        try {
+            String limitStr = sysConfigService.getValue("cf_exchange_daily_limit");
+            if (limitStr != null && !limitStr.isEmpty()) dailyLimit = Integer.parseInt(limitStr);
+        } catch (Exception ignored) {}
+        Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0);
+        long todayCount = exchangeRecordMapper.selectCount(
+                new LambdaQueryWrapper<FamilyPlatformExchangeRecord>()
+                        .eq(FamilyPlatformExchangeRecord::getFamilyId, familyId)
+                        .eq(FamilyPlatformExchangeRecord::getStatus, "completed")
+                        .ge(FamilyPlatformExchangeRecord::getCreatedAt, cal.getTime()));
+        if (todayCount >= dailyLimit) {
+            throw new RuntimeException("家庭今日CF值兑换次数已达上限(" + dailyLimit + "次)");
+        }
+
+        // 3. 单券模板日限次(cf_coupon_exchange_daily_limit,默认1)
+        int couponDailyLimit = 1;
+        try {
+            String cdlStr = sysConfigService.getValue("cf_coupon_exchange_daily_limit");
+            if (cdlStr != null && !cdlStr.isEmpty()) couponDailyLimit = Integer.parseInt(cdlStr);
+        } catch (Exception ignored) {}
+        long couponTodayCount = exchangeRecordMapper.selectCount(
+                new LambdaQueryWrapper<FamilyPlatformExchangeRecord>()
+                        .eq(FamilyPlatformExchangeRecord::getFamilyId, familyId)
+                        .eq(FamilyPlatformExchangeRecord::getCouponId, couponId)
+                        .eq(FamilyPlatformExchangeRecord::getStatus, "completed")
+                        .ge(FamilyPlatformExchangeRecord::getCreatedAt, cal.getTime()));
+        if (couponTodayCount >= couponDailyLimit) {
+            throw new RuntimeException("该优惠券今日兑换次数已达上限");
+        }
+
+        // 4. 扣减家庭池CF值
         spend(familyId, pointsPrice, "coupon_exchange", couponId,
               "兑换优惠券: " + (coupon.getName() != null ? coupon.getName() : "优惠券"));
-        // TODO: 实际发券逻辑(关联到 refUserId 的优惠券账户)
+
+        // 5. 发券给操作用户
+        couponService.grant(refUserId, couponId, "CF_EXCHANGE", null, "family:" + familyId);
+
+        // 6. 落兑换记录
+        FamilyPlatformExchangeRecord record = new FamilyPlatformExchangeRecord();
+        record.setUserId(refUserId);
+        record.setFamilyId(familyId);
+        record.setCouponId(couponId);
+        record.setCouponName(coupon.getName());
+        record.setCfCost(pointsPrice);
+        record.setStatus("completed");
+        record.setRedeemCode(java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase());
+        record.setCreatedAt(now);
+        record.setUpdatedAt(now);
+        exchangeRecordMapper.insert(record);
+    }
+
+    /** 查询家庭CF兑换记录分页 */
+    public Page<FamilyPlatformExchangeRecord> getExchangeRecords(Long familyId, Integer page, Integer size) {
+        Page<FamilyPlatformExchangeRecord> pageParam = new Page<>(page, size);
+        return exchangeRecordMapper.selectPage(pageParam,
+                new LambdaQueryWrapper<FamilyPlatformExchangeRecord>()
+                        .eq(FamilyPlatformExchangeRecord::getFamilyId, familyId)
+                        .orderByDesc(FamilyPlatformExchangeRecord::getCreatedAt));
     }
 
     /** 查询家庭 CF 值余额 */

+ 18 - 0
cfc-backend/src/main/resources/schema.sql

@@ -4465,6 +4465,24 @@ CREATE TABLE IF NOT EXISTS family_platform_balance_log (
     INDEX idx_ref (ref_type, ref_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭平台积分流水';
 
+-- ===== 家庭平台积分兑换记录 =====
+
+CREATE TABLE IF NOT EXISTS family_platform_exchange_record (
+    id            BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id       BIGINT       NOT NULL COMMENT '操作用户ID',
+    family_id     BIGINT       NOT NULL COMMENT '家庭ID',
+    coupon_id     BIGINT       NOT NULL COMMENT '关联优惠券ID',
+    coupon_name   VARCHAR(200) COMMENT '优惠券名称',
+    cf_cost       INT          NOT NULL COMMENT '消耗CF值数量',
+    status        VARCHAR(20)  NOT NULL DEFAULT 'completed' COMMENT 'completed/failed',
+    redeem_code   VARCHAR(32)  COMMENT '兑换码',
+    remark        VARCHAR(255) COMMENT '备注',
+    created_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at    DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_family_time (family_id, created_at),
+    INDEX idx_user (user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭CF值兑换记录';
+
 -- ===== 健康方案汇总 =====
 
 CREATE TABLE IF NOT EXISTS health_plans (

+ 9 - 0
cfc-frontend/pages/wealth-cf/index.vue

@@ -105,6 +105,7 @@ export default {
       page: 1,
       size: 20,
       hasMore: false,
+      exchangeRecords: [],
       showExchangeModal: false,
       exchangeableCoupons: []
     }
@@ -156,6 +157,14 @@ export default {
         this.loadLogs()
       }
     },
+    async loadExchangeRecords() {
+      try {
+        const res = await getFamilyPlatformLogs({ page: 1, size: 50, type: 'coupon_exchange' })
+        if (res.code === 200) {
+          this.exchangeRecords = res.data?.records || []
+        }
+      } catch (e) { console.error(e) }
+    },
     refreshData() {
       this.page = 1
       this.loadBalance()

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-379a822a1a7059e523b24fbccce96243fecfa377
+201adc963ff77f7ad1e851fbe462ae42575f50d2

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1017",
+  "version": "1.0.1018",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1017",
+      "version": "1.0.1018",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",