Преглед изворни кода

feat(coupon): 注册/绑定家庭自动发放 REGISTER 赠券 (COUPON-REG)

- CouponService 新增 grantFamilyRegisterCoupons:按 coupon.grant_type=REGISTER 模板调用 grantFamily 发到家庭维度(family_coupon),period=REGISTER,由 uk_family_grant 唯一键防重

- 触发点:UserService.createUserAndFamily(注册)、UserService.joinFamily / FamilyInvitationService.acceptInvitation / FamilyJoinRequestService.approveJoinRequest(绑定家庭);均 try/catch 包裹,发券异常不阻断主流程

- UserService/FamilyInvitationService/FamilyJoinRequestService 以 @Resource @Lazy 注入 CouponService,规避循环依赖

- DatabaseInitializer:种子「新用户专享10元券」grant_type 改为 REGISTER,并加幂等存量数据修正(无 DDL、无新表)

- schema.sql 注释补充 REGISTER;管理端 CouponManagement 下拉/标签与 CouponGrantLog 标签支持 REGISTER
Agnes Agent пре 2 недеља
родитељ
комит
055f6da25b

+ 9 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -4162,7 +4162,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
             if (count == null || count == 0) {
                 jdbcTemplate.update(
                         "INSERT INTO coupon (name, type, value, min_spend, applicable_to, valid_from, valid_until, total_count, used_count, grant_type, grant_level_code) " +
-                                "VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 YEAR), -1, 0, NULL, NULL)",
+                                "VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 YEAR), -1, 0, 'REGISTER', NULL)",
                         "新用户专享10元券", "FIXED", 1000, 0, "ALL");
                 jdbcTemplate.update(
                         "INSERT INTO coupon (name, type, value, min_spend, applicable_to, valid_from, valid_until, total_count, used_count, grant_type, grant_level_code) " +
@@ -4178,6 +4178,14 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
                                 "VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 YEAR), -1, 0, 'JOIN', 'PREMIUM', 1)");
                 log.info("优惠券种子数据已初始化(含 JOIN 赠券模板)");
             }
+            // COUPON-REG:幂等修正存量库——将「新用户专享10元券」模板标记为 REGISTER 自动发放
+            // (存量库种子创建时 grant_type 为 NULL,改后无需 DDL,仅数据修正)
+            int fixed = jdbcTemplate.update(
+                    "UPDATE coupon SET grant_type = 'REGISTER' " +
+                            "WHERE name = '新用户专享10元券' AND (grant_type IS NULL OR grant_type = '')");
+            if (fixed > 0) {
+                log.info("已标记 {} 张存量「新用户专享10元券」模板为 REGISTER 自动发放", fixed);
+            }
         } catch (Exception e) {
             log.warn("初始化优惠券数据失败: {}", e.getMessage());
         }

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

@@ -481,6 +481,28 @@ public class CouponService {
         }
     }
 
+    /** REGISTER: 新用户注册成功/绑定家庭时自动发放(2026-10-27 COUPON-REG)。
+     *  发到家庭维度(family_coupon),走 grantFamily 幂等链路(uk_family_grant 防重),
+     *  period 固定为 "REGISTER" 非空常量,确保每家庭每模板只发一次。 */
+    @Transactional
+    public void grantFamilyRegisterCoupons(Long familyId, String source) {
+        if (familyId == null) {
+            return;
+        }
+        List<Coupon> coupons = couponMapper.selectList(
+                new LambdaQueryWrapper<Coupon>()
+                        .eq(Coupon::getGrantType, "REGISTER")
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
+        if (coupons.isEmpty()) {
+            logger.warn("未找到 REGISTER 赠券模板: familyId={}", familyId);
+            return;
+        }
+        for (Coupon c : coupons) {
+            grantFamily(familyId, c.getId(), "REGISTER", "REGISTER", source);
+        }
+        logger.info("发放 REGISTER 赠券完成: familyId={}, count={}", familyId, coupons.size());
+    }
+
     /**
      * 收回家庭人口券:成员被管理员踢出时,撤销该家庭发放给该成员的 POPULATION 券。
      * 仅作废仍为 AVAILABLE 的券(已 USED 的券不追溯)。

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyInvitationService.java

@@ -12,6 +12,8 @@ import com.etotem.cfc.mapper.FamilyInvitationMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.UserMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -19,6 +21,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.UUID;
 
+@Slf4j
 @Service
 public class FamilyInvitationService {
 
@@ -37,6 +40,10 @@ public class FamilyInvitationService {
     @Resource
     private FamilyMemberService familyMemberService;
 
+    @Resource
+    @Lazy
+    private CouponService couponService;
+
     public InviteValidateDTO validateInvitation(String token) {
         FamilyInvitation invitation = familyInvitationMapper.selectOne(
                 new LambdaQueryWrapper<FamilyInvitation>()
@@ -201,6 +208,14 @@ public class FamilyInvitationService {
             familyMemberMapper.insert(newMember);
         }
 
+        // COUPON-REG:接受邀请绑定家庭后自动发放 REGISTER 赠券到家庭维度(发券异常不阻断加入)
+        try {
+            couponService.grantFamilyRegisterCoupons(invitation.getFamilyId(), "user:" + user.getId());
+        } catch (Exception e) {
+            // 记录日志但不阻断加入家庭
+            log.warn("接受邀请后发放 REGISTER 赠券异常: userId={}, familyId={}", user.getId(), invitation.getFamilyId(), e);
+        }
+
         return "加入家庭成功";
     }
 

+ 13 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyJoinRequestService.java

@@ -13,6 +13,7 @@ import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -48,6 +49,10 @@ public class FamilyJoinRequestService {
     @Resource
     private CommissionService commissionService;
 
+    @Resource
+    @Lazy
+    private CouponService couponService;
+
     /**
      * 创建家庭加入请求(待审批)
      */
@@ -216,6 +221,14 @@ public class FamilyJoinRequestService {
         // 平台推荐人绑定(需求:未注册/已注册但无推荐人 → 绑定为邀请人的上级推荐人;已有推荐人 → 不动)
         bindReferrerFromInviter(request, user);
 
+        // COUPON-REG:审批通过绑定家庭后自动发放 REGISTER 赠券到家庭维度(发券异常不阻断审批)
+        try {
+            couponService.grantFamilyRegisterCoupons(family.getId(), "user:" + user.getId());
+        } catch (Exception e) {
+            log.warn("审批通过后发放 REGISTER 赠券异常: userId={}, familyId={}, error={}",
+                    user.getId(), family.getId(), e.getMessage());
+        }
+
         request.setStatus("approved");
         request.setApproverUserId(approverUserId);
         request.setComment(comment);

+ 19 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -20,6 +20,7 @@ import com.etotem.cfc.service.api.UserServiceInterface;
 import lombok.extern.slf4j.Slf4j;
 import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.util.DigestUtils;
 
@@ -74,6 +75,10 @@ private FamilyInvitationService familyInvitationService;
     @Resource
     private FamilyJoinRequestService familyJoinRequestService;
 
+    @Resource
+    @Lazy
+    private CouponService couponService;
+
     @Resource
     private ConsigneeMapper consigneeMapper;
 
@@ -1412,6 +1417,13 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
             }
         }
         
+        // COUPON-REG:绑定家庭后自动发放 REGISTER 赠券到家庭维度(发券异常不阻断绑定)
+        try {
+            couponService.grantFamilyRegisterCoupons(family.getId(), "user:" + userId);
+        } catch (Exception e) {
+            log.error("绑定家庭后发放 REGISTER 赠券异常: userId={}, familyId={}", userId, family.getId(), e);
+        }
+
         return true;
     }
 
@@ -1444,6 +1456,13 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
         addCreatorAsMember(user, family.getId());
 
         log.info("已为用户 {} 自动创建家庭 {} ({})", user.getId(), family.getId(), familyName);
+
+        // COUPON-REG:注册成功自动发放 REGISTER 赠券到家庭维度(发券异常不阻断注册)
+        try {
+            couponService.grantFamilyRegisterCoupons(family.getId(), "user:" + user.getId());
+        } catch (Exception e) {
+            log.error("注册后发放 REGISTER 赠券异常: userId={}, familyId={}", user.getId(), family.getId(), e);
+        }
         return user;
     }
 

+ 2 - 2
cfc-backend/src/main/resources/schema.sql

@@ -2342,7 +2342,7 @@ CREATE TABLE IF NOT EXISTS coupon (
     product_id BIGINT COMMENT '绑定商品ID,NULL=全场通用',
     family_id BIGINT DEFAULT NULL COMMENT '模板块绑定家庭ID(NULL=全平台模板)',
     points_price INT DEFAULT 0 COMMENT '积分兑换价,0=不可积分兑换',
-    grant_type VARCHAR(16) COMMENT '发放规则:JOIN/PERIODIC/POPULATION,NULL=不自动发放',
+    grant_type VARCHAR(16) COMMENT '发放规则:JOIN/PERIODIC/POPULATION/REGISTER,NULL=不自动发放',
     grant_level_code VARCHAR(32) COMMENT '发放规则对应会员等级',
     grant_period VARCHAR(16) COMMENT 'PERIODIC周期:MONTHLY/QUARTERLY',
     grant_quantity INT DEFAULT 1 COMMENT '每次发放数量',
@@ -4719,7 +4719,7 @@ CREATE TABLE IF NOT EXISTS family_coupon_grant_log (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     family_id BIGINT NOT NULL COMMENT '受赠家庭ID',
     coupon_id BIGINT NOT NULL COMMENT '券模板ID',
-    grant_type VARCHAR(16) NOT NULL COMMENT 'JOIN/PERIODIC/POPULATION/EXCHANGE/CF_EXCHANGE',
+    grant_type VARCHAR(16) NOT NULL COMMENT 'JOIN/PERIODIC/POPULATION/REGISTER/EXCHANGE/CF_EXCHANGE',
     period VARCHAR(16) COMMENT '周期标识(YYYY-MM或YYYY-Qn),PERIODIC防重用',
     quantity INT DEFAULT 1 COMMENT '发放数量',
     source VARCHAR(64) COMMENT '触发来源(订单号/成员ID等)',

+ 2 - 2
cfc-web/src/views/admin/CouponGrantLog.vue

@@ -100,11 +100,11 @@ export default {
   },
   methods: {
     grantTypeLabel(val) {
-      const map = { JOIN: '开通赠券', PERIODIC: '周期补发', POPULATION: '家庭人口券', EXCHANGE: '积分兑换', CF_EXCHANGE: 'CF值兑换' }
+      const map = { JOIN: '开通赠券', PERIODIC: '周期补发', POPULATION: '家庭人口券', REGISTER: '新用户注册赠券', EXCHANGE: '积分兑换', CF_EXCHANGE: 'CF值兑换' }
       return map[val] || val
     },
     grantTypeTagType(val) {
-      const map = { JOIN: 'success', PERIODIC: 'primary', POPULATION: 'warning', EXCHANGE: 'danger', CF_EXCHANGE: 'info' }
+      const map = { JOIN: 'success', PERIODIC: 'primary', POPULATION: 'warning', REGISTER: 'info', EXCHANGE: 'danger', CF_EXCHANGE: 'info' }
       return map[val] || 'info'
     },
     async loadData() {

+ 2 - 1
cfc-web/src/views/admin/CouponManagement.vue

@@ -148,6 +148,7 @@
             <el-option label="JOIN(开通会员赠)" value="JOIN" />
             <el-option label="PERIODIC(周期补发)" value="PERIODIC" />
             <el-option label="POPULATION(家庭人口券)" value="POPULATION" />
+            <el-option label="REGISTER(新用户注册赠)" value="REGISTER" />
           </el-select>
         </el-form-item>
         <el-form-item v-if="form.grantType === 'JOIN' || form.grantType === 'PERIODIC'" label="适用会员等级">
@@ -292,7 +293,7 @@ export default {
       return map[val] || val
     },
     grantTypeLabel(val) {
-      const map = { JOIN: '开通赠券', PERIODIC: '周期补发', POPULATION: '家庭人口券' }
+      const map = { JOIN: '开通赠券', PERIODIC: '周期补发', POPULATION: '家庭人口券', REGISTER: '新用户注册赠券' }
       return map[val] || val
     },
     async loadData() {