feat(user): 新增注销账户接口
This commit is contained in:
@@ -73,6 +73,17 @@ public class UserController {
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/cancelAccount")
|
||||
@Operation(summary = "注销账户", description = "用户注销账户(软删除),同步写入 deleted_at,并强制退出登录")
|
||||
public BaseResponse<Boolean> cancelAccount() {
|
||||
final long userId = StpUtil.getLoginIdAsLong();
|
||||
final Boolean success = userService.cancelAccount(userId);
|
||||
final String tokenValue = StpUtil.getTokenValue();
|
||||
StpUtil.logout(userId);
|
||||
StpUtil.logoutByTokenValue(tokenValue);
|
||||
return ResultUtils.success(success);
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "登录", description = "登录接口")
|
||||
public BaseResponse<KeyboardUserRespVO> login(@RequestBody UserLoginDTO userLoginDTO, HttpServletRequest request) {
|
||||
|
||||
@@ -73,6 +73,13 @@ public class KeyboardUser {
|
||||
@Schema(description = "是否删除(默认否)")
|
||||
private Boolean deleted;
|
||||
|
||||
/**
|
||||
* 删除时间
|
||||
*/
|
||||
@TableField(value = "deleted_at")
|
||||
@Schema(description = "删除时间")
|
||||
private Date deletedAt;
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
@@ -125,4 +132,4 @@ public class KeyboardUser {
|
||||
@TableField(value = "vip_level")
|
||||
@Schema(description = "vip等级")
|
||||
private Integer vipLevel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,12 @@ public interface UserService extends IService<KeyboardUser> {
|
||||
|
||||
Boolean bindInviteCode(BindInviteCodeDTO bindInviteCodeDTO);
|
||||
|
||||
/**
|
||||
* 注销账户(软删除)
|
||||
*
|
||||
* @param userId 当前登录用户ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean cancelAccount(long userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.config.AppConfig;
|
||||
@@ -12,13 +13,13 @@ import com.yolo.keyborad.config.NacosAppConfigCenter;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.mapper.KeyboardUserMapper;
|
||||
import com.yolo.keyborad.model.dto.user.*;
|
||||
import com.yolo.keyborad.model.entity.*;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserQuotaTotal;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserWallet;
|
||||
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
|
||||
import com.yolo.keyborad.service.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import com.yolo.keyborad.utils.RedisUtil;
|
||||
import com.yolo.keyborad.utils.SendMailUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -26,7 +27,10 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.yolo.keyborad.service.impl.user.UserInviteCodeBinder;
|
||||
import com.yolo.keyborad.service.impl.user.UserMailVerificationHandler;
|
||||
import com.yolo.keyborad.service.impl.user.UserPasswordHandler;
|
||||
import com.yolo.keyborad.service.impl.user.UserRegistrationHandler;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
@@ -42,12 +46,6 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
||||
@Resource
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Resource
|
||||
private SendMailUtils sendMailUtils;
|
||||
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Resource
|
||||
private KeyboardCharacterService keyboardCharacterService;
|
||||
|
||||
@@ -64,10 +62,16 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
||||
private KeyboardUserInviteCodesService inviteCodesService;
|
||||
|
||||
@Resource
|
||||
private KeyboardUserInvitesService userInvitesService;
|
||||
private UserRegistrationHandler registrationHandler;
|
||||
|
||||
@Resource
|
||||
private HttpServletRequest request;
|
||||
private UserMailVerificationHandler mailVerificationHandler;
|
||||
|
||||
@Resource
|
||||
private UserPasswordHandler passwordHandler;
|
||||
|
||||
@Resource
|
||||
private UserInviteCodeBinder inviteCodeBinder;
|
||||
|
||||
private final NacosAppConfigCenter.DynamicAppConfig cfgHolder;
|
||||
|
||||
@@ -85,39 +89,16 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
||||
|
||||
@Override
|
||||
public KeyboardUser createUserWithSubjectId(String sub) {
|
||||
KeyboardUser keyboardUser = new KeyboardUser();
|
||||
keyboardUser.setSubjectId(sub);
|
||||
keyboardUser.setUid(IdUtil.getSnowflake().nextId());
|
||||
keyboardUser.setNickName("User_" + RandomUtil.randomString(6));
|
||||
KeyboardUser keyboardUser = buildNewUserWithSubjectId(sub);
|
||||
keyboardUserMapper.insert(keyboardUser);
|
||||
keyboardCharacterService.addDefaultUserCharacter(keyboardUser.getId());
|
||||
|
||||
AppConfig appConfig = cfgHolder.getRef().get();
|
||||
|
||||
// 初始化用户钱包(余额为0)
|
||||
KeyboardUserWallet wallet = new KeyboardUserWallet();
|
||||
wallet.setUserId(keyboardUser.getId());
|
||||
wallet.setBalance(BigDecimal.valueOf(appConfig.getUserRegisterProperties().getFreeTrialQuota()));
|
||||
wallet.setVersion(0);
|
||||
wallet.setStatus((short) 1);
|
||||
wallet.setCreatedAt(new Date());
|
||||
wallet.setUpdatedAt(new Date());
|
||||
walletService.save(wallet);
|
||||
// 初始化用户免费使用次数配额
|
||||
KeyboardUserQuotaTotal quotaTotal = new KeyboardUserQuotaTotal();
|
||||
quotaTotal.setUserId(keyboardUser.getId());
|
||||
quotaTotal.setTotalQuota(appConfig.getUserRegisterProperties().getFreeTrialQuota());
|
||||
quotaTotal.setUsedQuota(0);
|
||||
quotaTotal.setVersion(0);
|
||||
quotaTotal.setCreatedAt(new Date());
|
||||
quotaTotal.setUpdatedAt(new Date());
|
||||
quotaTotalService.save(quotaTotal);
|
||||
|
||||
initNewUserWalletAndQuota(keyboardUser.getId(), appConfig.getUserRegisterProperties().getFreeTrialQuota());
|
||||
inviteCodesService.createInviteCode(keyboardUser.getId());
|
||||
|
||||
log.info("User registered with Apple Sign-In, userId={}, freeQuota={}",
|
||||
keyboardUser.getId(), appConfig.getUserRegisterProperties().getFreeTrialQuota());
|
||||
|
||||
return keyboardUser;
|
||||
}
|
||||
|
||||
@@ -136,43 +117,7 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
||||
StpUtil.login(keyboardUser.getId());
|
||||
|
||||
// 记录登录日志
|
||||
try {
|
||||
String ipAddress = request.getRemoteAddr();
|
||||
String userAgent = request.getHeader("User-Agent");
|
||||
String platform = "Unknown";
|
||||
String os = "Unknown";
|
||||
|
||||
if (userAgent != null) {
|
||||
if (userAgent.contains("iOS")) {
|
||||
platform = "iOS";
|
||||
} else if (userAgent.contains("Android")) {
|
||||
platform = "Android";
|
||||
}
|
||||
|
||||
if (userAgent.contains("Windows")) {
|
||||
os = "Windows";
|
||||
} else if (userAgent.contains("Mac OS")) {
|
||||
os = "Mac OS";
|
||||
} else if (userAgent.contains("Linux")) {
|
||||
os = "Linux";
|
||||
} else if (userAgent.contains("iOS")) {
|
||||
os = "iOS";
|
||||
} else if (userAgent.contains("Android")) {
|
||||
os = "Android";
|
||||
}
|
||||
}
|
||||
|
||||
loginLogService.recordLoginLog(
|
||||
keyboardUser.getId(),
|
||||
ipAddress,
|
||||
userAgent,
|
||||
os,
|
||||
platform,
|
||||
"SUCCESS"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("记录登录日志失败", e);
|
||||
}
|
||||
recordLoginLogSafely(keyboardUser.getId(), request);
|
||||
|
||||
KeyboardUserRespVO keyboardUserRespVO = BeanUtil.copyProperties(keyboardUser, KeyboardUserRespVO.class);
|
||||
keyboardUserRespVO.setToken(StpUtil.getTokenValue());
|
||||
@@ -202,212 +147,135 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean userRegister(UserRegisterDTO userRegisterDTO) {
|
||||
KeyboardUser userMail = keyboardUserMapper.selectOne(new LambdaQueryWrapper<KeyboardUser>()
|
||||
.eq(KeyboardUser::getEmail, userRegisterDTO.getMailAddress()));
|
||||
|
||||
if (userMail != null) {
|
||||
throw new BusinessException(ErrorCode.USER_HAS_EXISTED);
|
||||
}
|
||||
|
||||
if (userRegisterDTO.getPassword() == null) {
|
||||
throw new BusinessException(ErrorCode.PASSWORD_CAN_NOT_NULL);
|
||||
}
|
||||
if (!userRegisterDTO.getPassword().equals(userRegisterDTO.getPasswordConfirm())) {
|
||||
throw new BusinessException(ErrorCode.CONFIRM_PASSWORD_NOT_MATCH);
|
||||
}
|
||||
|
||||
KeyboardUser keyboardUser = new KeyboardUser();
|
||||
keyboardUser.setUid(IdUtil.getSnowflake().nextId());
|
||||
keyboardUser.setNickName("User_" + RandomUtil.randomString(6));
|
||||
keyboardUser.setPassword(passwordEncoder.encode(userRegisterDTO.getPassword()));
|
||||
keyboardUser.setEmail(userRegisterDTO.getMailAddress());
|
||||
keyboardUser.setGender(userRegisterDTO.getGender());
|
||||
log.info(keyboardUser.toString());
|
||||
String s = redisUtil.get("user:" + userRegisterDTO.getMailAddress());
|
||||
if (!userRegisterDTO.getVerifyCode().equals(s)) {
|
||||
throw new BusinessException(ErrorCode.VERIFY_CODE_ERROR);
|
||||
}
|
||||
AppConfig appConfig = cfgHolder.getRef().get();
|
||||
keyboardUser.setEmailVerified(true);
|
||||
redisUtil.delete("user:" + userRegisterDTO.getMailAddress());
|
||||
int insertCount = keyboardUserMapper.insert(keyboardUser);
|
||||
if (insertCount > 0) {
|
||||
keyboardCharacterService.addDefaultUserCharacter(keyboardUser.getId());
|
||||
|
||||
// 初始化用户钱包(余额为0)
|
||||
KeyboardUserWallet wallet = new KeyboardUserWallet();
|
||||
wallet.setUserId(keyboardUser.getId());
|
||||
wallet.setBalance(appConfig.getUserRegisterProperties().getRewardBalance());
|
||||
wallet.setVersion(0);
|
||||
wallet.setStatus((short) 1);
|
||||
wallet.setCreatedAt(new Date());
|
||||
wallet.setUpdatedAt(new Date());
|
||||
walletService.save(wallet);
|
||||
|
||||
// 初始化用户免费使用次数配额
|
||||
com.yolo.keyborad.model.entity.KeyboardUserQuotaTotal quotaTotal =
|
||||
new com.yolo.keyborad.model.entity.KeyboardUserQuotaTotal();
|
||||
quotaTotal.setUserId(keyboardUser.getId());
|
||||
quotaTotal.setTotalQuota(appConfig.getUserRegisterProperties().getFreeTrialQuota());
|
||||
quotaTotal.setUsedQuota(0);
|
||||
quotaTotal.setVersion(0);
|
||||
quotaTotal.setCreatedAt(new Date());
|
||||
quotaTotal.setUpdatedAt(new Date());
|
||||
quotaTotalService.save(quotaTotal);
|
||||
inviteCodesService.createInviteCode(keyboardUser.getId());
|
||||
|
||||
// 处理邀请码绑定
|
||||
if (userRegisterDTO.getInviteCode() != null && !userRegisterDTO.getInviteCode().trim().isEmpty()) {
|
||||
try {
|
||||
// 验证邀请码
|
||||
KeyboardUserInviteCodes inviteCode = inviteCodesService.validateInviteCode(userRegisterDTO.getInviteCode().trim());
|
||||
|
||||
// 创建邀请关系绑定记录
|
||||
KeyboardUserInvites userInvite = new KeyboardUserInvites();
|
||||
userInvite.setInviterUserId(inviteCode.getOwnerUserId());
|
||||
userInvite.setInviteeUserId(keyboardUser.getId());
|
||||
userInvite.setInviteCodeId(inviteCode.getId());
|
||||
userInvite.setBindType((short) 1); // 1=手动填写邀请码
|
||||
userInvite.setBoundAt(new Date());
|
||||
userInvite.setBindIp(request.getRemoteAddr());
|
||||
userInvite.setBindUserAgent(request.getHeader("User-Agent"));
|
||||
// 记录邀请码类型快照(用户/租户)
|
||||
userInvite.setInviteType(inviteCode.getInviteType());
|
||||
userInvite.setInviteCode(inviteCode.getCode());
|
||||
// 如果是租户邀请码,记录租户ID
|
||||
if ("AGENT".equals(inviteCode.getInviteType()) && inviteCode.getOwnerTenantId() != null) {
|
||||
userInvite.setProfitTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setInviterTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setProfitEmployeeId(inviteCode.getOwnerSystemUserId());
|
||||
}
|
||||
userInvitesService.save(userInvite);
|
||||
|
||||
// 更新邀请码使用次数
|
||||
inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
|
||||
inviteCodesService.updateById(inviteCode);
|
||||
|
||||
log.info("User bound to invite code, userId={}, inviteCodeId={}, inviterUserId={}, inviteType={}",
|
||||
keyboardUser.getId(), inviteCode.getId(), inviteCode.getOwnerUserId(), inviteCode.getInviteType());
|
||||
} catch (BusinessException e) {
|
||||
// 邀请码验证失败,记录日志但不影响注册流程
|
||||
log.warn("Failed to bind invite code for user {}: {}", keyboardUser.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("User registered with email, userId={}, email={}, freeQuota={}",
|
||||
keyboardUser.getId(), keyboardUser.getEmail(), appConfig.getUserRegisterProperties().getFreeTrialQuota());
|
||||
}
|
||||
return insertCount > 0;
|
||||
return registrationHandler.userRegister(userRegisterDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendVerifyMail(SendMailDTO mailDTO) {
|
||||
|
||||
if (redisUtil.hasKey("limit_mail:" + mailDTO.getMailAddress())){
|
||||
throw new BusinessException(ErrorCode.MAIL_SEND_BUSY);
|
||||
}
|
||||
|
||||
String existCode = redisUtil.get("user:" + mailDTO.getMailAddress());
|
||||
|
||||
if (redisUtil.get("user:" + mailDTO.getMailAddress()) != null) {
|
||||
sendMailUtils.sendEmail(null,mailDTO.getMailAddress(),Integer.valueOf(existCode));
|
||||
return;
|
||||
}
|
||||
|
||||
int code = RandomUtil.randomInt(100000, 999999);
|
||||
|
||||
redisUtil.setEx("user:" + mailDTO.getMailAddress(), String.valueOf(code),600, TimeUnit.SECONDS);
|
||||
|
||||
sendMailUtils.sendEmail(null,mailDTO.getMailAddress(),code);
|
||||
|
||||
redisUtil.setEx("limit_mail:" + mailDTO.getMailAddress(), String.valueOf(code),60, TimeUnit.SECONDS);
|
||||
mailVerificationHandler.sendVerifyMail(mailDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean verifyMailCode(VerifyCodeDTO verifyCodeDTO) {
|
||||
|
||||
String s = redisUtil.get("user:" + verifyCodeDTO.getMailAddress());
|
||||
|
||||
if (s == null) {
|
||||
throw new BusinessException(ErrorCode.VERIFY_CODE_ERROR);
|
||||
}
|
||||
if (s.equals(String.valueOf(verifyCodeDTO.getVerifyCode()))){
|
||||
redisUtil.delete("user:" + verifyCodeDTO.getMailAddress());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return mailVerificationHandler.verifyMailCode(verifyCodeDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean resetPassWord(ResetPassWordDTO resetPassWordDTO) {
|
||||
KeyboardUser keyboardUser = keyboardUserMapper.selectOne(
|
||||
new LambdaQueryWrapper<KeyboardUser>()
|
||||
.eq(KeyboardUser::getEmail, resetPassWordDTO.getMailAddress())
|
||||
.eq(KeyboardUser::getStatus, false));
|
||||
|
||||
if (keyboardUser == null) {
|
||||
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
|
||||
}
|
||||
if (!resetPassWordDTO.getPassword().equals(resetPassWordDTO.getConfirmPassword())) {
|
||||
throw new BusinessException(ErrorCode.CONFIRM_PASSWORD_NOT_MATCH);
|
||||
}
|
||||
keyboardUser.setPassword(passwordEncoder.encode(resetPassWordDTO.getPassword()));
|
||||
return keyboardUserMapper.updateById(keyboardUser) > 0;
|
||||
|
||||
return passwordHandler.resetPassWord(resetPassWordDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean bindInviteCode(BindInviteCodeDTO bindInviteCodeDTO) {
|
||||
// 获取当前登录用户ID
|
||||
long userId = StpUtil.getLoginIdAsLong();
|
||||
return inviteCodeBinder.bind(userId, bindInviteCodeDTO.getInviteCode());
|
||||
}
|
||||
|
||||
// 检查用户是否已经绑定过邀请码
|
||||
long existingBindCount = userInvitesService.count(
|
||||
new LambdaQueryWrapper<KeyboardUserInvites>()
|
||||
.eq(KeyboardUserInvites::getInviteeUserId, userId)
|
||||
);
|
||||
if (existingBindCount > 0) {
|
||||
throw new BusinessException(ErrorCode.INVITE_CODE_ALREADY_BOUND);
|
||||
@Override
|
||||
public Boolean cancelAccount(long userId) {
|
||||
if (userId <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
|
||||
// 验证邀请码
|
||||
String inviteCodeStr = bindInviteCodeDTO.getInviteCode().trim();
|
||||
KeyboardUserInviteCodes inviteCode = inviteCodesService.validateInviteCode(inviteCodeStr);
|
||||
Date now = new Date();
|
||||
LambdaUpdateWrapper<KeyboardUser> updateWrapper = new LambdaUpdateWrapper<KeyboardUser>()
|
||||
.eq(KeyboardUser::getId, userId)
|
||||
.eq(KeyboardUser::getDeleted, false)
|
||||
.set(KeyboardUser::getDeleted, true)
|
||||
.set(KeyboardUser::getDeletedAt, now)
|
||||
.set(KeyboardUser::getUpdatedAt, now)
|
||||
.set(KeyboardUser::getStatus, true);
|
||||
|
||||
// 检查是否是绑定自己的邀请码
|
||||
if (inviteCode.getOwnerUserId().equals(userId)) {
|
||||
throw new BusinessException(ErrorCode.INVITE_CODE_CANNOT_BIND_SELF);
|
||||
int affectedRows = keyboardUserMapper.update(null, updateWrapper);
|
||||
if (affectedRows <= 0) {
|
||||
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 创建邀请关系绑定记录
|
||||
KeyboardUserInvites userInvite = new KeyboardUserInvites();
|
||||
userInvite.setInviterUserId(inviteCode.getOwnerUserId());
|
||||
userInvite.setInviteeUserId(userId);
|
||||
userInvite.setInviteCodeId(inviteCode.getId());
|
||||
userInvite.setBindType((short) 1); // 1=手动填写邀请码
|
||||
userInvite.setBoundAt(new Date());
|
||||
userInvite.setBindIp(request.getRemoteAddr());
|
||||
userInvite.setBindUserAgent(request.getHeader("User-Agent"));
|
||||
// 记录邀请码类型快照(用户/租户)
|
||||
userInvite.setInviteType(inviteCode.getInviteType());
|
||||
userInvite.setInviteCode(inviteCode.getCode());
|
||||
// 如果是租户邀请码,记录租户ID
|
||||
if ("AGENT".equals(inviteCode.getInviteType()) && inviteCode.getOwnerTenantId() != null) {
|
||||
userInvite.setProfitTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setInviterTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setProfitEmployeeId(inviteCode.getOwnerSystemUserId());
|
||||
}
|
||||
userInvitesService.save(userInvite);
|
||||
|
||||
// 更新邀请码使用次数
|
||||
inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
|
||||
inviteCodesService.updateById(inviteCode);
|
||||
|
||||
log.info("User bound invite code, userId={}, inviteCodeId={}, inviterUserId={}, inviteType={}",
|
||||
userId, inviteCode.getId(), inviteCode.getOwnerUserId(), inviteCode.getInviteType());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private KeyboardUser buildNewUserWithSubjectId(String sub) {
|
||||
KeyboardUser keyboardUser = new KeyboardUser();
|
||||
keyboardUser.setSubjectId(sub);
|
||||
keyboardUser.setUid(IdUtil.getSnowflake().nextId());
|
||||
keyboardUser.setNickName("User_" + RandomUtil.randomString(6));
|
||||
return keyboardUser;
|
||||
}
|
||||
|
||||
private void initNewUserWalletAndQuota(long userId, Integer freeTrialQuota) {
|
||||
Date now = new Date();
|
||||
KeyboardUserWallet wallet = new KeyboardUserWallet();
|
||||
wallet.setUserId(userId);
|
||||
wallet.setBalance(BigDecimal.valueOf(freeTrialQuota.longValue()));
|
||||
wallet.setVersion(0);
|
||||
wallet.setStatus((short) 1);
|
||||
wallet.setCreatedAt(now);
|
||||
wallet.setUpdatedAt(now);
|
||||
walletService.save(wallet);
|
||||
|
||||
KeyboardUserQuotaTotal quotaTotal = new KeyboardUserQuotaTotal();
|
||||
quotaTotal.setUserId(userId);
|
||||
quotaTotal.setTotalQuota(freeTrialQuota);
|
||||
quotaTotal.setUsedQuota(0);
|
||||
quotaTotal.setVersion(0);
|
||||
quotaTotal.setCreatedAt(now);
|
||||
quotaTotal.setUpdatedAt(now);
|
||||
quotaTotalService.save(quotaTotal);
|
||||
}
|
||||
|
||||
private void recordLoginLogSafely(Long userId, HttpServletRequest request) {
|
||||
try {
|
||||
String ipAddress = request.getRemoteAddr();
|
||||
String userAgent = request.getHeader("User-Agent");
|
||||
String platform = resolvePlatform(userAgent);
|
||||
String os = resolveOs(userAgent);
|
||||
|
||||
loginLogService.recordLoginLog(
|
||||
userId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
os,
|
||||
platform,
|
||||
"SUCCESS"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("记录登录日志失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolvePlatform(String userAgent) {
|
||||
if (userAgent == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
if (userAgent.contains("iOS")) {
|
||||
return "iOS";
|
||||
}
|
||||
if (userAgent.contains("Android")) {
|
||||
return "Android";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private String resolveOs(String userAgent) {
|
||||
if (userAgent == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
if (userAgent.contains("Windows")) {
|
||||
return "Windows";
|
||||
}
|
||||
if (userAgent.contains("Mac OS")) {
|
||||
return "Mac OS";
|
||||
}
|
||||
if (userAgent.contains("Linux")) {
|
||||
return "Linux";
|
||||
}
|
||||
if (userAgent.contains("iOS")) {
|
||||
return "iOS";
|
||||
}
|
||||
if (userAgent.contains("Android")) {
|
||||
return "Android";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.yolo.keyborad.service.impl.user;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserInviteCodes;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserInvites;
|
||||
import com.yolo.keyborad.service.KeyboardUserInviteCodesService;
|
||||
import com.yolo.keyborad.service.KeyboardUserInvitesService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Date;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/2/27
|
||||
*/
|
||||
@Component
|
||||
public class UserInviteCodeBinder {
|
||||
|
||||
private static final short MANUAL_BIND_TYPE = 1;
|
||||
private static final String INVITE_TYPE_AGENT = "AGENT";
|
||||
|
||||
private final KeyboardUserInvitesService userInvitesService;
|
||||
private final KeyboardUserInviteCodesService inviteCodesService;
|
||||
private final HttpServletRequest request;
|
||||
|
||||
public UserInviteCodeBinder(
|
||||
KeyboardUserInvitesService userInvitesService,
|
||||
KeyboardUserInviteCodesService inviteCodesService,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
this.userInvitesService = userInvitesService;
|
||||
this.inviteCodesService = inviteCodesService;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public Boolean bind(long userId, String inviteCodeStr) {
|
||||
if (userId <= 0 || inviteCodeStr == null || inviteCodeStr.trim().isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
|
||||
ensureNotBoundYet(userId);
|
||||
|
||||
KeyboardUserInviteCodes inviteCode = inviteCodesService.validateInviteCode(inviteCodeStr.trim());
|
||||
ensureNotSelfInvite(userId, inviteCode);
|
||||
|
||||
KeyboardUserInvites userInvite = buildInviteBind(userId, inviteCode);
|
||||
userInvitesService.save(userInvite);
|
||||
|
||||
increaseInviteCodeUsedCount(inviteCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ensureNotBoundYet(long userId) {
|
||||
long bindCount = userInvitesService.count(new LambdaQueryWrapper<KeyboardUserInvites>()
|
||||
.eq(KeyboardUserInvites::getInviteeUserId, userId));
|
||||
if (bindCount > 0) {
|
||||
throw new BusinessException(ErrorCode.INVITE_CODE_ALREADY_BOUND);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNotSelfInvite(long userId, KeyboardUserInviteCodes inviteCode) {
|
||||
if (inviteCode.getOwnerUserId().equals(userId)) {
|
||||
throw new BusinessException(ErrorCode.INVITE_CODE_CANNOT_BIND_SELF);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyboardUserInvites buildInviteBind(long userId, KeyboardUserInviteCodes inviteCode) {
|
||||
KeyboardUserInvites userInvite = new KeyboardUserInvites();
|
||||
userInvite.setInviterUserId(inviteCode.getOwnerUserId());
|
||||
userInvite.setInviteeUserId(userId);
|
||||
userInvite.setInviteCodeId(inviteCode.getId());
|
||||
userInvite.setBindType(MANUAL_BIND_TYPE);
|
||||
userInvite.setBoundAt(new Date());
|
||||
userInvite.setBindIp(request.getRemoteAddr());
|
||||
userInvite.setBindUserAgent(request.getHeader("User-Agent"));
|
||||
userInvite.setInviteType(inviteCode.getInviteType());
|
||||
userInvite.setInviteCode(inviteCode.getCode());
|
||||
|
||||
if (isAgentInvite(inviteCode)) {
|
||||
userInvite.setProfitTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setInviterTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setProfitEmployeeId(inviteCode.getOwnerSystemUserId());
|
||||
}
|
||||
return userInvite;
|
||||
}
|
||||
|
||||
private boolean isAgentInvite(KeyboardUserInviteCodes inviteCode) {
|
||||
return INVITE_TYPE_AGENT.equals(inviteCode.getInviteType()) && inviteCode.getOwnerTenantId() != null;
|
||||
}
|
||||
|
||||
private void increaseInviteCodeUsedCount(KeyboardUserInviteCodes inviteCode) {
|
||||
inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
|
||||
inviteCodesService.updateById(inviteCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.yolo.keyborad.service.impl.user;
|
||||
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.dto.user.SendMailDTO;
|
||||
import com.yolo.keyborad.model.dto.user.VerifyCodeDTO;
|
||||
import com.yolo.keyborad.utils.RedisUtil;
|
||||
import com.yolo.keyborad.utils.SendMailUtils;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/2/27
|
||||
*/
|
||||
@Component
|
||||
public class UserMailVerificationHandler {
|
||||
|
||||
private static final String USER_CODE_PREFIX = "user:";
|
||||
private static final String LIMIT_MAIL_PREFIX = "limit_mail:";
|
||||
private static final int VERIFY_CODE_TTL_SECONDS = 600;
|
||||
private static final int LIMIT_TTL_SECONDS = 60;
|
||||
private static final int VERIFY_CODE_MIN = 100000;
|
||||
private static final int VERIFY_CODE_MAX = 999999;
|
||||
|
||||
private final RedisUtil redisUtil;
|
||||
private final SendMailUtils sendMailUtils;
|
||||
|
||||
public UserMailVerificationHandler(RedisUtil redisUtil, SendMailUtils sendMailUtils) {
|
||||
this.redisUtil = redisUtil;
|
||||
this.sendMailUtils = sendMailUtils;
|
||||
}
|
||||
|
||||
public void sendVerifyMail(SendMailDTO mailDTO) {
|
||||
String mailAddress = mailDTO.getMailAddress();
|
||||
ensureNotRateLimited(mailAddress);
|
||||
|
||||
String existCode = redisUtil.get(USER_CODE_PREFIX + mailAddress);
|
||||
if (existCode != null) {
|
||||
sendMailUtils.sendEmail(null, mailAddress, Integer.valueOf(existCode));
|
||||
return;
|
||||
}
|
||||
|
||||
int code = cn.hutool.core.util.RandomUtil.randomInt(VERIFY_CODE_MIN, VERIFY_CODE_MAX);
|
||||
redisUtil.setEx(USER_CODE_PREFIX + mailAddress, String.valueOf(code), VERIFY_CODE_TTL_SECONDS, TimeUnit.SECONDS);
|
||||
sendMailUtils.sendEmail(null, mailAddress, code);
|
||||
redisUtil.setEx(LIMIT_MAIL_PREFIX + mailAddress, String.valueOf(code), LIMIT_TTL_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public Boolean verifyMailCode(VerifyCodeDTO verifyCodeDTO) {
|
||||
String mailAddress = verifyCodeDTO.getMailAddress();
|
||||
String storedCode = redisUtil.get(USER_CODE_PREFIX + mailAddress);
|
||||
if (storedCode == null) {
|
||||
throw new BusinessException(ErrorCode.VERIFY_CODE_ERROR);
|
||||
}
|
||||
|
||||
if (!storedCode.equals(String.valueOf(verifyCodeDTO.getVerifyCode()))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
redisUtil.delete(USER_CODE_PREFIX + mailAddress);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ensureNotRateLimited(String mailAddress) {
|
||||
if (redisUtil.hasKey(LIMIT_MAIL_PREFIX + mailAddress)) {
|
||||
throw new BusinessException(ErrorCode.MAIL_SEND_BUSY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.yolo.keyborad.service.impl.user;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.mapper.KeyboardUserMapper;
|
||||
import com.yolo.keyborad.model.dto.user.ResetPassWordDTO;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/2/27
|
||||
*/
|
||||
@Component
|
||||
public class UserPasswordHandler {
|
||||
|
||||
private final KeyboardUserMapper keyboardUserMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public UserPasswordHandler(KeyboardUserMapper keyboardUserMapper, PasswordEncoder passwordEncoder) {
|
||||
this.keyboardUserMapper = keyboardUserMapper;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
public Boolean resetPassWord(ResetPassWordDTO resetPassWordDTO) {
|
||||
KeyboardUser keyboardUser = keyboardUserMapper.selectOne(new LambdaQueryWrapper<KeyboardUser>()
|
||||
.eq(KeyboardUser::getEmail, resetPassWordDTO.getMailAddress())
|
||||
.eq(KeyboardUser::getStatus, false));
|
||||
|
||||
if (keyboardUser == null) {
|
||||
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
|
||||
}
|
||||
if (!resetPassWordDTO.getPassword().equals(resetPassWordDTO.getConfirmPassword())) {
|
||||
throw new BusinessException(ErrorCode.CONFIRM_PASSWORD_NOT_MATCH);
|
||||
}
|
||||
|
||||
keyboardUser.setPassword(passwordEncoder.encode(resetPassWordDTO.getPassword()));
|
||||
return keyboardUserMapper.updateById(keyboardUser) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.yolo.keyborad.service.impl.user;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.config.AppConfig;
|
||||
import com.yolo.keyborad.config.NacosAppConfigCenter;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.mapper.KeyboardUserMapper;
|
||||
import com.yolo.keyborad.model.dto.user.UserRegisterDTO;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserQuotaTotal;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUserWallet;
|
||||
import com.yolo.keyborad.service.KeyboardCharacterService;
|
||||
import com.yolo.keyborad.service.KeyboardUserInviteCodesService;
|
||||
import com.yolo.keyborad.service.KeyboardUserQuotaTotalService;
|
||||
import com.yolo.keyborad.service.KeyboardUserWalletService;
|
||||
import com.yolo.keyborad.utils.RedisUtil;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/2/27
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class UserRegistrationHandler {
|
||||
|
||||
private static final String USER_CODE_PREFIX = "user:";
|
||||
|
||||
private final KeyboardUserMapper keyboardUserMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final RedisUtil redisUtil;
|
||||
private final KeyboardCharacterService keyboardCharacterService;
|
||||
private final KeyboardUserWalletService walletService;
|
||||
private final KeyboardUserQuotaTotalService quotaTotalService;
|
||||
private final KeyboardUserInviteCodesService inviteCodesService;
|
||||
private final UserInviteCodeBinder inviteCodeBinder;
|
||||
private final NacosAppConfigCenter.DynamicAppConfig cfgHolder;
|
||||
|
||||
public UserRegistrationHandler(
|
||||
KeyboardUserMapper keyboardUserMapper,
|
||||
PasswordEncoder passwordEncoder,
|
||||
RedisUtil redisUtil,
|
||||
KeyboardCharacterService keyboardCharacterService,
|
||||
KeyboardUserWalletService walletService,
|
||||
KeyboardUserQuotaTotalService quotaTotalService,
|
||||
KeyboardUserInviteCodesService inviteCodesService,
|
||||
UserInviteCodeBinder inviteCodeBinder,
|
||||
NacosAppConfigCenter.DynamicAppConfig cfgHolder
|
||||
) {
|
||||
this.keyboardUserMapper = keyboardUserMapper;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.redisUtil = redisUtil;
|
||||
this.keyboardCharacterService = keyboardCharacterService;
|
||||
this.walletService = walletService;
|
||||
this.quotaTotalService = quotaTotalService;
|
||||
this.inviteCodesService = inviteCodesService;
|
||||
this.inviteCodeBinder = inviteCodeBinder;
|
||||
this.cfgHolder = cfgHolder;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean userRegister(UserRegisterDTO userRegisterDTO) {
|
||||
ensureUserNotExists(userRegisterDTO.getMailAddress());
|
||||
validatePasswords(userRegisterDTO);
|
||||
verifyCode(userRegisterDTO);
|
||||
|
||||
KeyboardUser keyboardUser = buildNewUser(userRegisterDTO);
|
||||
int insertCount = keyboardUserMapper.insert(keyboardUser);
|
||||
if (insertCount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
initNewUserAssets(keyboardUser);
|
||||
tryBindInviteCode(userRegisterDTO.getInviteCode(), keyboardUser.getId());
|
||||
logRegisterSuccess(keyboardUser);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ensureUserNotExists(String mailAddress) {
|
||||
KeyboardUser userMail = keyboardUserMapper.selectOne(new LambdaQueryWrapper<KeyboardUser>()
|
||||
.eq(KeyboardUser::getEmail, mailAddress));
|
||||
if (userMail != null) {
|
||||
throw new BusinessException(ErrorCode.USER_HAS_EXISTED);
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePasswords(UserRegisterDTO userRegisterDTO) {
|
||||
if (userRegisterDTO.getPassword() == null) {
|
||||
throw new BusinessException(ErrorCode.PASSWORD_CAN_NOT_NULL);
|
||||
}
|
||||
if (!userRegisterDTO.getPassword().equals(userRegisterDTO.getPasswordConfirm())) {
|
||||
throw new BusinessException(ErrorCode.CONFIRM_PASSWORD_NOT_MATCH);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyCode(UserRegisterDTO userRegisterDTO) {
|
||||
String stored = redisUtil.get(USER_CODE_PREFIX + userRegisterDTO.getMailAddress());
|
||||
if (!userRegisterDTO.getVerifyCode().equals(stored)) {
|
||||
throw new BusinessException(ErrorCode.VERIFY_CODE_ERROR);
|
||||
}
|
||||
redisUtil.delete(USER_CODE_PREFIX + userRegisterDTO.getMailAddress());
|
||||
}
|
||||
|
||||
private KeyboardUser buildNewUser(UserRegisterDTO userRegisterDTO) {
|
||||
KeyboardUser keyboardUser = new KeyboardUser();
|
||||
keyboardUser.setUid(IdUtil.getSnowflake().nextId());
|
||||
keyboardUser.setNickName("User_" + RandomUtil.randomString(6));
|
||||
keyboardUser.setPassword(passwordEncoder.encode(userRegisterDTO.getPassword()));
|
||||
keyboardUser.setEmail(userRegisterDTO.getMailAddress());
|
||||
keyboardUser.setGender(userRegisterDTO.getGender());
|
||||
keyboardUser.setEmailVerified(true);
|
||||
return keyboardUser;
|
||||
}
|
||||
|
||||
private void initNewUserAssets(KeyboardUser keyboardUser) {
|
||||
keyboardCharacterService.addDefaultUserCharacter(keyboardUser.getId());
|
||||
|
||||
AppConfig appConfig = cfgHolder.getRef().get();
|
||||
initWallet(keyboardUser.getId(), appConfig.getUserRegisterProperties().getRewardBalance());
|
||||
initQuota(
|
||||
keyboardUser.getId(),
|
||||
appConfig.getUserRegisterProperties().getFreeTrialQuota()
|
||||
);
|
||||
|
||||
inviteCodesService.createInviteCode(keyboardUser.getId());
|
||||
}
|
||||
|
||||
private void initWallet(long userId, BigDecimal rewardBalance) {
|
||||
KeyboardUserWallet wallet = new KeyboardUserWallet();
|
||||
wallet.setUserId(userId);
|
||||
wallet.setBalance(rewardBalance);
|
||||
wallet.setVersion(0);
|
||||
wallet.setStatus((short) 1);
|
||||
wallet.setCreatedAt(new Date());
|
||||
wallet.setUpdatedAt(new Date());
|
||||
walletService.save(wallet);
|
||||
}
|
||||
|
||||
private void initQuota(long userId, Integer freeTrialQuota) {
|
||||
KeyboardUserQuotaTotal quotaTotal = new KeyboardUserQuotaTotal();
|
||||
quotaTotal.setUserId(userId);
|
||||
quotaTotal.setTotalQuota(freeTrialQuota);
|
||||
quotaTotal.setUsedQuota(0);
|
||||
quotaTotal.setVersion(0);
|
||||
quotaTotal.setCreatedAt(new Date());
|
||||
quotaTotal.setUpdatedAt(new Date());
|
||||
quotaTotalService.save(quotaTotal);
|
||||
}
|
||||
|
||||
private void tryBindInviteCode(String inviteCode, long userId) {
|
||||
if (inviteCode == null || inviteCode.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
inviteCodeBinder.bind(userId, inviteCode.trim());
|
||||
log.info("User bound to invite code on register, userId={}", userId);
|
||||
} catch (BusinessException e) {
|
||||
log.warn("Failed to bind invite code for user {}: {}", userId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void logRegisterSuccess(KeyboardUser keyboardUser) {
|
||||
AppConfig appConfig = cfgHolder.getRef().get();
|
||||
log.info("User registered with email, userId={}, email={}, freeQuota={}",
|
||||
keyboardUser.getId(), keyboardUser.getEmail(), appConfig.getUserRegisterProperties().getFreeTrialQuota());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user