From fbe1f9a9e383f9546a7a14063f6c91b697a2933b Mon Sep 17 00:00:00 2001
From: 13404089107 <puwei@sinata.cn>
Date: 星期三, 06 五月 2026 17:23:25 +0800
Subject: [PATCH] Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/tcm-training

---
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java |   93 +++++++++++++++++++++++++++++++++++++---------
 1 files changed, 74 insertions(+), 19 deletions(-)

diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
index ed36eac..c871b69 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
@@ -12,7 +12,6 @@
 import com.tcm.training.admin.app.auth.vo.AppLoginUserVO;
 import com.tcm.training.admin.app.auth.vo.AppPasswordResetVerifyResponse;
 import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
-import com.tcm.training.admin.app.auth.exception.InviteCodeRequiredException;
 import com.tcm.training.admin.app.share.service.AppShareService;
 import com.tcm.training.admin.member.entity.AppUser;
 import com.tcm.training.admin.member.mapper.UserMapper;
@@ -36,7 +35,6 @@
     private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?![0-9]+$)(?![a-zA-Z]+$)(?![^0-9a-zA-Z]+$).{8,32}$");
     private static final String PASSWORD_RESET_TICKET_CACHE_PREFIX = "app:password-reset:ticket:";
     private static final long PASSWORD_RESET_TICKET_EXPIRE_MINUTES = 10L;
-
     private final UserMapper userMapper;
     private final AppSmsCodeService appSmsCodeService;
     private final BCryptPasswordEncoder passwordEncoder;
@@ -115,7 +113,6 @@
         AppWechatUserProfile profile = appWechatAuthClient.resolveUserProfile(request);
         AppUser user = findUserByWechatOpenid(profile.getOpenid());
         if (user == null) {
-            requireInviteCodeForThirdPartyRegister(request.getInviteCode());
             user = buildWechatUser(profile);
             bindInviteRelationIfAbsent(user, request.getInviteCode());
             userMapper.insert(user);
@@ -136,7 +133,6 @@
         AppAppleUserProfile profile = appAppleAuthClient.resolveUserProfile(request);
         AppUser user = findUserByAppleSubject(profile.getSubject());
         if (user == null) {
-            requireInviteCodeForThirdPartyRegister(request.getInviteCode());
             user = buildAppleUser(profile);
             bindInviteRelationIfAbsent(user, request.getInviteCode());
             userMapper.insert(user);
@@ -187,11 +183,28 @@
     }
 
     /**
+     * 刷新当前 APP 登录态,并重新签发一枚新的 token。
+     */
+    public AppLoginResponse refreshToken() {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        AppUser user = requireEnabledUserById(userId);
+        // 当前 Sa-Token 版本不支持按 token 原地续期,这里直接为当前用户重新签发新 token。
+        appAuthSessionService.login(user.getId());
+        return buildLoginResponse(user, appAuthSessionService.getTokenInfo());
+    }
+
+    /**
      * 为成功登录的 APP 用户建立登录态并拼装统一响应。
      */
     private AppLoginResponse createLoginResponse(AppUser user) {
         appAuthSessionService.login(user.getId());
-        SaTokenInfo tokenInfo = appAuthSessionService.getTokenInfo();
+        return buildLoginResponse(user, appAuthSessionService.getTokenInfo());
+    }
+
+    /**
+     * 统一构造 APP 登录响应,保证登录与续期接口返回结构一致。
+     */
+    private AppLoginResponse buildLoginResponse(AppUser user, SaTokenInfo tokenInfo) {
         return new AppLoginResponse(
                 tokenInfo.getTokenValue(),
                 "Bearer",
@@ -240,6 +253,18 @@
     }
 
     /**
+     * 按用户主键查询并校验 APP 用户状态。
+     */
+    private AppUser requireEnabledUserById(Long userId) {
+        AppUser user = userMapper.selectById(userId);
+        if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) {
+            throw new IllegalArgumentException("用户不存在");
+        }
+        requireEnabledUser(user);
+        return user;
+    }
+
+    /**
      * 校验给定 APP 用户是否处于启用状态。
      */
     private void requireEnabledUser(AppUser user) {
@@ -249,23 +274,27 @@
     }
 
     /**
-     * 按微信 openid 查询本地 APP 用户。
+     * 按微信 openid 查询本地 APP 用户,重复脏数据场景优先返回启用且已绑定手机号的账号。
      */
     private AppUser findUserByWechatOpenid(String openid) {
         return userMapper.selectOne(new LambdaQueryWrapper<AppUser>()
                 .eq(AppUser::getWechatOpenid, openid)
                 .eq(AppUser::getDeleted, 0)
-                .last("limit 1"));
+                .last("ORDER BY CASE WHEN status = 1 THEN 0 ELSE 1 END, "
+                        + "CASE WHEN phone IS NOT NULL AND TRIM(phone) <> '' THEN 0 ELSE 1 END, "
+                        + "updated_at DESC, id DESC LIMIT 1"));
     }
 
     /**
-     * 按苹果 subject 查询本地 APP 用户。
+     * 按苹果 subject 查询本地 APP 用户,重复脏数据场景优先返回启用且已绑定手机号的账号。
      */
     private AppUser findUserByAppleSubject(String subject) {
         return userMapper.selectOne(new LambdaQueryWrapper<AppUser>()
                 .eq(AppUser::getAppleIdentitySubject, subject)
                 .eq(AppUser::getDeleted, 0)
-                .last("limit 1"));
+                .last("ORDER BY CASE WHEN status = 1 THEN 0 ELSE 1 END, "
+                        + "CASE WHEN phone IS NOT NULL AND TRIM(phone) <> '' THEN 0 ELSE 1 END, "
+                        + "updated_at DESC, id DESC LIMIT 1"));
     }
 
     /**
@@ -277,15 +306,6 @@
         user.setStatus(1);
         applyWechatProfile(user, profile, true);
         return user;
-    }
-
-    /**
-     * 第三方授权首次创建账号时必须携带医生邀请码。
-     */
-    private void requireInviteCodeForThirdPartyRegister(String inviteCode) {
-        if (normalizeInviteCode(inviteCode) == null) {
-            throw new InviteCodeRequiredException();
-        }
     }
 
     /**
@@ -321,13 +341,14 @@
     private void applyWechatProfile(AppUser user, AppWechatUserProfile profile, boolean syncLocalProfile) {
         String nickname = trimToNull(profile.getNickname());
         String avatar = trimToNull(profile.getAvatar());
+        String currentNickname = trimToNull(user.getNickname());
 
         user.setWechatOpenid(profile.getOpenid());
         user.setWechatNickname(nickname);
         user.setWechatAvatar(avatar);
         user.setWechatBoundAt(LocalDateTime.now());
 
-        if (syncLocalProfile || trimToNull(user.getNickname()) == null) {
+        if (shouldRefreshLocalNickname(currentNickname, profile.getOpenid(), nickname, syncLocalProfile)) {
             user.setNickname(nickname == null ? profile.getOpenid() : nickname);
         }
         if (syncLocalProfile || trimToNull(user.getAvatar()) == null) {
@@ -339,6 +360,40 @@
     }
 
     /**
+     * 仅在首次建号、昵称为空、openid 兜底值或历史乱码场景下刷新本地昵称,避免覆盖用户手动修改结果。
+     */
+    private boolean shouldRefreshLocalNickname(String currentNickname,
+                                               String openid,
+                                               String latestWechatNickname,
+                                               boolean syncLocalProfile) {
+        if (syncLocalProfile) {
+            return true;
+        }
+        if (trimToNull(latestWechatNickname) == null) {
+            return false;
+        }
+        String normalizedCurrentNickname = trimToNull(currentNickname);
+        if (normalizedCurrentNickname == null) {
+            return true;
+        }
+        String normalizedOpenid = trimToNull(openid);
+        if (normalizedOpenid != null && normalizedOpenid.equals(normalizedCurrentNickname)) {
+            return true;
+        }
+        return containsNicknameMojibake(normalizedCurrentNickname);
+    }
+
+    /**
+     * 识别历史微信昵称乱码的常见特征,限定为已知乱码模式,避免误伤正常昵称。
+     */
+    private boolean containsNicknameMojibake(String nickname) {
+        return nickname.contains("ð")
+                || nickname.contains("Ã")
+                || nickname.contains("â")
+                || nickname.contains("�");
+    }
+
+    /**
      * 将苹果资料回填到本地用户,并在首次自动建号时补齐基础展示昵称。
      */
     private void applyAppleProfile(AppUser user, AppAppleUserProfile profile, boolean syncLocalProfile) {

--
Gitblit v1.7.1