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.settings.dto.AppSettingsNotificationUpdateRequest;
|
import com.tcm.training.admin.app.settings.dto.AppSettingsPasswordUpdateRequest;
|
import com.tcm.training.admin.app.settings.dto.AppSettingsPhoneBindRequest;
|
import com.tcm.training.admin.app.settings.dto.AppSettingsPhoneCodeRequest;
|
import com.tcm.training.admin.app.settings.dto.AppSettingsPhoneUpdateRequest;
|
import com.tcm.training.admin.app.settings.dto.AppSettingsProfileUpdateRequest;
|
import com.tcm.training.admin.app.settings.entity.AppUserNotificationSetting;
|
import com.tcm.training.admin.app.settings.mapper.AppUserNotificationSettingMapper;
|
import com.tcm.training.admin.app.settings.vo.AppSettingsNotificationVO;
|
import com.tcm.training.admin.app.settings.vo.AppSettingsProfileVO;
|
import com.tcm.training.admin.app.settings.vo.AppSettingsWechatBindingVO;
|
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;
|
import com.tcm.training.admin.upload.ObsUploadService;
|
import com.tcm.training.admin.upload.UploadController;
|
import com.tcm.training.admin.upload.UploadFileResult;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import java.io.IOException;
|
import java.time.LocalDateTime;
|
import java.util.HashSet;
|
import java.util.Arrays;
|
import java.util.List;
|
import java.util.Set;
|
import java.util.regex.Pattern;
|
|
/**
|
* APP 设置页服务。
|
*/
|
@Service
|
public class AppSettingsService {
|
|
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 List<Integer> SUPPORTED_ADVANCE_MINUTES = Arrays.asList(15, 30, 60, 120, 180);
|
private static final long AVATAR_MAX_SIZE = 5 * 1024 * 1024L;
|
private static final Set<String> ALLOWED_IMAGE_TYPES = new HashSet<String>(
|
Arrays.asList("image/jpeg", "image/png", "image/gif", "image/webp")
|
);
|
|
private final AppAuthSessionService appAuthSessionService;
|
private final UserMapper userMapper;
|
private final AppSmsCodeService appSmsCodeService;
|
private final BCryptPasswordEncoder passwordEncoder;
|
private final AppWechatAuthClient appWechatAuthClient;
|
private final AppShareService appShareService;
|
private final AppUserNotificationSettingMapper appUserNotificationSettingMapper;
|
private final ObsUploadService obsUploadService;
|
|
/**
|
* 注入 APP 设置页所需依赖。
|
*/
|
public AppSettingsService(AppAuthSessionService appAuthSessionService,
|
UserMapper userMapper,
|
AppSmsCodeService appSmsCodeService,
|
BCryptPasswordEncoder passwordEncoder,
|
AppWechatAuthClient appWechatAuthClient,
|
AppShareService appShareService,
|
AppUserNotificationSettingMapper appUserNotificationSettingMapper,
|
ObsUploadService obsUploadService) {
|
this.appAuthSessionService = appAuthSessionService;
|
this.userMapper = userMapper;
|
this.appSmsCodeService = appSmsCodeService;
|
this.passwordEncoder = passwordEncoder;
|
this.appWechatAuthClient = appWechatAuthClient;
|
this.appShareService = appShareService;
|
this.appUserNotificationSettingMapper = appUserNotificationSettingMapper;
|
this.obsUploadService = obsUploadService;
|
}
|
|
/**
|
* 获取当前登录用户设置页资料。
|
*/
|
public AppSettingsProfileVO getProfile() {
|
return toProfileVO(requireCurrentEnabledUser());
|
}
|
|
/**
|
* 上传当前登录用户头像。
|
*/
|
public UploadController.UploadResult uploadAvatar(MultipartFile file) throws IOException {
|
// APP 路由默认允许游客访问,因此头像上传必须在业务层显式校验登录用户。
|
requireCurrentEnabledUser();
|
validateAvatarImage(file);
|
UploadFileResult uploadResult = obsUploadService.upload(file, "app/avatar", null);
|
return new UploadController.UploadResult(uploadResult.getUrl(), uploadResult.getFileName());
|
}
|
|
/**
|
* 更新当前登录用户设置页资料。
|
*/
|
@Transactional
|
public AppSettingsProfileVO updateProfile(AppSettingsProfileUpdateRequest request) {
|
AppUser user = requireCurrentEnabledUser();
|
user.setNickname(normalizeNickname(request.getNickname()));
|
user.setAvatar(normalizeAvatar(request.getAvatar()));
|
user.setIntro(normalizeIntro(request.getIntro()));
|
userMapper.updateById(user);
|
return toProfileVO(user);
|
}
|
|
/**
|
* 发送修改手机号验证码。
|
*/
|
public void sendPhoneChangeCode(AppSettingsPhoneCodeRequest request) {
|
AppUser currentUser = requireCurrentEnabledUser();
|
String newPhone = normalizePhone(request.getNewPhone());
|
if (newPhone.equals(trimToEmpty(currentUser.getPhone()))) {
|
throw new IllegalArgumentException("新手机号不能与当前手机号一致");
|
}
|
if (trimToNull(currentUser.getPhone()) == null) {
|
validateThirdPartyPhoneBindable(currentUser, newPhone);
|
} else {
|
requirePhoneAvailable(newPhone, currentUser.getId());
|
}
|
appSmsCodeService.createPhoneChangeCode(newPhone);
|
}
|
|
/**
|
* 修改当前登录用户手机号。
|
*/
|
@Transactional
|
public AppSettingsProfileVO updatePhone(AppSettingsPhoneUpdateRequest request) {
|
AppUser user = requireCurrentEnabledUser();
|
String oldPhone = normalizePhone(request.getOldPhone());
|
String newPhone = normalizePhone(request.getNewPhone());
|
String code = normalizeRequired(request.getCode(), "请输入验证码");
|
if (!oldPhone.equals(trimToEmpty(user.getPhone()))) {
|
// 设置页保留旧手机号输入框,因此服务端需要显式校验,避免用户误改其它账号手机号。
|
throw new IllegalArgumentException("旧手机号校验失败");
|
}
|
if (newPhone.equals(oldPhone)) {
|
throw new IllegalArgumentException("新手机号不能与当前手机号一致");
|
}
|
requirePhoneAvailable(newPhone, user.getId());
|
appSmsCodeService.validatePhoneChangeCode(newPhone, code);
|
user.setPhone(newPhone);
|
userMapper.updateById(user);
|
return toProfileVO(user);
|
}
|
|
/**
|
* 首次绑定当前登录用户手机号,供微信登录未绑定手机号场景使用。
|
*/
|
@Transactional
|
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("当前账号已绑定手机号");
|
}
|
AppUser occupiedUser = findUserByPhone(newPhone);
|
validatePhoneBindTarget(user, occupiedUser);
|
appSmsCodeService.validatePhoneChangeCode(newPhone, code);
|
AppUser loginUser = occupiedUser == null
|
? bindPhoneToCurrentUser(user, newPhone, request.getInviteCode())
|
: mergeThirdPartyUserToPhoneUser(user, occupiedUser, request.getInviteCode());
|
return createLoginResponse(loginUser);
|
}
|
|
/**
|
* 用户尚未绑定诊所关系时,根据医生邀请码补齐上下级关系。
|
*/
|
private void bindInviteRelationIfAbsent(AppUser user, String inviteCode) {
|
if (user.getClinicId() != null || user.getSuperiorClinicUserId() != null) {
|
return;
|
}
|
String normalizedInviteCode = normalizeInviteCode(inviteCode);
|
if (normalizedInviteCode == null) {
|
return;
|
}
|
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);
|
bindInviteRelationIfAbsent(targetUser, inviteCode);
|
userMapper.updateById(targetUser);
|
disableMergedTemporaryUser(currentUser);
|
return targetUser;
|
}
|
|
/**
|
* 校验首次绑定手机号时目标手机号是否允许承接当前第三方身份。
|
*/
|
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));
|
}
|
|
/**
|
* 修改当前登录用户密码,并在成功后使当前登录态失效。
|
*/
|
@Transactional
|
public void updatePassword(AppSettingsPasswordUpdateRequest request) {
|
AppUser user = requireCurrentEnabledUser();
|
String oldPassword = normalizeRequired(request.getOldPassword(), "请输入原密码");
|
String newPassword = normalizePassword(request.getNewPassword(), "请输入新密码");
|
String confirmPassword = normalizePassword(request.getConfirmPassword(), "请再次输入密码");
|
if (!passwordEncoder.matches(oldPassword, trimToEmpty(user.getPassword()))) {
|
throw new IllegalArgumentException("原密码错误");
|
}
|
if (!newPassword.equals(confirmPassword)) {
|
throw new IllegalArgumentException("两次密码输入不一致");
|
}
|
user.setPassword(passwordEncoder.encode(newPassword));
|
userMapper.updateById(user);
|
appAuthSessionService.logout();
|
}
|
|
/**
|
* 绑定或换绑当前登录用户微信信息。
|
*/
|
@Transactional
|
public AppSettingsWechatBindingVO bindWechat(AppWechatLoginRequest request) {
|
AppUser currentUser = requireCurrentEnabledUser();
|
AppWechatUserProfile profile = appWechatAuthClient.resolveUserProfile(request);
|
AppUser occupiedUser = userMapper.selectOne(new LambdaQueryWrapper<AppUser>()
|
.eq(AppUser::getWechatOpenid, profile.getOpenid())
|
.eq(AppUser::getDeleted, 0)
|
.last("limit 1"));
|
// 同一个 openid 只允许归属一个有效账号,避免微信账号被多个本地账号共用。
|
if (occupiedUser != null && !occupiedUser.getId().equals(currentUser.getId())) {
|
throw new IllegalArgumentException("该微信已绑定其他账号");
|
}
|
|
String wechatNickname = trimToNull(profile.getNickname());
|
String wechatAvatar = trimToNull(profile.getAvatar());
|
currentUser.setWechatOpenid(profile.getOpenid());
|
currentUser.setWechatNickname(wechatNickname);
|
currentUser.setWechatAvatar(wechatAvatar);
|
currentUser.setWechatBoundAt(LocalDateTime.now());
|
if (trimToNull(currentUser.getNickname()) == null && wechatNickname != null) {
|
currentUser.setNickname(wechatNickname);
|
}
|
if (trimToNull(currentUser.getAvatar()) == null && wechatAvatar != null) {
|
currentUser.setAvatar(wechatAvatar);
|
}
|
if (currentUser.getGender() == null) {
|
currentUser.setGender(resolveWechatGender(profile.getSex()));
|
}
|
userMapper.updateById(currentUser);
|
return buildWechatBinding(currentUser);
|
}
|
|
/**
|
* 获取当前登录用户通知设置,没有记录时按默认值初始化。
|
*/
|
@Transactional
|
public AppSettingsNotificationVO getNotificationSettings() {
|
return toNotificationVO(requireNotificationSetting(requireCurrentEnabledUser().getId()));
|
}
|
|
/**
|
* 保存当前登录用户通知设置。
|
*/
|
@Transactional
|
public AppSettingsNotificationVO updateNotificationSettings(AppSettingsNotificationUpdateRequest request) {
|
AppUser user = requireCurrentEnabledUser();
|
AppUserNotificationSetting setting = requireNotificationSetting(user.getId());
|
Integer advanceMinutes = normalizeAdvanceMinutes(request.getAdvanceMinutes());
|
setting.setOrderStatusUpdateEnabled(booleanToInteger(request.getOrderStatusUpdateEnabled()));
|
setting.setActivityNoticeEnabled(booleanToInteger(request.getActivityNoticeEnabled()));
|
setting.setCourseUpdateEnabled(booleanToInteger(request.getCourseUpdateEnabled()));
|
setting.setLiveReservationEnabled(booleanToInteger(request.getLiveReservationEnabled()));
|
setting.setAdvanceNoticeMinutes(advanceMinutes);
|
appUserNotificationSettingMapper.updateById(setting);
|
return toNotificationVO(setting);
|
}
|
|
/**
|
* 注销当前登录账号,保留历史业务记录用于审计和数据回溯。
|
*/
|
@Transactional
|
public void cancelAccount() {
|
AppUser user = requireCurrentEnabledUser();
|
user.setStatus(0);
|
user.setDeleted(1);
|
userMapper.updateById(user);
|
appAuthSessionService.logout();
|
}
|
|
/**
|
* 校验当前登录用户存在且启用。
|
*/
|
private AppUser requireCurrentEnabledUser() {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
AppUser user = userMapper.selectById(userId);
|
if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) {
|
throw new IllegalArgumentException("用户不存在");
|
}
|
if (user.getStatus() == null || user.getStatus().intValue() != 1) {
|
throw new IllegalArgumentException("用户已禁用");
|
}
|
return user;
|
}
|
|
/**
|
* 读取当前用户通知设置,不存在时自动创建默认值。
|
*/
|
private AppUserNotificationSetting requireNotificationSetting(Long userId) {
|
AppUserNotificationSetting setting = appUserNotificationSettingMapper.selectOne(new LambdaQueryWrapper<AppUserNotificationSetting>()
|
.eq(AppUserNotificationSetting::getAppUserId, userId)
|
.eq(AppUserNotificationSetting::getDeleted, 0)
|
.last("limit 1"));
|
if (setting != null) {
|
return setting;
|
}
|
AppUserNotificationSetting created = new AppUserNotificationSetting();
|
created.setAppUserId(userId);
|
created.setOrderStatusUpdateEnabled(1);
|
created.setActivityNoticeEnabled(0);
|
created.setCourseUpdateEnabled(0);
|
created.setLiveReservationEnabled(1);
|
created.setAdvanceNoticeMinutes(60);
|
created.setDeleted(0);
|
appUserNotificationSettingMapper.insert(created);
|
return created;
|
}
|
|
/**
|
* 将用户实体转换为设置页资料响应。
|
*/
|
private AppSettingsProfileVO toProfileVO(AppUser user) {
|
AppSettingsProfileVO result = new AppSettingsProfileVO();
|
result.setId(user.getId());
|
result.setNickname(resolveNickname(user));
|
result.setAvatar(resolveAvatar(user));
|
result.setPhone(trimToEmpty(user.getPhone()));
|
result.setIntro(trimToEmpty(user.getIntro()));
|
result.setType(user.getType());
|
result.setWechatBinding(buildWechatBinding(user));
|
return result;
|
}
|
|
/**
|
* 将通知设置实体转换为响应结构。
|
*/
|
private AppSettingsNotificationVO toNotificationVO(AppUserNotificationSetting setting) {
|
AppSettingsNotificationVO result = new AppSettingsNotificationVO();
|
result.setOrderStatusUpdateEnabled(integerToBoolean(setting.getOrderStatusUpdateEnabled()));
|
result.setActivityNoticeEnabled(integerToBoolean(setting.getActivityNoticeEnabled()));
|
result.setCourseUpdateEnabled(integerToBoolean(setting.getCourseUpdateEnabled()));
|
result.setLiveReservationEnabled(integerToBoolean(setting.getLiveReservationEnabled()));
|
result.setAdvanceMinutes(setting.getAdvanceNoticeMinutes());
|
return result;
|
}
|
|
/**
|
* 构造微信绑定展示信息。
|
*/
|
private AppSettingsWechatBindingVO buildWechatBinding(AppUser user) {
|
AppSettingsWechatBindingVO result = new AppSettingsWechatBindingVO();
|
result.setBound(trimToNull(user.getWechatOpenid()) != null);
|
result.setNickname(trimToEmpty(user.getWechatNickname()));
|
result.setAvatar(trimToEmpty(user.getWechatAvatar()));
|
result.setBoundAt(user.getWechatBoundAt());
|
return result;
|
}
|
|
/**
|
* 按手机号查询未删除的 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;
|
}
|
|
/**
|
* 为绑定后的用户签发新的 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) {
|
AppUser occupiedUser = userMapper.selectOne(new LambdaQueryWrapper<AppUser>()
|
.eq(AppUser::getPhone, phone)
|
.eq(AppUser::getDeleted, 0)
|
.last("limit 1"));
|
if (occupiedUser != null && !occupiedUser.getId().equals(currentUserId)) {
|
throw new IllegalArgumentException("该手机号已注册");
|
}
|
}
|
|
/**
|
* 归一化昵称输入。
|
*/
|
private String normalizeNickname(String nickname) {
|
String normalized = trimToNull(nickname);
|
if (normalized == null) {
|
throw new IllegalArgumentException("请输入姓名");
|
}
|
return normalized;
|
}
|
|
/**
|
* 校验头像图片大小与格式。
|
*/
|
private void validateAvatarImage(MultipartFile file) {
|
if (file == null || file.isEmpty()) {
|
throw new IllegalArgumentException("请选择要上传的文件");
|
}
|
if (file.getSize() > AVATAR_MAX_SIZE) {
|
throw new IllegalArgumentException("文件大小超出限制");
|
}
|
String contentType = file.getContentType();
|
if (contentType == null || !ALLOWED_IMAGE_TYPES.contains(contentType)) {
|
throw new IllegalArgumentException("仅支持 JPG、PNG、GIF、WebP 格式");
|
}
|
}
|
|
/**
|
* 归一化头像地址输入。
|
*/
|
private String normalizeAvatar(String avatar) {
|
return trimToNull(avatar);
|
}
|
|
/**
|
* 归一化简介输入,允许为空。
|
*/
|
private String normalizeIntro(String intro) {
|
return trimToNull(intro);
|
}
|
|
/**
|
* 归一化并校验手机号。
|
*/
|
private String normalizePhone(String phone) {
|
String normalized = trimToNull(phone);
|
if (normalized == null || !PHONE_PATTERN.matcher(normalized).matches()) {
|
throw new IllegalArgumentException("请输入正确的手机号");
|
}
|
return normalized;
|
}
|
|
/**
|
* 归一化必填文本。
|
*/
|
private String normalizeRequired(String value, String message) {
|
String normalized = trimToNull(value);
|
if (normalized == null) {
|
throw new IllegalArgumentException(message);
|
}
|
return normalized;
|
}
|
|
/**
|
* 归一化并校验密码规则。
|
*/
|
private String normalizePassword(String password, String emptyMessage) {
|
String normalized = trimToNull(password);
|
if (normalized == null) {
|
throw new IllegalArgumentException(emptyMessage);
|
}
|
if (!PASSWORD_PATTERN.matcher(normalized).matches()) {
|
throw new IllegalArgumentException("密码需为8-32位,且包含数字、字母、符号至少2种");
|
}
|
return normalized;
|
}
|
|
/**
|
* 归一化医生邀请码,非法时忽略以保持绑定手机号主流程可用。
|
*/
|
private String normalizeInviteCode(String inviteCode) {
|
String normalized = trimToNull(inviteCode);
|
if (normalized == null) {
|
return null;
|
}
|
String upperInviteCode = normalized.toUpperCase();
|
return upperInviteCode.matches("^[A-Z0-9]{6}$") ? upperInviteCode : null;
|
}
|
|
/**
|
* 校验通知时间仅允许使用页面固定选项。
|
*/
|
private Integer normalizeAdvanceMinutes(Integer advanceMinutes) {
|
if (advanceMinutes == null || !SUPPORTED_ADVANCE_MINUTES.contains(advanceMinutes)) {
|
throw new IllegalArgumentException("通知时间不合法");
|
}
|
return advanceMinutes;
|
}
|
|
/**
|
* 将微信性别值转换为本地性别枚举。
|
*/
|
private Integer resolveWechatGender(Integer sex) {
|
if (sex == null) {
|
return null;
|
}
|
if (sex.intValue() == 1) {
|
return 1;
|
}
|
if (sex.intValue() == 2) {
|
return 0;
|
}
|
return null;
|
}
|
|
/**
|
* 优先使用本地昵称,没有时回退微信昵称和手机号。
|
*/
|
private String resolveNickname(AppUser user) {
|
String nickname = trimToNull(user.getNickname());
|
if (nickname != null) {
|
return nickname;
|
}
|
String wechatNickname = trimToNull(user.getWechatNickname());
|
if (wechatNickname != null) {
|
return wechatNickname;
|
}
|
return trimToEmpty(user.getPhone());
|
}
|
|
/**
|
* 优先使用本地头像,没有时回退微信头像。
|
*/
|
private String resolveAvatar(AppUser user) {
|
String avatar = trimToNull(user.getAvatar());
|
if (avatar != null) {
|
return avatar;
|
}
|
return trimToEmpty(user.getWechatAvatar());
|
}
|
|
/**
|
* 将布尔值转换为数据库使用的 0/1。
|
*/
|
private Integer booleanToInteger(Boolean value) {
|
return Boolean.TRUE.equals(value) ? 1 : 0;
|
}
|
|
/**
|
* 将数据库 0/1 转换为布尔值。
|
*/
|
private Boolean integerToBoolean(Integer value) {
|
return value != null && value.intValue() == 1;
|
}
|
|
/**
|
* 将空白文本归一化为 null。
|
*/
|
private String trimToNull(String value) {
|
if (value == null) {
|
return null;
|
}
|
String trimmed = value.trim();
|
return trimmed.isEmpty() ? null : trimmed;
|
}
|
|
/**
|
* 将可空文本转换为空字符串。
|
*/
|
private String trimToEmpty(String value) {
|
String normalized = trimToNull(value);
|
return normalized == null ? "" : normalized;
|
}
|
}
|