Ver Fonte

登录调整

liuchengsen há 1 mês atrás
pai
commit
cef528554a

+ 1 - 1
zhijiayun-gateway/src/main/resources/application.yml

@@ -23,7 +23,7 @@ spring:
       file-size-threshold: 2MB
 # JWT 配置(公共,各环境可覆盖)
 jwt:
-  secret: YourSuperSecretKeyForJWTTokenGenerationMustBeAtLeast256BitsLong!!
+  secret: bR9!vD2&mP5#zT7_sG3+nF8=Xj[0hL]aC|1dQ;eW,.yB{kN}
   user-expiration: 3600000         # 普通用户(PC等) 1小时(毫秒)
   admin-expiration: 43200000       # 管理员 12小时(毫秒)
   miniapp-expiration: 2592000000   # 小程序 30天(毫秒)

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

@@ -28,6 +28,12 @@ public interface BusinessLicenseMapper extends BaseMapper<BusinessLicense> {
     @Update("UPDATE t_business_license SET reward_granted = 1 WHERE id = #{id} AND reward_granted = 0")
     int markRewardGranted(@Param("id") Long id);
 
+    /**
+     * 查询用户已审核通过但尚未发放奖励的入驻记录
+     */
+    @Select("SELECT * FROM t_business_license WHERE user_id = #{userId} AND review_status = 'APPROVED' AND reward_granted = 0 LIMIT 1")
+    BusinessLicense selectApprovedUnrewarded(@Param("userId") Long userId);
+
     /**
      * 审核列表分页查询(LEFT JOIN t_user,支持按审核状态筛选 + 手机号/联系电话关键词搜索)
      */

+ 9 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/mapper/LoginLogMapper.java

@@ -28,4 +28,13 @@ public interface LoginLogMapper extends BaseMapper<LoginLog> {
             " GROUP BY user_id, login_source" +
             "</script>")
     List<LoginLog> selectLatestByUserIds(@Param("userIds") List<Long> userIds);
+
+
+    /**
+     * 检查用户是否有过 PC 或 SMS 来源的登录记录(非小程序登录)
+     * @param userId 用户ID
+     * @return 是否存在符合条件的登录记录
+     */
+    @Select("SELECT EXISTS(SELECT 1 FROM t_login_log WHERE user_id = #{userId} AND login_source IN ('PC', 'SMS'))")
+    boolean hasPcOrSmsLogin(@Param("userId") Long userId);
 }

+ 2 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/security/JwtAuthenticationFilter.java

@@ -74,6 +74,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
         String path = request.getRequestURI();
         boolean isHeartbeat = "/api/auth/heartbeat".equals(path);
 
+        // 打印所有请求的URL(含无JWT的请求)
+        log.info("[JWT解析] uri={}", path);
         // 尽早从 token 中提取 userId 注入 MDC(宽容模式,过期 token 也能提取)
         Claims claims1 = extractUidForMdcAndSetRequestAttr(token, request);
 
@@ -262,7 +264,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
                 if (loginSource != null) {
                     request.setAttribute("loginSource", loginSource);
                 }
-                log.info("[JWT解析] uri={}, userId={}, loginSource={}", request.getRequestURI(), uid, loginSource);
             }
         } catch (Exception ignored) {
             // token 完全无法解析,忽略

+ 33 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java

@@ -125,6 +125,14 @@ public class AuthService {
 
         // 登录时检查入驻信息,未同步成功则补推一次到第三方
         syncShopResourceOnLogin(user.getId());
+
+        // 登录时检查是否有待补发的高级会员奖励(入驻审核通过但此前无 PC/SMS 登录)
+        Long currentUserId = user.getId();
+        try {
+            businessLicenseService.tryGrantPendingLicenseReward(currentUserId);
+        } catch (Exception e) {
+            log.warn("PC/SMS登录后补发入驻奖励失败,不影响登录: userId={}", currentUserId, e);
+        }
         return response;
     }
 
@@ -150,6 +158,11 @@ public class AuthService {
             user.setLoginSource("PC");
             userMapper.updateById(user);
             syncShopResourceOnLogin(user.getId());
+            try {
+                businessLicenseService.tryGrantPendingLicenseReward(user.getId());
+            } catch (Exception e) {
+                log.warn("PC登录后补发入驻奖励失败,不影响登录: userId={}", user.getId(), e);
+            }
             return buildLoginResponse(user);
         }
 
@@ -253,6 +266,11 @@ public class AuthService {
 
         // 4. 生成正式Token
         syncShopResourceOnLogin(wechatUser.getId());
+        try {
+            businessLicenseService.tryGrantPendingLicenseReward(wechatUser.getId());
+        } catch (Exception e) {
+            log.warn("PC登录后补发入驻奖励失败,不影响登录: userId={}", wechatUser.getId(), e);
+        }
         return buildLoginResponse(wechatUser);
     }
 
@@ -1071,6 +1089,13 @@ public class AuthService {
      * 全局仅奖励一次,杜绝并发场景下的竞态条件。
      */
     private void grantLicenseApprovedReward(Long userId, Long licenseId) {
+        // 校验用户是否至少有过一次 PC/SMS 登录(小程序登录不算),
+        // 有登录记录才标记 reward_granted 并发 PRO,无登录则留待登录时补发
+        if (!loginLogMapper.hasPcOrSmsLogin(userId)) {
+            log.info("用户{}未有PC/SMS登录记录,暂不发放高级会员奖励,待登录后补发: licenseId={}", userId, licenseId);
+            return;
+        }
+
         // 原子标记奖励已发放(CAS:仅当 reward_granted=0 时更新为1),
         // 杜绝并发场景下 rewardGranted 检查与更新的竞态条件,确保全局仅发放一次
         int claimed = businessLicenseService.markRewardGrantedAtomic(licenseId);
@@ -1538,6 +1563,14 @@ public class AuthService {
         auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
                 OperationType.USER_LOGIN, "USER:" + user.getId(), user.getId());
         syncShopResourceOnLogin(user.getId());
+
+        // PC端登录时检查是否有待补发的高级会员奖励
+        Long currentUserId = user.getId();
+        try {
+            businessLicenseService.tryGrantPendingLicenseReward(currentUserId);
+        } catch (Exception e) {
+            log.warn("PC登录后补发入驻奖励失败,不影响登录: userId={}", currentUserId, e);
+        }
         return response;
     }
 

+ 59 - 22
zhijiayun-user/src/main/java/com/xuekairui/user/service/BusinessLicenseService.java

@@ -52,6 +52,7 @@ public class BusinessLicenseService {
     private final AdminMapper adminMapper;
     private final ShopResourceSyncService shopResourceSyncService;
     private final com.xuekairui.user.mapper.LicenseSyncDiffMapper licenseSyncDiffMapper;
+    private final com.xuekairui.user.mapper.LoginLogMapper loginLogMapper;
 
     // ======================== 查询方法 ========================
 
@@ -188,6 +189,33 @@ public class BusinessLicenseService {
         return businessLicenseMapper.markRewardGranted(licenseId);
     }
 
+    /**
+     * 登录时检查并提供奖励(入驻信息审核通过,但目前无PC/SMS登录记录 → 触发补发)
+     * <p>
+     * 场景:入驻信息审核通过但用户未PC/SMS登录 → rewardGranted 保持 0 不标记,
+     * 待用户首次 PC/SMS 登录时补发 PRO 会员。
+     *
+     * @param userId 用户ID
+     */
+    @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW)
+    public void tryGrantPendingLicenseReward(Long userId) {
+        BusinessLicense license = businessLicenseMapper.selectApprovedUnrewarded(userId);
+        if (license == null) {
+            return;
+        }
+        // 原子标记 + 发放(与 approve() 中的逻辑一致)
+        int claimed = businessLicenseMapper.markRewardGranted(license.getId());
+        if (claimed > 0) {
+            membershipService.grantOrExtendMembership(
+                    userId,
+                    MembershipLevel.PRO,
+                    30,
+                    "LICENSE",
+                    "入驻信息审核通过赠送30天高级会员");
+            log.info("用户PC/SMS登录后补发高级会员奖励: userId={}, licenseId={}", userId, license.getId());
+        }
+    }
+
     /** 获取管理员用户名(用于审计日志操作人) */
     private String getAdminName(Long adminId) {
         if (adminId == null) {
@@ -577,35 +605,44 @@ public class BusinessLicenseService {
         license.setShowVerifiedBadge(true);
         businessLicenseMapper.updateById(license);
 
-        // 原子标记奖励已发放(CAS:WHERE reward_granted = 0),
-        // 杜绝并发场景下 rewardGranted 检查与更新的竞态条件,确保全局仅发放一次
-        int claimed = businessLicenseMapper.markRewardGranted(licenseId);
-        if (claimed > 0) {
-            // 首次标记成功 → 赠送 30 天高级会员
-            membershipService.grantOrExtendMembership(
-                    userId,
-                    MembershipLevel.PRO,
-                    30,
-                    "LICENSE",
-                    "入驻信息审核通过赠送30天高级会员");
-            log.info("入驻信息审核通过,赠送用户{} 30天高级会员", userId);
+        // 校验用户是否至少有过一次 PC/SMS 登录(小程序登录不算),
+        // 有登录记录才标记 reward_granted,无登录则不标记,等登录时补发
+        boolean hasPcOrSmsLogin = loginLogMapper.hasPcOrSmsLogin(userId);
+        if (!hasPcOrSmsLogin) {
+            log.info("用户{}未有PC/SMS登录记录,暂不发放高级会员奖励,待登录后补发: licenseId={}", userId, licenseId);
+        } else {
+            // 原子标记奖励已发放(CAS:WHERE reward_granted = 0),
+            // 杜绝并发场景下 rewardGranted 检查与更新的竞态条件,确保全局仅发放一次
+            int claimed = businessLicenseMapper.markRewardGranted(licenseId);
+            if (claimed > 0) {
+                // 首次标记成功 → 赠送 30 天高级会员
+                membershipService.grantOrExtendMembership(
+                        userId,
+                        MembershipLevel.PRO,
+                        30,
+                        "LICENSE",
+                        "入驻信息审核通过赠送30天高级会员");
+                log.info("入驻信息审核通过,赠送用户{} 30天高级会员", userId);
+            } else {
+                log.info("入驻信息重新审核通过,奖励已发放(CAS防并发),跳过重复奖励: licenseId={}, userId={}",
+                        licenseId, userId);
+            }
+        }
 
-            // 事务提交后推送 Redis 事件,供活动模块消费(邀请有礼活动)
-            // 活动模块通过 uk_invitee 唯一索引保证同一被邀请人仅奖励一次
-            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
-                @Override
-                public void afterCommit() {
+        // 事务提交后推送 Redis 事件和同步(仅成功发放时推送事件)
+        final boolean finalHasLogin = hasPcOrSmsLogin;
+        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+            @Override
+            public void afterCommit() {
+                if (finalHasLogin) {
                     try {
                         activityEventRedisService.pushLicenseApproved(userId, license.getId(), "BUSINESS_LICENSE");
                     } catch (Exception e) {
                         log.warn("存入入驻审核通过事件失败,不影响主流程: userId={}", userId, e);
                     }
                 }
-            });
-        } else {
-            log.info("入驻信息重新审核通过,奖励已发放(CAS防并发),跳过重复奖励: licenseId={}, userId={}",
-                    licenseId, userId);
-        }
+            }
+        });
 
         // 事务提交后同步客户入驻信息到第三方(无论是否首次通过,每次审核状态变更都同步)
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {