| | |
| | | |
| | | 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; |
| | |
| | | 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; |
| | | |
| | |
| | | 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; |
| | |
| | | |
| | | 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; |
| | |
| | | */ |
| | | public AppRegisterService(UserMapper userMapper, |
| | | ClinicMapper clinicMapper, |
| | | BCryptPasswordEncoder passwordEncoder, |
| | | AppSmsCodeService appSmsCodeService, |
| | | AppAuthSessionService appAuthSessionService, |
| | | AppRegisterRegionService appRegisterRegionService, |
| | | HztAppUserMapper hztAppUserMapper, |
| | | HztClinicMapper hztClinicMapper, |
| | |
| | | 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; |
| | |
| | | * 完成普通用户注册,并按需绑定上级诊所用户。 |
| | | * 普通用户注册会先查询海诊通数据源,因此这里不包裹本地事务,避免事务先绑定 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()); |
| | | |
| | |
| | | |
| | | AppUser user = new AppUser(); |
| | | user.setPhone(phone); |
| | | user.setPassword(passwordEncoder.encode(password)); |
| | | user.setType(0); |
| | | user.setStatus(1); |
| | | |
| | |
| | | bindSuperiorClinicIfNecessary(user, inviteCode, bindTicket); |
| | | |
| | | userMapper.insert(user); |
| | | return new AppRegisterResultVO(user.getId(), user.getType(), user.getClinicId(), null); |
| | | return createLoginResponse(user); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | 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); |
| | | |
| | |
| | | * 完成经营者第二步,创建本地诊所与诊所用户。 |
| | | */ |
| | | @Transactional |
| | | public AppRegisterResultVO completeOperatorRegister(AppRegisterOperatorCompleteRequest request) { |
| | | public AppLoginResponse completeOperatorRegister(AppRegisterOperatorCompleteRequest request) { |
| | | OperatorRegisterDraft draft = loadDraft(normalizeRegisterTicket(request.getRegisterTicket())); |
| | | ensurePhoneAvailable(draft.getPhone()); |
| | | |
| | |
| | | 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); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 构造经营者注册草稿,避免第二步再次输入验证码与密码。 |
| | | * 构造经营者注册草稿,避免第二步再次输入验证码。 |
| | | */ |
| | | 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; |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 优先使用用户昵称,缺失时回退到手机号脱敏昵称。 |
| | | */ |
| | | private String resolveFallbackNickname(AppUser user) { |
| | | String nickname = trimToNull(user.getNickname()); |
| | | if (nickname != null) { |
| | | return nickname; |
| | | } |
| | | return resolveFallbackNickname(user.getPhone()); |
| | | } |
| | | |
| | | /** |
| | | * 归一化并校验手机号格式。 |
| | | */ |
| | | private String normalizePhone(String phone) { |
| | |
| | | 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; |
| | | } |
| | |
| | | public static class OperatorRegisterDraft { |
| | | |
| | | private String phone; |
| | | |
| | | private String encodedPassword; |
| | | |
| | | private String contactName; |
| | | |