Selaa lähdekoodia

feat(backend): 积分兑换优惠券exchangeCoupon+优惠券兑换/查询4端点

Xiaogang Liao 1 kuukausi sitten
vanhempi
sitoutus
273e58264e

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/CouponController.java

@@ -2,7 +2,9 @@ package com.etotem.cfc.controller;
 
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.Coupon;
+import com.etotem.cfc.entity.PointsExchangeRecord;
 import com.etotem.cfc.service.CouponService;
+import com.etotem.cfc.service.PointsExchangeService;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
@@ -16,6 +18,9 @@ public class CouponController {
     @Resource
     private CouponService couponService;
 
+    @Resource
+    private PointsExchangeService pointsExchangeService;
+
     @PostMapping("/list")
     public Result<List<Coupon>> list(@RequestAttribute("userId") Long userId) {
         return Result.success(couponService.listAvailable(userId));
@@ -44,4 +49,30 @@ public class CouponController {
         }
         return Result.success(java.util.Collections.singletonMap("discount", discount));
     }
+
+    @PostMapping("/exchange")
+    public Result<Map<String, Object>> exchange(@RequestAttribute("userId") Long userId,
+                                                @RequestBody Map<String, Object> params) {
+        Long couponId = Long.valueOf(params.get("couponId").toString());
+        Object memberId = params.get("memberId");
+        Long childId = memberId != null ? Long.valueOf(memberId.toString()) : null;
+        PointsExchangeRecord record = pointsExchangeService.exchangeCoupon(userId, childId, couponId);
+        return Result.success(java.util.Collections.singletonMap("recordId", record.getId()));
+    }
+
+    @PostMapping("/exchangeable")
+    public Result<List<Coupon>> exchangeable() {
+        return Result.success(couponService.listExchangeable());
+    }
+
+    @PostMapping("/product")
+    public Result<List<Coupon>> productCoupons(@RequestBody Map<String, Object> params) {
+        Long productId = Long.valueOf(params.get("productId").toString());
+        return Result.success(couponService.listByProduct(productId));
+    }
+
+    @PostMapping("/join-rules")
+    public Result<List<Coupon>> joinRules() {
+        return Result.success(couponService.listJoinRules());
+    }
 }

+ 132 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PointsExchangeService.java

@@ -2,9 +2,13 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.entity.Coupon;
+import com.etotem.cfc.entity.CouponGrantLog;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsExchangeProduct;
 import com.etotem.cfc.entity.PointsExchangeRecord;
+import com.etotem.cfc.mapper.CouponGrantLogMapper;
+import com.etotem.cfc.mapper.CouponMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsExchangeProductMapper;
 import com.etotem.cfc.mapper.PointsExchangeRecordMapper;
@@ -35,6 +39,15 @@ public class PointsExchangeService {
     @Resource
     private SysConfigService sysConfigService;
 
+    @Resource
+    private CouponMapper couponMapper;
+
+    @Resource
+    private CouponService couponService;
+
+    @Resource
+    private CouponGrantLogMapper couponGrantLogMapper;
+
     public Page<PointsExchangeProduct> getProductList(String category, Integer pageNum, Integer pageSize) {
         Page<PointsExchangeProduct> pageParam = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<PointsExchangeProduct> wrapper = new LambdaQueryWrapper<>();
@@ -157,6 +170,125 @@ public class PointsExchangeService {
         return record;
     }
 
+    /**
+     * 积分兑换优惠券:校验 -> 扣积分 -> 发券 -> 落兑换记录
+     */
+    @Transactional
+    public PointsExchangeRecord exchangeCoupon(Long userId, Long childId, Long couponId) {
+        Coupon coupon = couponMapper.selectById(couponId);
+        if (coupon == null) {
+            throw new RuntimeException("优惠券不存在");
+        }
+        if (coupon.getPointsPrice() == null || coupon.getPointsPrice() <= 0) {
+            throw new RuntimeException("该优惠券不支持积分兑换");
+        }
+        Date now = new Date();
+        if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
+            throw new RuntimeException("优惠券尚未开始");
+        }
+        if (coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
+            throw new RuntimeException("优惠券已过期");
+        }
+
+        // 目标家庭成员(复用现有解析逻辑)
+        Long targetFamilyMemberId = childId;
+        if (targetFamilyMemberId == null) {
+            FamilyMember child = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getUserId, userId).last("LIMIT 1")
+            );
+            if (child != null) {
+                targetFamilyMemberId = child.getId();
+            }
+        }
+        if (targetFamilyMemberId == null) {
+            throw new RuntimeException("未找到关联的家庭成员信息");
+        }
+
+        // 全局日限次(复用 exchange_daily_limit)
+        int dailyLimit = 5;
+        try {
+            String limitStr = sysConfigService.getValue("exchange_daily_limit");
+            if (limitStr != null && !limitStr.isEmpty()) {
+                dailyLimit = Integer.parseInt(limitStr);
+            }
+        } catch (Exception e) {
+            dailyLimit = 5;
+        }
+        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);
+        Date todayStart = cal.getTime();
+        long todayCount = pointsExchangeRecordMapper.selectCount(
+            new LambdaQueryWrapper<PointsExchangeRecord>()
+                .eq(PointsExchangeRecord::getUserId, userId)
+                .ge(PointsExchangeRecord::getCreatedAt, todayStart)
+        );
+        if (todayCount >= dailyLimit) {
+            throw new RuntimeException("今日兑换次数已达上限");
+        }
+
+        // 单券模板日限次(coupon_exchange_daily_limit 默认1)
+        int couponDailyLimit = 1;
+        try {
+            String cdlStr = sysConfigService.getValue("coupon_exchange_daily_limit");
+            if (cdlStr != null && !cdlStr.isEmpty()) {
+                couponDailyLimit = Integer.parseInt(cdlStr);
+            }
+        } catch (Exception e) {
+            couponDailyLimit = 1;
+        }
+        long couponTodayCount = couponGrantLogMapper.selectCount(
+            new LambdaQueryWrapper<CouponGrantLog>()
+                .eq(CouponGrantLog::getUserId, userId)
+                .eq(CouponGrantLog::getCouponId, couponId)
+                .eq(CouponGrantLog::getGrantType, "EXCHANGE")
+                .ge(CouponGrantLog::getCreatedAt, todayStart)
+        );
+        if (couponTodayCount >= couponDailyLimit) {
+            throw new RuntimeException("该优惠券今日兑换次数已达上限");
+        }
+
+        int minPoints = 100;
+        try {
+            String minStr = sysConfigService.getValue("exchange_points_min");
+            if (minStr != null && !minStr.isEmpty()) {
+                minPoints = Integer.parseInt(minStr);
+            }
+        } catch (Exception e) {
+            minPoints = 100;
+        }
+        if (coupon.getPointsPrice() < minPoints) {
+            throw new RuntimeException("兑换积分不得低于最低限制");
+        }
+
+        // 扣积分(system_points)
+        int deductResult = pointsService.deductSystemPoints(targetFamilyMemberId, coupon.getPointsPrice(),
+            "积分兑换优惠券: " + coupon.getName());
+        if (deductResult < 0) {
+            throw new RuntimeException("积分不足");
+        }
+
+        // 发券 + 流水(EXCHANGE,period=null)
+        couponService.grant(userId, couponId, "EXCHANGE", null, null);
+
+        String redeemCode = UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase();
+        PointsExchangeRecord record = new PointsExchangeRecord();
+        record.setUserId(userId);
+        record.setFamilyMemberId(targetFamilyMemberId);
+        record.setProductId(couponId);
+        record.setProductName(coupon.getName());
+        record.setPointsCost(coupon.getPointsPrice());
+        record.setQuantity(1);
+        record.setStatus("completed");
+        record.setRedeemCode(redeemCode);
+        record.setCreatedAt(new Date());
+        record.setUpdatedAt(new Date());
+        pointsExchangeRecordMapper.insert(record);
+        return record;
+    }
+
     public List<PointsExchangeRecord> getExchangeRecords(Long userId) {
         LambdaQueryWrapper<PointsExchangeRecord> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(PointsExchangeRecord::getUserId, userId)