完善注册功能事务管理
Sync All Branches to GitHub / sync (push) Successful in 2s

This commit is contained in:
2026-01-20 19:43:19 +08:00
parent b87abe0e3e
commit 37ba2a02ad
4 changed files with 98 additions and 74 deletions
@@ -10,6 +10,7 @@ import top.crushtj.xiaoyishu.auth.domain.entity.UserEntity;
import top.crushtj.xiaoyishu.auth.domain.mappers.UserMapper; import top.crushtj.xiaoyishu.auth.domain.mappers.UserMapper;
import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY; import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY;
import static top.crushtj.xiaoyishu.auth.constant.XiaoyiAuthConstants.XIAOYI_ID_INITIAL_VALUE;
/** /**
* @author ayi * @author ayi
@@ -39,9 +40,9 @@ public class CacheLoader {
queryWrapper.last("limit 1"); queryWrapper.last("limit 1");
UserEntity user = userMapper.selectOne(queryWrapper); UserEntity user = userMapper.selectOne(queryWrapper);
if (user != null){ if (user != null){
redisTemplate.opsForValue().set(XIAOYI_ID_GENERATOR_KEY, user.getXiaoyishuId()); redisTemplate.opsForValue().set(XIAOYI_ID_GENERATOR_KEY, Long.valueOf(user.getXiaoyishuId()));
}else { }else {
redisTemplate.opsForValue().set(XIAOYI_ID_GENERATOR_KEY, 1000000L); redisTemplate.opsForValue().set(XIAOYI_ID_GENERATOR_KEY, XIAOYI_ID_INITIAL_VALUE);
} }
log.info("加载用户自增ID缓存结束..."); log.info("加载用户自增ID缓存结束...");
} }
@@ -10,4 +10,5 @@ package top.crushtj.xiaoyishu.auth.constant;
public class XiaoyiAuthConstants { public class XiaoyiAuthConstants {
public static final String NICK_NAME_PREFIX = "咿呀_"; public static final String NICK_NAME_PREFIX = "咿呀_";
public static final Long XIAOYI_ID_INITIAL_VALUE = 1000000L;
} }
@@ -8,7 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate;
import top.crushtj.framework.common.enums.DeleteEnum; import top.crushtj.framework.common.enums.DeleteEnum;
import top.crushtj.framework.common.enums.StatusEnum; import top.crushtj.framework.common.enums.StatusEnum;
import top.crushtj.framework.common.exception.BizException; import top.crushtj.framework.common.exception.BizException;
@@ -34,6 +34,7 @@ import java.util.Objects;
import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY; import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.XIAOYI_ID_GENERATOR_KEY;
import static top.crushtj.xiaoyishu.auth.constant.RoleConstants.COMMON_USER_ROLE_ID; import static top.crushtj.xiaoyishu.auth.constant.RoleConstants.COMMON_USER_ROLE_ID;
import static top.crushtj.xiaoyishu.auth.constant.XiaoyiAuthConstants.NICK_NAME_PREFIX; import static top.crushtj.xiaoyishu.auth.constant.XiaoyiAuthConstants.NICK_NAME_PREFIX;
import static top.crushtj.xiaoyishu.auth.constant.XiaoyiAuthConstants.XIAOYI_ID_INITIAL_VALUE;
/** /**
* @author ayi * @author ayi
@@ -53,6 +54,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
@Resource @Resource
private UserRoleRelMapper userRoleRelMapper; private UserRoleRelMapper userRoleRelMapper;
@Resource
private TransactionTemplate transactionTemplate;
@Override @Override
public Response<String> loginOrRegister(UserLoginReqVO userLoginReqVO) { public Response<String> loginOrRegister(UserLoginReqVO userLoginReqVO) {
String phone = userLoginReqVO.getPhone(); String phone = userLoginReqVO.getPhone();
@@ -111,40 +115,61 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
return Response.success(tokenInfo.tokenValue); return Response.success(tokenInfo.tokenValue);
} }
@Transactional(rollbackFor = Exception.class) private Long registerUser(String phone) {
public Long registerUser(String phone) { return transactionTemplate.execute(status -> {
long userId = IdGenerator.getInstance() try {
.nextId(); long userId = IdGenerator.getInstance()
Long xiaoyishuId = redisTemplate.opsForValue() .nextId();
.increment(XIAOYI_ID_GENERATOR_KEY); // 小壹书用户ID,非数据库主键ID
UserEntity userEntity = UserEntity.builder() Long xiaoyishuId = redisTemplate.opsForValue()
.id(userId) .increment(XIAOYI_ID_GENERATOR_KEY);
.phone(phone) UserEntity userEntity = UserEntity.builder()
.xiaoyishuId(String.valueOf(xiaoyishuId)) .id(userId)
.nickname(NICK_NAME_PREFIX + xiaoyishuId) .phone(phone)
.status(StatusEnum.ENABLED.getValue()) .xiaoyishuId(String.valueOf(xiaoyishuId))
.createTime(LocalDateTime.now()) .nickname(NICK_NAME_PREFIX + xiaoyishuId) // 自动注册的用户昵称默认为"咿呀_" + 用户ID
.updateTime(LocalDateTime.now()) .status(StatusEnum.ENABLED.getValue())
.isDeleted(DeleteEnum.NO.getValue()) .createTime(LocalDateTime.now())
.build(); .updateTime(LocalDateTime.now())
save(userEntity); .isDeleted(DeleteEnum.NO.getValue())
.build();
save(userEntity);
UserRoleRelEntity userRoleRel = UserRoleRelEntity.builder() UserRoleRelEntity userRoleRel = UserRoleRelEntity.builder()
.id(IdGenerator.getInstance() .id(IdGenerator.getInstance()
.nextId()) .nextId())
.userId(userId) .userId(userId)
.roleId(COMMON_USER_ROLE_ID) .roleId(COMMON_USER_ROLE_ID) // 自动注册的用户角色为普通用户
.createTime(LocalDateTime.now()) .createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now()) .updateTime(LocalDateTime.now())
.isDeleted(DeleteEnum.NO.getValue()) .isDeleted(DeleteEnum.NO.getValue())
.build(); .build();
userRoleRelMapper.insert(userRoleRel); int i = 1 / 0;
List<Long> roles = new ArrayList<>(); userRoleRelMapper.insert(userRoleRel);
roles.add(COMMON_USER_ROLE_ID);
String userRolesKey = RedisKeyConstants.buildUserRolesKey(phone); // 将用户角色信息存储到 Redis 中
redisTemplate.opsForValue() List<Long> roles = new ArrayList<>();
.set(userRolesKey, JsonUtils.toJsonString(roles)); roles.add(COMMON_USER_ROLE_ID);
return userId; String userRolesKey = RedisKeyConstants.buildUserRolesKey(phone);
redisTemplate.opsForValue()
.set(userRolesKey, JsonUtils.toJsonString(roles));
return userId;
} catch (Exception e) {
// 回滚事务
log.error("==> 用户注册异常,开始回滚事务: ", e);
status.setRollbackOnly();
// 回滚 redis 自增ID
Long decrement = Long.valueOf(Objects.requireNonNull(redisTemplate.opsForValue()
.get(XIAOYI_ID_GENERATOR_KEY))
.toString().trim());
if (decrement.compareTo(XIAOYI_ID_INITIAL_VALUE) > 0) {
log.error("==> 用户注册异常,回滚redis自增ID: {}", decrement);
redisTemplate.opsForValue()
.decrement(XIAOYI_ID_GENERATOR_KEY);
}
}
return null;
});
} }
} }
@@ -15,10 +15,7 @@ import top.crushtj.xiaoyishu.auth.model.vo.verificationcode.SendVerificationCode
import top.crushtj.xiaoyishu.auth.service.VerificationCodeService; import top.crushtj.xiaoyishu.auth.service.VerificationCodeService;
import top.crushtj.xiaoyishu.auth.sms.AliyunSmsHelper; import top.crushtj.xiaoyishu.auth.sms.AliyunSmsHelper;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.VERIFICATION_CODE_EXPIRE_TIME; import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.VERIFICATION_CODE_EXPIRE_TIME;
@@ -58,41 +55,41 @@ public class VerificationCodeServiceImpl implements VerificationCodeService {
//=============== 开发环境不实际调用短信发送接口 =============== //=============== 开发环境不实际调用短信发送接口 ===============
// 4. 异步发送短信(用CompletableFuture跟踪任务状态,捕获异常) // 4. 异步发送短信(用CompletableFuture跟踪任务状态,捕获异常)
CompletableFuture<Boolean> smsSendFuture = CompletableFuture.supplyAsync(() -> { //CompletableFuture<Boolean> smsSendFuture = CompletableFuture.supplyAsync(() -> {
// 设置线程名称,便于日志排查 // // 设置线程名称,便于日志排查
Thread.currentThread().setName("sms-send-" + MaskUtils.maskMobile(phoneNumber)); // Thread.currentThread().setName("sms-send-" + MaskUtils.maskMobile(phoneNumber));
String signName = "速通互联验证码"; // String signName = "速通互联验证码";
String templateCode = "100001"; // String templateCode = "100001";
String templateParam = String.format("{\"code\":\"%s\",\"min\":\"%d\"}", verificationCode, VERIFICATION_CODE_EXPIRE_TIME); // String templateParam = String.format("{\"code\":\"%s\",\"min\":\"%d\"}", verificationCode, VERIFICATION_CODE_EXPIRE_TIME);
try { // try {
return aliyunSmsHelper.sendMessage(signName, templateCode, phoneNumber, templateParam); // return aliyunSmsHelper.sendMessage(signName, templateCode, phoneNumber, templateParam);
} catch (Exception e) { // } catch (Exception e) {
log.error("==> 手机号: {}, 短信发送接口调用异常", MaskUtils.maskMobile(phoneNumber), e); // log.error("==> 手机号: {}, 短信发送接口调用异常", MaskUtils.maskMobile(phoneNumber), e);
return false; // return false;
} // }
}, taskExecutor); //}, taskExecutor);
//
//5. 同步等待短信发送结果(超时控制,避免主线程阻塞过久) // //5. 同步等待短信发送结果(超时控制,避免主线程阻塞过久)
boolean smsSendSuccess; //boolean smsSendSuccess;
try { //try {
smsSendSuccess = smsSendFuture.get(SMS_SEND_TIMEOUT_SECONDS, TimeUnit.SECONDS); // smsSendSuccess = smsSendFuture.get(SMS_SEND_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) { //} catch (InterruptedException e) {
log.error("==> 手机号: {}, 短信发送任务被中断", MaskUtils.maskMobile(phoneNumber), e); // log.error("==> 手机号: {}, 短信发送任务被中断", MaskUtils.maskMobile(phoneNumber), e);
Thread.currentThread().interrupt(); // 恢复中断状态 // Thread.currentThread().interrupt(); // 恢复中断状态
throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED); // throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
} catch (ExecutionException e) { //} catch (ExecutionException e) {
log.error("==> 手机号: {}, 短信发送任务执行异常", MaskUtils.maskMobile(phoneNumber), e); // log.error("==> 手机号: {}, 短信发送任务执行异常", MaskUtils.maskMobile(phoneNumber), e);
throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED); // throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
} catch (TimeoutException e) { //} catch (TimeoutException e) {
log.error("==> 手机号: {}, 短信发送任务超时({}秒)", MaskUtils.maskMobile(phoneNumber), SMS_SEND_TIMEOUT_SECONDS, e); // log.error("==> 手机号: {}, 短信发送任务超时({}秒)", MaskUtils.maskMobile(phoneNumber), SMS_SEND_TIMEOUT_SECONDS, e);
throw new BizException(ResponseCodeEnum.SMS_SEND_TIMEOUT); // throw new BizException(ResponseCodeEnum.SMS_SEND_TIMEOUT);
} //}
//
// 6. 短信发送失败则直接抛异常,不存储Redis //// 6. 短信发送失败则直接抛异常,不存储Redis
if (!smsSendSuccess) { //if (!smsSendSuccess) {
log.error("==> 手机号: {}, 发送验证码失败(第三方接口返回失败)", MaskUtils.maskMobile(phoneNumber)); // log.error("==> 手机号: {}, 发送验证码失败(第三方接口返回失败)", MaskUtils.maskMobile(phoneNumber));
throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED); // throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
} //}
//=============== 开发环境不实际调用短信发送接口 =============== //=============== 开发环境不实际调用短信发送接口 ===============
// 7. 短信发送成功后,记录日志(验证码脱敏,仅保留后2位)+ 存储Redis // 7. 短信发送成功后,记录日志(验证码脱敏,仅保留后2位)+ 存储Redis