package com.tourguide.modules.user.service; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.user.dto.GuidePhoneChangeTokenResponse; import com.tourguide.modules.user.dto.GuideUserResponse; import com.tourguide.modules.user.dto.SmsCodeResponse; import com.tourguide.modules.user.entity.GuideUser; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.time.Duration; import java.util.UUID; /** 管理导游手机号更换流程,避免绕过旧手机号验证直接修改账号。 */ @Service public class GuidePhoneChangeService { private static final String OLD_PHONE_SCENE = "change-old"; private static final String NEW_PHONE_SCENE = "change-new"; private static final String TOKEN_KEY_PREFIX = "tourguide:guide-phone-change:token:"; private static final Duration TOKEN_TTL = Duration.ofMinutes(10); private final StringRedisTemplate redisTemplate; private final GuideVerificationCodeService verificationCodeService; private final GuideUserService userService; public GuidePhoneChangeService(StringRedisTemplate redisTemplate, GuideVerificationCodeService verificationCodeService, GuideUserService userService) { this.redisTemplate = redisTemplate; this.verificationCodeService = verificationCodeService; this.userService = userService; } public SmsCodeResponse sendOldPhoneCode(Long guideId, String phone) { requireCurrentPhone(guideId, phone); return verificationCodeService.send(phone, OLD_PHONE_SCENE); } public GuidePhoneChangeTokenResponse verifyOldPhone(Long guideId, String phone, String code) { requireCurrentPhone(guideId, phone); verificationCodeService.verifyAndConsume(phone, code, OLD_PHONE_SCENE); String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set(tokenKey(token), String.valueOf(guideId), TOKEN_TTL); return new GuidePhoneChangeTokenResponse(token, TOKEN_TTL.getSeconds()); } public SmsCodeResponse sendNewPhoneCode(Long guideId, String changeToken, String newPhone) { requireValidToken(guideId, changeToken); requireNewPhone(guideId, newPhone); return verificationCodeService.send(newPhone, NEW_PHONE_SCENE); } public GuideUserResponse changePhone(Long guideId, String changeToken, String newPhone, String code) { requireValidToken(guideId, changeToken); requireNewPhone(guideId, newPhone); verificationCodeService.verifyAndConsume(newPhone, code, NEW_PHONE_SCENE); GuideUserResponse response = userService.changeMobile(guideId, newPhone); redisTemplate.delete(tokenKey(changeToken)); return response; } private void requireCurrentPhone(Long guideId, String phone) { GuideUser user = userService.findEnabledById(guideId); if (user.getMobile() == null || !user.getMobile().equals(phone)) { throw new BusinessException(400, "旧手机号与当前账号不一致"); } } private void requireValidToken(Long guideId, String changeToken) { String tokenGuideId = redisTemplate.opsForValue().get(tokenKey(changeToken)); if (!String.valueOf(guideId).equals(tokenGuideId)) { throw new BusinessException(401, "换号验证已失效,请重新验证旧手机号"); } } private void requireNewPhone(Long guideId, String newPhone) { GuideUser user = userService.findEnabledById(guideId); if (newPhone.equals(user.getMobile())) { throw new BusinessException(400, "新手机号不能与当前手机号相同"); } userService.ensurePhoneAvailable(guideId, newPhone); } private String tokenKey(String token) { return TOKEN_KEY_PREFIX + token; } }