feat: 完善后端架构和service层实现
- 创建完整的entity实体类体系,包括所有业务实体 - 实现BaseEntity基类,统一管理公共字段 - 创建雪花算法ID生成器和自动填充处理器 - 简化所有mapper接口,只继承BaseMapper - 重构service层,使用LambdaQueryWrapper进行数据库操作 - 创建BasePageRequest分页查询基类 - 完善用户上下文管理和JWT认证 - 新增WebSocket聊天功能和相关控制器 - 更新前端配置和组件,完善用户认证流程 - 同步数据库建表脚本
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
package com.emotion;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 情感博物馆简化版启动类
|
||||
*
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-21
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.emotion.mapper")
|
||||
public class EmotionSimpleApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -21,7 +21,7 @@ public abstract class BaseEntity implements Serializable {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.emotion.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
/**
|
||||
* 分页查询基类请求参数
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
public class BasePageRequest {
|
||||
|
||||
/**
|
||||
* 当前页码,从1开始
|
||||
*/
|
||||
@Min(value = 1, message = "页码不能小于1")
|
||||
private Long current = 1L;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
@Min(value = 1, message = "每页大小不能小于1")
|
||||
@Max(value = 100, message = "每页大小不能超过100")
|
||||
private Long size = 10L;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private String orderBy;
|
||||
|
||||
/**
|
||||
* 排序方式:asc-升序,desc-降序
|
||||
*/
|
||||
private String orderDirection = "desc";
|
||||
|
||||
/**
|
||||
* 搜索关键词
|
||||
*/
|
||||
private String keyword;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
|
||||
/**
|
||||
* ID生成器配置类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class IdGeneratorConfig {
|
||||
|
||||
/**
|
||||
* 机器ID配置(可通过配置文件指定)
|
||||
*/
|
||||
@Value("${emotion.snowflake.machine-id:#{null}}")
|
||||
private Long machineId;
|
||||
|
||||
/**
|
||||
* 配置雪花算法ID生成器
|
||||
*
|
||||
* @return SnowflakeIdGenerator实例
|
||||
*/
|
||||
@Bean
|
||||
public SnowflakeIdGenerator snowflakeIdGenerator() {
|
||||
long finalMachineId;
|
||||
|
||||
if (machineId != null) {
|
||||
// 使用配置文件中指定的机器ID
|
||||
finalMachineId = machineId;
|
||||
log.info("使用配置文件指定的机器ID: {}", finalMachineId);
|
||||
} else {
|
||||
// 自动生成机器ID
|
||||
finalMachineId = generateMachineId();
|
||||
log.info("自动生成机器ID: {}", finalMachineId);
|
||||
}
|
||||
|
||||
return new SnowflakeIdGenerator(finalMachineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动生成机器ID
|
||||
* 基于MAC地址和IP地址生成唯一的机器ID
|
||||
*
|
||||
* @return 机器ID (0-1023)
|
||||
*/
|
||||
private long generateMachineId() {
|
||||
try {
|
||||
// 获取本机IP地址
|
||||
InetAddress localHost = InetAddress.getLocalHost();
|
||||
|
||||
// 获取网络接口
|
||||
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(localHost);
|
||||
|
||||
if (networkInterface != null) {
|
||||
// 获取MAC地址
|
||||
byte[] hardwareAddress = networkInterface.getHardwareAddress();
|
||||
|
||||
if (hardwareAddress != null && hardwareAddress.length >= 6) {
|
||||
// 使用MAC地址的后两个字节生成机器ID
|
||||
long machineId = ((hardwareAddress[4] & 0xFF) << 8) | (hardwareAddress[5] & 0xFF);
|
||||
// 确保机器ID在有效范围内 (0-1023)
|
||||
return machineId & 0x3FF;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法获取MAC地址,使用IP地址生成
|
||||
byte[] address = localHost.getAddress();
|
||||
if (address.length >= 4) {
|
||||
// 使用IP地址的后两个字节生成机器ID
|
||||
long machineId = ((address[2] & 0xFF) << 8) | (address[3] & 0xFF);
|
||||
return machineId & 0x3FF;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("自动生成机器ID失败,使用默认策略: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 如果所有方法都失败,使用当前时间戳生成
|
||||
return System.currentTimeMillis() % 1024;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus配置类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 分页插件配置
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 添加分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.interceptor.UserContextInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web配置类
|
||||
* 配置拦截器、跨域等
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private UserContextInterceptor userContextInterceptor;
|
||||
|
||||
/**
|
||||
* 添加拦截器
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(userContextInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/error",
|
||||
"/favicon.ico",
|
||||
"/actuator/**",
|
||||
"/swagger-ui/**",
|
||||
"/swagger-resources/**",
|
||||
"/v2/api-docs",
|
||||
"/v3/api-docs",
|
||||
"/webjars/**",
|
||||
"/doc.html",
|
||||
"/static/**",
|
||||
"/public/**"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*/
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.interceptor.JwtAuthInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC配置
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private JwtAuthInterceptor jwtAuthInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(jwtAuthInterceptor)
|
||||
.addPathPatterns("/api/**") // 拦截所有API请求
|
||||
.excludePathPatterns(
|
||||
"/api/auth/login", // 登录接口
|
||||
"/api/auth/register", // 注册接口
|
||||
"/api/auth/captcha", // 验证码接口
|
||||
"/api/auth/refresh-token", // 刷新token接口
|
||||
"/api/health", // 健康检查接口
|
||||
"/api/ws/**", // WebSocket接口
|
||||
"/swagger-ui/**", // Swagger UI
|
||||
"/v3/api-docs/**", // API文档
|
||||
"/actuator/**" // 监控端点
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.service.IAiService;
|
||||
import com.emotion.service.IMessageService;
|
||||
import com.emotion.service.IConversationService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI聊天控制器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/ai")
|
||||
public class AiChatController {
|
||||
|
||||
@Autowired
|
||||
private IAiService aiService;
|
||||
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private IConversationService conversationService;
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
*/
|
||||
@PostMapping("/chat")
|
||||
public Result<Map<String, Object>> sendChatMessage(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String conversationId = request.get("conversationId");
|
||||
String message = request.get("message");
|
||||
String userId = request.get("userId");
|
||||
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
return Result.error("消息内容不能为空");
|
||||
}
|
||||
|
||||
if (userId == null || userId.trim().isEmpty()) {
|
||||
userId = "guest_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
log.info("收到AI聊天请求: conversationId={}, userId={}, message={}",
|
||||
conversationId, userId, message);
|
||||
|
||||
// 调用AI服务
|
||||
String aiReply = aiService.sendChatMessage(conversationId, message, userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("conversationId", conversationId);
|
||||
response.put("userMessage", message);
|
||||
response.put("aiReply", aiReply);
|
||||
response.put("userId", userId);
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return Result.success(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI聊天请求处理失败", e);
|
||||
return Result.error("AI聊天服务暂时不可用,请稍后再试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成对话总结
|
||||
*/
|
||||
@PostMapping("/summary")
|
||||
public Result<Map<String, Object>> generateSummary(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String conversationId = request.get("conversationId");
|
||||
String userId = request.get("userId");
|
||||
|
||||
if (conversationId == null || conversationId.trim().isEmpty()) {
|
||||
return Result.error("会话ID不能为空");
|
||||
}
|
||||
|
||||
if (userId == null || userId.trim().isEmpty()) {
|
||||
userId = "guest_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
log.info("收到对话总结请求: conversationId={}, userId={}", conversationId, userId);
|
||||
|
||||
// 调用AI总结服务
|
||||
String summary = aiService.generateConversationSummary(conversationId, userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("conversationId", conversationId);
|
||||
response.put("summary", summary);
|
||||
response.put("userId", userId);
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return Result.success(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("对话总结请求处理失败", e);
|
||||
return Result.error("对话总结服务暂时不可用,请稍后再试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AI服务状态
|
||||
*/
|
||||
@GetMapping("/status")
|
||||
public Result<Map<String, Object>> getServiceStatus() {
|
||||
try {
|
||||
boolean available = aiService.isServiceAvailable();
|
||||
String status = aiService.getServiceStatus();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("available", available);
|
||||
response.put("status", status);
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return Result.success(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取AI服务状态失败", e);
|
||||
return Result.error("无法获取AI服务状态");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天记录统计
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
public Result<Map<String, Object>> getChatStats(@RequestParam(required = false) String userId,
|
||||
@RequestParam(required = false) String conversationId) {
|
||||
try {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
if (userId != null && !userId.trim().isEmpty()) {
|
||||
Long userConversationCount = conversationService.countByUserId(userId);
|
||||
Long activeConversationCount = conversationService.countActiveByUserId(userId);
|
||||
|
||||
stats.put("userConversationCount", userConversationCount);
|
||||
stats.put("activeConversationCount", activeConversationCount);
|
||||
}
|
||||
|
||||
if (conversationId != null && !conversationId.trim().isEmpty()) {
|
||||
Long conversationMessageCount = messageService.countByConversationId(conversationId);
|
||||
stats.put("conversationMessageCount", conversationMessageCount);
|
||||
}
|
||||
|
||||
stats.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return Result.success(stats);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取聊天统计失败", e);
|
||||
return Result.error("无法获取聊天统计信息");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,11 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import com.wf.captcha.SpecCaptcha;
|
||||
import com.wf.captcha.base.Captcha;
|
||||
import com.emotion.util.JwtUtil;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
@@ -24,9 +29,15 @@ public class AuthController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AuthController.class);
|
||||
|
||||
// 验证码存储(生产环境应使用Redis)
|
||||
private static final Map<String, String> captchaStore = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@@ -37,10 +48,30 @@ public class AuthController {
|
||||
try {
|
||||
String account = request.get("account");
|
||||
String password = request.get("password");
|
||||
|
||||
String captcha = request.get("captcha");
|
||||
String captchaKey = request.get("captchaKey");
|
||||
|
||||
if (account == null || password == null) {
|
||||
return Result.error("账号和密码不能为空");
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
if (captcha == null || captchaKey == null) {
|
||||
return Result.error("验证码不能为空");
|
||||
}
|
||||
|
||||
String storedCaptcha = captchaStore.get(captchaKey);
|
||||
if (storedCaptcha == null) {
|
||||
return Result.error("验证码已过期");
|
||||
}
|
||||
|
||||
if (!storedCaptcha.equals(captcha.toLowerCase())) {
|
||||
captchaStore.remove(captchaKey); // 验证失败后移除验证码
|
||||
return Result.error("验证码错误");
|
||||
}
|
||||
|
||||
// 验证成功后移除验证码
|
||||
captchaStore.remove(captchaKey);
|
||||
|
||||
// 查找用户
|
||||
User user = userService.findByAccount(account);
|
||||
@@ -56,9 +87,14 @@ public class AuthController {
|
||||
// 更新最后活跃时间
|
||||
userService.updateLastActiveTime(user.getId());
|
||||
|
||||
// 生成JWT token
|
||||
String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());
|
||||
|
||||
// 构建响应
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("accessToken", "token-" + user.getId() + "-" + System.currentTimeMillis());
|
||||
response.put("accessToken", accessToken);
|
||||
response.put("refreshToken", refreshToken);
|
||||
response.put("expiresIn", 86400L);
|
||||
|
||||
Map<String, Object> userInfo = new HashMap<>();
|
||||
@@ -85,7 +121,7 @@ public class AuthController {
|
||||
@PostMapping("/register")
|
||||
public Result<Map<String, Object>> register(@RequestBody Map<String, String> request) {
|
||||
log.info("用户注册请求: {}", request.get("account"));
|
||||
|
||||
|
||||
try {
|
||||
String account = request.get("account");
|
||||
String password = request.get("password");
|
||||
@@ -93,11 +129,31 @@ public class AuthController {
|
||||
String email = request.get("email");
|
||||
String phone = request.get("phone");
|
||||
String nickname = request.get("nickname");
|
||||
|
||||
String captcha = request.get("captcha");
|
||||
String captchaKey = request.get("captchaKey");
|
||||
|
||||
if (account == null || password == null) {
|
||||
return Result.error("账号和密码不能为空");
|
||||
}
|
||||
|
||||
|
||||
// 验证验证码
|
||||
if (captcha == null || captchaKey == null) {
|
||||
return Result.error("验证码不能为空");
|
||||
}
|
||||
|
||||
String storedCaptcha = captchaStore.get(captchaKey);
|
||||
if (storedCaptcha == null) {
|
||||
return Result.error("验证码已过期");
|
||||
}
|
||||
|
||||
if (!storedCaptcha.equals(captcha.toLowerCase())) {
|
||||
captchaStore.remove(captchaKey); // 验证失败后移除验证码
|
||||
return Result.error("验证码错误");
|
||||
}
|
||||
|
||||
// 验证成功后移除验证码
|
||||
captchaStore.remove(captchaKey);
|
||||
|
||||
// 检查账号是否已存在
|
||||
if (userService.accountExists(account)) {
|
||||
return Result.error("账号已存在");
|
||||
@@ -113,37 +169,112 @@ public class AuthController {
|
||||
user.setNickname(nickname != null ? nickname : username != null ? username : account);
|
||||
|
||||
User createdUser = userService.createUser(user);
|
||||
|
||||
// 构建响应
|
||||
|
||||
// 生成JWT token(注册成功后自动登录)
|
||||
String accessToken = jwtUtil.generateToken(createdUser.getId(), createdUser.getUsername());
|
||||
String refreshToken = jwtUtil.generateRefreshToken(createdUser.getId(), createdUser.getUsername());
|
||||
|
||||
// 构建用户信息
|
||||
Map<String, Object> userInfo = new HashMap<>();
|
||||
userInfo.put("id", createdUser.getId());
|
||||
userInfo.put("username", createdUser.getUsername());
|
||||
userInfo.put("account", createdUser.getAccount());
|
||||
userInfo.put("nickname", createdUser.getNickname());
|
||||
userInfo.put("avatar", createdUser.getAvatar());
|
||||
userInfo.put("status", createdUser.getStatus());
|
||||
userInfo.put("createTime", createdUser.getCreateTime());
|
||||
|
||||
return Result.success("注册成功", userInfo);
|
||||
|
||||
// 构建完整响应(包含token信息)
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("accessToken", accessToken);
|
||||
response.put("refreshToken", refreshToken);
|
||||
response.put("expiresIn", 86400L); // 24小时
|
||||
response.put("userInfo", userInfo);
|
||||
response.put("loginTime", LocalDateTime.now());
|
||||
|
||||
log.info("用户注册并自动登录成功: {}", createdUser.getAccount());
|
||||
return Result.success("注册成功,已自动登录", response);
|
||||
} catch (Exception e) {
|
||||
log.error("用户注册失败: {}", e.getMessage());
|
||||
return Result.error("注册失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/user-info")
|
||||
public Result<Map<String, Object>> getCurrentUserInfo(HttpServletRequest request) {
|
||||
try {
|
||||
// 从请求属性中获取用户信息(由JWT拦截器设置)
|
||||
String userId = (String) request.getAttribute("userId");
|
||||
String username = (String) request.getAttribute("username");
|
||||
|
||||
if (userId == null) {
|
||||
return Result.error("用户未登录");
|
||||
}
|
||||
|
||||
// 根据用户ID获取完整用户信息
|
||||
User user = userService.findById(userId);
|
||||
if (user == null) {
|
||||
return Result.error("用户不存在");
|
||||
}
|
||||
|
||||
// 构建用户信息响应
|
||||
Map<String, Object> userInfo = new HashMap<>();
|
||||
userInfo.put("id", user.getId());
|
||||
userInfo.put("username", user.getUsername());
|
||||
userInfo.put("account", user.getAccount());
|
||||
userInfo.put("nickname", user.getNickname());
|
||||
userInfo.put("avatar", user.getAvatar());
|
||||
userInfo.put("email", user.getEmail());
|
||||
userInfo.put("phone", user.getPhone());
|
||||
userInfo.put("status", user.getStatus());
|
||||
userInfo.put("createTime", user.getCreateTime());
|
||||
|
||||
return Result.success("获取用户信息成功", userInfo);
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户信息失败: {}", e.getMessage());
|
||||
return Result.error("获取用户信息失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
@GetMapping("/captcha")
|
||||
public Result<Map<String, Object>> getCaptcha() {
|
||||
log.info("获取验证码请求");
|
||||
|
||||
|
||||
try {
|
||||
// 生成验证码
|
||||
SpecCaptcha captcha = new SpecCaptcha(130, 48, 4);
|
||||
captcha.setCharType(Captcha.TYPE_DEFAULT);
|
||||
|
||||
// 生成验证码key
|
||||
String captchaKey = "captcha_" + System.currentTimeMillis();
|
||||
String captchaText = captcha.text().toLowerCase();
|
||||
|
||||
// 存储验证码(5分钟过期)
|
||||
captchaStore.put(captchaKey, captchaText);
|
||||
|
||||
// 5分钟后清理验证码
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(300000); // 5分钟
|
||||
captchaStore.remove(captchaKey);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}).start();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("captchaId", "captcha-" + System.currentTimeMillis());
|
||||
response.put("captchaImage", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==");
|
||||
response.put("type", "spec");
|
||||
response.put("key", captchaKey);
|
||||
response.put("image", captcha.toBase64().replace("data:image/png;base64,", ""));
|
||||
response.put("expireTime", 300);
|
||||
|
||||
|
||||
log.info("生成验证码成功,key: {}, text: {}", captchaKey, captchaText);
|
||||
|
||||
return Result.success("获取验证码成功", response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取验证码失败: {}", e.getMessage());
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.dto.websocket.ChatRequest;
|
||||
import com.emotion.dto.websocket.ConnectRequest;
|
||||
import com.emotion.service.WebSocketService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* 优化的WebSocket聊天控制器
|
||||
* 使用规范的请求对象封装参数
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
public class ChatWebSocketController {
|
||||
|
||||
@Autowired
|
||||
private WebSocketService webSocketService;
|
||||
|
||||
/**
|
||||
* 处理聊天消息
|
||||
* @param chatRequest 聊天请求对象
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@MessageMapping("/chat.send")
|
||||
public void handleChatMessage(@Valid @Payload ChatRequest chatRequest,
|
||||
SimpMessageHeaderAccessor headerAccessor,
|
||||
Principal principal) {
|
||||
try {
|
||||
log.info("收到WebSocket聊天消息: {}", chatRequest);
|
||||
|
||||
// 获取会话ID
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
|
||||
// 如果请求中没有发送者ID,尝试从Principal获取
|
||||
if (chatRequest.getSenderId() == null && principal != null) {
|
||||
chatRequest.setSenderId(principal.getName());
|
||||
}
|
||||
|
||||
// 如果还是没有发送者ID,使用会话ID作为访客ID
|
||||
if (chatRequest.getSenderId() == null) {
|
||||
chatRequest.setSenderId("guest_" + sessionId);
|
||||
chatRequest.setSenderType(ChatRequest.SenderType.GUEST);
|
||||
}
|
||||
|
||||
// 设置时间戳
|
||||
if (chatRequest.getTimestamp() == null) {
|
||||
chatRequest.setTimestamp(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// 处理聊天消息
|
||||
webSocketService.handleChatMessage(chatRequest, sessionId, principal);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理WebSocket聊天消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户连接
|
||||
* @param connectRequest 连接请求对象
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@MessageMapping("/chat.connect")
|
||||
public void connectUser(@Payload ConnectRequest connectRequest,
|
||||
SimpMessageHeaderAccessor headerAccessor,
|
||||
Principal principal) {
|
||||
try {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
log.info("用户连接WebSocket: connectRequest={}, sessionId={}, principal={}",
|
||||
connectRequest, sessionId, principal);
|
||||
|
||||
// 如果请求中没有用户ID,尝试从Principal获取
|
||||
if (connectRequest.getUserId() == null && principal != null) {
|
||||
connectRequest.setUserId(principal.getName());
|
||||
}
|
||||
|
||||
// 设置连接时间戳
|
||||
if (connectRequest.getTimestamp() == null) {
|
||||
connectRequest.setTimestamp(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// 处理用户连接
|
||||
webSocketService.handleUserConnect(connectRequest, sessionId, principal);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户WebSocket连接失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户断开连接
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@MessageMapping("/chat.disconnect")
|
||||
public void disconnectUser(SimpMessageHeaderAccessor headerAccessor, Principal principal) {
|
||||
try {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
log.info("用户断开WebSocket连接: sessionId={}, principal={}", sessionId, principal);
|
||||
|
||||
// 处理用户断开连接
|
||||
webSocketService.handleUserDisconnect(sessionId, principal);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户WebSocket断开连接失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心跳消息
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@MessageMapping("/chat.heartbeat")
|
||||
public void heartbeat(SimpMessageHeaderAccessor headerAccessor, Principal principal) {
|
||||
try {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
|
||||
// 处理心跳消息
|
||||
webSocketService.handleHeartbeat(sessionId, principal);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理WebSocket心跳失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.entity.Message;
|
||||
import com.emotion.service.IMessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 消息控制器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/message")
|
||||
public class MessageController {
|
||||
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
/**
|
||||
* 保存消息
|
||||
*/
|
||||
@PostMapping("/save")
|
||||
public Result<Message> saveMessage(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String conversationId = request.get("conversationId");
|
||||
String content = request.get("content");
|
||||
String type = request.get("type");
|
||||
String sender = request.get("sender");
|
||||
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
return Result.error("消息内容不能为空");
|
||||
}
|
||||
|
||||
if (sender == null || sender.trim().isEmpty()) {
|
||||
return Result.error("发送者不能为空");
|
||||
}
|
||||
|
||||
Message message = messageService.saveMessage(conversationId, content, type, sender);
|
||||
if (message != null) {
|
||||
return Result.success(message);
|
||||
} else {
|
||||
return Result.error("保存消息失败");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("保存消息失败", e);
|
||||
return Result.error("保存消息失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID分页查询消息
|
||||
*/
|
||||
@GetMapping("/conversation/{conversationId}")
|
||||
public Result<IPage<Message>> getMessagesByConversationId(
|
||||
@PathVariable String conversationId,
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size) {
|
||||
try {
|
||||
Page<Message> page = new Page<>(current, size);
|
||||
IPage<Message> result = messageService.getByConversationId(page, conversationId);
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("查询会话消息失败", e);
|
||||
return Result.error("查询会话消息失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据发送者分页查询消息
|
||||
*/
|
||||
@GetMapping("/sender/{sender}")
|
||||
public Result<IPage<Message>> getMessagesBySender(
|
||||
@PathVariable String sender,
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size) {
|
||||
try {
|
||||
Page<Message> page = new Page<>(current, size);
|
||||
IPage<Message> result = messageService.getBySender(page, sender);
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("查询发送者消息失败", e);
|
||||
return Result.error("查询发送者消息失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询会话的最后一条消息
|
||||
*/
|
||||
@GetMapping("/last/{conversationId}")
|
||||
public Result<Message> getLastMessage(@PathVariable String conversationId) {
|
||||
try {
|
||||
Message message = messageService.getLastMessageByConversationId(conversationId);
|
||||
return Result.success(message);
|
||||
} catch (Exception e) {
|
||||
log.error("查询最后一条消息失败", e);
|
||||
return Result.error("查询最后一条消息失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计会话消息数量
|
||||
*/
|
||||
@GetMapping("/count/{conversationId}")
|
||||
public Result<Long> countMessages(@PathVariable String conversationId) {
|
||||
try {
|
||||
Long count = messageService.countByConversationId(conversationId);
|
||||
return Result.success(count);
|
||||
} catch (Exception e) {
|
||||
log.error("统计消息数量失败", e);
|
||||
return Result.error("统计消息数量失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息状态
|
||||
*/
|
||||
@PutMapping("/{messageId}/status")
|
||||
public Result<Boolean> updateStatus(@PathVariable String messageId,
|
||||
@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String status = request.get("status");
|
||||
if (status == null || status.trim().isEmpty()) {
|
||||
return Result.error("状态不能为空");
|
||||
}
|
||||
|
||||
boolean success = messageService.updateStatus(messageId, status);
|
||||
return Result.success(success);
|
||||
} catch (Exception e) {
|
||||
log.error("更新消息状态失败", e);
|
||||
return Result.error("更新消息状态失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
*/
|
||||
@PutMapping("/{messageId}/read")
|
||||
public Result<Boolean> markAsRead(@PathVariable String messageId) {
|
||||
try {
|
||||
boolean success = messageService.updateReadStatus(messageId, 1);
|
||||
return Result.success(success);
|
||||
} catch (Exception e) {
|
||||
log.error("标记消息已读失败", e);
|
||||
return Result.error("标记消息已读失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量标记会话消息为已读
|
||||
*/
|
||||
@PutMapping("/conversation/{conversationId}/read")
|
||||
public Result<Boolean> markConversationAsRead(@PathVariable String conversationId) {
|
||||
try {
|
||||
boolean success = messageService.markConversationMessagesAsRead(conversationId);
|
||||
return Result.success(success);
|
||||
} catch (Exception e) {
|
||||
log.error("批量标记消息已读失败", e);
|
||||
return Result.error("批量标记消息已读失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息统计信息
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
public Result<Map<String, Object>> getMessageStats(
|
||||
@RequestParam(required = false) String conversationId,
|
||||
@RequestParam(required = false) String sender) {
|
||||
try {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
if (conversationId != null && !conversationId.trim().isEmpty()) {
|
||||
Long conversationCount = messageService.countByConversationId(conversationId);
|
||||
stats.put("conversationMessageCount", conversationCount);
|
||||
|
||||
Message lastMessage = messageService.getLastMessageByConversationId(conversationId);
|
||||
stats.put("lastMessage", lastMessage);
|
||||
}
|
||||
|
||||
if (sender != null && !sender.trim().isEmpty()) {
|
||||
Long senderCount = messageService.countBySender(sender);
|
||||
stats.put("senderMessageCount", senderCount);
|
||||
}
|
||||
|
||||
stats.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return Result.success(stats);
|
||||
} catch (Exception e) {
|
||||
log.error("获取消息统计失败", e);
|
||||
return Result.error("获取消息统计失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话的所有消息
|
||||
*/
|
||||
@DeleteMapping("/conversation/{conversationId}")
|
||||
public Result<Boolean> deleteConversationMessages(@PathVariable String conversationId) {
|
||||
try {
|
||||
boolean success = messageService.deleteByConversationId(conversationId);
|
||||
return Result.success(success);
|
||||
} catch (Exception e) {
|
||||
log.error("删除会话消息失败", e);
|
||||
return Result.error("删除会话消息失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.entity.SimpleUser;
|
||||
import com.emotion.entity.User;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -10,6 +10,8 @@ import org.springframework.messaging.handler.annotation.SendTo;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -31,59 +33,9 @@ public class WebSocketController {
|
||||
@Autowired
|
||||
private AiService aiService;
|
||||
|
||||
/**
|
||||
* 处理聊天消息
|
||||
*/
|
||||
@MessageMapping("/chat.send")
|
||||
@SendTo("/topic/public")
|
||||
public Map<String, Object> sendMessage(@Payload Map<String, Object> chatMessage) {
|
||||
log.info("收到WebSocket消息: {}", chatMessage);
|
||||
|
||||
try {
|
||||
String content = (String) chatMessage.get("content");
|
||||
String sender = (String) chatMessage.get("sender");
|
||||
String type = (String) chatMessage.get("type");
|
||||
|
||||
// 构建响应消息
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", content);
|
||||
response.put("sender", sender);
|
||||
response.put("type", type);
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("处理WebSocket消息失败", e);
|
||||
Map<String, Object> errorResponse = new HashMap<>();
|
||||
errorResponse.put("content", "消息处理失败");
|
||||
errorResponse.put("type", "ERROR");
|
||||
errorResponse.put("timestamp", System.currentTimeMillis());
|
||||
return errorResponse;
|
||||
}
|
||||
}
|
||||
// 已移除旧的WebSocket消息处理方法,使用新的ChatWebSocketController
|
||||
|
||||
/**
|
||||
* 处理用户连接
|
||||
*/
|
||||
@MessageMapping("/chat.connect")
|
||||
@SendTo("/topic/public")
|
||||
public Map<String, Object> connectUser(@Payload Map<String, Object> chatMessage,
|
||||
SimpMessageHeaderAccessor headerAccessor) {
|
||||
String username = (String) chatMessage.get("sender");
|
||||
|
||||
// 在WebSocket会话中添加用户名
|
||||
headerAccessor.getSessionAttributes().put("username", username);
|
||||
|
||||
log.info("用户连接: {}", username);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", username + " 加入了聊天");
|
||||
response.put("type", "JOIN");
|
||||
response.put("sender", "System");
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return response;
|
||||
}
|
||||
// 已移除旧的用户连接处理方法,使用新的ChatWebSocketController
|
||||
|
||||
/**
|
||||
* 处理AI聊天消息
|
||||
@@ -142,7 +94,22 @@ public class WebSocketController {
|
||||
systemMessage.put("sender", "System");
|
||||
systemMessage.put("type", "SYSTEM");
|
||||
systemMessage.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
|
||||
messagingTemplate.convertAndSend(destination, systemMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket状态监控接口
|
||||
*/
|
||||
@GetMapping("/api/ws/status")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getWebSocketStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
status.put("status", "active");
|
||||
status.put("timestamp", System.currentTimeMillis());
|
||||
status.put("endpoint", "/ws/chat");
|
||||
status.put("protocols", new String[]{"websocket", "sockjs"});
|
||||
status.put("message", "WebSocket服务正常运行");
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.emotion.dto.websocket;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* WebSocket聊天请求对象
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChatRequest {
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@NotBlank(message = "消息内容不能为空")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 发送者ID
|
||||
*/
|
||||
@NotBlank(message = "发送者ID不能为空")
|
||||
private String senderId;
|
||||
|
||||
/**
|
||||
* 发送者类型
|
||||
*/
|
||||
@NotNull(message = "发送者类型不能为空")
|
||||
private SenderType senderType;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
@NotNull(message = "消息类型不能为空")
|
||||
private MessageType messageType;
|
||||
|
||||
/**
|
||||
* 会话ID(可选)
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 发送时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 发送者类型枚举
|
||||
*/
|
||||
public enum SenderType {
|
||||
USER("用户"),
|
||||
GUEST("访客"),
|
||||
AI("AI助手"),
|
||||
SYSTEM("系统");
|
||||
|
||||
private final String description;
|
||||
|
||||
SenderType(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息类型枚举
|
||||
*/
|
||||
public enum MessageType {
|
||||
TEXT("文本消息"),
|
||||
IMAGE("图片消息"),
|
||||
FILE("文件消息"),
|
||||
SYSTEM("系统消息"),
|
||||
HEARTBEAT("心跳消息");
|
||||
|
||||
private final String description;
|
||||
|
||||
MessageType(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.emotion.dto.websocket;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* WebSocket连接请求对象
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ConnectRequest {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 客户端类型
|
||||
*/
|
||||
private String clientType;
|
||||
|
||||
/**
|
||||
* 客户端版本
|
||||
*/
|
||||
private String clientVersion;
|
||||
|
||||
/**
|
||||
* 连接时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.emotion.dto.websocket;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* WebSocket消息对象
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WebSocketMessage {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
private MessageType type;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 发送者ID
|
||||
*/
|
||||
private String senderId;
|
||||
|
||||
/**
|
||||
* 发送者类型
|
||||
*/
|
||||
private SenderType senderType;
|
||||
|
||||
/**
|
||||
* 消息状态
|
||||
*/
|
||||
private MessageStatus status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 扩展数据
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 消息类型枚举
|
||||
*/
|
||||
public enum MessageType {
|
||||
TEXT("文本消息"),
|
||||
TYPING("正在输入"),
|
||||
SYSTEM("系统消息"),
|
||||
ERROR("错误消息"),
|
||||
HEARTBEAT("心跳消息"),
|
||||
CONNECTION("连接消息"),
|
||||
AI_THINKING("AI思考中");
|
||||
|
||||
private final String description;
|
||||
|
||||
MessageType(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送者类型枚举
|
||||
*/
|
||||
public enum SenderType {
|
||||
USER("用户"),
|
||||
GUEST("访客"),
|
||||
AI("AI助手"),
|
||||
SYSTEM("系统");
|
||||
|
||||
private final String description;
|
||||
|
||||
SenderType(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息状态枚举
|
||||
*/
|
||||
public enum MessageStatus {
|
||||
SENDING("发送中"),
|
||||
SENT("已发送"),
|
||||
DELIVERED("已送达"),
|
||||
READ("已读"),
|
||||
FAILED("发送失败");
|
||||
|
||||
private final String description;
|
||||
|
||||
MessageStatus(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 成就实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("achievement")
|
||||
public class Achievement extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 成就标题
|
||||
*/
|
||||
@TableField("title")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 分类
|
||||
*/
|
||||
@TableField("category")
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
@TableField("icon")
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 稀有度
|
||||
*/
|
||||
@TableField("rarity")
|
||||
private String rarity;
|
||||
|
||||
/**
|
||||
* 条件类型
|
||||
*/
|
||||
@TableField("condition_type")
|
||||
private String conditionType;
|
||||
|
||||
/**
|
||||
* 条件值
|
||||
*/
|
||||
@TableField("condition_value")
|
||||
private String conditionValue;
|
||||
|
||||
/**
|
||||
* 奖励
|
||||
*/
|
||||
@TableField("rewards")
|
||||
private String rewards;
|
||||
|
||||
/**
|
||||
* 解锁时间
|
||||
*/
|
||||
@TableField("unlocked_time")
|
||||
private LocalDateTime unlockedTime;
|
||||
|
||||
/**
|
||||
* 进度
|
||||
*/
|
||||
@TableField("progress")
|
||||
private BigDecimal progress;
|
||||
|
||||
/**
|
||||
* 是否隐藏
|
||||
*/
|
||||
@TableField("is_hidden")
|
||||
private Integer isHidden;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 评论实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("comment")
|
||||
public class Comment extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 帖子ID
|
||||
*/
|
||||
@TableField("post_id")
|
||||
private String postId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 评论内容
|
||||
*/
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 回复的评论ID
|
||||
*/
|
||||
@TableField("reply_to_id")
|
||||
private String replyToId;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
@TableField("likes")
|
||||
private Integer likes;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 社区帖子实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("community_post")
|
||||
public class CommunityPost extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 地点ID
|
||||
*/
|
||||
@TableField("location_id")
|
||||
private String locationId;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
@TableField("title")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 内容
|
||||
*/
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 帖子类型
|
||||
*/
|
||||
@TableField("type")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 图片列表
|
||||
*/
|
||||
@TableField("images")
|
||||
private String images;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
@TableField("tags")
|
||||
private String tags;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
@TableField("likes")
|
||||
private Integer likes;
|
||||
|
||||
/**
|
||||
* 浏览数
|
||||
*/
|
||||
@TableField("view_count")
|
||||
private Integer viewCount;
|
||||
|
||||
/**
|
||||
* 评论数
|
||||
*/
|
||||
@TableField("comment_count")
|
||||
private Integer commentCount;
|
||||
|
||||
/**
|
||||
* 是否私密
|
||||
*/
|
||||
@TableField("is_private")
|
||||
private Integer isPrivate;
|
||||
}
|
||||
@@ -1,91 +1,191 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 对话实体
|
||||
*
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-22
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public class Conversation {
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("conversation")
|
||||
public class Conversation extends BaseEntity {
|
||||
|
||||
private String id;
|
||||
/**
|
||||
* 用户ID (关联user.id)
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户类型: registered-注册用户, guest-访客用户
|
||||
*/
|
||||
@TableField("user_type")
|
||||
private String userType;
|
||||
|
||||
/**
|
||||
* 对话标题
|
||||
*/
|
||||
@TableField("title")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 对话类型
|
||||
*/
|
||||
@TableField("type")
|
||||
private String type;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime endTime;
|
||||
private Integer messageCount;
|
||||
private Integer status;
|
||||
private String clientIp;
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 状态: active-活跃, ended-结束, archived-归档
|
||||
*/
|
||||
@TableField("status")
|
||||
private String conversationStatus;
|
||||
|
||||
/**
|
||||
* Coze对话ID
|
||||
*/
|
||||
@TableField("coze_conversation_id")
|
||||
private String cozeConversationId;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
private String createBy;
|
||||
private String updateBy;
|
||||
private Integer isDeleted;
|
||||
|
||||
/**
|
||||
* 使用的Bot ID
|
||||
*/
|
||||
@TableField("bot_id")
|
||||
private String botId;
|
||||
|
||||
/**
|
||||
* 使用的Workflow ID
|
||||
*/
|
||||
@TableField("workflow_id")
|
||||
private String workflowId;
|
||||
|
||||
/**
|
||||
* 初始消息
|
||||
*/
|
||||
@TableField("initial_message")
|
||||
private String initialMessage;
|
||||
|
||||
/**
|
||||
* 上下文信息
|
||||
*/
|
||||
@TableField("context")
|
||||
private String context;
|
||||
|
||||
/**
|
||||
* 主要情绪
|
||||
*/
|
||||
@TableField("primary_emotion")
|
||||
private String primaryEmotion;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
@TableField("emotion_intensity")
|
||||
private BigDecimal emotionIntensity;
|
||||
|
||||
/**
|
||||
* 情绪趋势
|
||||
*/
|
||||
@TableField("emotion_trend")
|
||||
private String emotionTrend;
|
||||
|
||||
/**
|
||||
* 关键词
|
||||
*/
|
||||
@TableField("keywords")
|
||||
private String keywords;
|
||||
|
||||
/**
|
||||
* AI洞察
|
||||
*/
|
||||
@TableField("ai_insights")
|
||||
private String aiInsights;
|
||||
|
||||
/**
|
||||
* 分析置信度
|
||||
*/
|
||||
@TableField("confidence")
|
||||
private BigDecimal confidence;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
@TableField("end_time")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
@TableField("last_active_time")
|
||||
private LocalDateTime lastActiveTime;
|
||||
|
||||
/**
|
||||
* 消息数量
|
||||
*/
|
||||
@TableField("message_count")
|
||||
private Integer messageCount;
|
||||
|
||||
/**
|
||||
* 总Token使用量
|
||||
*/
|
||||
@TableField("total_tokens")
|
||||
private Integer totalTokens;
|
||||
|
||||
/**
|
||||
* 总费用
|
||||
*/
|
||||
@TableField("total_cost")
|
||||
private BigDecimal totalCost;
|
||||
|
||||
/**
|
||||
* 客户端IP地址 (支持IPv6)
|
||||
*/
|
||||
@TableField("client_ip")
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 用户代理信息
|
||||
*/
|
||||
@TableField("user_agent")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 对话摘要
|
||||
*/
|
||||
@TableField("summary")
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
@TableField("tags")
|
||||
private String tags;
|
||||
|
||||
/**
|
||||
* 扩展元数据
|
||||
*/
|
||||
@TableField("metadata")
|
||||
private String metadata;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@TableField("remarks")
|
||||
private String remarks;
|
||||
|
||||
// 构造函数
|
||||
public Conversation() {
|
||||
this.createTime = LocalDateTime.now();
|
||||
this.updateTime = LocalDateTime.now();
|
||||
this.status = 1;
|
||||
this.isDeleted = 0;
|
||||
this.messageCount = 0;
|
||||
}
|
||||
|
||||
// Getter和Setter方法
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
|
||||
public LocalDateTime getStartTime() { return startTime; }
|
||||
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
|
||||
|
||||
public LocalDateTime getEndTime() { return endTime; }
|
||||
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
|
||||
|
||||
public Integer getMessageCount() { return messageCount; }
|
||||
public void setMessageCount(Integer messageCount) { this.messageCount = messageCount; }
|
||||
|
||||
public Integer getStatus() { return status; }
|
||||
public void setStatus(Integer status) { this.status = status; }
|
||||
|
||||
public String getClientIp() { return clientIp; }
|
||||
public void setClientIp(String clientIp) { this.clientIp = clientIp; }
|
||||
|
||||
public String getUserAgent() { return userAgent; }
|
||||
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
|
||||
|
||||
public String getCozeConversationId() { return cozeConversationId; }
|
||||
public void setCozeConversationId(String cozeConversationId) { this.cozeConversationId = cozeConversationId; }
|
||||
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
public String getCreateBy() { return createBy; }
|
||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||
|
||||
public String getUpdateBy() { return updateBy; }
|
||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||
|
||||
public Integer getIsDeleted() { return isDeleted; }
|
||||
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||
|
||||
public String getRemarks() { return remarks; }
|
||||
public void setRemarks(String remarks) { this.remarks = remarks; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Coze API调用记录实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("coze_api_call")
|
||||
public class CozeApiCall {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 对话ID
|
||||
*/
|
||||
@TableField("conversation_id")
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@TableField("message_id")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* Coze聊天ID
|
||||
*/
|
||||
@TableField("coze_chat_id")
|
||||
private String cozeChatId;
|
||||
|
||||
/**
|
||||
* Coze对话ID
|
||||
*/
|
||||
@TableField("coze_conversation_id")
|
||||
private String cozeConversationId;
|
||||
|
||||
/**
|
||||
* Bot ID
|
||||
*/
|
||||
@TableField("bot_id")
|
||||
private String botId;
|
||||
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
@TableField("workflow_id")
|
||||
private String workflowId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 请求类型: chat/stream/retrieve/messages
|
||||
*/
|
||||
@TableField("request_type")
|
||||
private String requestType;
|
||||
|
||||
/**
|
||||
* 请求URL
|
||||
*/
|
||||
@TableField("request_url")
|
||||
private String requestUrl;
|
||||
|
||||
/**
|
||||
* 请求体
|
||||
*/
|
||||
@TableField("request_body")
|
||||
private String requestBody;
|
||||
|
||||
/**
|
||||
* 请求头
|
||||
*/
|
||||
@TableField("request_headers")
|
||||
private String requestHeaders;
|
||||
|
||||
/**
|
||||
* 用户输入的消息内容
|
||||
*/
|
||||
@TableField("user_message")
|
||||
private String userMessage;
|
||||
|
||||
/**
|
||||
* 用户消息类型: text/image/file
|
||||
*/
|
||||
@TableField("user_message_type")
|
||||
private String userMessageType;
|
||||
|
||||
/**
|
||||
* AI回复的消息内容
|
||||
*/
|
||||
@TableField("ai_reply")
|
||||
private String aiReply;
|
||||
|
||||
/**
|
||||
* AI回复类型: text/image/file
|
||||
*/
|
||||
@TableField("ai_reply_type")
|
||||
private String aiReplyType;
|
||||
|
||||
/**
|
||||
* HTTP状态码
|
||||
*/
|
||||
@TableField("response_status")
|
||||
private Integer responseStatus;
|
||||
|
||||
/**
|
||||
* 响应体
|
||||
*/
|
||||
@TableField("response_body")
|
||||
private String responseBody;
|
||||
|
||||
/**
|
||||
* 响应头
|
||||
*/
|
||||
@TableField("response_headers")
|
||||
private String responseHeaders;
|
||||
|
||||
/**
|
||||
* 轮询次数
|
||||
*/
|
||||
@TableField("poll_count")
|
||||
private Integer pollCount;
|
||||
|
||||
/**
|
||||
* 轮询开始时间
|
||||
*/
|
||||
@TableField("poll_start_time")
|
||||
private LocalDateTime pollStartTime;
|
||||
|
||||
/**
|
||||
* 轮询结束时间
|
||||
*/
|
||||
@TableField("poll_end_time")
|
||||
private LocalDateTime pollEndTime;
|
||||
|
||||
/**
|
||||
* 最终状态: completed/failed/timeout
|
||||
*/
|
||||
@TableField("final_status")
|
||||
private String finalStatus;
|
||||
|
||||
/**
|
||||
* 调用状态: pending/success/failed/timeout
|
||||
*/
|
||||
@TableField("status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
@TableField("start_time")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
@TableField("end_time")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 耗时(毫秒)
|
||||
*/
|
||||
@TableField("duration_ms")
|
||||
private Integer durationMs;
|
||||
|
||||
/**
|
||||
* 输入Token数
|
||||
*/
|
||||
@TableField("prompt_tokens")
|
||||
private Integer promptTokens;
|
||||
|
||||
/**
|
||||
* 输出Token数
|
||||
*/
|
||||
@TableField("completion_tokens")
|
||||
private Integer completionTokens;
|
||||
|
||||
/**
|
||||
* 总Token数
|
||||
*/
|
||||
@TableField("total_tokens")
|
||||
private Integer totalTokens;
|
||||
|
||||
/**
|
||||
* 费用
|
||||
*/
|
||||
@TableField("cost")
|
||||
private BigDecimal cost;
|
||||
|
||||
/**
|
||||
* 函数调用记录
|
||||
*/
|
||||
@TableField("function_calls")
|
||||
private String functionCalls;
|
||||
|
||||
/**
|
||||
* 函数调用结果
|
||||
*/
|
||||
@TableField("function_results")
|
||||
private String functionResults;
|
||||
|
||||
/**
|
||||
* 错误代码
|
||||
*/
|
||||
@TableField("error_code")
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
@TableField("error_message")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 客户端IP
|
||||
*/
|
||||
@TableField("client_ip")
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 用户代理
|
||||
*/
|
||||
@TableField("user_agent")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@TableField("session_id")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 追踪ID
|
||||
*/
|
||||
@TableField("trace_id")
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* 扩展元数据
|
||||
*/
|
||||
@TableField("metadata")
|
||||
private String metadata;
|
||||
|
||||
/**
|
||||
* 创建人ID
|
||||
*/
|
||||
@TableField("create_by")
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新人ID
|
||||
*/
|
||||
@TableField("update_by")
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 是否删除: 0-未删除, 1-已删除
|
||||
*/
|
||||
@TableField("is_deleted")
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@TableField("remarks")
|
||||
private String remarks;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 情绪分析实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("emotion_analysis")
|
||||
public class EmotionAnalysis extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 关联消息ID
|
||||
*/
|
||||
@TableField("message_id")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 分析文本
|
||||
*/
|
||||
@TableField("text")
|
||||
private String text;
|
||||
|
||||
/**
|
||||
* 主要情绪
|
||||
*/
|
||||
@TableField("primary_emotion")
|
||||
private String primaryEmotion;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
@TableField("intensity")
|
||||
private BigDecimal intensity;
|
||||
|
||||
/**
|
||||
* 情绪极性: positive-积极, negative-消极, neutral-中性
|
||||
*/
|
||||
@TableField("polarity")
|
||||
private String polarity;
|
||||
|
||||
/**
|
||||
* 置信度
|
||||
*/
|
||||
@TableField("confidence")
|
||||
private BigDecimal confidence;
|
||||
|
||||
/**
|
||||
* 情绪分布详情
|
||||
*/
|
||||
@TableField("emotions")
|
||||
private String emotions;
|
||||
|
||||
/**
|
||||
* 关键词列表
|
||||
*/
|
||||
@TableField("keywords")
|
||||
private String keywords;
|
||||
|
||||
/**
|
||||
* 建议
|
||||
*/
|
||||
@TableField("suggestion")
|
||||
private String suggestion;
|
||||
|
||||
/**
|
||||
* 分析时间
|
||||
*/
|
||||
@TableField("analysis_time")
|
||||
private LocalDateTime analysisTime;
|
||||
|
||||
/**
|
||||
* 扩展元数据
|
||||
*/
|
||||
@TableField("metadata")
|
||||
private String metadata;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 情绪记录实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("emotion_record")
|
||||
public class EmotionRecord extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 记录日期
|
||||
*/
|
||||
@TableField("record_date")
|
||||
private LocalDate recordDate;
|
||||
|
||||
/**
|
||||
* 情绪类型
|
||||
*/
|
||||
@TableField("emotion_type")
|
||||
private String emotionType;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
@TableField("intensity")
|
||||
private BigDecimal intensity;
|
||||
|
||||
/**
|
||||
* 触发因素
|
||||
*/
|
||||
@TableField("triggers")
|
||||
private String triggers;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
@TableField("tags")
|
||||
private String tags;
|
||||
|
||||
/**
|
||||
* 天气
|
||||
*/
|
||||
@TableField("weather")
|
||||
private String weather;
|
||||
|
||||
/**
|
||||
* 地点
|
||||
*/
|
||||
@TableField("location")
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 活动
|
||||
*/
|
||||
@TableField("activity")
|
||||
private String activity;
|
||||
|
||||
/**
|
||||
* 相关人物
|
||||
*/
|
||||
@TableField("people")
|
||||
private String people;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@TableField("notes")
|
||||
private String notes;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 成长课题实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("growth_topic")
|
||||
public class GrowthTopic extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 课题标题
|
||||
*/
|
||||
@TableField("title")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 分类
|
||||
*/
|
||||
@TableField("category")
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 难度: easy-简单, medium-中等, hard-困难
|
||||
*/
|
||||
@TableField("difficulty")
|
||||
private String difficulty;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 内容
|
||||
*/
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 持续天数
|
||||
*/
|
||||
@TableField("duration_days")
|
||||
private Integer durationDays;
|
||||
|
||||
/**
|
||||
* 解锁条件
|
||||
*/
|
||||
@TableField("unlock_conditions")
|
||||
private String unlockConditions;
|
||||
|
||||
/**
|
||||
* 是否解锁
|
||||
*/
|
||||
@TableField("is_unlocked")
|
||||
private Integer isUnlocked;
|
||||
|
||||
/**
|
||||
* 进度百分比
|
||||
*/
|
||||
@TableField("progress")
|
||||
private BigDecimal progress;
|
||||
|
||||
/**
|
||||
* 完成时间
|
||||
*/
|
||||
@TableField("completed_time")
|
||||
private LocalDateTime completedTime;
|
||||
|
||||
/**
|
||||
* 奖励
|
||||
*/
|
||||
@TableField("rewards")
|
||||
private String rewards;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 访客用户实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("guest_user")
|
||||
public class GuestUser extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 访客用户ID (格式: guest_xxx)
|
||||
*/
|
||||
@TableField("guest_user_id")
|
||||
private String guestUserId;
|
||||
|
||||
/**
|
||||
* 客户端IP地址 (支持IPv6)
|
||||
*/
|
||||
@TableField("ip_address")
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* 用户代理信息
|
||||
*/
|
||||
@TableField("user_agent")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 访客昵称
|
||||
*/
|
||||
@TableField("nickname")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 访客头像
|
||||
*/
|
||||
@TableField("avatar")
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
@TableField("last_active_time")
|
||||
private LocalDateTime lastActiveTime;
|
||||
|
||||
/**
|
||||
* 会话数量
|
||||
*/
|
||||
@TableField("conversation_count")
|
||||
private Integer conversationCount;
|
||||
|
||||
/**
|
||||
* 消息数量
|
||||
*/
|
||||
@TableField("message_count")
|
||||
private Integer messageCount;
|
||||
|
||||
/**
|
||||
* IP地址的地理位置信息
|
||||
*/
|
||||
@TableField("location")
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 设备信息
|
||||
*/
|
||||
@TableField("device_info")
|
||||
private String deviceInfo;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 地点标记实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("location_pin")
|
||||
public class LocationPin extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 地点名称
|
||||
*/
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 地点类型
|
||||
*/
|
||||
@TableField("type")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 地点分类
|
||||
*/
|
||||
@TableField("category")
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
@TableField("latitude")
|
||||
private BigDecimal latitude;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
@TableField("longitude")
|
||||
private BigDecimal longitude;
|
||||
|
||||
/**
|
||||
* 地址
|
||||
*/
|
||||
@TableField("address")
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField("created_by")
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
@TableField("likes")
|
||||
private Integer likes;
|
||||
|
||||
/**
|
||||
* 访问数
|
||||
*/
|
||||
@TableField("visits")
|
||||
private Integer visits;
|
||||
|
||||
/**
|
||||
* 是否收藏
|
||||
*/
|
||||
@TableField("is_bookmarked")
|
||||
private Integer isBookmarked;
|
||||
|
||||
/**
|
||||
* 最后访问时间
|
||||
*/
|
||||
@TableField("last_visit_time")
|
||||
private LocalDateTime lastVisitTime;
|
||||
}
|
||||
@@ -1,108 +1,149 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 消息实体
|
||||
*
|
||||
* 消息实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-22
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public class Message {
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("message")
|
||||
public class Message extends BaseEntity {
|
||||
|
||||
private String id;
|
||||
/**
|
||||
* 对话ID
|
||||
*/
|
||||
@TableField("conversation_id")
|
||||
private String conversationId;
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@TableField("content")
|
||||
private String content;
|
||||
private String contentType;
|
||||
private String senderType;
|
||||
private String senderId;
|
||||
private String status;
|
||||
private LocalDateTime sendTime;
|
||||
private Integer isRead;
|
||||
private String parentMessageId;
|
||||
private String cozeRole;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
@TableField("type")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 发送者: user-用户, assistant-AI助手
|
||||
*/
|
||||
@TableField("sender")
|
||||
private String sender;
|
||||
|
||||
/**
|
||||
* 消息时间戳
|
||||
*/
|
||||
@TableField("timestamp")
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
/**
|
||||
* Coze平台的聊天ID
|
||||
*/
|
||||
@TableField("coze_chat_id")
|
||||
private String cozeChatId;
|
||||
|
||||
/**
|
||||
* Coze平台的消息ID
|
||||
*/
|
||||
@TableField("coze_message_id")
|
||||
private String cozeMessageId;
|
||||
|
||||
/**
|
||||
* 消息状态: sending/sent/failed/processing
|
||||
*/
|
||||
@TableField("status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
@TableField("error_message")
|
||||
private String errorMessage;
|
||||
private Integer retryCount;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
private String createBy;
|
||||
private String updateBy;
|
||||
private Integer isDeleted;
|
||||
private String remarks;
|
||||
|
||||
// 构造函数
|
||||
public Message() {
|
||||
this.createTime = LocalDateTime.now();
|
||||
this.updateTime = LocalDateTime.now();
|
||||
this.sendTime = LocalDateTime.now();
|
||||
this.isDeleted = 0;
|
||||
this.isRead = 0;
|
||||
this.retryCount = 0;
|
||||
}
|
||||
/**
|
||||
* 情绪评分
|
||||
*/
|
||||
@TableField("emotion_score")
|
||||
private BigDecimal emotionScore;
|
||||
|
||||
// Getter和Setter方法
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
/**
|
||||
* 情绪类型
|
||||
*/
|
||||
@TableField("emotion_type")
|
||||
private String emotionType;
|
||||
|
||||
public String getConversationId() { return conversationId; }
|
||||
public void setConversationId(String conversationId) { this.conversationId = conversationId; }
|
||||
/**
|
||||
* 情绪分析置信度
|
||||
*/
|
||||
@TableField("emotion_confidence")
|
||||
private BigDecimal emotionConfidence;
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
/**
|
||||
* 输入Token数
|
||||
*/
|
||||
@TableField("prompt_tokens")
|
||||
private Integer promptTokens;
|
||||
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
/**
|
||||
* 输出Token数
|
||||
*/
|
||||
@TableField("completion_tokens")
|
||||
private Integer completionTokens;
|
||||
|
||||
public String getContentType() { return contentType; }
|
||||
public void setContentType(String contentType) { this.contentType = contentType; }
|
||||
/**
|
||||
* 总Token数
|
||||
*/
|
||||
@TableField("total_tokens")
|
||||
private Integer totalTokens;
|
||||
|
||||
public String getSenderType() { return senderType; }
|
||||
public void setSenderType(String senderType) { this.senderType = senderType; }
|
||||
/**
|
||||
* API调用费用
|
||||
*/
|
||||
@TableField("api_cost")
|
||||
private BigDecimal apiCost;
|
||||
|
||||
public String getSenderId() { return senderId; }
|
||||
public void setSenderId(String senderId) { this.senderId = senderId; }
|
||||
/**
|
||||
* 是否已读: 0-未读, 1-已读
|
||||
*/
|
||||
@TableField("is_read")
|
||||
private Integer isRead;
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
/**
|
||||
* 父消息ID(用于回复链)
|
||||
*/
|
||||
@TableField("parent_message_id")
|
||||
private String parentMessageId;
|
||||
|
||||
public LocalDateTime getSendTime() { return sendTime; }
|
||||
public void setSendTime(LocalDateTime sendTime) { this.sendTime = sendTime; }
|
||||
/**
|
||||
* 情绪分析结果
|
||||
*/
|
||||
@TableField("emotion_analysis")
|
||||
private String emotionAnalysis;
|
||||
|
||||
public Integer getIsRead() { return isRead; }
|
||||
public void setIsRead(Integer isRead) { this.isRead = isRead; }
|
||||
/**
|
||||
* 扩展元数据
|
||||
*/
|
||||
@TableField("metadata")
|
||||
private String metadata;
|
||||
|
||||
public String getParentMessageId() { return parentMessageId; }
|
||||
public void setParentMessageId(String parentMessageId) { this.parentMessageId = parentMessageId; }
|
||||
|
||||
public String getCozeRole() { return cozeRole; }
|
||||
public void setCozeRole(String cozeRole) { this.cozeRole = cozeRole; }
|
||||
|
||||
public String getCozeMessageId() { return cozeMessageId; }
|
||||
public void setCozeMessageId(String cozeMessageId) { this.cozeMessageId = cozeMessageId; }
|
||||
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||
|
||||
public Integer getRetryCount() { return retryCount; }
|
||||
public void setRetryCount(Integer retryCount) { this.retryCount = retryCount; }
|
||||
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
public String getCreateBy() { return createBy; }
|
||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||
|
||||
public String getUpdateBy() { return updateBy; }
|
||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||
|
||||
public Integer getIsDeleted() { return isDeleted; }
|
||||
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||
|
||||
public String getRemarks() { return remarks; }
|
||||
public void setRemarks(String remarks) { this.remarks = remarks; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 奖励实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("reward")
|
||||
public class Reward extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 课题ID
|
||||
*/
|
||||
@TableField("topic_id")
|
||||
private String topicId;
|
||||
|
||||
/**
|
||||
* 成就ID
|
||||
*/
|
||||
@TableField("achievement_id")
|
||||
private String achievementId;
|
||||
|
||||
/**
|
||||
* 奖励类型
|
||||
*/
|
||||
@TableField("type")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 奖励名称
|
||||
*/
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
@TableField("icon")
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 稀有度
|
||||
*/
|
||||
@TableField("rarity")
|
||||
private String rarity;
|
||||
|
||||
/**
|
||||
* 奖励值
|
||||
*/
|
||||
@TableField("value")
|
||||
private String value;
|
||||
|
||||
/**
|
||||
* 获得时间
|
||||
*/
|
||||
@TableField("earned_time")
|
||||
private LocalDateTime earnedTime;
|
||||
|
||||
/**
|
||||
* 是否新获得
|
||||
*/
|
||||
@TableField("is_new")
|
||||
private Integer isNew;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 简化用户实体(不使用Lombok)
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-22
|
||||
*/
|
||||
public class SimpleUser {
|
||||
|
||||
private String id;
|
||||
private String username;
|
||||
private String account;
|
||||
private String password;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String nickname;
|
||||
private String avatar;
|
||||
private Integer status;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
// 构造函数
|
||||
public SimpleUser() {}
|
||||
|
||||
public SimpleUser(String id, String username, String account) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.account = account;
|
||||
this.createTime = LocalDateTime.now();
|
||||
this.updateTime = LocalDateTime.now();
|
||||
this.status = 1;
|
||||
}
|
||||
|
||||
// Getter和Setter方法
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getAccount() { return account; }
|
||||
public void setAccount(String account) { this.account = account; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getNickname() { return nickname; }
|
||||
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||
|
||||
public String getAvatar() { return avatar; }
|
||||
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||
|
||||
public Integer getStatus() { return status; }
|
||||
public void setStatus(Integer status) { this.status = status; }
|
||||
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 课题互动实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("topic_interaction")
|
||||
public class TopicInteraction extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 课题ID
|
||||
*/
|
||||
@TableField("topic_id")
|
||||
private String topicId;
|
||||
|
||||
/**
|
||||
* 互动类型
|
||||
*/
|
||||
@TableField("type")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 内容
|
||||
*/
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 用户输入
|
||||
*/
|
||||
@TableField("user_input")
|
||||
private String userInput;
|
||||
|
||||
/**
|
||||
* AI回应
|
||||
*/
|
||||
@TableField("ai_response")
|
||||
private String aiResponse;
|
||||
|
||||
/**
|
||||
* 评分
|
||||
*/
|
||||
@TableField("rating")
|
||||
private Integer rating;
|
||||
|
||||
/**
|
||||
* 反馈
|
||||
*/
|
||||
@TableField("feedback")
|
||||
private String feedback;
|
||||
|
||||
/**
|
||||
* 完成时间
|
||||
*/
|
||||
@TableField("completed_time")
|
||||
private LocalDateTime completedTime;
|
||||
}
|
||||
@@ -1,106 +1,162 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*
|
||||
* 用户实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-22
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public class User {
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("user")
|
||||
public class User extends BaseEntity {
|
||||
|
||||
private String id;
|
||||
private String username;
|
||||
/**
|
||||
* 账号
|
||||
*/
|
||||
@TableField("account")
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* 密码(加密后)
|
||||
*/
|
||||
@TableField("password")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@TableField("username")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@TableField("email")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@TableField("phone")
|
||||
private String phone;
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
@TableField("avatar")
|
||||
private String avatar;
|
||||
private Integer gender;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@TableField("nickname")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
@TableField("birth_date")
|
||||
private LocalDate birthDate;
|
||||
|
||||
/**
|
||||
* 所在地
|
||||
*/
|
||||
@TableField("location")
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 个人简介
|
||||
*/
|
||||
@TableField("bio")
|
||||
private String bio;
|
||||
|
||||
/**
|
||||
* 会员等级
|
||||
*/
|
||||
@TableField("member_level")
|
||||
private String memberLevel;
|
||||
|
||||
/**
|
||||
* 使用天数
|
||||
*/
|
||||
@TableField("total_days")
|
||||
private Integer totalDays;
|
||||
|
||||
/**
|
||||
* 自我感知
|
||||
*/
|
||||
@TableField("self_awareness")
|
||||
private BigDecimal selfAwareness;
|
||||
|
||||
/**
|
||||
* 情绪韧性
|
||||
*/
|
||||
@TableField("emotional_resilience")
|
||||
private BigDecimal emotionalResilience;
|
||||
|
||||
/**
|
||||
* 行动力
|
||||
*/
|
||||
@TableField("action_power")
|
||||
private BigDecimal actionPower;
|
||||
|
||||
/**
|
||||
* 共情力
|
||||
*/
|
||||
@TableField("empathy")
|
||||
private BigDecimal empathy;
|
||||
|
||||
/**
|
||||
* 生活热度
|
||||
*/
|
||||
@TableField("life_enthusiasm")
|
||||
private BigDecimal lifeEnthusiasm;
|
||||
|
||||
/**
|
||||
* 状态: 0-禁用, 1-正常
|
||||
*/
|
||||
@TableField("status")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 是否已验证: 0-未验证, 1-已验证
|
||||
*/
|
||||
@TableField("is_verified")
|
||||
private Integer isVerified;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
@TableField("last_active_time")
|
||||
private LocalDateTime lastActiveTime;
|
||||
private String createBy;
|
||||
private String updateBy;
|
||||
private Integer isDeleted;
|
||||
private String remarks;
|
||||
|
||||
// 构造函数
|
||||
public User() {
|
||||
this.createTime = LocalDateTime.now();
|
||||
this.updateTime = LocalDateTime.now();
|
||||
this.status = 1;
|
||||
this.isDeleted = 0;
|
||||
}
|
||||
/**
|
||||
* 第三方平台ID
|
||||
*/
|
||||
@TableField("third_party_id")
|
||||
private String thirdPartyId;
|
||||
|
||||
// Getter和Setter方法
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
/**
|
||||
* 第三方平台类型
|
||||
*/
|
||||
@TableField("third_party_type")
|
||||
private String thirdPartyType;
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getAccount() { return account; }
|
||||
public void setAccount(String account) { this.account = account; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getNickname() { return nickname; }
|
||||
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||
|
||||
public String getAvatar() { return avatar; }
|
||||
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||
|
||||
public Integer getGender() { return gender; }
|
||||
public void setGender(Integer gender) { this.gender = gender; }
|
||||
|
||||
public String getBio() { return bio; }
|
||||
public void setBio(String bio) { this.bio = bio; }
|
||||
|
||||
public String getMemberLevel() { return memberLevel; }
|
||||
public void setMemberLevel(String memberLevel) { this.memberLevel = memberLevel; }
|
||||
|
||||
public Integer getTotalDays() { return totalDays; }
|
||||
public void setTotalDays(Integer totalDays) { this.totalDays = totalDays; }
|
||||
|
||||
public Integer getStatus() { return status; }
|
||||
public void setStatus(Integer status) { this.status = status; }
|
||||
|
||||
public Integer getIsVerified() { return isVerified; }
|
||||
public void setIsVerified(Integer isVerified) { this.isVerified = isVerified; }
|
||||
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
|
||||
public LocalDateTime getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
|
||||
|
||||
public LocalDateTime getLastActiveTime() { return lastActiveTime; }
|
||||
public void setLastActiveTime(LocalDateTime lastActiveTime) { this.lastActiveTime = lastActiveTime; }
|
||||
|
||||
public String getCreateBy() { return createBy; }
|
||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||
|
||||
public String getUpdateBy() { return updateBy; }
|
||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||
|
||||
public Integer getIsDeleted() { return isDeleted; }
|
||||
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||
|
||||
public String getRemarks() { return remarks; }
|
||||
public void setRemarks(String remarks) { this.remarks = remarks; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.emotion.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.emotion.common.BaseEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 用户统计实体类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("user_stats")
|
||||
public class UserStats extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 总对话数
|
||||
*/
|
||||
@TableField("total_conversations")
|
||||
private Integer totalConversations;
|
||||
|
||||
/**
|
||||
* 总消息数
|
||||
*/
|
||||
@TableField("total_messages")
|
||||
private Integer totalMessages;
|
||||
|
||||
/**
|
||||
* 总情绪记录数
|
||||
*/
|
||||
@TableField("total_emotions_recorded")
|
||||
private Integer totalEmotionsRecorded;
|
||||
|
||||
/**
|
||||
* 完成的课题数
|
||||
*/
|
||||
@TableField("topics_completed")
|
||||
private Integer topicsCompleted;
|
||||
|
||||
/**
|
||||
* 解锁的成就数
|
||||
*/
|
||||
@TableField("achievements_unlocked")
|
||||
private Integer achievementsUnlocked;
|
||||
|
||||
/**
|
||||
* 总积分
|
||||
*/
|
||||
@TableField("total_points")
|
||||
private Integer totalPoints;
|
||||
|
||||
/**
|
||||
* 连续使用天数
|
||||
*/
|
||||
@TableField("consecutive_days")
|
||||
private Integer consecutiveDays;
|
||||
|
||||
/**
|
||||
* 最大连续天数
|
||||
*/
|
||||
@TableField("max_consecutive_days")
|
||||
private Integer maxConsecutiveDays;
|
||||
|
||||
/**
|
||||
* 访问的地点数
|
||||
*/
|
||||
@TableField("locations_visited")
|
||||
private Integer locationsVisited;
|
||||
|
||||
/**
|
||||
* 创建的帖子数
|
||||
*/
|
||||
@TableField("posts_created")
|
||||
private Integer postsCreated;
|
||||
|
||||
/**
|
||||
* 评论数
|
||||
*/
|
||||
@TableField("comments_made")
|
||||
private Integer commentsMade;
|
||||
|
||||
/**
|
||||
* 收到的点赞数
|
||||
*/
|
||||
@TableField("likes_received")
|
||||
private Integer likesReceived;
|
||||
|
||||
/**
|
||||
* 社交互动数
|
||||
*/
|
||||
@TableField("social_interactions")
|
||||
private Integer socialInteractions;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.emotion.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 自动填充处理器
|
||||
* 自动填充公共字段:id, create_by, create_time, update_by, update_time
|
||||
* 支持雪花算法自动生成主键ID
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class EmotionMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 雪花算法ID生成器
|
||||
*/
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
/**
|
||||
* 插入时自动填充
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String currentUserId = getCurrentUserId();
|
||||
|
||||
// 填充主键ID(如果为空)
|
||||
fillPrimaryKey(metaObject);
|
||||
|
||||
// 填充创建时间
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
|
||||
|
||||
// 填充更新时间
|
||||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);
|
||||
|
||||
// 填充创建人ID
|
||||
if (currentUserId != null) {
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, currentUserId);
|
||||
}
|
||||
|
||||
// 填充更新人ID
|
||||
if (currentUserId != null) {
|
||||
this.strictInsertFill(metaObject, "updateBy", String.class, currentUserId);
|
||||
}
|
||||
|
||||
// 填充逻辑删除字段默认值
|
||||
this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0);
|
||||
|
||||
log.debug("插入时自动填充完成: createTime={}, updateTime={}, createBy={}, updateBy={}",
|
||||
now, now, currentUserId, currentUserId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 自动填充失败不应该影响业务逻辑
|
||||
log.warn("插入时自动填充失败,但不影响业务逻辑: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新时自动填充
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String currentUserId = getCurrentUserId();
|
||||
|
||||
// 填充更新时间
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, now);
|
||||
|
||||
// 填充更新人ID
|
||||
if (currentUserId != null) {
|
||||
this.strictUpdateFill(metaObject, "updateBy", String.class, currentUserId);
|
||||
}
|
||||
|
||||
log.debug("更新时自动填充完成: updateTime={}, updateBy={}", now, currentUserId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 自动填充失败不应该影响业务逻辑
|
||||
log.warn("更新时自动填充失败,但不影响业务逻辑: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充主键ID
|
||||
* 如果主键ID为空,则使用雪花算法生成
|
||||
*
|
||||
* @param metaObject 元对象
|
||||
*/
|
||||
private void fillPrimaryKey(MetaObject metaObject) {
|
||||
try {
|
||||
// 检查是否有id字段
|
||||
if (metaObject.hasSetter("id")) {
|
||||
Object idValue = metaObject.getValue("id");
|
||||
// 如果ID为空,则生成新的ID
|
||||
if (idValue == null || (idValue instanceof String && ((String) idValue).isEmpty())) {
|
||||
String newId = snowflakeIdGenerator.nextIdAsString();
|
||||
this.strictInsertFill(metaObject, "id", String.class, newId);
|
||||
log.debug("自动生成主键ID: {}", newId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("主键ID自动填充失败,但不影响业务逻辑: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
* 优先级:
|
||||
* 1. 从ThreadLocal获取(如果有用户上下文)
|
||||
* 2. 从Spring Security获取(如果有认证信息)
|
||||
* 3. 返回系统默认值
|
||||
*
|
||||
* @return 当前用户ID,如果获取失败返回null
|
||||
*/
|
||||
private String getCurrentUserId() {
|
||||
try {
|
||||
// 1. 尝试从ThreadLocal获取用户ID(如果有用户上下文工具类)
|
||||
String userIdFromContext = getUserIdFromContext();
|
||||
if (userIdFromContext != null) {
|
||||
return userIdFromContext;
|
||||
}
|
||||
|
||||
// 2. 尝试从Spring Security获取用户ID
|
||||
String userIdFromSecurity = getUserIdFromSecurity();
|
||||
if (userIdFromSecurity != null) {
|
||||
return userIdFromSecurity;
|
||||
}
|
||||
|
||||
// 3. 返回系统默认值(用于系统操作或未登录用户)
|
||||
return "system";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("获取当前用户ID失败: {}", e.getMessage());
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户上下文获取用户ID
|
||||
* 这里可以集成自定义的用户上下文工具类
|
||||
*
|
||||
* @return 用户ID或null
|
||||
*/
|
||||
private String getUserIdFromContext() {
|
||||
try {
|
||||
// 从线程变量获取
|
||||
return UserContextHolder.getCurrentUserId();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("从用户上下文获取用户ID失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Spring Security获取用户ID
|
||||
*
|
||||
* @return 用户ID或null
|
||||
*/
|
||||
private String getUserIdFromSecurity() {
|
||||
try {
|
||||
// TODO: 集成Spring Security
|
||||
// Authentication authentication =
|
||||
// SecurityContextHolder.getContext().getAuthentication();
|
||||
// if (authentication != null && authentication.isAuthenticated()
|
||||
// && !"anonymousUser".equals(authentication.getPrincipal())) {
|
||||
// UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||
// return userDetails.getUsername(); // 或者从UserDetails中获取用户ID
|
||||
// }
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("从Spring Security获取用户ID失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.emotion.interceptor;
|
||||
|
||||
import com.emotion.util.JwtUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* JWT认证拦截器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Component
|
||||
public class JwtAuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JwtAuthInterceptor.class);
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
String requestURI = request.getRequestURI();
|
||||
String method = request.getMethod();
|
||||
|
||||
log.debug("JWT拦截器处理请求: {} {}", method, requestURI);
|
||||
|
||||
// 跨域预检请求直接放行
|
||||
if ("OPTIONS".equals(method)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 不需要认证的接口
|
||||
if (isPublicEndpoint(requestURI)) {
|
||||
log.debug("公开接口,无需认证: {}", requestURI);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取Authorization头
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
log.warn("请求缺少Authorization头或格式错误: {}", requestURI);
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\",\"data\":null}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 提取token
|
||||
String token = authHeader.substring(7);
|
||||
|
||||
// 验证token
|
||||
if (!jwtUtil.validateToken(token)) {
|
||||
log.warn("Token验证失败: {}", requestURI);
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期\",\"data\":null}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从token中获取用户信息并设置到请求属性中
|
||||
String userId = jwtUtil.getUserIdFromToken(token);
|
||||
String username = jwtUtil.getUsernameFromToken(token);
|
||||
|
||||
request.setAttribute("userId", userId);
|
||||
request.setAttribute("username", username);
|
||||
request.setAttribute("token", token);
|
||||
|
||||
log.debug("Token验证成功,用户: {} ({})", username, userId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为公开接口(不需要认证)
|
||||
*/
|
||||
private boolean isPublicEndpoint(String requestURI) {
|
||||
// 公开接口列表
|
||||
String[] publicEndpoints = {
|
||||
"/api/auth/login",
|
||||
"/api/auth/register",
|
||||
"/api/auth/captcha",
|
||||
"/api/auth/refresh-token",
|
||||
"/api/health",
|
||||
"/api/ws/chat",
|
||||
"/swagger-ui",
|
||||
"/v3/api-docs",
|
||||
"/actuator"
|
||||
};
|
||||
|
||||
for (String endpoint : publicEndpoints) {
|
||||
if (requestURI.startsWith(endpoint)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package com.emotion.interceptor;
|
||||
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 用户上下文拦截器
|
||||
* 用于在请求处理前设置用户上下文信息,请求处理后清理上下文
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserContextInterceptor implements HandlerInterceptor {
|
||||
|
||||
/**
|
||||
* 请求处理前
|
||||
*/
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
try {
|
||||
// 生成请求ID
|
||||
String requestId = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
// 获取用户信息
|
||||
String userId = getUserIdFromRequest(request);
|
||||
String username = getUsernameFromRequest(request);
|
||||
String userType = getUserTypeFromRequest(request);
|
||||
String clientIp = getClientIpFromRequest(request);
|
||||
|
||||
// 设置用户上下文
|
||||
UserContextHolder.setUserContext(userId, username, userType, clientIp, requestId);
|
||||
|
||||
// 设置响应头中的请求ID,便于追踪
|
||||
response.setHeader("X-Request-Id", requestId);
|
||||
|
||||
log.debug("设置用户上下文: {}", UserContextHolder.getContextSummary());
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("设置用户上下文失败: {}", e.getMessage());
|
||||
// 即使设置失败也不影响请求处理
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求处理后
|
||||
*/
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler, Exception ex) {
|
||||
try {
|
||||
log.debug("清理用户上下文: {}", UserContextHolder.getContextSummary());
|
||||
// 清理用户上下文
|
||||
UserContextHolder.clear();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("清理用户上下文失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取用户ID
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 用户ID
|
||||
*/
|
||||
private String getUserIdFromRequest(HttpServletRequest request) {
|
||||
// 1. 从请求头获取
|
||||
String userId = request.getHeader("X-User-Id");
|
||||
if (StringUtils.hasText(userId)) {
|
||||
return userId;
|
||||
}
|
||||
|
||||
// 2. 从请求参数获取
|
||||
userId = request.getParameter("userId");
|
||||
if (StringUtils.hasText(userId)) {
|
||||
return userId;
|
||||
}
|
||||
|
||||
// 3. 从Session获取
|
||||
Object sessionUserId = request.getSession().getAttribute("userId");
|
||||
if (sessionUserId != null) {
|
||||
return sessionUserId.toString();
|
||||
}
|
||||
|
||||
// 4. 生成访客ID
|
||||
return "guest_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取用户名
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 用户名
|
||||
*/
|
||||
private String getUsernameFromRequest(HttpServletRequest request) {
|
||||
// 1. 从请求头获取
|
||||
String username = request.getHeader("X-Username");
|
||||
if (StringUtils.hasText(username)) {
|
||||
return username;
|
||||
}
|
||||
|
||||
// 2. 从请求参数获取
|
||||
username = request.getParameter("username");
|
||||
if (StringUtils.hasText(username)) {
|
||||
return username;
|
||||
}
|
||||
|
||||
// 3. 从Session获取
|
||||
Object sessionUsername = request.getSession().getAttribute("username");
|
||||
if (sessionUsername != null) {
|
||||
return sessionUsername.toString();
|
||||
}
|
||||
|
||||
return "guest";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取用户类型
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 用户类型
|
||||
*/
|
||||
private String getUserTypeFromRequest(HttpServletRequest request) {
|
||||
// 1. 从请求头获取
|
||||
String userType = request.getHeader("X-User-Type");
|
||||
if (StringUtils.hasText(userType)) {
|
||||
return userType;
|
||||
}
|
||||
|
||||
// 2. 从请求参数获取
|
||||
userType = request.getParameter("userType");
|
||||
if (StringUtils.hasText(userType)) {
|
||||
return userType;
|
||||
}
|
||||
|
||||
// 3. 从Session获取
|
||||
Object sessionUserType = request.getSession().getAttribute("userType");
|
||||
if (sessionUserType != null) {
|
||||
return sessionUserType.toString();
|
||||
}
|
||||
|
||||
return "GUEST";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取客户端IP
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 客户端IP
|
||||
*/
|
||||
private String getClientIpFromRequest(HttpServletRequest request) {
|
||||
String ip = null;
|
||||
|
||||
// 1. 从X-Forwarded-For获取(经过代理的情况)
|
||||
ip = request.getHeader("X-Forwarded-For");
|
||||
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
||||
// 多个IP的情况,取第一个
|
||||
int index = ip.indexOf(',');
|
||||
if (index != -1) {
|
||||
ip = ip.substring(0, index);
|
||||
}
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 2. 从X-Real-IP获取
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 3. 从Proxy-Client-IP获取
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 4. 从WL-Proxy-Client-IP获取
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 5. 从HTTP_CLIENT_IP获取
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 6. 从HTTP_X_FORWARDED_FOR获取
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 7. 最后从getRemoteAddr获取
|
||||
ip = request.getRemoteAddr();
|
||||
return StringUtils.hasText(ip) ? ip.trim() : "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.Achievement;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 成就Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface AchievementMapper extends BaseMapper<Achievement> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.Comment;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 评论Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface CommentMapper extends BaseMapper<Comment> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.CommunityPost;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 社区帖子Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface CommunityPostMapper extends BaseMapper<CommunityPost> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.Conversation;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 会话Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface ConversationMapper extends BaseMapper<Conversation> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.CozeApiCall;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* Coze API调用记录Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface CozeApiCallMapper extends BaseMapper<CozeApiCall> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.EmotionAnalysis;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 情绪分析Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface EmotionAnalysisMapper extends BaseMapper<EmotionAnalysis> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.EmotionRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 情绪记录Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface EmotionRecordMapper extends BaseMapper<EmotionRecord> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.GrowthTopic;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 成长课题Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface GrowthTopicMapper extends BaseMapper<GrowthTopic> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.GuestUser;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 访客用户Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface GuestUserMapper extends BaseMapper<GuestUser> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.LocationPin;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 地点标记Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface LocationPinMapper extends BaseMapper<LocationPin> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.Message;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 消息Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface MessageMapper extends BaseMapper<Message> {
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.TopicInteraction;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 课题互动Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface TopicInteractionMapper extends BaseMapper<TopicInteraction> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.User;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.emotion.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotion.entity.UserStats;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户统计Mapper接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserStatsMapper extends BaseMapper<UserStats> {
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public class AiService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AiService.class);
|
||||
|
||||
|
||||
private String cozeApiToken = "your-coze-api-token";
|
||||
private String cozeBaseUrl = "https://api.coze.cn";
|
||||
private String botId = "7523042446285439016";
|
||||
|
||||
@@ -1,149 +1,88 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.entity.Conversation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 对话服务
|
||||
*
|
||||
* 会话服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-22
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class ConversationService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ConversationService.class);
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
public interface ConversationService extends IService<Conversation> {
|
||||
|
||||
/**
|
||||
* 对话行映射器
|
||||
* 分页查询会话
|
||||
*/
|
||||
private static class ConversationRowMapper implements RowMapper<Conversation> {
|
||||
@Override
|
||||
public Conversation mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(rs.getString("id"));
|
||||
conversation.setUserId(rs.getString("user_id"));
|
||||
conversation.setTitle(rs.getString("title"));
|
||||
conversation.setType(rs.getString("type"));
|
||||
conversation.setStartTime(rs.getTimestamp("start_time") != null ?
|
||||
rs.getTimestamp("start_time").toLocalDateTime() : null);
|
||||
conversation.setEndTime(rs.getTimestamp("end_time") != null ?
|
||||
rs.getTimestamp("end_time").toLocalDateTime() : null);
|
||||
conversation.setMessageCount(rs.getInt("message_count"));
|
||||
conversation.setStatus(rs.getInt("status"));
|
||||
conversation.setClientIp(rs.getString("client_ip"));
|
||||
conversation.setUserAgent(rs.getString("user_agent"));
|
||||
conversation.setCozeConversationId(rs.getString("coze_conversation_id"));
|
||||
conversation.setCreateTime(rs.getTimestamp("create_time") != null ?
|
||||
rs.getTimestamp("create_time").toLocalDateTime() : null);
|
||||
conversation.setUpdateTime(rs.getTimestamp("update_time") != null ?
|
||||
rs.getTimestamp("update_time").toLocalDateTime() : null);
|
||||
return conversation;
|
||||
}
|
||||
}
|
||||
IPage<Conversation> getPage(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 创建对话
|
||||
* 根据用户ID分页查询会话
|
||||
*/
|
||||
public Conversation createConversation(String userId, String title, String type, String clientIp) {
|
||||
try {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
conversation.setUserId(userId);
|
||||
conversation.setTitle(title != null ? title : "新对话");
|
||||
conversation.setType(type != null ? type : "user");
|
||||
conversation.setStartTime(LocalDateTime.now());
|
||||
conversation.setMessageCount(0);
|
||||
conversation.setStatus(1);
|
||||
conversation.setClientIp(clientIp);
|
||||
conversation.setCreateTime(LocalDateTime.now());
|
||||
conversation.setUpdateTime(LocalDateTime.now());
|
||||
conversation.setIsDeleted(0);
|
||||
|
||||
String sql = "INSERT INTO conversation (id, user_id, title, type, start_time, " +
|
||||
"message_count, status, client_ip, user_agent, create_time, update_time, is_deleted) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
jdbcTemplate.update(sql,
|
||||
conversation.getId(), conversation.getUserId(), conversation.getTitle(),
|
||||
conversation.getType(), conversation.getStartTime(), conversation.getMessageCount(),
|
||||
conversation.getStatus(), conversation.getClientIp(), conversation.getUserAgent(),
|
||||
conversation.getCreateTime(), conversation.getUpdateTime(), conversation.getIsDeleted());
|
||||
|
||||
log.info("对话创建成功: {}", conversation.getId());
|
||||
return conversation;
|
||||
} catch (Exception e) {
|
||||
log.error("创建对话失败: {}", e.getMessage());
|
||||
throw new RuntimeException("创建对话失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
IPage<Conversation> getPageByUserId(BasePageRequest request, String userId);
|
||||
|
||||
/**
|
||||
* 根据ID查询对话
|
||||
* 根据用户ID查询会话列表
|
||||
*/
|
||||
public Conversation findById(String id) {
|
||||
try {
|
||||
String sql = "SELECT * FROM conversation WHERE id = ? AND is_deleted = 0";
|
||||
List<Conversation> conversations = jdbcTemplate.query(sql, new ConversationRowMapper(), id);
|
||||
return conversations.isEmpty() ? null : conversations.get(0);
|
||||
} catch (Exception e) {
|
||||
log.error("根据ID查询对话失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
List<Conversation> getByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询对话列表
|
||||
* 根据用户ID查询活跃会话列表
|
||||
*/
|
||||
public List<Conversation> findByUserId(String userId) {
|
||||
try {
|
||||
String sql = "SELECT * FROM conversation WHERE user_id = ? AND is_deleted = 0 ORDER BY create_time DESC";
|
||||
return jdbcTemplate.query(sql, new ConversationRowMapper(), userId);
|
||||
} catch (Exception e) {
|
||||
log.error("根据用户ID查询对话列表失败: {}", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
List<Conversation> getActiveByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 更新消息数量
|
||||
* 根据Coze会话ID查询会话
|
||||
*/
|
||||
public boolean updateMessageCount(String conversationId, int messageCount) {
|
||||
try {
|
||||
String sql = "UPDATE conversation SET message_count = ?, update_time = ? WHERE id = ? AND is_deleted = 0";
|
||||
int rows = jdbcTemplate.update(sql, messageCount, LocalDateTime.now(), conversationId);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新消息数量失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Conversation getByCozeConversationId(String cozeConversationId);
|
||||
|
||||
/**
|
||||
* 结束对话
|
||||
* 更新会话消息数量
|
||||
*/
|
||||
public boolean endConversation(String conversationId) {
|
||||
try {
|
||||
String sql = "UPDATE conversation SET status = 0, end_time = ?, update_time = ? WHERE id = ? AND is_deleted = 0";
|
||||
int rows = jdbcTemplate.update(sql, LocalDateTime.now(), LocalDateTime.now(), conversationId);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("结束对话失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean updateMessageCount(String conversationId, Integer messageCount);
|
||||
|
||||
/**
|
||||
* 更新会话状态
|
||||
*/
|
||||
boolean updateStatus(String conversationId, Integer status);
|
||||
|
||||
/**
|
||||
* 更新会话结束时间
|
||||
*/
|
||||
boolean updateEndTime(String conversationId, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计用户的会话数量
|
||||
*/
|
||||
Long countByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计用户的活跃会话数量
|
||||
*/
|
||||
Long countActiveByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 查询需要归档的会话(超过指定天数未活跃)
|
||||
*/
|
||||
List<Conversation> getForArchive(Integer days);
|
||||
|
||||
/**
|
||||
* 批量归档会话
|
||||
*/
|
||||
boolean batchArchive(List<String> conversationIds);
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*/
|
||||
Conversation createConversation(String userId, String title, String cozeConversationId);
|
||||
|
||||
/**
|
||||
* 结束会话
|
||||
*/
|
||||
boolean endConversation(String conversationId);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.entity.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface IAiService {
|
||||
|
||||
/**
|
||||
* 发送聊天消息到AI
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param message 用户消息
|
||||
* @param userId 用户ID
|
||||
* @return AI回复内容
|
||||
*/
|
||||
String sendChatMessage(String conversationId, String message, String userId);
|
||||
|
||||
/**
|
||||
* 根据聊天记录生成对话总结
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param userId 用户ID
|
||||
* @return 对话总结
|
||||
*/
|
||||
String generateConversationSummary(String conversationId, String userId);
|
||||
|
||||
/**
|
||||
* 根据消息列表生成总结
|
||||
*
|
||||
* @param messages 消息列表
|
||||
* @param userId 用户ID
|
||||
* @return 对话总结
|
||||
*/
|
||||
String generateSummaryFromRecords(List<Message> messages, String userId);
|
||||
|
||||
/**
|
||||
* 检查AI服务是否可用
|
||||
*
|
||||
* @return 是否可用
|
||||
*/
|
||||
boolean isServiceAvailable();
|
||||
|
||||
/**
|
||||
* 获取AI服务状态信息
|
||||
*
|
||||
* @return 状态信息
|
||||
*/
|
||||
String getServiceStatus();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.entity.Conversation;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会话服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface IConversationService extends IService<Conversation> {
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param title 会话标题
|
||||
* @param type 会话类型
|
||||
* @return 会话信息
|
||||
*/
|
||||
Conversation createConversation(String userId, String title, String type);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询会话列表
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param userId 用户ID
|
||||
* @return 会话分页数据
|
||||
*/
|
||||
IPage<Conversation> getByUserId(Page<Conversation> page, String userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询活跃会话列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 活跃会话列表
|
||||
*/
|
||||
List<Conversation> getActiveByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 根据Coze会话ID查询会话
|
||||
*
|
||||
* @param cozeConversationId Coze会话ID
|
||||
* @return 会话信息
|
||||
*/
|
||||
Conversation getByCozeConversationId(String cozeConversationId);
|
||||
|
||||
/**
|
||||
* 更新会话消息数量
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param messageCount 消息数量
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateMessageCount(String conversationId, Integer messageCount);
|
||||
|
||||
/**
|
||||
* 更新会话状态
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param status 状态
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateStatus(String conversationId, Integer status);
|
||||
|
||||
/**
|
||||
* 结束会话
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean endConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* 统计用户的会话数量
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 会话数量
|
||||
*/
|
||||
Long countByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计用户的活跃会话数量
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 活跃会话数量
|
||||
*/
|
||||
Long countActiveByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 归档超时会话
|
||||
*
|
||||
* @param days 超时天数
|
||||
* @return 归档的会话数量
|
||||
*/
|
||||
int archiveTimeoutConversations(Integer days);
|
||||
|
||||
/**
|
||||
* 批量归档会话
|
||||
*
|
||||
* @param conversationIds 会话ID列表
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean batchArchive(List<String> conversationIds);
|
||||
|
||||
/**
|
||||
* 获取或创建会话
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param cozeConversationId Coze会话ID
|
||||
* @param title 会话标题
|
||||
* @return 会话信息
|
||||
*/
|
||||
Conversation getOrCreateConversation(String userId, String cozeConversationId, String title);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.entity.CozeApiCall;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Coze API调用记录服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface ICozeApiCallService extends IService<CozeApiCall> {
|
||||
|
||||
/**
|
||||
* 根据会话ID分页查询API调用记录
|
||||
*/
|
||||
IPage<CozeApiCall> getByConversationId(Page<CozeApiCall> page, String conversationId);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查询API调用记录
|
||||
*/
|
||||
IPage<CozeApiCall> getByUserId(Page<CozeApiCall> page, String userId);
|
||||
|
||||
/**
|
||||
* 根据Bot ID查询API调用记录
|
||||
*/
|
||||
List<CozeApiCall> getByBotId(String botId);
|
||||
|
||||
/**
|
||||
* 根据状态查询API调用记录
|
||||
*/
|
||||
List<CozeApiCall> getByStatus(String status);
|
||||
|
||||
/**
|
||||
* 根据请求类型查询API调用记录
|
||||
*/
|
||||
List<CozeApiCall> getByRequestType(String requestType);
|
||||
|
||||
/**
|
||||
* 根据时间范围查询API调用记录
|
||||
*/
|
||||
List<CozeApiCall> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计用户的API调用次数
|
||||
*/
|
||||
Long countByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计Bot的API调用次数
|
||||
*/
|
||||
Long countByBotId(String botId);
|
||||
|
||||
/**
|
||||
* 统计指定状态的API调用次数
|
||||
*/
|
||||
Long countByStatus(String status);
|
||||
|
||||
/**
|
||||
* 统计用户的Token使用量
|
||||
*/
|
||||
Long sumTokensByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 统计用户的API调用费用
|
||||
*/
|
||||
BigDecimal sumCostByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 查询失败的API调用记录
|
||||
*/
|
||||
List<CozeApiCall> getFailedCalls();
|
||||
|
||||
/**
|
||||
* 查询超时的API调用记录
|
||||
*/
|
||||
List<CozeApiCall> getTimeoutCalls();
|
||||
|
||||
/**
|
||||
* 根据追踪ID查询API调用记录
|
||||
*/
|
||||
CozeApiCall getByTraceId(String traceId);
|
||||
|
||||
/**
|
||||
* 根据会话ID和请求类型查询API调用记录
|
||||
*/
|
||||
List<CozeApiCall> getByConversationIdAndRequestType(String conversationId, String requestType);
|
||||
|
||||
/**
|
||||
* 更新API调用状态
|
||||
*/
|
||||
boolean updateStatus(String id, String status, String finalStatus, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 更新API调用结果
|
||||
*/
|
||||
boolean updateResult(String id, Integer responseStatus, String responseBody, String aiReply,
|
||||
Integer totalTokens, BigDecimal cost, String status, String finalStatus,
|
||||
LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 创建API调用记录
|
||||
*/
|
||||
CozeApiCall createApiCall(String conversationId, String messageId, String userId,
|
||||
String requestType, String requestUrl, String requestBody);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.entity.Message;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 消息服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public interface IMessageService extends IService<Message> {
|
||||
|
||||
/**
|
||||
* 保存消息
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param content 消息内容
|
||||
* @param type 消息类型
|
||||
* @param sender 发送者
|
||||
* @return 消息
|
||||
*/
|
||||
Message saveMessage(String conversationId, String content, String type, String sender);
|
||||
|
||||
/**
|
||||
* 根据会话ID分页查询消息
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param conversationId 会话ID
|
||||
* @return 消息分页数据
|
||||
*/
|
||||
IPage<Message> getByConversationId(Page<Message> page, String conversationId);
|
||||
|
||||
/**
|
||||
* 根据发送者分页查询消息
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param sender 发送者
|
||||
* @return 消息分页数据
|
||||
*/
|
||||
IPage<Message> getBySender(Page<Message> page, String sender);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询消息列表(用于总结)
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param limit 限制数量
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<Message> getByConversationIdForSummary(String conversationId, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据时间范围查询消息
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计会话的消息数量
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 消息数量
|
||||
*/
|
||||
Long countByConversationId(String conversationId);
|
||||
|
||||
/**
|
||||
* 统计发送者的消息数量
|
||||
*
|
||||
* @param sender 发送者
|
||||
* @return 消息数量
|
||||
*/
|
||||
Long countBySender(String sender);
|
||||
|
||||
/**
|
||||
* 查询会话的最后一条消息
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 最后一条消息
|
||||
*/
|
||||
Message getLastMessageByConversationId(String conversationId);
|
||||
|
||||
/**
|
||||
* 根据发送者查询消息
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param sender 发送者
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<Message> getByConversationIdAndSender(String conversationId, String sender);
|
||||
|
||||
/**
|
||||
* 更新消息状态
|
||||
*
|
||||
* @param messageId 消息ID
|
||||
* @param status 状态
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateStatus(String messageId, String status);
|
||||
|
||||
/**
|
||||
* 更新消息已读状态
|
||||
*
|
||||
* @param messageId 消息ID
|
||||
* @param isRead 是否已读
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateReadStatus(String messageId, Integer isRead);
|
||||
|
||||
/**
|
||||
* 批量更新会话消息为已读
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean markConversationMessagesAsRead(String conversationId);
|
||||
|
||||
/**
|
||||
* 批量保存消息
|
||||
*
|
||||
* @param messages 消息列表
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean saveBatch(List<Message> messages);
|
||||
|
||||
/**
|
||||
* 删除会话的所有消息
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteByConversationId(String conversationId);
|
||||
}
|
||||
@@ -1,165 +1,94 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.entity.Message;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 消息服务
|
||||
* 消息服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-22
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class MessageService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MessageService.class);
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
private ConversationService conversationService;
|
||||
|
||||
public interface MessageService extends IService<Message> {
|
||||
|
||||
/**
|
||||
* 消息行映射器
|
||||
* 分页查询消息
|
||||
*/
|
||||
private static class MessageRowMapper implements RowMapper<Message> {
|
||||
@Override
|
||||
public Message mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
Message message = new Message();
|
||||
message.setId(rs.getString("id"));
|
||||
message.setConversationId(rs.getString("conversation_id"));
|
||||
message.setUserId(rs.getString("user_id"));
|
||||
message.setContent(rs.getString("content"));
|
||||
message.setContentType(rs.getString("content_type"));
|
||||
message.setSenderType(rs.getString("sender_type"));
|
||||
message.setSenderId(rs.getString("sender_id"));
|
||||
message.setStatus(rs.getString("status"));
|
||||
message.setSendTime(rs.getTimestamp("send_time") != null ?
|
||||
rs.getTimestamp("send_time").toLocalDateTime() : null);
|
||||
message.setIsRead(rs.getInt("is_read"));
|
||||
message.setParentMessageId(rs.getString("parent_message_id"));
|
||||
message.setCozeRole(rs.getString("coze_role"));
|
||||
message.setCozeMessageId(rs.getString("coze_message_id"));
|
||||
message.setErrorMessage(rs.getString("error_message"));
|
||||
message.setRetryCount(rs.getInt("retry_count"));
|
||||
message.setCreateTime(rs.getTimestamp("create_time") != null ?
|
||||
rs.getTimestamp("create_time").toLocalDateTime() : null);
|
||||
message.setUpdateTime(rs.getTimestamp("update_time") != null ?
|
||||
rs.getTimestamp("update_time").toLocalDateTime() : null);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
IPage<Message> getPage(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 根据会话ID分页查询消息
|
||||
*/
|
||||
IPage<Message> getPageByConversationId(BasePageRequest request, String conversationId);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询消息列表
|
||||
*/
|
||||
List<Message> getByConversationId(String conversationId);
|
||||
|
||||
/**
|
||||
* 根据发送者查询消息列表
|
||||
*/
|
||||
List<Message> getBySender(String sender);
|
||||
|
||||
/**
|
||||
* 根据时间范围查询消息
|
||||
*/
|
||||
List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 查询会话的最后一条消息
|
||||
*/
|
||||
Message getLastMessageByConversationId(String conversationId);
|
||||
|
||||
/**
|
||||
* 根据父消息ID查询回复消息
|
||||
*/
|
||||
List<Message> getRepliesByParentId(String parentMessageId);
|
||||
|
||||
/**
|
||||
* 统计会话的消息数量
|
||||
*/
|
||||
Long countByConversationId(String conversationId);
|
||||
|
||||
/**
|
||||
* 统计发送者的消息数量
|
||||
*/
|
||||
Long countBySender(String sender);
|
||||
|
||||
/**
|
||||
* 查询未读消息数量
|
||||
*/
|
||||
Long countUnreadMessages(String conversationId);
|
||||
|
||||
/**
|
||||
* 更新消息状态
|
||||
*/
|
||||
boolean updateStatus(String messageId, String status);
|
||||
|
||||
/**
|
||||
* 更新消息已读状态
|
||||
*/
|
||||
boolean updateReadStatus(String messageId, Integer isRead);
|
||||
|
||||
/**
|
||||
* 批量更新会话消息为已读
|
||||
*/
|
||||
boolean markConversationMessagesAsRead(String conversationId);
|
||||
|
||||
/**
|
||||
* 创建消息
|
||||
*/
|
||||
public Message createMessage(String conversationId, String userId, String content,
|
||||
String senderType, String senderId) {
|
||||
try {
|
||||
Message message = new Message();
|
||||
message.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
message.setConversationId(conversationId);
|
||||
message.setUserId(userId);
|
||||
message.setContent(content);
|
||||
message.setContentType("text");
|
||||
message.setSenderType(senderType);
|
||||
message.setSenderId(senderId);
|
||||
message.setStatus("sent");
|
||||
message.setSendTime(LocalDateTime.now());
|
||||
message.setIsRead(0);
|
||||
message.setRetryCount(0);
|
||||
message.setCreateTime(LocalDateTime.now());
|
||||
message.setUpdateTime(LocalDateTime.now());
|
||||
message.setIsDeleted(0);
|
||||
|
||||
String sql = "INSERT INTO message (id, conversation_id, user_id, content, content_type, " +
|
||||
"sender_type, sender_id, status, send_time, is_read, retry_count, " +
|
||||
"create_time, update_time, is_deleted) VALUES " +
|
||||
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
jdbcTemplate.update(sql,
|
||||
message.getId(), message.getConversationId(), message.getUserId(),
|
||||
message.getContent(), message.getContentType(), message.getSenderType(),
|
||||
message.getSenderId(), message.getStatus(), message.getSendTime(),
|
||||
message.getIsRead(), message.getRetryCount(), message.getCreateTime(),
|
||||
message.getUpdateTime(), message.getIsDeleted());
|
||||
|
||||
// 更新对话的消息数量
|
||||
updateConversationMessageCount(conversationId);
|
||||
|
||||
log.info("消息创建成功: {}", message.getId());
|
||||
return message;
|
||||
} catch (Exception e) {
|
||||
log.error("创建消息失败: {}", e.getMessage());
|
||||
throw new RuntimeException("创建消息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据对话ID查询消息列表
|
||||
*/
|
||||
public List<Message> findByConversationId(String conversationId) {
|
||||
try {
|
||||
String sql = "SELECT * FROM message WHERE conversation_id = ? AND is_deleted = 0 ORDER BY send_time ASC";
|
||||
return jdbcTemplate.query(sql, new MessageRowMapper(), conversationId);
|
||||
} catch (Exception e) {
|
||||
log.error("根据对话ID查询消息列表失败: {}", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询消息
|
||||
*/
|
||||
public Message findById(String id) {
|
||||
try {
|
||||
String sql = "SELECT * FROM message WHERE id = ? AND is_deleted = 0";
|
||||
List<Message> messages = jdbcTemplate.query(sql, new MessageRowMapper(), id);
|
||||
return messages.isEmpty() ? null : messages.get(0);
|
||||
} catch (Exception e) {
|
||||
log.error("根据ID查询消息失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Message createMessage(String conversationId, String userId, String content,
|
||||
String contentType, String senderType, String senderId);
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
*/
|
||||
public boolean markAsRead(String messageId) {
|
||||
try {
|
||||
String sql = "UPDATE message SET is_read = 1, update_time = ? WHERE id = ? AND is_deleted = 0";
|
||||
int rows = jdbcTemplate.update(sql, LocalDateTime.now(), messageId);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("标记消息为已读失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话的消息数量
|
||||
*/
|
||||
private void updateConversationMessageCount(String conversationId) {
|
||||
try {
|
||||
String sql = "SELECT COUNT(*) FROM message WHERE conversation_id = ? AND is_deleted = 0";
|
||||
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, conversationId);
|
||||
if (count != null) {
|
||||
conversationService.updateMessageCount(conversationId, count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("更新对话消息数量失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
boolean markAsRead(String messageId);
|
||||
}
|
||||
|
||||
@@ -1,193 +1,108 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.entity.User;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 用户服务
|
||||
* 用户服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-22
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(UserService.class);
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
public interface UserService extends IService<User> {
|
||||
|
||||
/**
|
||||
* 用户行映射器
|
||||
* 分页查询用户
|
||||
*/
|
||||
private static class UserRowMapper implements RowMapper<User> {
|
||||
@Override
|
||||
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
User user = new User();
|
||||
user.setId(rs.getString("id"));
|
||||
user.setUsername(rs.getString("username"));
|
||||
user.setAccount(rs.getString("account"));
|
||||
user.setPassword(rs.getString("password"));
|
||||
user.setEmail(rs.getString("email"));
|
||||
user.setPhone(rs.getString("phone"));
|
||||
user.setNickname(rs.getString("nickname"));
|
||||
user.setAvatar(rs.getString("avatar"));
|
||||
user.setGender(rs.getInt("gender"));
|
||||
user.setBio(rs.getString("bio"));
|
||||
user.setMemberLevel(rs.getString("member_level"));
|
||||
user.setTotalDays(rs.getInt("total_days"));
|
||||
user.setStatus(rs.getInt("status"));
|
||||
user.setIsVerified(rs.getInt("is_verified"));
|
||||
user.setCreateTime(rs.getTimestamp("create_time") != null ?
|
||||
rs.getTimestamp("create_time").toLocalDateTime() : null);
|
||||
user.setUpdateTime(rs.getTimestamp("update_time") != null ?
|
||||
rs.getTimestamp("update_time").toLocalDateTime() : null);
|
||||
user.setLastActiveTime(rs.getTimestamp("last_active_time") != null ?
|
||||
rs.getTimestamp("last_active_time").toLocalDateTime() : null);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
IPage<User> getPage(BasePageRequest request);
|
||||
|
||||
/**
|
||||
* 根据账号查询用户
|
||||
*/
|
||||
public User findByAccount(String account) {
|
||||
try {
|
||||
String sql = "SELECT * FROM user WHERE account = ? AND is_deleted = 0";
|
||||
List<User> users = jdbcTemplate.query(sql, new UserRowMapper(), account);
|
||||
return users.isEmpty() ? null : users.get(0);
|
||||
} catch (Exception e) {
|
||||
log.error("根据账号查询用户失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
User getByAccount(String account);
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
* 根据用户名查询用户
|
||||
*/
|
||||
public User findById(String id) {
|
||||
try {
|
||||
String sql = "SELECT * FROM user WHERE id = ? AND is_deleted = 0";
|
||||
List<User> users = jdbcTemplate.query(sql, new UserRowMapper(), id);
|
||||
return users.isEmpty() ? null : users.get(0);
|
||||
} catch (Exception e) {
|
||||
log.error("根据ID查询用户失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
User getByUsername(String username);
|
||||
|
||||
/**
|
||||
* 根据邮箱查询用户
|
||||
*/
|
||||
User getByEmail(String email);
|
||||
|
||||
/**
|
||||
* 根据手机号查询用户
|
||||
*/
|
||||
User getByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 根据第三方平台信息查询用户
|
||||
*/
|
||||
User getByThirdParty(String thirdPartyId, String thirdPartyType);
|
||||
|
||||
/**
|
||||
* 根据状态查询用户列表
|
||||
*/
|
||||
List<User> getByStatus(Integer status);
|
||||
|
||||
/**
|
||||
* 根据会员等级查询用户列表
|
||||
*/
|
||||
List<User> getByMemberLevel(String memberLevel);
|
||||
|
||||
/**
|
||||
* 查询活跃用户(最近N天有活动)
|
||||
*/
|
||||
List<User> getActiveUsers(Integer days);
|
||||
|
||||
/**
|
||||
* 查询新注册用户(最近N天注册)
|
||||
*/
|
||||
List<User> getNewUsers(Integer days);
|
||||
|
||||
/**
|
||||
* 更新用户最后活跃时间
|
||||
*/
|
||||
boolean updateLastActiveTime(String userId, LocalDateTime lastActiveTime);
|
||||
|
||||
/**
|
||||
* 更新用户状态
|
||||
*/
|
||||
boolean updateStatus(String userId, Integer status);
|
||||
|
||||
/**
|
||||
* 更新用户使用天数
|
||||
*/
|
||||
boolean updateTotalDays(String userId, Integer totalDays);
|
||||
|
||||
/**
|
||||
* 统计指定状态的用户数量
|
||||
*/
|
||||
Long countByStatus(Integer status);
|
||||
|
||||
/**
|
||||
* 统计指定会员等级的用户数量
|
||||
*/
|
||||
Long countByMemberLevel(String memberLevel);
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
public User createUser(User user) {
|
||||
try {
|
||||
// 生成ID
|
||||
user.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
|
||||
// 加密密码
|
||||
if (user.getPassword() != null) {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
user.setStatus(1);
|
||||
user.setIsDeleted(0);
|
||||
user.setIsVerified(0);
|
||||
user.setTotalDays(0);
|
||||
user.setMemberLevel("free");
|
||||
|
||||
String sql = "INSERT INTO user (id, username, account, password, email, phone, nickname, " +
|
||||
"avatar, gender, bio, member_level, total_days, status, is_verified, " +
|
||||
"create_time, update_time, last_active_time, is_deleted) VALUES " +
|
||||
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
jdbcTemplate.update(sql,
|
||||
user.getId(), user.getUsername(), user.getAccount(), user.getPassword(),
|
||||
user.getEmail(), user.getPhone(), user.getNickname(), user.getAvatar(),
|
||||
user.getGender(), user.getBio(), user.getMemberLevel(), user.getTotalDays(),
|
||||
user.getStatus(), user.getIsVerified(), user.getCreateTime(), user.getUpdateTime(),
|
||||
user.getLastActiveTime(), user.getIsDeleted());
|
||||
|
||||
log.info("用户创建成功: {}", user.getAccount());
|
||||
return user;
|
||||
} catch (Exception e) {
|
||||
log.error("创建用户失败: {}", e.getMessage());
|
||||
throw new RuntimeException("创建用户失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
User createUser(String account, String username, String password, String email, String phone);
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
* 验证用户密码
|
||||
*/
|
||||
public boolean updateUser(User user) {
|
||||
try {
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
String sql = "UPDATE user SET username = ?, email = ?, phone = ?, nickname = ?, " +
|
||||
"avatar = ?, gender = ?, bio = ?, update_time = ? WHERE id = ? AND is_deleted = 0";
|
||||
|
||||
int rows = jdbcTemplate.update(sql,
|
||||
user.getUsername(), user.getEmail(), user.getPhone(), user.getNickname(),
|
||||
user.getAvatar(), user.getGender(), user.getBio(), user.getUpdateTime(),
|
||||
user.getId());
|
||||
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新用户失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
boolean validatePassword(String userId, String password);
|
||||
|
||||
/**
|
||||
* 更新最后活跃时间
|
||||
* 更新用户密码
|
||||
*/
|
||||
public boolean updateLastActiveTime(String userId) {
|
||||
try {
|
||||
String sql = "UPDATE user SET last_active_time = ?, update_time = ? WHERE id = ? AND is_deleted = 0";
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int rows = jdbcTemplate.update(sql, now, now, userId);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新最后活跃时间失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*/
|
||||
public boolean validatePassword(String rawPassword, String encodedPassword) {
|
||||
return passwordEncoder.matches(rawPassword, encodedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否存在
|
||||
*/
|
||||
public boolean accountExists(String account) {
|
||||
try {
|
||||
String sql = "SELECT COUNT(*) FROM user WHERE account = ? AND is_deleted = 0";
|
||||
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, account);
|
||||
return count != null && count > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("检查账号是否存在失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
boolean updatePassword(String userId, String newPassword);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.dto.websocket.ChatRequest;
|
||||
import com.emotion.dto.websocket.ConnectRequest;
|
||||
import com.emotion.dto.websocket.WebSocketMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* WebSocket服务
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WebSocketService {
|
||||
|
||||
@Autowired
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@Autowired
|
||||
private IAiService aiService;
|
||||
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private IConversationService conversationService;
|
||||
|
||||
// 在线用户管理
|
||||
private final ConcurrentHashMap<String, String> onlineUsers = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 处理聊天消息
|
||||
*/
|
||||
public void handleChatMessage(ChatRequest request, String sessionId, Principal principal) {
|
||||
try {
|
||||
log.info("处理聊天消息: {}", request);
|
||||
|
||||
// 验证请求参数
|
||||
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
|
||||
sendErrorMessage(request.getSenderId(), "消息内容不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建用户消息
|
||||
WebSocketMessage userMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(request.getConversationId())
|
||||
.type(WebSocketMessage.MessageType.TEXT)
|
||||
.content(request.getContent())
|
||||
.senderId(request.getSenderId())
|
||||
.senderType(WebSocketMessage.SenderType.valueOf(request.getSenderType().name()))
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 发送用户消息到会话
|
||||
if (request.getConversationId() != null) {
|
||||
messagingTemplate.convertAndSend("/topic/conversation/" + request.getConversationId(), userMessage);
|
||||
}
|
||||
|
||||
// 发送给用户私有队列
|
||||
messagingTemplate.convertAndSendToUser(request.getSenderId(), "/queue/messages", userMessage);
|
||||
|
||||
// 发送AI思考状态
|
||||
sendAiThinkingMessage(request.getSenderId(), request.getConversationId());
|
||||
|
||||
// 异步调用AI服务
|
||||
processAiResponse(request);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理聊天消息失败", e);
|
||||
sendErrorMessage(request.getSenderId(), "消息处理失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户连接
|
||||
*/
|
||||
public void handleUserConnect(ConnectRequest request, String sessionId, Principal principal) {
|
||||
try {
|
||||
String userId = request.getUserId();
|
||||
if (userId == null && principal != null) {
|
||||
userId = principal.getName();
|
||||
}
|
||||
if (userId == null) {
|
||||
userId = "guest_" + sessionId;
|
||||
}
|
||||
|
||||
log.info("用户连接WebSocket: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
// 记录在线用户
|
||||
onlineUsers.put(sessionId, userId);
|
||||
|
||||
// 发送连接成功消息
|
||||
WebSocketMessage connectMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.type(WebSocketMessage.MessageType.CONNECTION)
|
||||
.content("连接成功")
|
||||
.senderId("system")
|
||||
.senderType(WebSocketMessage.SenderType.SYSTEM)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", connectMessage);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户连接失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户断开连接
|
||||
*/
|
||||
public void handleUserDisconnect(String sessionId, Principal principal) {
|
||||
try {
|
||||
String userId = onlineUsers.remove(sessionId);
|
||||
log.info("用户断开WebSocket连接: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户断开连接失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心跳消息
|
||||
*/
|
||||
public void handleHeartbeat(String sessionId, Principal principal) {
|
||||
try {
|
||||
String userId = onlineUsers.get(sessionId);
|
||||
if (userId == null && principal != null) {
|
||||
userId = principal.getName();
|
||||
}
|
||||
|
||||
// 发送心跳响应
|
||||
WebSocketMessage heartbeatMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.type(WebSocketMessage.MessageType.HEARTBEAT)
|
||||
.content("pong")
|
||||
.senderId("system")
|
||||
.senderType(WebSocketMessage.SenderType.SYSTEM)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
if (userId != null) {
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", heartbeatMessage);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理心跳消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送AI思考状态消息
|
||||
*/
|
||||
private void sendAiThinkingMessage(String userId, String conversationId) {
|
||||
WebSocketMessage thinkingMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(conversationId)
|
||||
.type(WebSocketMessage.MessageType.AI_THINKING)
|
||||
.content("AI正在思考中...")
|
||||
.senderId("ai")
|
||||
.senderType(WebSocketMessage.SenderType.AI)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", thinkingMessage);
|
||||
|
||||
if (conversationId != null) {
|
||||
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, thinkingMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步处理AI响应
|
||||
*/
|
||||
private void processAiResponse(ChatRequest request) {
|
||||
// 使用线程池异步处理AI响应
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// 保存用户消息到数据库
|
||||
messageService.saveMessage(
|
||||
request.getConversationId(),
|
||||
request.getContent(),
|
||||
request.getMessageType().name(),
|
||||
request.getSenderType().name()
|
||||
);
|
||||
|
||||
// 调用AI服务
|
||||
String aiReply = aiService.sendChatMessage(
|
||||
request.getConversationId(),
|
||||
request.getContent(),
|
||||
request.getSenderId()
|
||||
);
|
||||
|
||||
// 构建AI回复消息
|
||||
WebSocketMessage aiMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(request.getConversationId())
|
||||
.type(WebSocketMessage.MessageType.TEXT)
|
||||
.content(aiReply)
|
||||
.senderId("ai")
|
||||
.senderType(WebSocketMessage.SenderType.AI)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 保存AI回复到数据库
|
||||
messageService.saveMessage(
|
||||
request.getConversationId(),
|
||||
aiReply,
|
||||
"text",
|
||||
"assistant"
|
||||
);
|
||||
|
||||
// 发送AI回复
|
||||
messagingTemplate.convertAndSendToUser(request.getSenderId(), "/queue/messages", aiMessage);
|
||||
|
||||
if (request.getConversationId() != null) {
|
||||
messagingTemplate.convertAndSend("/topic/conversation/" + request.getConversationId(), aiMessage);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI响应处理失败", e);
|
||||
sendErrorMessage(request.getSenderId(), "AI服务暂时不可用,请稍后重试");
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误消息
|
||||
*/
|
||||
private void sendErrorMessage(String userId, String errorContent) {
|
||||
WebSocketMessage errorMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.type(WebSocketMessage.MessageType.ERROR)
|
||||
.content(errorContent)
|
||||
.senderId("system")
|
||||
.senderType(WebSocketMessage.SenderType.SYSTEM)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线用户数量
|
||||
*/
|
||||
public int getOnlineUserCount() {
|
||||
return onlineUsers.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.entity.Message;
|
||||
import com.emotion.service.IAiService;
|
||||
import com.emotion.service.IMessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI服务实现类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiServiceImpl implements IAiService {
|
||||
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
// Coze平台配置 - 对话聊天
|
||||
@Value("${coze.api.token}")
|
||||
private String cozeApiToken;
|
||||
|
||||
@Value("${coze.api.base-url:https://api.coze.cn}")
|
||||
private String cozeBaseUrl;
|
||||
|
||||
@Value("${coze.api.chat.bot-id}")
|
||||
private String chatBotId;
|
||||
|
||||
// Coze平台配置 - 聊天记录总结
|
||||
@Value("${coze.api.summary.bot-id}")
|
||||
private String summaryBotId;
|
||||
|
||||
public AiServiceImpl() {
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sendChatMessage(String conversationId, String message, String userId) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
log.info("发送聊天消息到AI: conversationId={}, userId={}, message={}",
|
||||
conversationId, userId, message);
|
||||
|
||||
// 构建聊天请求数据
|
||||
Map<String, Object> requestData = buildChatRequestData(conversationId, message, userId);
|
||||
|
||||
// 发送请求到Coze API
|
||||
String response = sendToCozeApi(requestData, chatBotId);
|
||||
|
||||
// 解析响应
|
||||
String aiReply = parseCozeResponse(response);
|
||||
|
||||
log.info("AI聊天回复成功: conversationId={}, 耗时={}ms, 回复长度={}",
|
||||
conversationId, System.currentTimeMillis() - startTime, aiReply.length());
|
||||
|
||||
return aiReply;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI聊天服务调用失败: conversationId={}, error={}", conversationId, e.getMessage(), e);
|
||||
return "抱歉,我现在无法回复您的消息,请稍后再试。";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateConversationSummary(String conversationId, String userId) {
|
||||
try {
|
||||
log.info("生成对话总结: conversationId={}, userId={}", conversationId, userId);
|
||||
|
||||
// 获取消息记录(限制数量避免token过多)
|
||||
List<Message> messages = messageService.getByConversationIdForSummary(conversationId, 100);
|
||||
|
||||
if (messages.isEmpty()) {
|
||||
return "暂无对话记录可供总结。";
|
||||
}
|
||||
|
||||
return generateSummaryFromRecords(messages, userId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成对话总结失败: conversationId={}, error={}", conversationId, e.getMessage(), e);
|
||||
return "对话总结生成失败,请稍后再试。";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummaryFromRecords(List<Message> messages, String userId) {
|
||||
try {
|
||||
if (messages.isEmpty()) {
|
||||
return "暂无对话记录可供总结。";
|
||||
}
|
||||
|
||||
// 构建对话历史文本
|
||||
String conversationText = buildConversationText(messages);
|
||||
|
||||
// 构建总结请求数据
|
||||
Map<String, Object> requestData = buildSummaryRequestData(conversationText, userId);
|
||||
|
||||
// 发送请求到Coze API
|
||||
String response = sendToCozeApi(requestData, summaryBotId);
|
||||
|
||||
// 解析响应
|
||||
String summary = parseCozeResponse(response);
|
||||
|
||||
log.info("对话总结生成成功: userId={}, 记录数量={}, 总结长度={}",
|
||||
userId, messages.size(), summary.length());
|
||||
|
||||
return summary;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("根据记录生成总结失败: userId={}, error={}", userId, e.getMessage(), e);
|
||||
return "对话总结生成失败,请稍后再试。";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isServiceAvailable() {
|
||||
try {
|
||||
// 简单的健康检查
|
||||
return StringUtils.hasText(cozeApiToken) &&
|
||||
StringUtils.hasText(chatBotId) &&
|
||||
StringUtils.hasText(summaryBotId);
|
||||
} catch (Exception e) {
|
||||
log.error("AI服务可用性检查失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceStatus() {
|
||||
try {
|
||||
boolean available = isServiceAvailable();
|
||||
return String.format("AI服务状态: %s, 聊天Bot: %s, 总结Bot: %s",
|
||||
available ? "可用" : "不可用", chatBotId, summaryBotId);
|
||||
} catch (Exception e) {
|
||||
return "AI服务状态检查失败: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建聊天请求数据
|
||||
*/
|
||||
private Map<String, Object> buildChatRequestData(String conversationId, String message, String userId) {
|
||||
Map<String, Object> requestData = new HashMap<>();
|
||||
requestData.put("bot_id", chatBotId);
|
||||
requestData.put("user_id", userId);
|
||||
requestData.put("query", message);
|
||||
requestData.put("stream", false);
|
||||
|
||||
if (StringUtils.hasText(conversationId)) {
|
||||
requestData.put("conversation_id", conversationId);
|
||||
}
|
||||
|
||||
return requestData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建总结请求数据
|
||||
*/
|
||||
private Map<String, Object> buildSummaryRequestData(String conversationText, String userId) {
|
||||
Map<String, Object> requestData = new HashMap<>();
|
||||
requestData.put("bot_id", summaryBotId);
|
||||
requestData.put("user_id", userId);
|
||||
requestData.put("query", "请对以下对话内容进行总结,提取关键信息和主要话题:\n\n" + conversationText);
|
||||
requestData.put("stream", false);
|
||||
|
||||
return requestData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求到Coze API
|
||||
*/
|
||||
private String sendToCozeApi(Map<String, Object> requestData, String botId) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(cozeApiToken);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestData, headers);
|
||||
|
||||
String url = cozeBaseUrl + "/v3/chat";
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK) {
|
||||
return response.getBody();
|
||||
} else {
|
||||
throw new RuntimeException("Coze API调用失败: " + response.getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Coze响应
|
||||
*/
|
||||
private String parseCozeResponse(String response) {
|
||||
try {
|
||||
JSONObject jsonResponse = JSON.parseObject(response);
|
||||
|
||||
if (jsonResponse.getInteger("code") == 0) {
|
||||
JSONArray messages = jsonResponse.getJSONArray("messages");
|
||||
if (messages != null && !messages.isEmpty()) {
|
||||
JSONObject lastMessage = messages.getJSONObject(messages.size() - 1);
|
||||
return lastMessage.getString("content");
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("Coze响应解析异常: {}", response);
|
||||
return "AI服务响应异常,请稍后再试。";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析Coze响应失败", e);
|
||||
return "AI服务响应解析失败,请稍后再试。";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建对话历史文本
|
||||
*/
|
||||
private String buildConversationText(List<Message> messages) {
|
||||
return messages.stream()
|
||||
.map(message -> {
|
||||
String senderName = "user".equals(message.getSender()) ? "用户" : "AI助手";
|
||||
return senderName + ": " + message.getContent();
|
||||
})
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.entity.CozeApiCall;
|
||||
import com.emotion.mapper.CozeApiCallMapper;
|
||||
import com.emotion.service.ICozeApiCallService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Coze API调用记录服务实现类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class CozeApiCallServiceImpl extends ServiceImpl<CozeApiCallMapper, CozeApiCall> implements ICozeApiCallService {
|
||||
|
||||
@Override
|
||||
public IPage<CozeApiCall> getByConversationId(Page<CozeApiCall> page, String conversationId) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getConversationId, conversationId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.orderByDesc(CozeApiCall::getStartTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<CozeApiCall> getByUserId(Page<CozeApiCall> page, String userId) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getUserId, userId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.orderByDesc(CozeApiCall::getStartTime);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CozeApiCall> getByBotId(String botId) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getBotId, botId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.orderByDesc(CozeApiCall::getStartTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CozeApiCall> getByStatus(String status) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getStatus, status)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.orderByDesc(CozeApiCall::getStartTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CozeApiCall> getByRequestType(String requestType) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getRequestType, requestType)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.orderByDesc(CozeApiCall::getStartTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CozeApiCall> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.between(CozeApiCall::getStartTime, startTime, endTime)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.orderByDesc(CozeApiCall::getStartTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByUserId(String userId) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getUserId, userId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByBotId(String botId) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getBotId, botId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByStatus(String status) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getStatus, status)
|
||||
.eq(CozeApiCall::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long sumTokensByUserId(String userId) {
|
||||
// 使用原生SQL或者查询后计算
|
||||
List<CozeApiCall> calls = this.list(new LambdaQueryWrapper<CozeApiCall>()
|
||||
.eq(CozeApiCall::getUserId, userId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.isNotNull(CozeApiCall::getTotalTokens));
|
||||
return calls.stream().mapToLong(call -> call.getTotalTokens() != null ? call.getTotalTokens() : 0).sum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal sumCostByUserId(String userId) {
|
||||
List<CozeApiCall> calls = this.list(new LambdaQueryWrapper<CozeApiCall>()
|
||||
.eq(CozeApiCall::getUserId, userId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.isNotNull(CozeApiCall::getCost));
|
||||
return calls.stream()
|
||||
.map(call -> call.getCost() != null ? call.getCost() : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CozeApiCall> getFailedCalls() {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.and(w -> w.eq(CozeApiCall::getStatus, "failed").or().eq(CozeApiCall::getFinalStatus, "failed"))
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.orderByDesc(CozeApiCall::getStartTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CozeApiCall> getTimeoutCalls() {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.and(w -> w.eq(CozeApiCall::getStatus, "timeout").or().eq(CozeApiCall::getFinalStatus, "timeout"))
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.orderByDesc(CozeApiCall::getStartTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CozeApiCall getByTraceId(String traceId) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getTraceId, traceId)
|
||||
.eq(CozeApiCall::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CozeApiCall> getByConversationIdAndRequestType(String conversationId, String requestType) {
|
||||
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getConversationId, conversationId)
|
||||
.eq(CozeApiCall::getRequestType, requestType)
|
||||
.eq(CozeApiCall::getIsDeleted, 0)
|
||||
.orderByDesc(CozeApiCall::getStartTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(String id, String status, String finalStatus, LocalDateTime endTime) {
|
||||
LambdaUpdateWrapper<CozeApiCall> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getId, id)
|
||||
.set(CozeApiCall::getStatus, status)
|
||||
.set(CozeApiCall::getFinalStatus, finalStatus)
|
||||
.set(CozeApiCall::getEndTime, endTime)
|
||||
.set(CozeApiCall::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateResult(String id, Integer responseStatus, String responseBody, String aiReply,
|
||||
Integer totalTokens, BigDecimal cost, String status, String finalStatus,
|
||||
LocalDateTime endTime) {
|
||||
LambdaUpdateWrapper<CozeApiCall> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(CozeApiCall::getId, id)
|
||||
.set(CozeApiCall::getResponseStatus, responseStatus)
|
||||
.set(CozeApiCall::getResponseBody, responseBody)
|
||||
.set(CozeApiCall::getAiReply, aiReply)
|
||||
.set(CozeApiCall::getTotalTokens, totalTokens)
|
||||
.set(CozeApiCall::getCost, cost)
|
||||
.set(CozeApiCall::getStatus, status)
|
||||
.set(CozeApiCall::getFinalStatus, finalStatus)
|
||||
.set(CozeApiCall::getEndTime, endTime)
|
||||
.set(CozeApiCall::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CozeApiCall createApiCall(String conversationId, String messageId, String userId,
|
||||
String requestType, String requestUrl, String requestBody) {
|
||||
CozeApiCall apiCall = CozeApiCall.builder()
|
||||
.conversationId(conversationId)
|
||||
.messageId(messageId)
|
||||
.userId(userId)
|
||||
.requestType(requestType)
|
||||
.requestUrl(requestUrl)
|
||||
.requestBody(requestBody)
|
||||
.status("pending")
|
||||
.startTime(LocalDateTime.now())
|
||||
.build();
|
||||
this.save(apiCall);
|
||||
return apiCall;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.entity.Message;
|
||||
import com.emotion.mapper.MessageMapper;
|
||||
import com.emotion.service.MessageService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 消息服务实现类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements MessageService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MessageServiceImpl.class);
|
||||
|
||||
@Override
|
||||
public IPage<Message> getPage(BasePageRequest request) {
|
||||
Page<Message> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.like(Message::getContent, request.getKeyword());
|
||||
}
|
||||
|
||||
wrapper.eq(Message::getIsDeleted, 0);
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(Message::getTimestamp);
|
||||
} else {
|
||||
wrapper.orderByDesc(Message::getTimestamp);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(Message::getTimestamp);
|
||||
}
|
||||
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<Message> getPageByConversationId(BasePageRequest request, String conversationId) {
|
||||
Page<Message> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsDeleted, 0);
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.like(Message::getContent, request.getKeyword());
|
||||
}
|
||||
|
||||
wrapper.orderByAsc(Message::getTimestamp);
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getByConversationId(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByAsc(Message::getTimestamp);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getBySender(String sender) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getSender, sender)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByDesc(Message::getTimestamp);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.between(Message::getTimestamp, startTime, endTime)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByAsc(Message::getTimestamp);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message getLastMessageByConversationId(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByDesc(Message::getTimestamp)
|
||||
.last("LIMIT 1");
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getRepliesByParentId(String parentMessageId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getParentMessageId, parentMessageId)
|
||||
.eq(Message::getIsDeleted, 0)
|
||||
.orderByAsc(Message::getTimestamp);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByConversationId(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countBySender(String sender) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getSender, sender)
|
||||
.eq(Message::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countUnreadMessages(String conversationId) {
|
||||
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsRead, 0)
|
||||
.eq(Message::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(String messageId, String status) {
|
||||
LambdaUpdateWrapper<Message> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(Message::getId, messageId)
|
||||
.set(Message::getStatus, status)
|
||||
.set(Message::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateReadStatus(String messageId, Integer isRead) {
|
||||
LambdaUpdateWrapper<Message> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(Message::getId, messageId)
|
||||
.set(Message::getIsRead, isRead)
|
||||
.set(Message::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markConversationMessagesAsRead(String conversationId) {
|
||||
LambdaUpdateWrapper<Message> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(Message::getConversationId, conversationId)
|
||||
.eq(Message::getIsRead, 0)
|
||||
.set(Message::getIsRead, 1)
|
||||
.set(Message::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message createMessage(String conversationId, String userId, String content,
|
||||
String contentType, String senderType, String senderId) {
|
||||
try {
|
||||
Message message = Message.builder()
|
||||
.id(UUID.randomUUID().toString())
|
||||
.conversationId(conversationId)
|
||||
.userId(userId)
|
||||
.content(content)
|
||||
.contentType(StringUtils.hasText(contentType) ? contentType : "text")
|
||||
.senderType(senderType)
|
||||
.sender(senderId)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.status("sent")
|
||||
.isRead(0)
|
||||
.build();
|
||||
|
||||
boolean saved = this.save(message);
|
||||
if (saved) {
|
||||
log.info("保存消息成功: id={}, conversationId={}, sender={}",
|
||||
message.getId(), conversationId, senderId);
|
||||
return message;
|
||||
} else {
|
||||
log.error("保存消息失败: conversationId={}, sender={}", conversationId, senderId);
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("保存消息异常: conversationId={}, error={}", conversationId, e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markAsRead(String messageId) {
|
||||
return updateReadStatus(messageId, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package com.emotion.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import com.emotion.entity.User;
|
||||
import com.emotion.mapper.UserMapper;
|
||||
import com.emotion.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户服务实现类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Service
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public IPage<User> getPage(BasePageRequest request) {
|
||||
Page<User> page = new Page<>(request.getCurrent(), request.getSize());
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(User::getUsername, request.getKeyword())
|
||||
.or().like(User::getAccount, request.getKeyword())
|
||||
.or().like(User::getEmail, request.getKeyword())
|
||||
.or().like(User::getPhone, request.getKeyword()));
|
||||
}
|
||||
|
||||
wrapper.eq(User::getIsDeleted, 0);
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(request.getOrderBy())) {
|
||||
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
|
||||
wrapper.orderByAsc(getColumnByField(request.getOrderBy()));
|
||||
} else {
|
||||
wrapper.orderByDesc(getColumnByField(request.getOrderBy()));
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(User::getCreateTime);
|
||||
}
|
||||
|
||||
return this.page(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByAccount(String account) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getAccount, account)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByUsername(String username) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getUsername, username)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByEmail(String email) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getEmail, email)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByPhone(String phone) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getPhone, phone)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByThirdParty(String thirdPartyId, String thirdPartyType) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getThirdPartyId, thirdPartyId)
|
||||
.eq(User::getThirdPartyType, thirdPartyType)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> getByStatus(Integer status) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getStatus, status)
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.orderByDesc(User::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> getByMemberLevel(String memberLevel) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getMemberLevel, memberLevel)
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.orderByDesc(User::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> getActiveUsers(Integer days) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.ge(User::getLastActiveTime, LocalDateTime.now().minusDays(days))
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.orderByDesc(User::getLastActiveTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> getNewUsers(Integer days) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.ge(User::getCreateTime, LocalDateTime.now().minusDays(days))
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.orderByDesc(User::getCreateTime);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateLastActiveTime(String userId, LocalDateTime lastActiveTime) {
|
||||
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(User::getId, userId)
|
||||
.set(User::getLastActiveTime, lastActiveTime)
|
||||
.set(User::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(String userId, Integer status) {
|
||||
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(User::getId, userId)
|
||||
.set(User::getStatus, status)
|
||||
.set(User::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateTotalDays(String userId, Integer totalDays) {
|
||||
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(User::getId, userId)
|
||||
.set(User::getTotalDays, totalDays)
|
||||
.set(User::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByStatus(Integer status) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getStatus, status)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countByMemberLevel(String memberLevel) {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getMemberLevel, memberLevel)
|
||||
.eq(User::getIsDeleted, 0);
|
||||
return this.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User createUser(String account, String username, String password, String email, String phone) {
|
||||
User user = User.builder()
|
||||
.account(account)
|
||||
.username(username)
|
||||
.password(passwordEncoder.encode(password))
|
||||
.email(email)
|
||||
.phone(phone)
|
||||
.status(1)
|
||||
.memberLevel("basic")
|
||||
.totalDays(0)
|
||||
.lastActiveTime(LocalDateTime.now())
|
||||
.build();
|
||||
this.save(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validatePassword(String userId, String password) {
|
||||
User user = this.getById(userId);
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
return passwordEncoder.matches(password, user.getPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updatePassword(String userId, String newPassword) {
|
||||
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(User::getId, userId)
|
||||
.set(User::getPassword, passwordEncoder.encode(newPassword))
|
||||
.set(User::getUpdateTime, LocalDateTime.now());
|
||||
return this.update(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字段名获取对应的数据库列
|
||||
*/
|
||||
private String getColumnByField(String field) {
|
||||
// 这里可以根据需要扩展更多字段映射
|
||||
switch (field) {
|
||||
case "createTime":
|
||||
return "create_time";
|
||||
case "updateTime":
|
||||
return "update_time";
|
||||
case "lastActiveTime":
|
||||
return "last_active_time";
|
||||
default:
|
||||
return field;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.emotion.util;
|
||||
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* JWT工具类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);
|
||||
|
||||
/**
|
||||
* JWT密钥
|
||||
*/
|
||||
@Value("${emotion.jwt.secret:emotion-museum-secret-key-2025}")
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* JWT过期时间(毫秒)
|
||||
*/
|
||||
@Value("${emotion.jwt.expiration:86400000}")
|
||||
private Long expiration;
|
||||
|
||||
/**
|
||||
* 刷新Token过期时间(毫秒)
|
||||
*/
|
||||
@Value("${emotion.jwt.refresh-expiration:604800000}")
|
||||
private Long refreshExpiration;
|
||||
|
||||
/**
|
||||
* 获取密钥
|
||||
*/
|
||||
private SecretKey getSecretKey() {
|
||||
return Keys.hmacShaKeyFor(secret.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Token
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @return Token
|
||||
*/
|
||||
public String generateToken(String userId, String username) {
|
||||
return generateToken(userId, username, expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成刷新Token
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @return 刷新Token
|
||||
*/
|
||||
public String generateRefreshToken(String userId, String username) {
|
||||
return generateToken(userId, username, refreshExpiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Token
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @param expiration 过期时间(毫秒)
|
||||
* @return Token
|
||||
*/
|
||||
private String generateToken(String userId, String username, Long expiration) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
|
||||
return Jwts.builder()
|
||||
.setSubject(userId)
|
||||
.claim("username", username)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSecretKey(), SignatureAlgorithm.HS512)
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取用户ID
|
||||
*
|
||||
* @param token Token
|
||||
* @return 用户ID
|
||||
*/
|
||||
public String getUserIdFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.getSubject() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取用户名
|
||||
*
|
||||
* @param token Token
|
||||
* @return 用户名
|
||||
*/
|
||||
public String getUsernameFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.get("username", String.class) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取过期时间
|
||||
*
|
||||
* @param token Token
|
||||
* @return 过期时间
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.getExpiration() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取Claims
|
||||
*
|
||||
* @param token Token
|
||||
* @return Claims
|
||||
*/
|
||||
private Claims getClaimsFromToken(String token) {
|
||||
try {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(getSecretKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
} catch (Exception e) {
|
||||
log.warn("解析Token失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token是否有效
|
||||
*
|
||||
* @param token Token
|
||||
* @return 是否有效
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
if (token == null || token.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
if (claims == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
Date expiration = claims.getExpiration();
|
||||
return expiration != null && expiration.after(new Date());
|
||||
} catch (Exception e) {
|
||||
log.warn("Token验证失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Token是否过期
|
||||
*
|
||||
* @param token Token
|
||||
* @return 是否过期
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration != null && expiration.before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*
|
||||
* @param token 原Token
|
||||
* @return 新Token
|
||||
*/
|
||||
public String refreshToken(String token) {
|
||||
try {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
if (claims == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String userId = claims.getSubject();
|
||||
String username = claims.get("username", String.class);
|
||||
|
||||
return generateToken(userId, username);
|
||||
} catch (Exception e) {
|
||||
log.warn("刷新Token失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package com.emotion.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 雪花算法ID生成器
|
||||
* 生成64位长整型ID,转换为字符串避免前端精度丢失问题
|
||||
*
|
||||
* 雪花算法结构:
|
||||
* 1位符号位(固定为0) + 41位时间戳 + 10位机器ID + 12位序列号
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
public class SnowflakeIdGenerator {
|
||||
|
||||
/**
|
||||
* 起始时间戳 (2024-01-01 00:00:00)
|
||||
*/
|
||||
private static final long START_TIMESTAMP = 1704067200000L;
|
||||
|
||||
/**
|
||||
* 机器ID位数
|
||||
*/
|
||||
private static final long MACHINE_ID_BITS = 10L;
|
||||
|
||||
/**
|
||||
* 序列号位数
|
||||
*/
|
||||
private static final long SEQUENCE_BITS = 12L;
|
||||
|
||||
/**
|
||||
* 机器ID最大值
|
||||
*/
|
||||
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
|
||||
|
||||
/**
|
||||
* 序列号最大值
|
||||
*/
|
||||
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
|
||||
|
||||
/**
|
||||
* 机器ID左移位数
|
||||
*/
|
||||
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
|
||||
|
||||
/**
|
||||
* 时间戳左移位数
|
||||
*/
|
||||
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
|
||||
|
||||
/**
|
||||
* 机器ID
|
||||
*/
|
||||
private final long machineId;
|
||||
|
||||
/**
|
||||
* 序列号
|
||||
*/
|
||||
private long sequence = 0L;
|
||||
|
||||
/**
|
||||
* 上次生成ID的时间戳
|
||||
*/
|
||||
private long lastTimestamp = -1L;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param machineId 机器ID (0-1023)
|
||||
*/
|
||||
public SnowflakeIdGenerator(long machineId) {
|
||||
if (machineId > MAX_MACHINE_ID || machineId < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("机器ID必须在0到%d之间", MAX_MACHINE_ID));
|
||||
}
|
||||
this.machineId = machineId;
|
||||
log.info("雪花算法ID生成器初始化完成,机器ID: {}", machineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认构造函数,使用默认机器ID
|
||||
*/
|
||||
public SnowflakeIdGenerator() {
|
||||
// 使用当前时间戳的后10位作为默认机器ID
|
||||
this(System.currentTimeMillis() % (MAX_MACHINE_ID + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成下一个ID
|
||||
*
|
||||
* @return 生成的ID
|
||||
*/
|
||||
public synchronized long nextId() {
|
||||
long timestamp = getCurrentTimestamp();
|
||||
|
||||
// 如果当前时间小于上次ID生成的时间戳,说明系统时钟回退过,抛出异常
|
||||
if (timestamp < lastTimestamp) {
|
||||
throw new RuntimeException(
|
||||
String.format("系统时钟回退,拒绝生成ID。当前时间戳: %d, 上次时间戳: %d",
|
||||
timestamp, lastTimestamp));
|
||||
}
|
||||
|
||||
// 如果是同一时间戳,则在序列号上自增
|
||||
if (lastTimestamp == timestamp) {
|
||||
sequence = (sequence + 1) & MAX_SEQUENCE;
|
||||
// 如果序列号溢出,则等待下一个毫秒
|
||||
if (sequence == 0) {
|
||||
timestamp = getNextTimestamp(lastTimestamp);
|
||||
}
|
||||
} else {
|
||||
// 如果是新的时间戳,则序列号重置为0
|
||||
sequence = 0L;
|
||||
}
|
||||
|
||||
// 更新上次生成ID的时间戳
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
// 移位并通过或运算拼到一起组成64位的ID
|
||||
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
|
||||
| (machineId << MACHINE_ID_SHIFT)
|
||||
| sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成字符串格式的ID
|
||||
*
|
||||
* @return 字符串格式的ID
|
||||
*/
|
||||
public String nextIdAsString() {
|
||||
return String.valueOf(nextId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳
|
||||
*
|
||||
* @return 当前时间戳
|
||||
*/
|
||||
private long getCurrentTimestamp() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个时间戳
|
||||
*
|
||||
* @param lastTimestamp 上次时间戳
|
||||
* @return 下一个时间戳
|
||||
*/
|
||||
private long getNextTimestamp(long lastTimestamp) {
|
||||
long timestamp = getCurrentTimestamp();
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = getCurrentTimestamp();
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析ID获取时间戳
|
||||
*
|
||||
* @param id 雪花算法生成的ID
|
||||
* @return 时间戳
|
||||
*/
|
||||
public long parseTimestamp(long id) {
|
||||
return (id >> TIMESTAMP_SHIFT) + START_TIMESTAMP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析ID获取机器ID
|
||||
*
|
||||
* @param id 雪花算法生成的ID
|
||||
* @return 机器ID
|
||||
*/
|
||||
public long parseMachineId(long id) {
|
||||
return (id >> MACHINE_ID_SHIFT) & MAX_MACHINE_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析ID获取序列号
|
||||
*
|
||||
* @param id 雪花算法生成的ID
|
||||
* @return 序列号
|
||||
*/
|
||||
public long parseSequence(long id) {
|
||||
return id & MAX_SEQUENCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器ID
|
||||
*
|
||||
* @return 机器ID
|
||||
*/
|
||||
public long getMachineId() {
|
||||
return machineId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成ID
|
||||
*
|
||||
* @param count 生成数量
|
||||
* @return ID数组
|
||||
*/
|
||||
public long[] nextIds(int count) {
|
||||
if (count <= 0) {
|
||||
throw new IllegalArgumentException("生成数量必须大于0");
|
||||
}
|
||||
|
||||
long[] ids = new long[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
ids[i] = nextId();
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成字符串格式的ID
|
||||
*
|
||||
* @param count 生成数量
|
||||
* @return 字符串ID数组
|
||||
*/
|
||||
public String[] nextIdsAsString(int count) {
|
||||
if (count <= 0) {
|
||||
throw new IllegalArgumentException("生成数量必须大于0");
|
||||
}
|
||||
|
||||
String[] ids = new String[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
ids[i] = nextIdAsString();
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.emotion.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 用户上下文持有者
|
||||
* 用于在当前线程中存储用户信息
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserContextHolder {
|
||||
|
||||
/**
|
||||
* 用户ID线程本地变量
|
||||
*/
|
||||
private static final ThreadLocal<String> USER_ID_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 用户名线程本地变量
|
||||
*/
|
||||
private static final ThreadLocal<String> USERNAME_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 用户类型线程本地变量
|
||||
*/
|
||||
private static final ThreadLocal<String> USER_TYPE_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 客户端IP线程本地变量
|
||||
*/
|
||||
private static final ThreadLocal<String> CLIENT_IP_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 请求ID线程本地变量
|
||||
*/
|
||||
private static final ThreadLocal<String> REQUEST_ID_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 设置当前用户ID
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public static void setCurrentUserId(String userId) {
|
||||
USER_ID_HOLDER.set(userId);
|
||||
log.debug("设置当前用户ID: {}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*
|
||||
* @return 用户ID
|
||||
*/
|
||||
public static String getCurrentUserId() {
|
||||
return USER_ID_HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前用户名
|
||||
*
|
||||
* @param username 用户名
|
||||
*/
|
||||
public static void setCurrentUsername(String username) {
|
||||
USERNAME_HOLDER.set(username);
|
||||
log.debug("设置当前用户名: {}", username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户名
|
||||
*
|
||||
* @return 用户名
|
||||
*/
|
||||
public static String getCurrentUsername() {
|
||||
return USERNAME_HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前用户类型
|
||||
*
|
||||
* @param userType 用户类型
|
||||
*/
|
||||
public static void setCurrentUserType(String userType) {
|
||||
USER_TYPE_HOLDER.set(userType);
|
||||
log.debug("设置当前用户类型: {}", userType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户类型
|
||||
*
|
||||
* @return 用户类型
|
||||
*/
|
||||
public static String getCurrentUserType() {
|
||||
return USER_TYPE_HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客户端IP
|
||||
*
|
||||
* @param clientIp 客户端IP
|
||||
*/
|
||||
public static void setClientIp(String clientIp) {
|
||||
CLIENT_IP_HOLDER.set(clientIp);
|
||||
log.debug("设置客户端IP: {}", clientIp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*
|
||||
* @return 客户端IP
|
||||
*/
|
||||
public static String getClientIp() {
|
||||
return CLIENT_IP_HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求ID
|
||||
*
|
||||
* @param requestId 请求ID
|
||||
*/
|
||||
public static void setRequestId(String requestId) {
|
||||
REQUEST_ID_HOLDER.set(requestId);
|
||||
log.debug("设置请求ID: {}", requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求ID
|
||||
*
|
||||
* @return 请求ID
|
||||
*/
|
||||
public static String getRequestId() {
|
||||
return REQUEST_ID_HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户上下文信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @param userType 用户类型
|
||||
* @param clientIp 客户端IP
|
||||
* @param requestId 请求ID
|
||||
*/
|
||||
public static void setUserContext(String userId, String username, String userType,
|
||||
String clientIp, String requestId) {
|
||||
setCurrentUserId(userId);
|
||||
setCurrentUsername(username);
|
||||
setCurrentUserType(userType);
|
||||
setClientIp(clientIp);
|
||||
setRequestId(requestId);
|
||||
|
||||
log.debug("设置用户上下文: userId={}, username={}, userType={}, clientIp={}, requestId={}",
|
||||
userId, username, userType, clientIp, requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前用户ID
|
||||
*/
|
||||
public static void clearUserId() {
|
||||
USER_ID_HOLDER.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前用户名
|
||||
*/
|
||||
public static void clearUsername() {
|
||||
USERNAME_HOLDER.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前用户类型
|
||||
*/
|
||||
public static void clearUserType() {
|
||||
USER_TYPE_HOLDER.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除客户端IP
|
||||
*/
|
||||
public static void clearClientIp() {
|
||||
CLIENT_IP_HOLDER.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除请求ID
|
||||
*/
|
||||
public static void clearRequestId() {
|
||||
REQUEST_ID_HOLDER.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有用户上下文信息
|
||||
*/
|
||||
public static void clear() {
|
||||
clearUserId();
|
||||
clearUsername();
|
||||
clearUserType();
|
||||
clearClientIp();
|
||||
clearRequestId();
|
||||
log.debug("清除所有用户上下文信息");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户上下文摘要信息
|
||||
*
|
||||
* @return 用户上下文摘要
|
||||
*/
|
||||
public static String getContextSummary() {
|
||||
return String.format("UserContext[userId=%s, username=%s, userType=%s, clientIp=%s, requestId=%s]",
|
||||
getCurrentUserId(), getCurrentUsername(), getCurrentUserType(),
|
||||
getClientIp(), getRequestId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有用户上下文
|
||||
*
|
||||
* @return 是否有用户上下文
|
||||
*/
|
||||
public static boolean hasUserContext() {
|
||||
return getCurrentUserId() != null || getCurrentUsername() != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.emotion.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 用户上下文工具类
|
||||
* 提供便捷的用户上下文操作方法
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserContextUtils {
|
||||
|
||||
/**
|
||||
* 获取当前用户ID,如果为空则返回默认值
|
||||
*
|
||||
* @param defaultValue 默认值
|
||||
* @return 用户ID
|
||||
*/
|
||||
public static String getCurrentUserIdOrDefault(String defaultValue) {
|
||||
String userId = UserContextHolder.getCurrentUserId();
|
||||
return userId != null ? userId : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID,如果为空则返回"system"
|
||||
*
|
||||
* @return 用户ID
|
||||
*/
|
||||
public static String getCurrentUserIdOrSystem() {
|
||||
return getCurrentUserIdOrDefault("system");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户名,如果为空则返回默认值
|
||||
*
|
||||
* @param defaultValue 默认值
|
||||
* @return 用户名
|
||||
*/
|
||||
public static String getCurrentUsernameOrDefault(String defaultValue) {
|
||||
String username = UserContextHolder.getCurrentUsername();
|
||||
return username != null ? username : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户名,如果为空则返回"guest"
|
||||
*
|
||||
* @return 用户名
|
||||
*/
|
||||
public static String getCurrentUsernameOrGuest() {
|
||||
return getCurrentUsernameOrDefault("guest");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户类型,如果为空则返回默认值
|
||||
*
|
||||
* @param defaultValue 默认值
|
||||
* @return 用户类型
|
||||
*/
|
||||
public static String getCurrentUserTypeOrDefault(String defaultValue) {
|
||||
String userType = UserContextHolder.getCurrentUserType();
|
||||
return userType != null ? userType : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户类型,如果为空则返回"GUEST"
|
||||
*
|
||||
* @return 用户类型
|
||||
*/
|
||||
public static String getCurrentUserTypeOrGuest() {
|
||||
return getCurrentUserTypeOrDefault("GUEST");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP,如果为空则返回默认值
|
||||
*
|
||||
* @param defaultValue 默认值
|
||||
* @return 客户端IP
|
||||
*/
|
||||
public static String getClientIpOrDefault(String defaultValue) {
|
||||
String clientIp = UserContextHolder.getClientIp();
|
||||
return clientIp != null ? clientIp : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP,如果为空则返回"unknown"
|
||||
*
|
||||
* @return 客户端IP
|
||||
*/
|
||||
public static String getClientIpOrUnknown() {
|
||||
return getClientIpOrDefault("unknown");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求ID,如果为空则返回默认值
|
||||
*
|
||||
* @param defaultValue 默认值
|
||||
* @return 请求ID
|
||||
*/
|
||||
public static String getRequestIdOrDefault(String defaultValue) {
|
||||
String requestId = UserContextHolder.getRequestId();
|
||||
return requestId != null ? requestId : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求ID,如果为空则返回"unknown"
|
||||
*
|
||||
* @return 请求ID
|
||||
*/
|
||||
public static String getRequestIdOrUnknown() {
|
||||
return getRequestIdOrDefault("unknown");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前用户是否为访客
|
||||
*
|
||||
* @return 是否为访客
|
||||
*/
|
||||
public static boolean isGuest() {
|
||||
String userId = UserContextHolder.getCurrentUserId();
|
||||
String userType = UserContextHolder.getCurrentUserType();
|
||||
|
||||
return userId == null ||
|
||||
userId.startsWith("guest_") ||
|
||||
"GUEST".equalsIgnoreCase(userType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前用户是否为系统用户
|
||||
*
|
||||
* @return 是否为系统用户
|
||||
*/
|
||||
public static boolean isSystem() {
|
||||
String userId = UserContextHolder.getCurrentUserId();
|
||||
String userType = UserContextHolder.getCurrentUserType();
|
||||
|
||||
return "system".equals(userId) ||
|
||||
"SYSTEM".equalsIgnoreCase(userType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前用户是否为注册用户
|
||||
*
|
||||
* @return 是否为注册用户
|
||||
*/
|
||||
public static boolean isRegisteredUser() {
|
||||
return !isGuest() && !isSystem();
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地执行需要用户上下文的操作
|
||||
* 如果没有用户上下文,会设置默认的系统上下文
|
||||
*
|
||||
* @param operation 操作
|
||||
*/
|
||||
public static void executeWithContext(Runnable operation) {
|
||||
boolean hasContext = UserContextHolder.hasUserContext();
|
||||
|
||||
try {
|
||||
// 如果没有用户上下文,设置默认的系统上下文
|
||||
if (!hasContext) {
|
||||
UserContextHolder.setUserContext("system", "system", "SYSTEM", "127.0.0.1", "system");
|
||||
log.debug("设置默认系统用户上下文");
|
||||
}
|
||||
|
||||
// 执行操作
|
||||
operation.run();
|
||||
|
||||
} finally {
|
||||
// 如果是我们设置的默认上下文,执行后清理
|
||||
if (!hasContext) {
|
||||
UserContextHolder.clear();
|
||||
log.debug("清理默认系统用户上下文");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 临时设置用户上下文执行操作
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @param userType 用户类型
|
||||
* @param operation 操作
|
||||
*/
|
||||
public static void executeWithTempContext(String userId, String username, String userType, Runnable operation) {
|
||||
// 保存当前上下文
|
||||
String originalUserId = UserContextHolder.getCurrentUserId();
|
||||
String originalUsername = UserContextHolder.getCurrentUsername();
|
||||
String originalUserType = UserContextHolder.getCurrentUserType();
|
||||
String originalClientIp = UserContextHolder.getClientIp();
|
||||
String originalRequestId = UserContextHolder.getRequestId();
|
||||
|
||||
try {
|
||||
// 设置临时上下文
|
||||
UserContextHolder.setUserContext(userId, username, userType,
|
||||
originalClientIp != null ? originalClientIp : "127.0.0.1",
|
||||
originalRequestId != null ? originalRequestId : "temp");
|
||||
|
||||
// 执行操作
|
||||
operation.run();
|
||||
|
||||
} finally {
|
||||
// 恢复原始上下文
|
||||
if (originalUserId != null || originalUsername != null) {
|
||||
UserContextHolder.setUserContext(originalUserId, originalUsername, originalUserType,
|
||||
originalClientIp, originalRequestId);
|
||||
} else {
|
||||
UserContextHolder.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user