feat: 完善后端架构和service层实现

- 创建完整的entity实体类体系,包括所有业务实体
- 实现BaseEntity基类,统一管理公共字段
- 创建雪花算法ID生成器和自动填充处理器
- 简化所有mapper接口,只继承BaseMapper
- 重构service层,使用LambdaQueryWrapper进行数据库操作
- 创建BasePageRequest分页查询基类
- 完善用户上下文管理和JWT认证
- 新增WebSocket聊天功能和相关控制器
- 更新前端配置和组件,完善用户认证流程
- 同步数据库建表脚本
This commit is contained in:
2025-07-24 00:37:23 +08:00
parent 645036fcd2
commit 880e0e3c88
87 changed files with 8114 additions and 1106 deletions
@@ -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> {
}
@@ -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();
}
}
}
}