hejianhao
2026-08-28 16a57a8fc6868cea9171a1119698572407a2fc03
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/service/AppSettingsService.java
@@ -1,11 +1,16 @@
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;
@@ -120,7 +125,11 @@
        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);
    }
@@ -151,19 +160,21 @@
     * 首次绑定当前登录用户手机号,供微信登录未绑定手机号场景使用。
     */
    @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);
    }
    /**
@@ -180,6 +191,122 @@
        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));
    }
    /**
@@ -353,6 +480,76 @@
    }
    /**
     * 按手机号查询未删除的 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) {