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; } /** 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); 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)); } /** 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)); } 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; } 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; 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; 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') } /** 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) { 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; } } 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; } 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 = () => { 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 = '' 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', }) } 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