소스 검색

第三方接口调整提交代码

liuchengsen 1 개월 전
부모
커밋
32bfc77e1d

+ 7 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/mapper/BusinessLicenseMapper.java

@@ -19,6 +19,13 @@ public interface BusinessLicenseMapper extends BaseMapper<BusinessLicense> {
     @Select("SELECT * FROM t_business_license WHERE user_id = #{userId} LIMIT 1")
     BusinessLicense selectByUserId(Long userId);
 
+    /**
+     * 原子标记入驻奖励已发放(CAS:仅当 reward_granted=0 时更新为1)
+     * @return 受影响行数(1=首次标记成功,0=已被其他请求标记)
+     */
+    @Update("UPDATE t_business_license SET reward_granted = 1 WHERE id = #{id} AND reward_granted = 0")
+    int markRewardGranted(@Param("id") Long id);
+
     /**
      * 审核列表查询(LEFT JOIN t_user,支持按审核状态筛选 + 手机号/联系电话关键词搜索)
      */

+ 7 - 11
zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java

@@ -1132,13 +1132,15 @@ public class AuthService {
     /**
      * 首次入驻审核通过:赠送30天高级会员 + 推送Redis事件供活动模块消费
      * <p>与 BusinessLicenseService.approve() 的奖励逻辑保持一致。
-     * <p>幂等性检查:通过 rewardGranted 标记保证同一入驻记录仅奖励一次,
-     * 即使审核通过后重新编辑→重新审核,也不会重复发放
+     * <p>幂等性检查:通过 DB 原子 CAS(UPDATE WHERE reward_granted = 0)保证同一入驻记录
+     * 全局仅奖励一次,杜绝并发场景下的竞态条件
      */
     private void grantLicenseApprovedReward(Long userId, Long licenseId) {
-        com.xuekairui.user.entity.BusinessLicense license = businessLicenseService.getById(licenseId);
-        if (license != null && Boolean.TRUE.equals(license.getRewardGranted())) {
-            log.info("用户{}的入驻记录奖励已发放,跳过重复赠送: licenseId={}", userId, licenseId);
+        // 原子标记奖励已发放(CAS:仅当 reward_granted=0 时更新为1),
+        // 杜绝并发场景下 rewardGranted 检查与更新的竞态条件,确保全局仅发放一次
+        int claimed = businessLicenseService.markRewardGrantedAtomic(licenseId);
+        if (claimed == 0) {
+            log.info("用户{}的入驻记录奖励已被标记,跳过重复赠送: licenseId={}", userId, licenseId);
             return;
         }
 
@@ -1150,12 +1152,6 @@ public class AuthService {
                 "入驻信息审核通过赠送30天高级会员");
         log.info("入驻信息审核通过(第三方同步),赠送用户{} 30天高级会员", userId);
 
-        // 标记奖励已发放,防止重复
-        if (license != null) {
-            license.setRewardGranted(true);
-            businessLicenseService.updateById(license);
-        }
-
         // 事务提交后推送 Redis 事件,供活动模块消费(邀请有礼等)
         // 活动模块通过 uk_invitee 唯一索引保证同一被邀请人仅奖励一次
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {

+ 29 - 18
zhijiayun-user/src/main/java/com/xuekairui/user/service/BusinessLicenseService.java

@@ -133,6 +133,18 @@ public class BusinessLicenseService {
         return hasOtherApprovedLicense(userId, excludeLicenseId);
     }
 
+    /**
+     * 原子标记入驻奖励已发放(CAS:仅当 reward_granted=0 时更新为1)。
+     * <p>供 AuthService 第三方同步场景使用,与 approve() 方法使用同一 CAS 机制,
+     * 确保全局仅发放一次高级会员权益。
+     *
+     * @param licenseId 入驻信息 ID
+     * @return 受影响行数(1=首次标记成功,0=已被其他请求标记,调用方据此决定是否发放奖励)
+     */
+    public int markRewardGrantedAtomic(Long licenseId) {
+        return businessLicenseMapper.markRewardGranted(licenseId);
+    }
+
     // ======================== 用户侧:提交入驻信息 ========================
 
     /**
@@ -471,8 +483,11 @@ public class BusinessLicenseService {
         license.setShowVerifiedBadge(true);
         businessLicenseMapper.updateById(license);
 
-        // 首次审核通过,赠送 30 天高级会员(rewardGranted 保证同一入驻记录仅发一次奖励)
-        if (!Boolean.TRUE.equals(license.getRewardGranted())) {
+        // 原子标记奖励已发放(CAS:WHERE reward_granted = 0),
+        // 杜绝并发场景下 rewardGranted 检查与更新的竞态条件,确保全局仅发放一次
+        int claimed = businessLicenseMapper.markRewardGranted(licenseId);
+        if (claimed > 0) {
+            // 首次标记成功 → 赠送 30 天高级会员
             membershipService.grantOrExtendMembership(
                     userId,
                     MembershipLevel.PRO,
@@ -481,10 +496,6 @@ public class BusinessLicenseService {
                     "入驻信息审核通过赠送30天高级会员");
             log.info("入驻信息审核通过,赠送用户{} 30天高级会员", userId);
 
-            // 标记奖励已发放
-            license.setRewardGranted(true);
-            businessLicenseMapper.updateById(license);
-
             // 事务提交后推送 Redis 事件,供活动模块消费(邀请有礼活动)
             // 活动模块通过 uk_invitee 唯一索引保证同一被邀请人仅奖励一次
             TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@@ -497,19 +508,19 @@ public class BusinessLicenseService {
                     }
                 }
             });
+
+            // 事务提交后同步客户入驻信息到第三方系统(智价云药店版),仅首次审核通过时同步
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCommit() {
+                    syncShopResourceToThirdParty(submitter, license, "APPROVE");
+                }
+            });
         } else {
-            log.info("入驻信息重新审核通过,奖励已发放,跳过重复奖励: licenseId={}, userId={}",
+            log.info("入驻信息重新审核通过,奖励已发放(CAS防并发),跳过重复奖励及第三方同步: licenseId={}, userId={}",
                     licenseId, userId);
         }
 
-        // 事务提交后同步客户入驻信息到第三方系统(智价云药店版),失败不阻塞主流程
-        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
-            @Override
-            public void afterCommit() {
-                syncShopResourceToThirdParty(submitter, license, "APPROVE");
-            }
-        });
-
         // 审计日志:审核通过,记录客户信息
         String userName = submitter.getNickname() != null
                 ? submitter.getNickname() : String.valueOf(userId);
@@ -918,9 +929,9 @@ public class BusinessLicenseService {
         if (license == null) {
             throw new BusinessException(ErrorCode.NOT_FOUND, "入驻信息记录不存在");
         }
-//        if (!LicenseStatus.PENDING.name().equals(license.getReviewStatus())) {
-//            throw new BusinessException(ErrorCode.BAD_REQUEST, "该入驻信息已审核,无法重复操作");
-//        }
+        if (!LicenseStatus.PENDING.name().equals(license.getReviewStatus())) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "该入驻信息已审核,无法重复操作");
+        }
         return license;
     }