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