Browse Source

feat(coupon): COUPON-C2 清理优惠券死代码 + used_count 还券收回递减

任务:COUPON-C2(docs/优惠券需求设计与实现核查.md §五 C2)

背景:pub-iwintrue 实例上已完成的 C2 提交(be16b8eb3 删死代码 /
3379d4ad1 used_count 递减)已丢失(本地提交被 auto-bump/仓库重置,origin 无
对应对象),按核查文档 §五 C2 重新实现。

改动(6 文件,仅 cfc-backend):
- 删除 entity/UserCoupon.java、mapper/UserCouponMapper.java
  (user_coupon 旧维度实例表,08-31 起由 family_coupon 承接,无业务引用)
- 删除 entity/CouponGrantLog.java、mapper/CouponGrantLogMapper.java
  (coupon_grant_log 旧维度流水表;C1 起统计源已改 family_coupon_grant_log)
- AdminCouponController:移除 CouponGrantLog / CouponGrantLogMapper 的
  import 与未使用的 couponGrantLogMapper 字段(grant-log 端点本就查
  family_coupon_grant_log,对外行为不变)
- CouponService:
  · 新增 decrementUsedCount():原子递减 coupon.used_count(used_count>0 才减,
    避免减成负数;NULL 视为未知不动)
  · revert():还券(USED/RESERVED → AVAILABLE)时同步递减 used_count
  · revokeFamilyPopulationCoupons():收回人口券时同步递减 used_count

约束遵守:
- 表不 DROP(user_coupon / coupon_grant_log 保留,遵循 08-31「保留不迁移」)
- 不新增 DB 迁移;未按他表 id 列表做主键 DELETE(仅删 Java 死代码 + 递减)
- cfc-web / cfc-frontend 未改动

验证:cfc-backend mvn compile 通过;mvn test-compile 通过;
cfc-web vue-cli-service build 通过
Sisyphus 2 tuần trước cách đây
mục cha
commit
b8ade0fab

+ 0 - 5
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCouponController.java

@@ -4,10 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.Coupon;
-import com.etotem.cfc.entity.CouponGrantLog;
 import com.etotem.cfc.entity.FamilyCoupon;
 import com.etotem.cfc.entity.FamilyCouponGrantLog;
-import com.etotem.cfc.mapper.CouponGrantLogMapper;
 import com.etotem.cfc.mapper.CouponMapper;
 import com.etotem.cfc.mapper.FamilyCouponGrantLogMapper;
 import com.etotem.cfc.mapper.FamilyCouponMapper;
@@ -35,9 +33,6 @@ public class AdminCouponController {
     @Resource
     private CouponMapper couponMapper;
 
-    @Resource
-    private CouponGrantLogMapper couponGrantLogMapper;
-
     @Resource
     private FamilyCouponGrantLogMapper familyCouponGrantLogMapper;
 

+ 0 - 34
cfc-backend/src/main/java/com/etotem/cfc/entity/CouponGrantLog.java

@@ -1,34 +0,0 @@
-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("coupon_grant_log")
-public class CouponGrantLog implements Serializable {
-
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    private Long userId;
-
-    private Long couponId;
-
-    /** JOIN/PERIODIC/POPULATION/EXCHANGE */
-    private String grantType;
-
-    /** 周期标识(YYYY-MM或YYYY-Qn), PERIODIC防重用 */
-    private String period;
-
-    private Integer quantity;
-
-    /** 触发来源(订单号/成员ID等) */
-    private String source;
-
-    private Date createdAt;
-}

+ 0 - 33
cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java

@@ -1,33 +0,0 @@
-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("user_coupon")
-public class UserCoupon implements Serializable {
-
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    private Long userId;
-
-    private Long couponId;
-
-    /** 状态: AVAILABLE / USED */
-    private String status;
-
-    private Date receivedAt;
-
-    private Date usedAt;
-
-    private Long orderId;
-
-    /** 券实例归属家庭ID(优惠券家庭化 N3,家庭内任意成员可用) */
-    private Long familyId;
-}

+ 0 - 7
cfc-backend/src/main/java/com/etotem/cfc/mapper/CouponGrantLogMapper.java

@@ -1,7 +0,0 @@
-package com.etotem.cfc.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.etotem.cfc.entity.CouponGrantLog;
-
-public interface CouponGrantLogMapper extends BaseMapper<CouponGrantLog> {
-}

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

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

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java

@@ -2,6 +2,7 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.etotem.cfc.entity.Coupon;
 import com.etotem.cfc.entity.FamilyCoupon;
 import com.etotem.cfc.entity.FamilyCouponGrantLog;
@@ -59,6 +60,36 @@ public class CouponService {
         return user.getFamilyId();
     }
 
+    /** 券实例是否处于已核销/占用中状态(决定还券时是否需要递减 used_count) */
+    private boolean isConsumedCoupon(String status) {
+        return "USED".equals(status) || "RESERVED".equals(status);
+    }
+
+    /**
+     * 还券/收回时同步递减券模板 used_count。
+     *
+     * <p>背景:{@link #claim} 领取时对 coupon.used_count 做 +1,但订单取消/超时还券
+     * ({@link #revert})与家庭人口券收回({@link #revokeFamilyPopulationCoupons})原先不减,
+     * 导致 used_count 只增不减、与家庭维度实际券实例数漂移,并使 claim() 的库存校验
+     * (used_count &gt;= total_count 判定「已领完」)失真。
+     *
+     * <p>原子更新(used_count &gt; 0 才 -1),避免并发下减成负数;NULL 视为未知,不做处理。
+     *
+     * @param couponId 券模板ID(coupon.id)
+     */
+    private void decrementUsedCount(Long couponId) {
+        if (couponId == null) {
+            return;
+        }
+        int updated = couponMapper.update(null, new LambdaUpdateWrapper<Coupon>()
+                .eq(Coupon::getId, couponId)
+                .gt(Coupon::getUsedCount, 0)
+                .setSql("used_count = used_count - 1"));
+        if (updated > 0) {
+            logger.info("还券/收回:券模板 used_count 递减 couponId={}, updated={}", couponId, updated);
+        }
+    }
+
     /** 构建与 CouponService.listMyCoupons 相同格式的响应 Map */
     private Map<String, Object> toCouponVO(FamilyCoupon fc) {
         Coupon coupon = couponMapper.selectById(fc.getCouponId());
@@ -227,10 +258,16 @@ public class CouponService {
     public void revert(Long userCouponId) {
         FamilyCoupon fc = familyCouponMapper.selectById(userCouponId);
         if (fc != null) {
+            // 先记录还券前状态:仅从 USED/RESERVED 归还时才递减 used_count,避免重复还券误减
+            boolean consumed = isConsumedCoupon(fc.getStatus());
             fc.setStatus("AVAILABLE");
             fc.setUsedAt(null);
             fc.setOrderId(null);
             familyCouponMapper.updateById(fc);
+            if (consumed) {
+                // 还券:同步递减券模板 used_count,与 claim() 的递增配平
+                decrementUsedCount(fc.getCouponId());
+            }
         }
     }
 
@@ -505,6 +542,8 @@ public class CouponService {
             for (FamilyCoupon fc : coupons) {
                 fc.setStatus("REVOKED");
                 familyCouponMapper.updateById(fc);
+                // 收回人口券:同步递减券模板 used_count,与 revert() 口径一致
+                decrementUsedCount(fc.getCouponId());
             }
         }
         logger.info("收回家庭人口券完成: familyId={}, memberId={}, 作废券数={}", familyId, memberId,