From 4f367056ab0e73d044e920d44b1d7dd0c8b65d80 Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期二, 12 五月 2026 14:25:28 +0800
Subject: [PATCH] fix:注册流程优化、屏蔽账号密码登录、已登录后token永不过期

---
 traditionalApp/pages/settings/settings.vue                                                                              |   18 -
 tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/register/service/AppRegisterServiceTest.java        |  112 ++++++-
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java                    |  145 +--------
 tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java                |  160 +++------
 traditionalApp/pages/login/login.vue                                                                                    |   81 ----
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java             |    2 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterUserRequest.java            |    3 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterOperatorPrepareRequest.java |    3 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/service/AppRegisterService.java            |   95 ++++--
 tcm-training-admin-backend/src/main/resources/static/app-download/index.html                                            |  168 -----------
 traditionalApp/pages/register/register.vue                                                                              |   43 --
 traditionalApp/pages/login/service.js                                                                                   |    7 
 traditionalApp/utils/auth-session.js                                                                                    |    1 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java                    |    6 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/controller/AppRegisterController.java      |    6 
 15 files changed, 254 insertions(+), 596 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 c871b69..2506692 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
@@ -21,20 +21,16 @@
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
 import java.util.regex.Pattern;
 
 /**
- * APP 登录认证服务,负责短信登录、密码登录、微信登录与忘记密码流程。
+ * APP 登录认证服务,负责短信登录、第三方登录与登录态刷新流程。
  */
 @Service
 public class AppAuthService {
 
     private static final Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
-    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 static final String PASSWORD_AUTH_DISABLED_MESSAGE = "当前版本仅支持验证码登录";
     private final UserMapper userMapper;
     private final AppSmsCodeService appSmsCodeService;
     private final BCryptPasswordEncoder passwordEncoder;
@@ -75,12 +71,11 @@
     }
 
     /**
-     * 为已注册 APP 用户生成忘记密码验证码。
+     * 当前版本已下线密码能力,旧客户端调用时返回统一提示。
      */
     public AppSmsCodeSendResponse sendPasswordResetCode(AppSmsCodeSendRequest request) {
-        String phone = normalizePhone(request.getPhone());
-        requireEnabledUserByPhone(phone);
-        return appSmsCodeService.createPasswordResetCode(phone);
+        throwPasswordAuthDisabled();
+        return null;
     }
 
     /**
@@ -94,16 +89,11 @@
     }
 
     /**
-     * 使用手机号密码完成 APP 用户登录。
+     * 当前版本已下线密码登录,旧客户端调用时返回统一提示。
      */
     public AppLoginResponse passwordLogin(AppPasswordLoginRequest request) {
-        String phone = normalizePhone(request.getPhone());
-        String password = normalizeLoginPassword(request.getPassword());
-        AppUser user = requireEnabledUserByPhone(phone);
-        if (!passwordEncoder.matches(password, user.getPassword())) {
-            throw new IllegalArgumentException("账号或密码错误");
-        }
-        return createLoginResponse(user);
+        throwPasswordAuthDisabled();
+        return null;
     }
 
     /**
@@ -147,32 +137,18 @@
     }
 
     /**
-     * 校验忘记密码验证码并生成一次性重置凭证。
+     * 当前版本已下线密码能力,旧客户端调用时返回统一提示。
      */
     public AppPasswordResetVerifyResponse verifyPasswordResetCode(AppPasswordResetVerifyRequest request) {
-        String phone = normalizePhone(request.getPhone());
-        requireEnabledUserByPhone(phone);
-        appSmsCodeService.validatePasswordResetCode(phone, normalizeCode(request.getCode()));
-
-        String resetTicket = createResetTicket();
-        cacheResetTicket(resetTicket, phone);
-        return new AppPasswordResetVerifyResponse(resetTicket);
+        throwPasswordAuthDisabled();
+        return null;
     }
 
     /**
-     * 使用一次性重置凭证更新 APP 用户密码。
+     * 当前版本已下线密码能力,旧客户端调用时返回统一提示。
      */
     public void resetPassword(AppPasswordResetRequest request) {
-        String resetTicket = normalizeResetTicket(request.getResetTicket());
-        String password = normalizeNewPassword(request.getPassword());
-        String confirmPassword = normalizeConfirmPassword(request.getConfirmPassword());
-        validatePasswordConfirmation(password, confirmPassword);
-
-        String phone = loadPhoneByResetTicket(resetTicket);
-        AppUser user = requireEnabledUserByPhone(phone);
-        user.setPassword(passwordEncoder.encode(password));
-        userMapper.updateById(user);
-        stringRedisTemplate.delete(buildResetTicketKey(resetTicket));
+        throwPasswordAuthDisabled();
     }
 
     /**
@@ -445,14 +421,10 @@
     }
 
     /**
-     * 归一化密码登录场景下的密码输入,仅要求非空。
+     * 抛出 APP 密码能力下线后的统一业务提示。
      */
-    private String normalizeLoginPassword(String password) {
-        String normalized = trimToNull(password);
-        if (normalized == null) {
-            throw new IllegalArgumentException("请输入密码");
-        }
-        return normalized;
+    private void throwPasswordAuthDisabled() {
+        throw new IllegalArgumentException(PASSWORD_AUTH_DISABLED_MESSAGE);
     }
 
     /**
@@ -465,91 +437,6 @@
         }
         String upperInviteCode = normalized.toUpperCase();
         return upperInviteCode.matches("^[A-Z0-9]{6}$") ? upperInviteCode : null;
-    }
-
-    /**
-     * 归一化并校验忘记密码场景下的新密码格式。
-     */
-    private String normalizeNewPassword(String password) {
-        String normalized = trimToNull(password);
-        if (normalized == null) {
-            throw new IllegalArgumentException("请输入新密码");
-        }
-        if (!PASSWORD_PATTERN.matcher(normalized).matches()) {
-            throw new IllegalArgumentException("密码需为8-32位,且包含数字、字母、符号至少2种");
-        }
-        return normalized;
-    }
-
-    /**
-     * 归一化并校验确认密码输入。
-     */
-    private String normalizeConfirmPassword(String confirmPassword) {
-        String normalized = trimToNull(confirmPassword);
-        if (normalized == null) {
-            throw new IllegalArgumentException("请再次输入密码");
-        }
-        if (!PASSWORD_PATTERN.matcher(normalized).matches()) {
-            throw new IllegalArgumentException("密码需为8-32位,且包含数字、字母、符号至少2种");
-        }
-        return normalized;
-    }
-
-    /**
-     * 归一化并校验一次性重置凭证。
-     */
-    private String normalizeResetTicket(String resetTicket) {
-        String normalized = trimToNull(resetTicket);
-        if (normalized == null) {
-            throw new IllegalArgumentException("重置凭证已失效,请重新验证");
-        }
-        return normalized;
-    }
-
-    /**
-     * 校验两次输入的新密码保持一致。
-     */
-    private void validatePasswordConfirmation(String password, String confirmPassword) {
-        if (!password.equals(confirmPassword)) {
-            throw new IllegalArgumentException("两次密码输入不一致");
-        }
-    }
-
-    /**
-     * 生成忘记密码场景使用的一次性重置凭证。
-     */
-    private String createResetTicket() {
-        return UUID.randomUUID().toString().replace("-", "");
-    }
-
-    /**
-     * 将重置凭证与手机号绑定写入 Redis,供下一步更新密码使用。
-     */
-    private void cacheResetTicket(String resetTicket, String phone) {
-        stringRedisTemplate.opsForValue().set(
-                buildResetTicketKey(resetTicket),
-                phone,
-                PASSWORD_RESET_TICKET_EXPIRE_MINUTES,
-                TimeUnit.MINUTES
-        );
-    }
-
-    /**
-     * 从 Redis 读取并校验一次性重置凭证对应的手机号。
-     */
-    private String loadPhoneByResetTicket(String resetTicket) {
-        String phone = stringRedisTemplate.opsForValue().get(buildResetTicketKey(resetTicket));
-        if (phone == null || phone.trim().isEmpty()) {
-            throw new IllegalArgumentException("重置凭证已失效,请重新验证");
-        }
-        return phone;
-    }
-
-    /**
-     * 构造忘记密码重置凭证的 Redis key。
-     */
-    private String buildResetTicketKey(String resetTicket) {
-        return PASSWORD_RESET_TICKET_CACHE_PREFIX + resetTicket;
     }
 
     /**
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
index 2d03c28..e684517 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
@@ -11,7 +11,7 @@
 public class AppAuthSessionService {
 
     /**
-     * 为指定 APP 用户建立登录态。
+     * 为指定 APP 用户建立永久登录态。
      */
     public void login(Long userId) {
         StpAppUserUtil.login(userId);
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java
index dc9b577..44adc37 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java
@@ -1,5 +1,6 @@
 package com.tcm.training.admin.app.auth.support;
 
+import cn.dev33.satoken.stp.SaLoginModel;
 import cn.dev33.satoken.stp.SaTokenInfo;
 import cn.dev33.satoken.stp.StpLogic;
 
@@ -9,15 +10,16 @@
 public final class StpAppUserUtil {
 
     private static final StpLogic STP_LOGIC = new StpLogic("app-user");
+    private static final long NEVER_EXPIRE_TIMEOUT = -1L;
 
     private StpAppUserUtil() {
     }
 
     /**
-     * 为 APP 用户建立登录态。
+     * 为 APP 用户建立永久登录态。
      */
     public static void login(Object loginId) {
-        STP_LOGIC.login(loginId);
+        STP_LOGIC.login(loginId, new SaLoginModel().setTimeout(NEVER_EXPIRE_TIMEOUT));
     }
 
     /**
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/controller/AppRegisterController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/controller/AppRegisterController.java
index 7a7253c..a357933 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/controller/AppRegisterController.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/controller/AppRegisterController.java
@@ -1,6 +1,7 @@
 package com.tcm.training.admin.app.register.controller;
 
 import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest;
+import com.tcm.training.admin.app.auth.vo.AppLoginResponse;
 import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
 import com.tcm.training.admin.app.register.dto.AppRegisterOperatorCompleteRequest;
 import com.tcm.training.admin.app.register.dto.AppRegisterOperatorPrepareRequest;
@@ -9,7 +10,6 @@
 import com.tcm.training.admin.app.register.vo.AppRegisterOperatorPrepareResponse;
 import com.tcm.training.admin.app.register.vo.AppRegisterRegionDatasetVO;
 import com.tcm.training.admin.app.register.vo.AppRegisterRegionOptionVO;
-import com.tcm.training.admin.app.register.vo.AppRegisterResultVO;
 import com.tcm.training.admin.common.ApiResponse;
 import com.tcm.training.admin.upload.ObsUploadService;
 import com.tcm.training.admin.upload.UploadController;
@@ -82,7 +82,7 @@
      * 提交普通用户注册。
      */
     @PostMapping("/user")
-    public ApiResponse<AppRegisterResultVO> registerUser(@Valid @RequestBody AppRegisterUserRequest request) {
+    public ApiResponse<AppLoginResponse> registerUser(@Valid @RequestBody AppRegisterUserRequest request) {
         return ApiResponse.ok(appRegisterService.registerUser(request));
     }
 
@@ -98,7 +98,7 @@
      * 提交经营者第二步并创建本地诊所与诊所用户。
      */
     @PostMapping("/operator/complete")
-    public ApiResponse<AppRegisterResultVO> completeOperator(@Valid @RequestBody AppRegisterOperatorCompleteRequest request) {
+    public ApiResponse<AppLoginResponse> completeOperator(@Valid @RequestBody AppRegisterOperatorCompleteRequest request) {
         return ApiResponse.ok(appRegisterService.completeOperatorRegister(request));
     }
 
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterOperatorPrepareRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterOperatorPrepareRequest.java
index 3cbb64a..310866f 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterOperatorPrepareRequest.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterOperatorPrepareRequest.java
@@ -19,7 +19,4 @@
 
     @NotBlank(message = "请输入验证码")
     private String code;
-
-    @NotBlank(message = "请输入密码")
-    private String password;
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterUserRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterUserRequest.java
index f51644e..44b26fd 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterUserRequest.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/dto/AppRegisterUserRequest.java
@@ -20,9 +20,6 @@
     @NotBlank(message = "请输入验证码")
     private String code;
 
-    @NotBlank(message = "请输入密码")
-    private String password;
-
     private String inviteCode;
 
     private String bindTicket;
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/service/AppRegisterService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/service/AppRegisterService.java
index 384f3e2..8e06c55 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/service/AppRegisterService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/service/AppRegisterService.java
@@ -2,7 +2,10 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
 import com.tcm.training.admin.app.auth.service.AppSmsCodeService;
+import com.tcm.training.admin.app.auth.vo.AppLoginResponse;
+import com.tcm.training.admin.app.auth.vo.AppLoginUserVO;
 import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
 import com.tcm.training.admin.app.integration.haizhentong.entity.HztAppUser;
 import com.tcm.training.admin.app.integration.haizhentong.entity.HztClinic;
@@ -16,14 +19,13 @@
 import com.tcm.training.admin.app.register.vo.AppRegisterOperatorPrepareResponse;
 import com.tcm.training.admin.app.register.vo.AppRegisterRegionDatasetVO;
 import com.tcm.training.admin.app.register.vo.AppRegisterRegionOptionVO;
-import com.tcm.training.admin.app.register.vo.AppRegisterResultVO;
 import com.tcm.training.admin.common.PhoneMasker;
 import com.tcm.training.admin.live.entity.Clinic;
 import com.tcm.training.admin.live.mapper.ClinicMapper;
 import com.tcm.training.admin.member.entity.AppUser;
 import com.tcm.training.admin.member.mapper.UserMapper;
+import cn.dev33.satoken.stp.SaTokenInfo;
 import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -40,7 +42,6 @@
 public class AppRegisterService {
 
     private static final Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
-    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?![0-9]+$)(?![a-zA-Z]+$)(?![^0-9a-zA-Z]+$).{8,32}$");
     private static final String REGISTER_TICKET_CACHE_PREFIX = "app:register:operator:";
     private static final String INVITE_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
     private static final int INVITE_CODE_LENGTH = 6;
@@ -48,8 +49,8 @@
 
     private final UserMapper userMapper;
     private final ClinicMapper clinicMapper;
-    private final BCryptPasswordEncoder passwordEncoder;
     private final AppSmsCodeService appSmsCodeService;
+    private final AppAuthSessionService appAuthSessionService;
     private final AppRegisterRegionService appRegisterRegionService;
     private final HztAppUserMapper hztAppUserMapper;
     private final HztClinicMapper hztClinicMapper;
@@ -62,8 +63,8 @@
      */
     public AppRegisterService(UserMapper userMapper,
                               ClinicMapper clinicMapper,
-                              BCryptPasswordEncoder passwordEncoder,
                               AppSmsCodeService appSmsCodeService,
+                              AppAuthSessionService appAuthSessionService,
                               AppRegisterRegionService appRegisterRegionService,
                               HztAppUserMapper hztAppUserMapper,
                               HztClinicMapper hztClinicMapper,
@@ -72,8 +73,8 @@
                               AppShareService appShareService) {
         this.userMapper = userMapper;
         this.clinicMapper = clinicMapper;
-        this.passwordEncoder = passwordEncoder;
         this.appSmsCodeService = appSmsCodeService;
+        this.appAuthSessionService = appAuthSessionService;
         this.appRegisterRegionService = appRegisterRegionService;
         this.hztAppUserMapper = hztAppUserMapper;
         this.hztClinicMapper = hztClinicMapper;
@@ -109,9 +110,8 @@
      * 完成普通用户注册,并按需绑定上级诊所用户。
      * 普通用户注册会先查询海诊通数据源,因此这里不包裹本地事务,避免事务先绑定 master 导致跨库查询失效。
      */
-    public AppRegisterResultVO registerUser(AppRegisterUserRequest request) {
+    public AppLoginResponse registerUser(AppRegisterUserRequest request) {
         String phone = normalizePhone(request.getPhone());
-        String password = normalizePassword(request.getPassword());
         String inviteCode = normalizeInviteCode(request.getInviteCode());
         String bindTicket = normalizeBindTicket(request.getBindTicket());
 
@@ -120,7 +120,6 @@
 
         AppUser user = new AppUser();
         user.setPhone(phone);
-        user.setPassword(passwordEncoder.encode(password));
         user.setType(0);
         user.setStatus(1);
 
@@ -132,7 +131,7 @@
         bindSuperiorClinicIfNecessary(user, inviteCode, bindTicket);
 
         userMapper.insert(user);
-        return new AppRegisterResultVO(user.getId(), user.getType(), user.getClinicId(), null);
+        return createLoginResponse(user);
     }
 
     /**
@@ -140,13 +139,12 @@
      */
     public AppRegisterOperatorPrepareResponse prepareOperatorRegister(AppRegisterOperatorPrepareRequest request) {
         String phone = normalizePhone(request.getPhone());
-        String password = normalizePassword(request.getPassword());
 
         ensurePhoneAvailable(phone);
         appSmsCodeService.validateRegisterCode(phone, normalizeCode(request.getCode()));
 
         HztClinic hztClinic = findHztClinicByPhone(phone);
-        OperatorRegisterDraft draft = buildOperatorDraft(phone, password, hztClinic);
+        OperatorRegisterDraft draft = buildOperatorDraft(phone, hztClinic);
         String registerTicket = createRegisterTicket();
         cacheDraft(registerTicket, draft);
 
@@ -161,7 +159,7 @@
      * 完成经营者第二步,创建本地诊所与诊所用户。
      */
     @Transactional
-    public AppRegisterResultVO completeOperatorRegister(AppRegisterOperatorCompleteRequest request) {
+    public AppLoginResponse completeOperatorRegister(AppRegisterOperatorCompleteRequest request) {
         OperatorRegisterDraft draft = loadDraft(normalizeRegisterTicket(request.getRegisterTicket()));
         ensurePhoneAvailable(draft.getPhone());
 
@@ -184,14 +182,13 @@
         AppUser user = new AppUser();
         user.setNickname(clinic.getName());
         user.setPhone(draft.getPhone());
-        user.setPassword(draft.getEncodedPassword());
         user.setType(1);
         user.setStatus(1);
         user.setClinicId(clinic.getId());
         userMapper.insert(user);
 
         clearDraft(request.getRegisterTicket());
-        return new AppRegisterResultVO(user.getId(), user.getType(), clinic.getId(), clinic.getInviteCode());
+        return createLoginResponse(user);
     }
 
     /**
@@ -218,6 +215,42 @@
         }
         user.setClinicId(bindingTarget.getClinicId());
         user.setSuperiorClinicUserId(bindingTarget.getSuperiorClinicUserId());
+    }
+
+    /**
+     * 注册成功后立即建立 APP 登录态并返回统一登录响应。
+     */
+    private AppLoginResponse createLoginResponse(AppUser user) {
+        appAuthSessionService.login(user.getId());
+        return buildLoginResponse(user, appAuthSessionService.getTokenInfo());
+    }
+
+    /**
+     * 使用 Sa-Token 信息构造 APP 登录响应。
+     */
+    private AppLoginResponse buildLoginResponse(AppUser user, SaTokenInfo tokenInfo) {
+        return new AppLoginResponse(
+                tokenInfo.getTokenValue(),
+                "Bearer",
+                tokenInfo.getTokenTimeout(),
+                buildLoginUser(user)
+        );
+    }
+
+    /**
+     * 将注册后的 APP 用户实体转换为登录响应用户摘要。
+     */
+    private AppLoginUserVO buildLoginUser(AppUser user) {
+        AppLoginUserVO loginUser = new AppLoginUserVO();
+        loginUser.setId(user.getId());
+        loginUser.setNickname(resolveFallbackNickname(user));
+        loginUser.setPhone(user.getPhone());
+        loginUser.setAvatar(trimToNull(user.getAvatar()));
+        loginUser.setType(user.getType());
+        loginUser.setStatus(user.getStatus());
+        loginUser.setClinicId(user.getClinicId());
+        loginUser.setMemberExpireAt(user.getMemberExpireAt());
+        return loginUser;
     }
 
     /**
@@ -258,12 +291,11 @@
     }
 
     /**
-     * 构造经营者注册草稿,避免第二步再次输入验证码与密码。
+     * 构造经营者注册草稿,避免第二步再次输入验证码。
      */
-    private OperatorRegisterDraft buildOperatorDraft(String phone, String password, HztClinic hztClinic) {
+    private OperatorRegisterDraft buildOperatorDraft(String phone, HztClinic hztClinic) {
         OperatorRegisterDraft draft = new OperatorRegisterDraft();
         draft.setPhone(phone);
-        draft.setEncodedPassword(passwordEncoder.encode(password));
         if (hztClinic == null) {
             return draft;
         }
@@ -423,6 +455,17 @@
     }
 
     /**
+     * 优先使用用户昵称,缺失时回退到手机号脱敏昵称。
+     */
+    private String resolveFallbackNickname(AppUser user) {
+        String nickname = trimToNull(user.getNickname());
+        if (nickname != null) {
+            return nickname;
+        }
+        return resolveFallbackNickname(user.getPhone());
+    }
+
+    /**
      * 归一化并校验手机号格式。
      */
     private String normalizePhone(String phone) {
@@ -440,20 +483,6 @@
         String normalized = trimToNull(code);
         if (normalized == null) {
             throw new IllegalArgumentException("请输入验证码");
-        }
-        return normalized;
-    }
-
-    /**
-     * 归一化并校验注册密码规则。
-     */
-    private String normalizePassword(String password) {
-        String normalized = trimToNull(password);
-        if (normalized == null) {
-            throw new IllegalArgumentException("请输入密码");
-        }
-        if (!PASSWORD_PATTERN.matcher(normalized).matches()) {
-            throw new IllegalArgumentException("密码需为8-32位,且包含数字、字母、符号至少2种");
         }
         return normalized;
     }
@@ -515,8 +544,6 @@
     public static class OperatorRegisterDraft {
 
         private String phone;
-
-        private String encodedPassword;
 
         private String contactName;
 
diff --git a/tcm-training-admin-backend/src/main/resources/static/app-download/index.html b/tcm-training-admin-backend/src/main/resources/static/app-download/index.html
index 3bc0852..f5a5e9c 100644
--- a/tcm-training-admin-backend/src/main/resources/static/app-download/index.html
+++ b/tcm-training-admin-backend/src/main/resources/static/app-download/index.html
@@ -131,98 +131,9 @@
             color: #c63520;
             min-height: 20px;
         }
-        .wechat-guide {
-            position: fixed;
-            top: 0;
-            right: 0;
-            bottom: 0;
-            left: 0;
-            z-index: 999;
-            display: none;
-            background: rgba(47, 29, 18, 0.72);
-            padding: 20px 16px 28px;
-            overflow-y: auto;
-        }
-        .wechat-guide.is-visible {
-            display: block;
-        }
-        .wechat-guide__panel {
-            width: 100%;
-            max-width: 520px;
-            margin: 12px auto 0;
-            background: linear-gradient(180deg, #fffaf4 0%, #fff3e6 100%);
-            border-radius: 24px;
-            padding: 22px 18px 18px;
-            box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22);
-            position: relative;
-        }
-        .wechat-guide__badge {
-            display: inline-flex;
-            align-items: center;
-            border-radius: 999px;
-            background: #ffe8cf;
-            color: #b24a11;
-            font-size: 12px;
-            font-weight: 700;
-            padding: 6px 10px;
-        }
-        .wechat-guide__title {
-            margin: 14px 0 10px;
-            font-size: 24px;
-            line-height: 1.35;
-            font-weight: 700;
-            color: #2f1d12;
-        }
-        .wechat-guide__desc {
-            margin: 0;
-            color: #6f5b4d;
-            font-size: 14px;
-            line-height: 1.8;
-        }
-        .wechat-guide__steps {
-            margin: 18px 0 0;
-            padding: 16px 16px 16px 38px;
-            border-radius: 18px;
-            background: rgba(255, 255, 255, 0.72);
-            color: #5b4638;
-            font-size: 14px;
-            line-height: 1.9;
-        }
-        .wechat-guide__hint {
-            margin-top: 14px;
-            color: #9b5f33;
-            font-size: 13px;
-            line-height: 1.7;
-        }
-        .wechat-guide__close {
-            margin-top: 18px;
-            width: 100%;
-            border: none;
-            border-radius: 14px;
-            background: #c63520;
-            color: #fff;
-            font-size: 15px;
-            font-weight: 600;
-            padding: 14px 16px;
-            cursor: pointer;
-        }
     </style>
 </head>
 <body>
-<div class="wechat-guide" id="wechatGuide">
-    <div class="wechat-guide__panel">
-        <div class="wechat-guide__badge">微信内暂不支持直接下载</div>
-        <h2 class="wechat-guide__title">请使用系统浏览器打开后再下载 APP</h2>
-        <p class="wechat-guide__desc">微信内置浏览器会拦截安装包下载。请先通过右上角菜单切换到系统浏览器,再继续安装。</p>
-        <ol class="wechat-guide__steps">
-            <li>点击右上角“...”。</li>
-            <li>选择“在浏览器打开”。</li>
-            <li>进入浏览器后,再点击下载按钮。</li>
-        </ol>
-        <div class="wechat-guide__hint">邀请码会保留在当前页面,切换到浏览器后仍可复制并用于注册绑定诊所。</div>
-        <button class="wechat-guide__close" id="wechatGuideClose" type="button">我知道了</button>
-    </div>
-</div>
 <div class="page">
     <div class="card">
         <div class="badge">中医培训 APP 下载</div>
@@ -263,7 +174,6 @@
         inviteCode: '',
         androidDownloadUrl: '',
         iosDownloadUrl: '',
-        wechatBrowser: false,
         openAppBaseUrl: '',
         bindTicketLoading: false,
         openAppUrl: '',
@@ -290,8 +200,6 @@
      */
     async function bootstrap() {
         bindActions()
-        detectWechatEnvironment()
-        bindWechatEnvironmentListeners()
         bindResumeAutoOpenListeners()
         const inviteCode = normalizeInviteCode(readQueryParam('inviteCode'))
         state.inviteCode = inviteCode
@@ -367,20 +275,12 @@
             }
             copyInviteCode(state.inviteCode, true)
         })
-        document.getElementById('wechatGuideClose').addEventListener('click', function () {
-            hideWechatGuide()
-        })
     }
 
     /**
      * 处理主按钮动作,优先拉起 APP,未安装时自动下载。
      */
     async function handlePrimaryAction(fromResume) {
-        if (shouldBlockDirectDownload()) {
-            showWechatGuide()
-            showStatus('请点击右上角,选择在浏览器中打开后继续')
-            return
-        }
         if (!state.inviteCode) {
             showStatus('当前二维码未包含邀请码,请复制邀请码后手动注册')
             return
@@ -782,74 +682,6 @@
      */
     function renderInviteCode(inviteCode) {
         document.getElementById('inviteCodeValue').textContent = inviteCode || '--'
-    }
-
-    /**
-     * 根据当前环境刷新微信内提示。
-     */
-    function detectWechatEnvironment() {
-        const matched = isWechatBrowser()
-        state.wechatBrowser = matched
-        if (matched) {
-            showWechatGuide()
-            showStatus('检测到微信内打开,请点击右上角,选择在浏览器中打开后再下载')
-        }
-    }
-
-    /**
-     * 绑定微信桥注入后的补充检测。
-     */
-    function bindWechatEnvironmentListeners() {
-        document.addEventListener('WeixinJSBridgeReady', function () {
-            detectWechatEnvironment()
-        }, false)
-        window.addEventListener('load', function () {
-            window.setTimeout(function () {
-                detectWechatEnvironment()
-            }, 300)
-        })
-    }
-
-    /**
-     * 判断当前是否为微信内置浏览器。
-     */
-    function isWechatBrowser() {
-        if (state.wechatBrowser) {
-            return true
-        }
-        const userAgent = String(window.navigator.userAgent || '').toLowerCase()
-        if (userAgent.indexOf('micromessenger') >= 0 || userAgent.indexOf('wechat') >= 0) {
-            return true
-        }
-        if (window.__wxjs_environment === 'miniprogram') {
-            return true
-        }
-        if (typeof window.WeixinJSBridge !== 'undefined') {
-            return true
-        }
-        return false
-    }
-
-    /**
-     * 判断当前下载动作是否需要被微信环境拦截。
-     */
-    function shouldBlockDirectDownload() {
-        state.wechatBrowser = isWechatBrowser()
-        return state.wechatBrowser
-    }
-
-    /**
-     * 展示微信内打开浏览器指引。
-     */
-    function showWechatGuide() {
-        document.getElementById('wechatGuide').classList.add('is-visible')
-    }
-
-    /**
-     * 隐藏微信内打开浏览器指引。
-     */
-    function hideWechatGuide() {
-        document.getElementById('wechatGuide').classList.remove('is-visible')
     }
 
     /**
diff --git a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java
index 41dd1a6..00e455e 100644
--- a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java
+++ b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java
@@ -10,8 +10,6 @@
 import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest;
 import com.tcm.training.admin.app.auth.vo.AppLoginResponse;
 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;
@@ -26,13 +24,9 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.data.redis.core.ValueOperations;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
-import java.util.concurrent.TimeUnit;
-
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.mockito.ArgumentMatchers.any;
@@ -79,9 +73,6 @@
     @Mock
     private StringRedisTemplate stringRedisTemplate;
 
-    @Mock
-    private ValueOperations<String, String> valueOperations;
-
     private AppAuthService appAuthService;
 
     @Captor
@@ -105,128 +96,69 @@
     }
 
     /**
-     * 密码登录成功时应返回统一登录响应并建立 APP 登录态。
+     * 密码登录能力下线后旧客户端调用应返回统一提示。
      */
     @Test
-    void passwordLoginShouldReturnTokenWhenPasswordMatches() {
+    void passwordLoginShouldReturnDisabledMessage() {
         AppPasswordLoginRequest request = new AppPasswordLoginRequest();
         request.setPhone("13800138000");
         request.setPassword("abc123!!");
 
-        AppUser user = buildEnabledUser("13800138000", "ENCODED");
-        SaTokenInfo tokenInfo = new SaTokenInfo();
-        tokenInfo.setTokenValue("token-value");
-        tokenInfo.setTokenTimeout(7200L);
-
-        when(userMapper.selectOne(any())).thenReturn(user);
-        when(passwordEncoder.matches("abc123!!", "ENCODED")).thenReturn(true);
-        when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo);
-
-        AppLoginResponse response = appAuthService.passwordLogin(request);
-
-        verify(appAuthSessionService).login(1L);
-        assertEquals("token-value", response.getToken());
-        assertEquals("Bearer", response.getTokenType());
-        assertEquals(7200L, response.getExpiresIn());
-        assertEquals(Long.valueOf(1L), response.getUser().getId());
-        assertEquals("13800138000", response.getUser().getPhone());
-    }
-
-    /**
-     * 密码错误时应返回统一错误文案。
-     */
-    @Test
-    void passwordLoginShouldFailWhenPasswordMismatch() {
-        AppPasswordLoginRequest request = new AppPasswordLoginRequest();
-        request.setPhone("13800138000");
-        request.setPassword("bad-password");
-
-        AppUser user = buildEnabledUser("13800138000", "ENCODED");
-        when(userMapper.selectOne(any())).thenReturn(user);
-        when(passwordEncoder.matches("bad-password", "ENCODED")).thenReturn(false);
-
         IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                 () -> appAuthService.passwordLogin(request));
 
-        assertEquals("账号或密码错误", exception.getMessage());
+        assertEquals("当前版本仅支持验证码登录", exception.getMessage());
+        verify(passwordEncoder, never()).matches(anyString(), anyString());
+        verify(appAuthSessionService, never()).login(anyLong());
     }
 
     /**
-     * 忘记密码发码仅允许已启用用户发送。
+     * 忘记密码发码能力下线后旧客户端调用应返回统一提示。
      */
     @Test
-    void sendPasswordResetCodeShouldReturnExpireSeconds() {
+    void sendPasswordResetCodeShouldReturnDisabledMessage() {
         AppSmsCodeSendRequest request = new AppSmsCodeSendRequest();
         request.setPhone("13800138001");
 
-        when(userMapper.selectOne(any())).thenReturn(buildEnabledUser("13800138001", "ENCODED"));
-        when(appSmsCodeService.createPasswordResetCode("13800138001")).thenReturn(new AppSmsCodeSendResponse(300));
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> appAuthService.sendPasswordResetCode(request));
 
-        AppSmsCodeSendResponse response = appAuthService.sendPasswordResetCode(request);
-
-        assertEquals(300, response.getExpireInSeconds());
+        assertEquals("当前版本仅支持验证码登录", exception.getMessage());
+        verify(appSmsCodeService, never()).createPasswordResetCode(anyString());
     }
 
     /**
-     * 忘记密码验证码校验成功后应签发一次性重置凭证并写入缓存。
+     * 忘记密码验证码校验能力下线后旧客户端调用应返回统一提示。
      */
     @Test
-    void verifyPasswordResetCodeShouldCacheTicket() {
+    void verifyPasswordResetCodeShouldReturnDisabledMessage() {
         AppPasswordResetVerifyRequest request = new AppPasswordResetVerifyRequest();
         request.setPhone("13800138002");
         request.setCode("666666");
 
-        when(userMapper.selectOne(any())).thenReturn(buildEnabledUser("13800138002", "ENCODED"));
-        when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
-        doNothing().when(appSmsCodeService).validatePasswordResetCode("13800138002", "666666");
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> appAuthService.verifyPasswordResetCode(request));
 
-        AppPasswordResetVerifyResponse response = appAuthService.verifyPasswordResetCode(request);
-
-        assertNotNull(response.getResetTicket());
-        verify(valueOperations).set(anyString(), anyString(), anyLong(), any(TimeUnit.class));
+        assertEquals("当前版本仅支持验证码登录", exception.getMessage());
+        verify(appSmsCodeService, never()).validatePasswordResetCode(anyString(), anyString());
     }
 
     /**
-     * 使用有效重置凭证时应更新数据库中的密码并销毁凭证。
+     * 重置密码能力下线后旧客户端调用应返回统一提示。
      */
     @Test
-    void resetPasswordShouldUpdatePasswordAndDeleteTicket() {
+    void resetPasswordShouldReturnDisabledMessage() {
         AppPasswordResetRequest request = new AppPasswordResetRequest();
         request.setResetTicket("ticket-1");
         request.setPassword("abc123!!");
         request.setConfirmPassword("abc123!!");
 
-        AppUser user = buildEnabledUser("13800138003", "OLD");
-
-        when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
-        when(valueOperations.get(anyString())).thenReturn("13800138003");
-        when(userMapper.selectOne(any())).thenReturn(user);
-        when(passwordEncoder.encode("abc123!!")).thenReturn("NEW-ENCODED");
-
-        appAuthService.resetPassword(request);
-
-        verify(userMapper).updateById(appUserCaptor.capture());
-        verify(stringRedisTemplate).delete(anyString());
-        assertEquals("NEW-ENCODED", appUserCaptor.getValue().getPassword());
-    }
-
-    /**
-     * 重置凭证失效时应拒绝更新密码。
-     */
-    @Test
-    void resetPasswordShouldFailWhenTicketExpired() {
-        AppPasswordResetRequest request = new AppPasswordResetRequest();
-        request.setResetTicket("expired-ticket");
-        request.setPassword("abc123!!");
-        request.setConfirmPassword("abc123!!");
-
-        when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
-        when(valueOperations.get(anyString())).thenReturn(null);
-
         IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                 () -> appAuthService.resetPassword(request));
 
-        assertEquals("重置凭证已失效,请重新验证", exception.getMessage());
+        assertEquals("当前版本仅支持验证码登录", exception.getMessage());
+        verify(passwordEncoder, never()).encode(anyString());
+        verify(userMapper, never()).updateById(any(AppUser.class));
     }
 
     /**
@@ -310,10 +242,10 @@
     }
 
     /**
-     * 微信首次登录未携带邀请码时应提示联系管理员获取。
+     * 微信首次登录未携带邀请码时应创建待绑定手机号的临时用户。
      */
     @Test
-    void wechatLoginShouldRequireInviteCodeWhenOpenidNotExists() {
+    void wechatLoginShouldCreatePendingUserWhenInviteCodeMissing() {
         AppWechatLoginRequest request = new AppWechatLoginRequest();
         request.setAccessToken("access-token");
         request.setOpenid("openid-new");
@@ -323,12 +255,19 @@
 
         when(appWechatAuthClient.resolveUserProfile(request)).thenReturn(profile);
         when(userMapper.selectOne(any())).thenReturn(null);
+        when(userMapper.insert(any(AppUser.class))).thenAnswer(invocation -> {
+            AppUser inserted = invocation.getArgument(0);
+            inserted.setId(2001L);
+            return 1;
+        });
+        when(appAuthSessionService.getTokenInfo()).thenReturn(buildTokenInfo("wechat-pending-token", 7200L));
 
-        InviteCodeRequiredException exception = assertThrows(InviteCodeRequiredException.class,
-                () -> appAuthService.wechatLogin(request));
+        AppLoginResponse response = appAuthService.wechatLogin(request);
 
-        assertEquals("暂无邀请码,请联系管理员获取", exception.getMessage());
-        verify(userMapper, never()).insert(any());
+        verify(userMapper).insert(appUserCaptor.capture());
+        assertNull(appUserCaptor.getValue().getPhone());
+        assertEquals("openid-new", appUserCaptor.getValue().getWechatOpenid());
+        assertEquals("wechat-pending-token", response.getToken());
     }
 
     /**
@@ -405,10 +344,10 @@
     }
 
     /**
-     * 苹果首次登录未携带邀请码时应提示联系管理员获取。
+     * 苹果首次登录未携带邀请码时应创建待绑定手机号的临时用户。
      */
     @Test
-    void appleLoginShouldRequireInviteCodeWhenSubjectNotExists() {
+    void appleLoginShouldCreatePendingUserWhenInviteCodeMissing() {
         AppAppleLoginRequest request = new AppAppleLoginRequest();
         request.setIdentityToken("identity-token");
         request.setUserIdentifier("apple-user-new");
@@ -419,12 +358,19 @@
 
         when(appAppleAuthClient.resolveUserProfile(request)).thenReturn(profile);
         when(userMapper.selectOne(any())).thenReturn(null);
+        when(userMapper.insert(any(AppUser.class))).thenAnswer(invocation -> {
+            AppUser inserted = invocation.getArgument(0);
+            inserted.setId(3001L);
+            return 1;
+        });
+        when(appAuthSessionService.getTokenInfo()).thenReturn(buildTokenInfo("apple-pending-token", 7200L));
 
-        InviteCodeRequiredException exception = assertThrows(InviteCodeRequiredException.class,
-                () -> appAuthService.appleLogin(request));
+        AppLoginResponse response = appAuthService.appleLogin(request);
 
-        assertEquals("暂无邀请码,请联系管理员获取", exception.getMessage());
-        verify(userMapper, never()).insert(any());
+        verify(userMapper).insert(appUserCaptor.capture());
+        assertNull(appUserCaptor.getValue().getPhone());
+        assertEquals("apple-subject-new", appUserCaptor.getValue().getAppleIdentitySubject());
+        assertEquals("apple-pending-token", response.getToken());
     }
 
     /**
@@ -456,6 +402,16 @@
     }
 
     /**
+     * 构造测试用 token 信息。
+     */
+    private SaTokenInfo buildTokenInfo(String tokenValue, long timeout) {
+        SaTokenInfo tokenInfo = new SaTokenInfo();
+        tokenInfo.setTokenValue(tokenValue);
+        tokenInfo.setTokenTimeout(timeout);
+        return tokenInfo;
+    }
+
+    /**
      * 统一构造启用中的 APP 用户测试桩。
      */
     private AppUser buildEnabledUser(String phone, String password) {
diff --git a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/register/service/AppRegisterServiceTest.java b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/register/service/AppRegisterServiceTest.java
index 5193807..9aa5cc2 100644
--- a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/register/service/AppRegisterServiceTest.java
+++ b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/register/service/AppRegisterServiceTest.java
@@ -3,20 +3,23 @@
 import com.baomidou.mybatisplus.core.MybatisConfiguration;
 import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
 import com.tcm.training.admin.app.auth.service.AppSmsCodeService;
+import com.tcm.training.admin.app.auth.vo.AppLoginResponse;
 import com.tcm.training.admin.app.integration.haizhentong.entity.HztAppUser;
 import com.tcm.training.admin.app.integration.haizhentong.entity.HztClinic;
 import com.tcm.training.admin.app.integration.haizhentong.mapper.HztAppUserMapper;
 import com.tcm.training.admin.app.integration.haizhentong.mapper.HztClinicMapper;
+import com.tcm.training.admin.app.register.dto.AppRegisterOperatorCompleteRequest;
 import com.tcm.training.admin.app.register.dto.AppRegisterOperatorPrepareRequest;
 import com.tcm.training.admin.app.register.dto.AppRegisterUserRequest;
 import com.tcm.training.admin.app.register.vo.AppRegisterOperatorPrepareResponse;
-import com.tcm.training.admin.app.register.vo.AppRegisterResultVO;
 import com.tcm.training.admin.app.share.service.AppShareService;
 import com.tcm.training.admin.live.entity.Clinic;
 import com.tcm.training.admin.live.mapper.ClinicMapper;
 import com.tcm.training.admin.member.entity.AppUser;
 import com.tcm.training.admin.member.mapper.UserMapper;
+import cn.dev33.satoken.stp.SaTokenInfo;
 import org.apache.ibatis.builder.MapperBuilderAssistant;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
@@ -28,7 +31,6 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.core.ValueOperations;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
 import java.util.concurrent.TimeUnit;
 
@@ -40,6 +42,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -65,10 +68,10 @@
     private ClinicMapper clinicMapper;
 
     @Mock
-    private BCryptPasswordEncoder passwordEncoder;
+    private AppSmsCodeService appSmsCodeService;
 
     @Mock
-    private AppSmsCodeService appSmsCodeService;
+    private AppAuthSessionService appAuthSessionService;
 
     @Mock
     private AppRegisterRegionService appRegisterRegionService;
@@ -99,8 +102,8 @@
         appRegisterService = new AppRegisterService(
                 userMapper,
                 clinicMapper,
-                passwordEncoder,
                 appSmsCodeService,
+                appAuthSessionService,
                 appRegisterRegionService,
                 hztAppUserMapper,
                 hztClinicMapper,
@@ -118,7 +121,6 @@
         AppRegisterUserRequest request = new AppRegisterUserRequest();
         request.setPhone("13800138000");
         request.setCode("666666");
-        request.setPassword("abc123!!");
         request.setInviteCode("abc123");
 
         HztAppUser hztAppUser = new HztAppUser();
@@ -133,29 +135,36 @@
         clinicUser.setId(88L);
         clinicUser.setClinicId(19L);
 
-        when(passwordEncoder.encode("abc123!!")).thenReturn("ENCODED");
+        AppShareService.InviteBindingTarget bindingTarget = new AppShareService.InviteBindingTarget();
+        bindingTarget.setClinicId(19L);
+        bindingTarget.setSuperiorClinicUserId(88L);
+        when(appShareService.resolveInviteBindingTarget("ABC123")).thenReturn(bindingTarget);
         when(hztAppUserMapper.selectOne(any())).thenReturn(hztAppUser);
-        when(clinicMapper.selectOne(any())).thenReturn(clinic);
-        when(userMapper.selectOne(any())).thenReturn(null, clinicUser);
+        when(userMapper.selectOne(any())).thenReturn(null);
         doNothing().when(appSmsCodeService).validateRegisterCode("13800138000", "666666");
         when(userMapper.insert(any(AppUser.class))).thenAnswer(invocation -> {
             AppUser inserted = invocation.getArgument(0);
             inserted.setId(1001L);
             return 1;
         });
+        when(appAuthSessionService.getTokenInfo()).thenReturn(buildTokenInfo("register-token", 7200L));
 
-        AppRegisterResultVO result = appRegisterService.registerUser(request);
+        AppLoginResponse result = appRegisterService.registerUser(request);
 
         verify(userMapper).insert(appUserCaptor.capture());
+        verify(appAuthSessionService).login(1001L);
         AppUser inserted = appUserCaptor.getValue();
         assertEquals("海诊通昵称", inserted.getNickname());
         assertEquals("https://cdn.test/avatar.png", inserted.getAvatar());
         assertEquals(Integer.valueOf(0), inserted.getGender());
         assertEquals(Long.valueOf(19L), inserted.getClinicId());
         assertEquals(Long.valueOf(88L), inserted.getSuperiorClinicUserId());
-        assertEquals("ENCODED", inserted.getPassword());
-        assertEquals(Long.valueOf(1001L), result.getUserId());
-        assertEquals(Integer.valueOf(0), result.getUserType());
+        assertNull(inserted.getPassword());
+        assertEquals("register-token", result.getToken());
+        assertEquals("Bearer", result.getTokenType());
+        assertEquals(7200L, result.getExpiresIn());
+        assertEquals(Long.valueOf(1001L), result.getUser().getId());
+        assertEquals(Integer.valueOf(0), result.getUser().getType());
     }
 
     /**
@@ -166,9 +175,7 @@
         AppRegisterUserRequest request = new AppRegisterUserRequest();
         request.setPhone("13800138123");
         request.setCode("666666");
-        request.setPassword("abc123!!");
 
-        when(passwordEncoder.encode("abc123!!")).thenReturn("ENCODED");
         when(userMapper.selectOne(any())).thenReturn(null);
         when(hztAppUserMapper.selectOne(any())).thenReturn(null);
         doNothing().when(appSmsCodeService).validateRegisterCode("13800138123", "666666");
@@ -177,16 +184,19 @@
             inserted.setId(1002L);
             return 1;
         });
+        when(appAuthSessionService.getTokenInfo()).thenReturn(buildTokenInfo("fallback-token", 3600L));
 
-        AppRegisterResultVO result = appRegisterService.registerUser(request);
+        AppLoginResponse result = appRegisterService.registerUser(request);
 
         verify(userMapper).insert(appUserCaptor.capture());
+        verify(appAuthSessionService).login(1002L);
         AppUser inserted = appUserCaptor.getValue();
         assertEquals("138****8123", inserted.getNickname());
         assertNull(inserted.getAvatar());
         assertNull(inserted.getGender());
-        assertEquals(Long.valueOf(1002L), result.getUserId());
-        assertEquals(Integer.valueOf(0), result.getUserType());
+        assertNull(inserted.getPassword());
+        assertEquals(Long.valueOf(1002L), result.getUser().getId());
+        assertEquals(Integer.valueOf(0), result.getUser().getType());
     }
 
     /**
@@ -197,9 +207,7 @@
         AppRegisterOperatorPrepareRequest request = new AppRegisterOperatorPrepareRequest();
         request.setPhone("13800138001");
         request.setCode("666666");
-        request.setPassword("abc123!!");
 
-        when(passwordEncoder.encode("abc123!!")).thenReturn("ENCODED");
         when(userMapper.selectOne(any())).thenReturn(null);
         when(hztClinicMapper.selectOne(any())).thenReturn(null);
         when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
@@ -222,7 +230,6 @@
         AppRegisterOperatorPrepareRequest request = new AppRegisterOperatorPrepareRequest();
         request.setPhone("13800138002");
         request.setCode("666666");
-        request.setPassword("abc123!!");
 
         HztClinic hztClinic = new HztClinic();
         hztClinic.setClinicName("海诊通诊所");
@@ -239,7 +246,6 @@
         regionSelection.setDistrictCode("330106");
         regionSelection.setDistrictName("西湖区");
 
-        when(passwordEncoder.encode("abc123!!")).thenReturn("ENCODED");
         when(userMapper.selectOne(any())).thenReturn(null);
         when(hztClinicMapper.selectOne(any())).thenReturn(hztClinic);
         when(appRegisterRegionService.resolveRegionSelection(null, "浙江省", null, "杭州市", null, "西湖区")).thenReturn(regionSelection);
@@ -255,4 +261,66 @@
         assertEquals("330106", response.getClinicInfo().getDistrictCode());
         assertEquals("文三路 1 号", response.getClinicInfo().getDetailAddress());
     }
+
+    /**
+     * 经营者第二步完成注册后应直接返回登录响应。
+     */
+    @Test
+    void completeOperatorShouldReturnLoginResponse() throws Exception {
+        AppRegisterOperatorCompleteRequest request = new AppRegisterOperatorCompleteRequest();
+        request.setRegisterTicket("ticket-1");
+        request.setInstitutionName("本地诊所");
+        request.setProvinceName("浙江省");
+        request.setCityName("杭州市");
+        request.setDistrictName("西湖区");
+        request.setDetailAddress("文三路 2 号");
+        request.setBusinessLicenseUrl("https://cdn.test/license.png");
+
+        AppRegisterService.OperatorRegisterDraft draft = new AppRegisterService.OperatorRegisterDraft();
+        draft.setPhone("13800138003");
+        draft.setContactName("负责人");
+
+        Clinic clinicExists = null;
+        when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
+        when(valueOperations.get("app:register:operator:ticket-1")).thenReturn(new ObjectMapper().writeValueAsString(draft));
+        when(userMapper.selectOne(any())).thenReturn(null);
+        when(clinicMapper.selectOne(any())).thenReturn(clinicExists);
+        when(clinicMapper.insert(any(Clinic.class))).thenAnswer(invocation -> {
+            Clinic inserted = invocation.getArgument(0);
+            inserted.setId(2001L);
+            inserted.setInviteCode("ABC123");
+            return 1;
+        });
+        when(userMapper.insert(any(AppUser.class))).thenAnswer(invocation -> {
+            AppUser inserted = invocation.getArgument(0);
+            inserted.setId(3001L);
+            return 1;
+        });
+        when(appAuthSessionService.getTokenInfo()).thenReturn(buildTokenInfo("operator-token", 7200L));
+
+        AppLoginResponse response = appRegisterService.completeOperatorRegister(request);
+
+        verify(userMapper).insert(appUserCaptor.capture());
+        verify(appAuthSessionService).login(3001L);
+        AppUser inserted = appUserCaptor.getValue();
+        assertEquals("本地诊所", inserted.getNickname());
+        assertEquals("13800138003", inserted.getPhone());
+        assertNull(inserted.getPassword());
+        assertEquals(Integer.valueOf(1), inserted.getType());
+        assertEquals(Long.valueOf(2001L), inserted.getClinicId());
+        assertEquals("operator-token", response.getToken());
+        assertEquals(Long.valueOf(3001L), response.getUser().getId());
+        assertEquals(Integer.valueOf(1), response.getUser().getType());
+        assertEquals(Long.valueOf(2001L), response.getUser().getClinicId());
+    }
+
+    /**
+     * 构造测试用登录 token 信息。
+     */
+    private SaTokenInfo buildTokenInfo(String tokenValue, long timeout) {
+        SaTokenInfo tokenInfo = new SaTokenInfo();
+        tokenInfo.setTokenValue(tokenValue);
+        tokenInfo.setTokenTimeout(timeout);
+        return tokenInfo;
+    }
 }
diff --git a/traditionalApp/pages/login/login.vue b/traditionalApp/pages/login/login.vue
index 40276d2..ad33f09 100644
--- a/traditionalApp/pages/login/login.vue
+++ b/traditionalApp/pages/login/login.vue
@@ -3,27 +3,18 @@
 		<image class="close-icon" src="@/static/image/login/close@2x.png" mode="aspectFit" @tap="handleClose"></image>
 		<view class="title">欢迎登录</view>
 
-		<view class="tabs">
-			<view class="tab" :class="loginType === 'code' ? 'active' : 'normal'" @tap="switchLoginType('code')">验证码登录
-			</view>
-			<view class="tab" :class="loginType === 'password' ? 'active' : 'normal'"
-				@tap="switchLoginType('password')">密码登录</view>
-		</view>
-
 		<view class="form">
 			<view class="input-group">
 				<input class="input" type="number" maxlength="11" v-model="phone" placeholder="输入手机号"
 					placeholder-class="placeholder-style" />
-				<view class="get-code-btn" :class="{ disabled: countdown > 0 }" v-if="loginType === 'code'"
-					@tap="handleSendCode">
+				<view class="get-code-btn" :class="{ disabled: countdown > 0 }" @tap="handleSendCode">
 					{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
 				</view>
 			</view>
 
 			<view class="input-group">
-				<input class="input" :type="loginType === 'code' ? 'number' : 'text'"
-					:password="loginType === 'password'" :maxlength="loginType === 'code' ? 6 : 32" v-model="credential"
-					:placeholder="loginType === 'code' ? '输入验证码' : '输入密码'" placeholder-class="placeholder-style" />
+				<input class="input" type="number" maxlength="6" v-model="credential" placeholder="输入验证码"
+					placeholder-class="placeholder-style" />
 			</view>
 		</view>
 
@@ -38,10 +29,8 @@
 
 		<view class="login-btn" @tap="handleLogin">{{ loading ? '登录中...' : '登录' }}</view>
 
-		<view class="action-links"
-			:class="{ 'center': loginType === 'code', 'space-between': loginType === 'password' }">
+		<view class="action-links center">
 			<text class="register-link" @tap="goRegister">没有账号?点击注册</text>
-			<text class="forgot-password" v-if="loginType === 'password'" @tap="goForgotPassword">忘记密码?</text>
 		</view>
 
 		<view class="oauth-actions">
@@ -60,12 +49,11 @@
 <script setup lang="ts">
 	import { onLoad } from '@dcloudio/uni-app'
 	import { computed, onUnmounted, ref } from 'vue'
-	import { appleLogin, passwordLogin, sendSmsCode, smsLogin, wechatLogin } from './service.js'
+	import { appleLogin, sendSmsCode, smsLogin, wechatLogin } from './service.js'
 	import { saveAuthSession, savePendingBindSession } from '@/utils/auth-session.js'
 	import { syncJPushAliasWithCurrentUser } from '@/utils/push.js'
 	import { clearCachedInviteCode, getCachedInviteCode } from '@/utils/invite-cache.js'
 
-	const loginType = ref<'code' | 'password'>('code')
 	const phone = ref('')
 	const credential = ref('')
 	const isAgreed = ref(false)
@@ -178,14 +166,6 @@
 	})
 
 	/**
-	 * 切换登录方式时重置当前输入态。
-	 */
-	const switchLoginType = (type : 'code' | 'password') => {
-		loginType.value = type
-		credential.value = ''
-	}
-
-	/**
 	 * 关闭登录页,优先返回上一页,没有页面栈时回首页。
 	 */
 	const handleClose = () => {
@@ -204,15 +184,6 @@
 	const goRegister = () => {
 		uni.navigateTo({
 			url: '/pages/register/register'
-		})
-	}
-
-	/**
-	 * 跳转到忘记密码页。
-	 */
-	const goForgotPassword = () => {
-		uni.navigateTo({
-			url: '/pages/forgotPassword/forgotPassword'
 		})
 	}
 
@@ -583,7 +554,7 @@
 	}
 
 	/**
-	 * 执行登录动作,根据当前 tab 走验证码登录或密码登录。
+	 * 执行短信验证码登录动作。
 	 */
 	const handleLogin = async () => {
 		if (loading.value) {
@@ -598,9 +569,7 @@
 		}
 		loading.value = true
 		try {
-			const res = loginType.value === 'code'
-				? await loginWithCode(normalizedPhone)
-				: await loginWithPassword(normalizedPhone)
+			const res = await loginWithCode(normalizedPhone)
 			if (!res) {
 				return
 			}
@@ -633,20 +602,6 @@
 		return smsLogin({
 			phone: normalizedPhone,
 			code: normalizedCode
-		})
-	}
-
-	/**
-	 * 使用手机号密码完成登录。
-	 */
-	const loginWithPassword = (normalizedPhone : string) => {
-		const normalizedPassword = normalizePassword()
-		if (!normalizedPassword) {
-			return null
-		}
-		return passwordLogin({
-			phone: normalizedPhone,
-			password: normalizedPassword
 		})
 	}
 
@@ -719,21 +674,6 @@
 			return ''
 		}
 		return normalizedCode
-	}
-
-	/**
-	 * 校验密码输入是否合法。
-	 */
-	const normalizePassword = () => {
-		const normalizedPassword = (credential.value || '').trim()
-		if (!normalizedPassword) {
-			uni.showToast({
-				title: '请输入密码',
-				icon: 'none'
-			})
-			return ''
-		}
-		return normalizedPassword
 	}
 
 	/**
@@ -963,12 +903,7 @@
 			justify-content: center;
 		}
 
-		&.space-between {
-			justify-content: space-between;
-		}
-
-		.register-link,
-		.forgot-password {
+		.register-link {
 			font-size: 28rpx;
 			color: #C63520;
 		}
diff --git a/traditionalApp/pages/login/service.js b/traditionalApp/pages/login/service.js
index 5e48756..dd96e7d 100644
--- a/traditionalApp/pages/login/service.js
+++ b/traditionalApp/pages/login/service.js
@@ -29,13 +29,6 @@
 }
 
 /**
- * 使用手机号密码完成 APP 登录。
- */
-export const passwordLogin = (data) => {
-	return request.post('/api/app/auth/password-login', data)
-}
-
-/**
  * 刷新当前 APP 登录态有效期。
  */
 export const refreshAuthToken = () => {
diff --git a/traditionalApp/pages/register/register.vue b/traditionalApp/pages/register/register.vue
index a2c96ca..9104b95 100644
--- a/traditionalApp/pages/register/register.vue
+++ b/traditionalApp/pages/register/register.vue
@@ -46,21 +46,6 @@
 				<view class="line"></view>
 			</view>
 
-			<view class="form-item">
-				<view class="label">密码</view>
-				<view class="input-wrapper">
-					<input
-						class="input"
-						type="text"
-						password
-						placeholder="8-32位数字、字母、符号至少2种或以上"
-						placeholder-class="placeholder-style"
-						v-model="form.password"
-					/>
-				</view>
-				<view class="line"></view>
-			</view>
-
 			<template v-if="registerType === 'user'">
 				<view class="form-item">
 					<view class="label">邀请码</view>
@@ -179,6 +164,8 @@
 import { reportOpeninstallRegister } from '@/utils/openinstall.js'
 import { cacheInviteCode, clearCachedInviteCode, getCachedInviteCode } from '@/utils/invite-cache.js'
 import { confirmCameraForInviteCodeScan, confirmMediaForLicense } from '@/utils/permission.js'
+import { saveAuthSession } from '@/utils/auth-session.js'
+import { syncJPushAliasWithCurrentUser } from '@/utils/push.js'
 
 interface RegionItem {
 	label?: string
@@ -215,8 +202,6 @@
 	clinicInfo?: AppRegisterClinicInfo
 }
 
-const PASSWORD_REGEX = /^(?![0-9]+$)(?![a-zA-Z]+$)(?![^0-9a-zA-Z]+$).{8,32}$/
-
 const registerType = ref<'user' | 'operator'>('user')
 const currentStep = ref(1)
 const isAgreed = ref(false)
@@ -243,7 +228,6 @@
 const form = reactive({
 	phone: '',
 	code: '',
-	password: '',
 	inviteCode: '',
 	institutionName: '',
 	provinceCode: '',
@@ -504,14 +488,13 @@
 	const res = await registerUser({
 		phone: form.phone.trim(),
 		code: form.code.trim(),
-		password: form.password,
 		inviteCode: form.inviteCode.trim().toUpperCase(),
 		bindTicket: bindTicket.value
 	})
 	if (!isSuccessResponse(res)) {
 		return
 	}
-	await handleRegisterSuccess('注册成功')
+	await handleRegisterSuccess('注册成功', res.data)
 }
 
 /**
@@ -520,8 +503,7 @@
 const prepareOperatorStep = async () => {
 	const res = await prepareOperatorRegister({
 		phone: form.phone.trim(),
-		code: form.code.trim(),
-		password: form.password
+		code: form.code.trim()
 	})
 	if (!isSuccessResponse(res)) {
 		return
@@ -558,7 +540,7 @@
 	if (!isSuccessResponse(res)) {
 		return
 	}
-	await handleRegisterSuccess('经营者注册成功')
+	await handleRegisterSuccess('经营者注册成功', res.data)
 }
 
 /**
@@ -571,10 +553,6 @@
 	}
 	if (!form.code.trim()) {
 		showToast('请输入验证码')
-		return false
-	}
-	if (!PASSWORD_REGEX.test(form.password)) {
-		showToast('密码需为8-32位,且包含数字、字母、符号至少2种')
 		return false
 	}
 	return true
@@ -822,9 +800,9 @@
 }
 
 /**
- * 注册成功后提示并返回登录页。
+ * 注册成功后保存登录态并进入首页。
  */
-const handleRegisterSuccess = async (message: string) => {
+const handleRegisterSuccess = async (message: string, loginData: any) => {
 	// 注册成功后通知 openinstall,用于统计推广链路的注册转化。
 	reportOpeninstallRegister()
 	clearCachedInviteCode()
@@ -832,9 +810,13 @@
 		title: message,
 		icon: 'success'
 	})
+	saveAuthSession(loginData)
+	syncJPushAliasWithCurrentUser()
 	resetForm()
 	setTimeout(() => {
-		goLogin()
+		uni.switchTab({
+			url: '/pages/home/home'
+		})
 	}, 800)
 }
 
@@ -916,7 +898,6 @@
 	bindTicket.value = ''
 	form.phone = ''
 	form.code = ''
-	form.password = ''
 	form.inviteCode = ''
 	form.institutionName = ''
 	form.provinceCode = ''
diff --git a/traditionalApp/pages/settings/settings.vue b/traditionalApp/pages/settings/settings.vue
index 1d01ac0..de2ebd9 100644
--- a/traditionalApp/pages/settings/settings.vue
+++ b/traditionalApp/pages/settings/settings.vue
@@ -35,15 +35,6 @@
 						<image class="edit-icon" src="/static/image/mine/edit.png" mode="aspectFit" @tap="editPhone"></image>
 					</view>
 				</view>
-				<view class="list-item">
-					<view class="list-left">
-						<image class="list-icon" src="/static/image/mine/password.png" mode="aspectFit"></image>
-						<text class="list-title">登录密码</text>
-					</view>
-					<view class="list-right">
-						<image class="edit-icon" src="/static/image/mine/edit.png" mode="aspectFit" @tap="editPassword"></image>
-					</view>
-				</view>
 				<view class="list-item" @tap="swapWechat">
 					<view class="list-left">
 						<image class="list-icon" src="/static/image/mine/weixinIcon.png" mode="aspectFit"></image>
@@ -206,15 +197,6 @@
 function editPhone() {
 	uni.navigateTo({
 		url: `/pages/modifyPhone/modifyPhone?phone=${encodeURIComponent(normalizeOptionalText(profile.phone))}`,
-	})
-}
-
-/**
- * 跳转修改密码页。
- */
-function editPassword() {
-	uni.navigateTo({
-		url: '/pages/modifyPassword/modifyPassword',
 	})
 }
 
diff --git a/traditionalApp/utils/auth-session.js b/traditionalApp/utils/auth-session.js
index 2a0e637..042d9cb 100644
--- a/traditionalApp/utils/auth-session.js
+++ b/traditionalApp/utils/auth-session.js
@@ -115,6 +115,7 @@
  */
 export function persistTokenExpireAt(expiresIn) {
 	const expireSeconds = Number(expiresIn)
+	// 后端 APP 永久 token 返回 -1,此时不保存过期时间,后续请求也不会触发定时刷新。
 	if (!Number.isFinite(expireSeconds) || expireSeconds <= 0) {
 		uni.removeStorageSync(TOKEN_EXPIRE_AT_KEY)
 		return

--
Gitblit v1.7.1