| | |
| | | package com.tcm.training.admin.app.settings.service; |
| | | |
| | | import cn.dev33.satoken.stp.SaTokenInfo; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest; |
| | | import com.tcm.training.admin.app.auth.service.AppAuthSessionService; |
| | | 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.service.AppSmsCodeService; |
| | | import com.tcm.training.admin.app.auth.service.AppWechatAuthClient; |
| | | import com.tcm.training.admin.app.auth.service.AppWechatUserProfile; |
| | | import com.tcm.training.admin.app.auth.exception.InviteCodeRequiredException; |
| | | import com.tcm.training.admin.app.settings.dto.AppSettingsNotificationUpdateRequest; |
| | | import com.tcm.training.admin.app.settings.dto.AppSettingsPasswordUpdateRequest; |
| | | import com.tcm.training.admin.app.settings.dto.AppSettingsPhoneBindRequest; |
| | |
| | | if (newPhone.equals(trimToEmpty(currentUser.getPhone()))) { |
| | | throw new IllegalArgumentException("新手机号不能与当前手机号一致"); |
| | | } |
| | | requirePhoneAvailable(newPhone, currentUser.getId()); |
| | | if (trimToNull(currentUser.getPhone()) == null) { |
| | | validateThirdPartyPhoneBindable(currentUser, newPhone); |
| | | } else { |
| | | requirePhoneAvailable(newPhone, currentUser.getId()); |
| | | } |
| | | appSmsCodeService.createPhoneChangeCode(newPhone); |
| | | } |
| | | |
| | |
| | | * 首次绑定当前登录用户手机号,供微信登录未绑定手机号场景使用。 |
| | | */ |
| | | @Transactional |
| | | public AppSettingsProfileVO bindPhone(AppSettingsPhoneBindRequest request) { |
| | | public AppLoginResponse bindPhone(AppSettingsPhoneBindRequest request) { |
| | | AppUser user = requireCurrentEnabledUser(); |
| | | String newPhone = normalizePhone(request.getNewPhone()); |
| | | String code = normalizeRequired(request.getCode(), "请输入验证码"); |
| | | if (trimToNull(user.getPhone()) != null) { |
| | | throw new IllegalArgumentException("当前账号已绑定手机号"); |
| | | } |
| | | requirePhoneAvailable(newPhone, user.getId()); |
| | | AppUser occupiedUser = findUserByPhone(newPhone); |
| | | validatePhoneBindTarget(user, occupiedUser); |
| | | requireInviteCodeWhenDoctorRelationMissing(user, occupiedUser, request.getInviteCode()); |
| | | appSmsCodeService.validatePhoneChangeCode(newPhone, code); |
| | | user.setPhone(newPhone); |
| | | bindInviteRelationIfAbsent(user, request.getInviteCode()); |
| | | userMapper.updateById(user); |
| | | return toProfileVO(user); |
| | | AppUser loginUser = occupiedUser == null |
| | | ? bindPhoneToCurrentUser(user, newPhone, request.getInviteCode()) |
| | | : mergeThirdPartyUserToPhoneUser(user, occupiedUser, request.getInviteCode()); |
| | | return createLoginResponse(loginUser); |
| | | } |
| | | |
| | | /** |
| | |
| | | AppShareService.InviteBindingTarget bindingTarget = appShareService.resolveInviteBindingTarget(normalizedInviteCode); |
| | | user.setClinicId(bindingTarget.getClinicId()); |
| | | user.setSuperiorClinicUserId(bindingTarget.getSuperiorClinicUserId()); |
| | | } |
| | | |
| | | /** |
| | | * 将手机号直接绑定到当前临时账号。 |
| | | */ |
| | | private AppUser bindPhoneToCurrentUser(AppUser user, String phone, String inviteCode) { |
| | | user.setPhone(phone); |
| | | bindInviteRelationIfAbsent(user, inviteCode); |
| | | userMapper.updateById(user); |
| | | return user; |
| | | } |
| | | |
| | | /** |
| | | * 将当前第三方临时账号合并到已存在的手机号账号。 |
| | | */ |
| | | private AppUser mergeThirdPartyUserToPhoneUser(AppUser currentUser, AppUser targetUser, String inviteCode) { |
| | | requireEnabledUser(targetUser); |
| | | copyThirdPartyBinding(currentUser, targetUser); |
| | | fillMissingProfile(targetUser, currentUser); |
| | | inheritDoctorRelationIfAbsent(targetUser, currentUser); |
| | | bindInviteRelationIfAbsent(targetUser, inviteCode); |
| | | userMapper.updateById(targetUser); |
| | | disableMergedTemporaryUser(currentUser); |
| | | return targetUser; |
| | | } |
| | | |
| | | /** |
| | | * 当临时账号和目标手机号账号都没有医生关系时,绑定手机号阶段必须携带邀请码。 |
| | | */ |
| | | private void requireInviteCodeWhenDoctorRelationMissing(AppUser currentUser, AppUser targetUser, String inviteCode) { |
| | | if (hasDoctorRelation(currentUser) || hasDoctorRelation(targetUser)) { |
| | | return; |
| | | } |
| | | if (normalizeInviteCode(inviteCode) == null) { |
| | | throw new InviteCodeRequiredException(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 合并到已有手机号账号时,优先继承临时账号上已存在的医生关系。 |
| | | */ |
| | | private void inheritDoctorRelationIfAbsent(AppUser targetUser, AppUser sourceUser) { |
| | | if (hasDoctorRelation(targetUser) || !hasDoctorRelation(sourceUser)) { |
| | | return; |
| | | } |
| | | targetUser.setClinicId(sourceUser.getClinicId()); |
| | | targetUser.setSuperiorClinicUserId(sourceUser.getSuperiorClinicUserId()); |
| | | } |
| | | |
| | | /** |
| | | * 校验首次绑定手机号时目标手机号是否允许承接当前第三方身份。 |
| | | */ |
| | | private void validateThirdPartyPhoneBindable(AppUser currentUser, String phone) { |
| | | validatePhoneBindTarget(currentUser, findUserByPhone(phone)); |
| | | } |
| | | |
| | | /** |
| | | * 校验目标手机号账号未绑定当前第三方类型。 |
| | | */ |
| | | private void validatePhoneBindTarget(AppUser currentUser, AppUser targetUser) { |
| | | if (targetUser == null || targetUser.getId().equals(currentUser.getId())) { |
| | | return; |
| | | } |
| | | if (hasWechatBinding(currentUser) || hasAppleBinding(currentUser)) { |
| | | return; |
| | | } |
| | | throw new IllegalArgumentException("该手机号已注册"); |
| | | } |
| | | |
| | | /** |
| | | * 将当前临时账号上的第三方票据写入目标手机号账号。 |
| | | */ |
| | | private void copyThirdPartyBinding(AppUser sourceUser, AppUser targetUser) { |
| | | if (hasWechatBinding(sourceUser)) { |
| | | targetUser.setWechatOpenid(sourceUser.getWechatOpenid()); |
| | | targetUser.setWechatNickname(sourceUser.getWechatNickname()); |
| | | targetUser.setWechatAvatar(sourceUser.getWechatAvatar()); |
| | | targetUser.setWechatBoundAt(sourceUser.getWechatBoundAt() == null ? LocalDateTime.now() : sourceUser.getWechatBoundAt()); |
| | | } |
| | | if (hasAppleBinding(sourceUser)) { |
| | | targetUser.setAppleUserIdentifier(sourceUser.getAppleUserIdentifier()); |
| | | targetUser.setAppleIdentitySubject(sourceUser.getAppleIdentitySubject()); |
| | | targetUser.setAppleBoundAt(sourceUser.getAppleBoundAt() == null ? LocalDateTime.now() : sourceUser.getAppleBoundAt()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 目标账号资料为空时,复用临时第三方账号资料补齐展示信息。 |
| | | */ |
| | | private void fillMissingProfile(AppUser targetUser, AppUser sourceUser) { |
| | | if (trimToNull(targetUser.getNickname()) == null) { |
| | | targetUser.setNickname(resolveNickname(sourceUser)); |
| | | } |
| | | if (trimToNull(targetUser.getAvatar()) == null) { |
| | | targetUser.setAvatar(resolveAvatar(sourceUser)); |
| | | } |
| | | if (targetUser.getGender() == null) { |
| | | targetUser.setGender(sourceUser.getGender()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 合并后清空第三方票据并逻辑删除临时账号,避免同一 openid 或苹果 subject 命中多个账号。 |
| | | */ |
| | | private void disableMergedTemporaryUser(AppUser currentUser) { |
| | | userMapper.update(null, new LambdaUpdateWrapper<AppUser>() |
| | | .eq(AppUser::getId, currentUser.getId()) |
| | | .set(AppUser::getStatus, 0) |
| | | .set(AppUser::getWechatOpenid, null) |
| | | .set(AppUser::getWechatNickname, null) |
| | | .set(AppUser::getWechatAvatar, null) |
| | | .set(AppUser::getWechatBoundAt, null) |
| | | .set(AppUser::getAppleUserIdentifier, null) |
| | | .set(AppUser::getAppleIdentitySubject, null) |
| | | .set(AppUser::getAppleBoundAt, null) |
| | | .set(AppUser::getDeleted, 1)); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 按手机号查询未删除的 APP 用户。 |
| | | */ |
| | | private AppUser findUserByPhone(String phone) { |
| | | return userMapper.selectOne(new LambdaQueryWrapper<AppUser>() |
| | | .eq(AppUser::getPhone, phone) |
| | | .eq(AppUser::getDeleted, 0) |
| | | .last("limit 1")); |
| | | } |
| | | |
| | | /** |
| | | * 校验用户启用状态。 |
| | | */ |
| | | private void requireEnabledUser(AppUser user) { |
| | | if (user.getStatus() == null || user.getStatus().intValue() != 1) { |
| | | throw new IllegalArgumentException("账号已被禁用"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断账号是否已绑定微信。 |
| | | */ |
| | | private boolean hasWechatBinding(AppUser user) { |
| | | return trimToNull(user.getWechatOpenid()) != null; |
| | | } |
| | | |
| | | /** |
| | | * 判断账号是否已绑定苹果。 |
| | | */ |
| | | private boolean hasAppleBinding(AppUser user) { |
| | | return trimToNull(user.getAppleIdentitySubject()) != null; |
| | | } |
| | | |
| | | /** |
| | | * 判断账号是否已绑定医生上下级关系。 |
| | | */ |
| | | private boolean hasDoctorRelation(AppUser user) { |
| | | return user != null && (user.getClinicId() != null || user.getSuperiorClinicUserId() != null); |
| | | } |
| | | |
| | | /** |
| | | * 为绑定后的用户签发新的 APP 登录态。 |
| | | */ |
| | | private AppLoginResponse createLoginResponse(AppUser user) { |
| | | appAuthSessionService.login(user.getId()); |
| | | SaTokenInfo tokenInfo = appAuthSessionService.getTokenInfo(); |
| | | return new AppLoginResponse( |
| | | tokenInfo.getTokenValue(), |
| | | "Bearer", |
| | | tokenInfo.getTokenTimeout(), |
| | | buildLoginUser(user) |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 将用户实体转换为登录响应中的用户摘要。 |
| | | */ |
| | | private AppLoginUserVO buildLoginUser(AppUser user) { |
| | | AppLoginUserVO loginUser = new AppLoginUserVO(); |
| | | loginUser.setId(user.getId()); |
| | | loginUser.setNickname(resolveNickname(user)); |
| | | loginUser.setPhone(trimToEmpty(user.getPhone())); |
| | | loginUser.setAvatar(resolveAvatar(user)); |
| | | loginUser.setType(user.getType()); |
| | | loginUser.setStatus(user.getStatus()); |
| | | loginUser.setClinicId(user.getClinicId()); |
| | | loginUser.setMemberExpireAt(user.getMemberExpireAt()); |
| | | return loginUser; |
| | | } |
| | | |
| | | /** |
| | | * 校验手机号未被其他有效用户占用。 |
| | | */ |
| | | private void requirePhoneAvailable(String phone, Long currentUserId) { |