package com.tourguide.modules.user.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tourguide.common.exception.BusinessException; import com.tourguide.common.validation.ChineseIdCardValidator; import com.tourguide.modules.operation.entity.OperationCity; import com.tourguide.modules.operation.mapper.OperationCityMapper; import com.tourguide.modules.user.dto.GuideRegistrationResponse; import com.tourguide.modules.user.dto.GuideRegistrationSubmitRequest; import com.tourguide.modules.user.dto.GuideRegistrationTokenResponse; import com.tourguide.modules.user.dto.SmsCodeResponse; import com.tourguide.modules.user.entity.GuideApplication; import com.tourguide.modules.user.entity.GuideTeam; import com.tourguide.modules.user.mapper.GuideApplicationMapper; import com.tourguide.modules.user.mapper.GuideTeamMapper; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; @Service public class GuideRegistrationService { private static final int PENDING = 0; private static final int APPROVED = 1; private static final Duration TOKEN_TTL = Duration.ofMinutes(30); private static final String TOKEN_KEY_PREFIX = "tourguide:guide-registration:token:"; private static final DateTimeFormatter APPLICATION_NO_TIME = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); private final GuideVerificationCodeService verificationCodeService; private final StringRedisTemplate redisTemplate; private final GuideApplicationMapper applicationMapper; private final GuideUserService guideUserService; private final OperationCityMapper cityMapper; private final GuideTeamMapper teamMapper; public GuideRegistrationService(GuideVerificationCodeService verificationCodeService, StringRedisTemplate redisTemplate, GuideApplicationMapper applicationMapper, GuideUserService guideUserService, OperationCityMapper cityMapper, GuideTeamMapper teamMapper) { this.verificationCodeService = verificationCodeService; this.redisTemplate = redisTemplate; this.applicationMapper = applicationMapper; this.guideUserService = guideUserService; this.cityMapper = cityMapper; this.teamMapper = teamMapper; } public GuideRegistrationTokenResponse verifyPhone(String phone, String code) { ensurePhoneCanRegister(phone); verificationCodeService.verifyAndConsume(phone, code); String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set(tokenKey(token), phone, TOKEN_TTL); return new GuideRegistrationTokenResponse(token, TOKEN_TTL.getSeconds()); } public SmsCodeResponse sendCode(String phone) { ensurePhoneCanRegister(phone); return verificationCodeService.send(phone); } public String requireVerifiedPhone(String registrationToken) { if (!StringUtils.hasText(registrationToken)) { throw new BusinessException(401, "注册验证已失效,请重新验证手机号"); } String phone = redisTemplate.opsForValue().get(tokenKey(registrationToken)); if (!StringUtils.hasText(phone)) { throw new BusinessException(401, "注册验证已失效,请重新验证手机号"); } return phone; } @Transactional public GuideRegistrationResponse submit(String registrationToken, GuideRegistrationSubmitRequest request) { String phone = requireVerifiedPhone(registrationToken); ensurePhoneCanRegister(phone); String idCardNo = request.getIdCardNo().trim().toUpperCase(); if (!ChineseIdCardValidator.isValid(idCardNo)) { throw new BusinessException(400, "身份证号格式不正确"); } if (applicationMapper.selectCount(new LambdaQueryWrapper() .eq(GuideApplication::getIdCardNo, idCardNo) .in(GuideApplication::getAuditStatus, PENDING, APPROVED)) > 0) { throw new BusinessException(409, "该身份证号已提交过导游申请"); } OperationCity city = findEnabledBankCity(request.getBankCityId()); GuideTeam team = findEnabledGuideTeam(request.getGuideType(), request.getTeamId()); GuideApplication application = new GuideApplication(); application.setApplicationNo(buildApplicationNo()); application.setRealName(request.getRealName().trim()); application.setMobile(phone); application.setGender(request.getGender()); application.setAgeRange(normalizeOptional(request.getAgeRange())); application.setWorkYearRange(normalizeOptional(request.getWorkYearRange())); application.setGuideType(request.getGuideType()); application.setTeamId(team == null ? null : team.getId()); application.setIdCardNo(idCardNo); application.setIdCardFrontUrl(request.getIdCardFrontUrl()); application.setIdCardBackUrl(request.getIdCardBackUrl()); application.setCertificateUrl(request.getGuideCertificateUrl()); application.setQualificationCertificateUrl(normalizeOptional(request.getQualificationCertificateUrl())); application.setCertificateValidUntil(request.getCertificateValidUntil()); application.setBankCardNo(normalizeOptional(request.getBankCardNo())); if (city != null) { application.setBankCityId(city.getId()); application.setBankCityName(city.getCityName()); } application.setAuditStatus(PENDING); application.setDeleted(0); applicationMapper.insert(application); guideUserService.createPendingFromApplication(application); redisTemplate.delete(tokenKey(registrationToken)); return new GuideRegistrationResponse(application); } private void ensurePhoneCanRegister(String phone) { com.tourguide.modules.user.entity.GuideUser guide = guideUserService.findByPhone(phone); if (guide != null && (guide.getStatus() == null || guide.getStatus() != 2)) { throw new BusinessException(409, "该手机号已注册为导游"); } if (applicationMapper.selectCount(new LambdaQueryWrapper() .eq(GuideApplication::getMobile, phone) .eq(GuideApplication::getAuditStatus, PENDING)) > 0) { throw new BusinessException(409, "该手机号已有待审核申请"); } } private String buildApplicationNo() { return "GA" + LocalDateTime.now().format(APPLICATION_NO_TIME) + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(); } private String normalizeOptional(String value) { return StringUtils.hasText(value) ? value.trim() : null; } private OperationCity findEnabledBankCity(Long bankCityId) { if (bankCityId == null) { return null; } OperationCity city = cityMapper.selectById(bankCityId); if (city == null || city.getStatus() == null || city.getStatus() != 1) { throw new BusinessException(400, "开户城市未开放或已停用"); } return city; } private GuideTeam findEnabledGuideTeam(String guideType, Long teamId) { if ("personal".equals(guideType)) { return null; } if (teamId == null) { throw new BusinessException(400, "请选择讲解队"); } GuideTeam team = teamMapper.selectById(teamId); if (team == null || team.getStatus() == null || team.getStatus() != 1) { throw new BusinessException(400, "讲解队不存在或已停用"); } return team; } private String tokenKey(String token) { return TOKEN_KEY_PREFIX + token; } }