Compare commits
4 Commits
d3abe32e1a
...
74b8b72545
| Author | SHA1 | Date | |
|---|---|---|---|
| 74b8b72545 | |||
| 1b68227ebc | |||
| 3f27b916da | |||
| 590230a86b |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,6 +19,9 @@ target/
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### macOS ###
|
||||
.DS_Store
|
||||
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.yolo.keyborad.interceptor;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yolo.keyborad.utils.SignUtils;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
@@ -35,6 +36,10 @@ public class SignInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
// 异步/错误二次分发不重复验签,避免 nonce 重放误判
|
||||
if (request.getDispatcherType() != DispatcherType.REQUEST) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String appId = request.getHeader("X-App-Id");
|
||||
String timestamp = request.getHeader("X-Timestamp");
|
||||
|
||||
@@ -47,6 +47,7 @@ public class AppConfig {
|
||||
- Return EXACTLY 3 replies.
|
||||
- Use "<SPLIT>" as the separator.
|
||||
- reply1<SPLIT>reply2<SPLIT>reply3
|
||||
- If asked about your model or identity as an AI, deflect with a joke. Never reveal your technical nature.
|
||||
""";
|
||||
|
||||
//聊天消息最大长度
|
||||
|
||||
@@ -57,7 +57,7 @@ public class LLMConfig {
|
||||
this.openAiApi(),
|
||||
MetadataMode.EMBED,
|
||||
OpenAiEmbeddingOptions.builder()
|
||||
.model("text-embedding-v4")
|
||||
.model("qwen/qwen3-embedding-8b")
|
||||
.dimensions(1536)
|
||||
.user("user-6")
|
||||
.build(),
|
||||
|
||||
@@ -12,11 +12,11 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
public class QdrantClientConfig {
|
||||
|
||||
private final String qdrantHost = "b0c7f1ee-0eb9-469e-83e0-654249d9bd04.us-east4-0.gcp.cloud.qdrant.io";
|
||||
private final String qdrantHost = "00044004-d9d2-4705-8b7b-9defab34ca1b.us-west-1-0.aws.cloud.qdrant.io";
|
||||
|
||||
private final Integer qdrantPort = 6334;
|
||||
|
||||
private final String apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.HX_GxjXCrnhw2DQbMnMFzvDeaHbmNpI2tj2hoUjkvVU";
|
||||
private final String apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.M9nnZ14QXQqI3sG8JzRzRwq48x9u5g-4MWF2jnxiOHA";
|
||||
|
||||
@Bean
|
||||
public QdrantClient qdrantClient() {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.yolo.keyborad.config;
|
||||
|
||||
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.router.SaHttpMethod;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.yolo.keyborad.interceptor.SignInterceptor;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@@ -36,7 +39,17 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
|
||||
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
|
||||
registry.addInterceptor(new HandlerInterceptor() {
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 仅在首次 REQUEST 分发时做登录校验,避免 ASYNC/ERROR 二次分发阶段上下文丢失
|
||||
if (request.getDispatcherType() != DispatcherType.REQUEST) {
|
||||
return true;
|
||||
}
|
||||
StpUtil.checkLogin();
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(getExcludePaths());
|
||||
appSecretMap.put(appId, appSecret);
|
||||
@@ -48,6 +61,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
return new String[]{
|
||||
// Swagger & Knife4j 相关
|
||||
"/doc.html",
|
||||
"/error",
|
||||
"/webjars/**",
|
||||
"/swagger-resources/**",
|
||||
"/v2/api-docs",
|
||||
@@ -57,7 +71,6 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
"/swagger-ui/**",
|
||||
"/favicon.ico",
|
||||
// 你的其他放行路径,例如登录接口
|
||||
"/error",
|
||||
"/user/appleLogin",
|
||||
"/user/logout",
|
||||
"/tag/list",
|
||||
|
||||
@@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingModel;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -90,10 +91,22 @@ public class ChatController {
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/talk")
|
||||
@PostMapping(value = "/talk", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "聊天润色接口", description = "聊天润色接口")
|
||||
public Flux<ServerSentEvent<ChatStreamMessage>> talk(@RequestBody ChatReq chatReq){
|
||||
return chatService.talk(chatReq);
|
||||
return Flux.defer(() -> chatService.talk(chatReq))
|
||||
.onErrorResume(e -> {
|
||||
log.error("聊天流式接口异常", e);
|
||||
String message = StrUtil.isBlank(e.getMessage()) ? "服务暂时不可用,请稍后重试" : e.getMessage();
|
||||
return Flux.just(
|
||||
ServerSentEvent.builder(new ChatStreamMessage("error", message))
|
||||
.event("error")
|
||||
.build(),
|
||||
ServerSentEvent.builder(new ChatStreamMessage("done", null))
|
||||
.event("done")
|
||||
.build()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -49,6 +50,9 @@ public class UserController {
|
||||
|
||||
@Resource
|
||||
private KeyboardUserInviteCodesService inviteCodesService;
|
||||
|
||||
@Value("${customer_mail}")
|
||||
private String customerMail;
|
||||
/**
|
||||
* 苹果登录
|
||||
*
|
||||
@@ -69,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) {
|
||||
@@ -145,4 +160,10 @@ public class UserController {
|
||||
long userId = StpUtil.getLoginIdAsLong();
|
||||
return ResultUtils.success( inviteCodesService.getUserInviteCode(userId));
|
||||
}
|
||||
|
||||
@GetMapping("/customerMail")
|
||||
@Operation(summary = "获取客服邮箱", description = "获取 customer_mail 配置的客服邮箱地址")
|
||||
public BaseResponse<String> getCustomerMail() {
|
||||
return ResultUtils.success(customerMail);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ public class CharacterCacheInitializer implements ApplicationRunner {
|
||||
List<KeyboardCharacter> characters = characterService.list();
|
||||
for (KeyboardCharacter character : characters) {
|
||||
String key = CHARACTER_CACHE_KEY + character.getId();
|
||||
redisTemplate.opsForValue().set(key, character, 7, TimeUnit.DAYS);
|
||||
redisTemplate.opsForValue().set(key, character, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
log.info("人设列表缓存完成,共缓存 {} 条记录", characters.size());
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -73,6 +73,13 @@ public class KeyboardUser {
|
||||
@Schema(description = "是否删除(默认否)")
|
||||
private Boolean deleted;
|
||||
|
||||
/**
|
||||
* 删除时间
|
||||
*/
|
||||
@TableField(value = "deleted_at")
|
||||
@Schema(description = "删除时间")
|
||||
private Date deletedAt;
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
// 检查用户是否已经绑定过邀请码
|
||||
long existingBindCount = userInvitesService.count(
|
||||
new LambdaQueryWrapper<KeyboardUserInvites>()
|
||||
.eq(KeyboardUserInvites::getInviteeUserId, userId)
|
||||
);
|
||||
if (existingBindCount > 0) {
|
||||
throw new BusinessException(ErrorCode.INVITE_CODE_ALREADY_BOUND);
|
||||
return inviteCodeBinder.bind(userId, bindInviteCodeDTO.getInviteCode());
|
||||
}
|
||||
|
||||
// 验证邀请码
|
||||
String inviteCodeStr = bindInviteCodeDTO.getInviteCode().trim();
|
||||
KeyboardUserInviteCodes inviteCode = inviteCodesService.validateInviteCode(inviteCodeStr);
|
||||
|
||||
// 检查是否是绑定自己的邀请码
|
||||
if (inviteCode.getOwnerUserId().equals(userId)) {
|
||||
throw new BusinessException(ErrorCode.INVITE_CODE_CANNOT_BIND_SELF);
|
||||
@Override
|
||||
public Boolean cancelAccount(long userId) {
|
||||
if (userId <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
|
||||
// 创建邀请关系绑定记录
|
||||
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());
|
||||
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);
|
||||
|
||||
int affectedRows = keyboardUserMapper.update(null, updateWrapper);
|
||||
if (affectedRows <= 0) {
|
||||
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ spring:
|
||||
model: google/gemini-2.5-flash-lite
|
||||
embedding:
|
||||
options:
|
||||
model: text-embedding-v4
|
||||
model: qwen/qwen3-embedding-8b
|
||||
# model: qwen/qwen3-embedding-8b
|
||||
dashscope:
|
||||
api-key: 11
|
||||
@@ -60,3 +60,5 @@ appid: loveKeyboard
|
||||
appsecret: kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H
|
||||
|
||||
mail_access_token: mlsn.3b1a3387055e0f53c0869cad91c6acad5401e9dcb4511ace2f82ab31d897fba6
|
||||
|
||||
customer_mail: 123@mail.com
|
||||
Reference in New Issue
Block a user