diff --git a/backend-distributed/mysql_emotion_museum_final.sql b/backend-distributed/mysql_emotion_museum_final.sql index 3e6409f..7c41677 100644 --- a/backend-distributed/mysql_emotion_museum_final.sql +++ b/backend-distributed/mysql_emotion_museum_final.sql @@ -633,8 +633,6 @@ CREATE INDEX idx_coze_api_call_start_time ON coze_api_call (start_time); CREATE INDEX idx_coze_api_call_request_type ON coze_api_call (request_type); -CREATE INDEX idx_coze_api_call_final_status ON coze_api_call (final_status); - CREATE INDEX idx_coze_api_call_client_ip ON coze_api_call (client_ip); CREATE INDEX idx_coze_api_call_session_id ON coze_api_call (session_id); @@ -853,10 +851,4 @@ ORDER BY TABLE_NAME; -- 提交事务 -COMMIT; - --- 完成消息 -SELECT - 'Emotion Museum Database v3.0 Final (雪花算法主键版本) - 开发版本 deployment completed successfully!' as message, - NOW () as completion_time, - 'All tables dropped and recreated with VARCHAR(36) primary keys. Development version - data will be lost on re-execution!' as description; \ No newline at end of file +COMMIT; \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/EmotionSimpleApplication.java b/backend-single/src/main/java/com/emotion/EmotionSimpleApplication.java index 317d1d6..ed42217 100644 --- a/backend-single/src/main/java/com/emotion/EmotionSimpleApplication.java +++ b/backend-single/src/main/java/com/emotion/EmotionSimpleApplication.java @@ -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) { diff --git a/backend-single/src/main/java/com/emotion/common/BaseEntity.java b/backend-single/src/main/java/com/emotion/common/BaseEntity.java index 5dd55cd..2c88c5a 100644 --- a/backend-single/src/main/java/com/emotion/common/BaseEntity.java +++ b/backend-single/src/main/java/com/emotion/common/BaseEntity.java @@ -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; /** diff --git a/backend-single/src/main/java/com/emotion/common/BasePageRequest.java b/backend-single/src/main/java/com/emotion/common/BasePageRequest.java new file mode 100644 index 0000000..430e39b --- /dev/null +++ b/backend-single/src/main/java/com/emotion/common/BasePageRequest.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/config/IdGeneratorConfig.java b/backend-single/src/main/java/com/emotion/config/IdGeneratorConfig.java new file mode 100644 index 0000000..485cf6d --- /dev/null +++ b/backend-single/src/main/java/com/emotion/config/IdGeneratorConfig.java @@ -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; + } +} diff --git a/backend-single/src/main/java/com/emotion/config/MybatisPlusConfig.java b/backend-single/src/main/java/com/emotion/config/MybatisPlusConfig.java new file mode 100644 index 0000000..725951a --- /dev/null +++ b/backend-single/src/main/java/com/emotion/config/MybatisPlusConfig.java @@ -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; + } +} diff --git a/backend-single/src/main/java/com/emotion/config/WebConfig.java b/backend-single/src/main/java/com/emotion/config/WebConfig.java new file mode 100644 index 0000000..abab906 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/config/WebConfig.java @@ -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); + } +} diff --git a/backend-single/src/main/java/com/emotion/config/WebMvcConfig.java b/backend-single/src/main/java/com/emotion/config/WebMvcConfig.java new file mode 100644 index 0000000..42e5717 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/config/WebMvcConfig.java @@ -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/**" // 监控端点 + ); + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/AiChatController.java b/backend-single/src/main/java/com/emotion/controller/AiChatController.java new file mode 100644 index 0000000..5b283c7 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/AiChatController.java @@ -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> sendChatMessage(@RequestBody Map 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 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> generateSummary(@RequestBody Map 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 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> getServiceStatus() { + try { + boolean available = aiService.isServiceAvailable(); + String status = aiService.getServiceStatus(); + + Map 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> getChatStats(@RequestParam(required = false) String userId, + @RequestParam(required = false) String conversationId) { + try { + Map 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("无法获取聊天统计信息"); + } + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/AuthController.java b/backend-single/src/main/java/com/emotion/controller/AuthController.java index 20c0cdd..e53cd40 100644 --- a/backend-single/src/main/java/com/emotion/controller/AuthController.java +++ b/backend-single/src/main/java/com/emotion/controller/AuthController.java @@ -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 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 response = new HashMap<>(); - response.put("accessToken", "token-" + user.getId() + "-" + System.currentTimeMillis()); + response.put("accessToken", accessToken); + response.put("refreshToken", refreshToken); response.put("expiresIn", 86400L); Map userInfo = new HashMap<>(); @@ -85,7 +121,7 @@ public class AuthController { @PostMapping("/register") public Result> register(@RequestBody Map 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 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 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> 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 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> 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 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()); diff --git a/backend-single/src/main/java/com/emotion/controller/ChatWebSocketController.java b/backend-single/src/main/java/com/emotion/controller/ChatWebSocketController.java new file mode 100644 index 0000000..085b1bf --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/ChatWebSocketController.java @@ -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); + } + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/MessageController.java b/backend-single/src/main/java/com/emotion/controller/MessageController.java new file mode 100644 index 0000000..3222d96 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/controller/MessageController.java @@ -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 saveMessage(@RequestBody Map 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> getMessagesByConversationId( + @PathVariable String conversationId, + @RequestParam(defaultValue = "1") Integer current, + @RequestParam(defaultValue = "20") Integer size) { + try { + Page page = new Page<>(current, size); + IPage 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> getMessagesBySender( + @PathVariable String sender, + @RequestParam(defaultValue = "1") Integer current, + @RequestParam(defaultValue = "20") Integer size) { + try { + Page page = new Page<>(current, size); + IPage 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 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 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 updateStatus(@PathVariable String messageId, + @RequestBody Map 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 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 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> getMessageStats( + @RequestParam(required = false) String conversationId, + @RequestParam(required = false) String sender) { + try { + Map 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 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()); + } + } +} diff --git a/backend-single/src/main/java/com/emotion/controller/UserController.java b/backend-single/src/main/java/com/emotion/controller/UserController.java index eebc118..c0dea21 100644 --- a/backend-single/src/main/java/com/emotion/controller/UserController.java +++ b/backend-single/src/main/java/com/emotion/controller/UserController.java @@ -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.*; diff --git a/backend-single/src/main/java/com/emotion/controller/WebSocketController.java b/backend-single/src/main/java/com/emotion/controller/WebSocketController.java index d661245..54c576c 100644 --- a/backend-single/src/main/java/com/emotion/controller/WebSocketController.java +++ b/backend-single/src/main/java/com/emotion/controller/WebSocketController.java @@ -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 sendMessage(@Payload Map 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 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 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 connectUser(@Payload Map chatMessage, - SimpMessageHeaderAccessor headerAccessor) { - String username = (String) chatMessage.get("sender"); - - // 在WebSocket会话中添加用户名 - headerAccessor.getSessionAttributes().put("username", username); - - log.info("用户连接: {}", username); - - Map 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 getWebSocketStatus() { + Map 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; + } } diff --git a/backend-single/src/main/java/com/emotion/dto/websocket/ChatRequest.java b/backend-single/src/main/java/com/emotion/dto/websocket/ChatRequest.java new file mode 100644 index 0000000..0db758f --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/websocket/ChatRequest.java @@ -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; + } + } +} diff --git a/backend-single/src/main/java/com/emotion/dto/websocket/ConnectRequest.java b/backend-single/src/main/java/com/emotion/dto/websocket/ConnectRequest.java new file mode 100644 index 0000000..bae1cbf --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/websocket/ConnectRequest.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java b/backend-single/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java new file mode 100644 index 0000000..dbc5c19 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java @@ -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; + } + } +} diff --git a/backend-single/src/main/java/com/emotion/entity/Achievement.java b/backend-single/src/main/java/com/emotion/entity/Achievement.java new file mode 100644 index 0000000..9dc4215 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/Achievement.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/Comment.java b/backend-single/src/main/java/com/emotion/entity/Comment.java new file mode 100644 index 0000000..7ef2de9 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/Comment.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/CommunityPost.java b/backend-single/src/main/java/com/emotion/entity/CommunityPost.java new file mode 100644 index 0000000..751a713 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/CommunityPost.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/Conversation.java b/backend-single/src/main/java/com/emotion/entity/Conversation.java index 86f93a5..731a4be 100644 --- a/backend-single/src/main/java/com/emotion/entity/Conversation.java +++ b/backend-single/src/main/java/com/emotion/entity/Conversation.java @@ -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; } } diff --git a/backend-single/src/main/java/com/emotion/entity/CozeApiCall.java b/backend-single/src/main/java/com/emotion/entity/CozeApiCall.java new file mode 100644 index 0000000..655c502 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/CozeApiCall.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/EmotionAnalysis.java b/backend-single/src/main/java/com/emotion/entity/EmotionAnalysis.java new file mode 100644 index 0000000..b2c0623 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/EmotionAnalysis.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/EmotionRecord.java b/backend-single/src/main/java/com/emotion/entity/EmotionRecord.java new file mode 100644 index 0000000..bd3609d --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/EmotionRecord.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/GrowthTopic.java b/backend-single/src/main/java/com/emotion/entity/GrowthTopic.java new file mode 100644 index 0000000..748ec72 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/GrowthTopic.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/GuestUser.java b/backend-single/src/main/java/com/emotion/entity/GuestUser.java new file mode 100644 index 0000000..3e739a3 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/GuestUser.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/LocationPin.java b/backend-single/src/main/java/com/emotion/entity/LocationPin.java new file mode 100644 index 0000000..043c954 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/LocationPin.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/Message.java b/backend-single/src/main/java/com/emotion/entity/Message.java index 0361e87..22d1e95 100644 --- a/backend-single/src/main/java/com/emotion/entity/Message.java +++ b/backend-single/src/main/java/com/emotion/entity/Message.java @@ -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; } } diff --git a/backend-single/src/main/java/com/emotion/entity/Reward.java b/backend-single/src/main/java/com/emotion/entity/Reward.java new file mode 100644 index 0000000..fe9c928 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/Reward.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/SimpleUser.java b/backend-single/src/main/java/com/emotion/entity/SimpleUser.java deleted file mode 100644 index 6e9b6a4..0000000 --- a/backend-single/src/main/java/com/emotion/entity/SimpleUser.java +++ /dev/null @@ -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; } -} diff --git a/backend-single/src/main/java/com/emotion/entity/TopicInteraction.java b/backend-single/src/main/java/com/emotion/entity/TopicInteraction.java new file mode 100644 index 0000000..81e9b74 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/TopicInteraction.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/entity/User.java b/backend-single/src/main/java/com/emotion/entity/User.java index ab464fb..5533761 100644 --- a/backend-single/src/main/java/com/emotion/entity/User.java +++ b/backend-single/src/main/java/com/emotion/entity/User.java @@ -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; } } diff --git a/backend-single/src/main/java/com/emotion/entity/UserStats.java b/backend-single/src/main/java/com/emotion/entity/UserStats.java new file mode 100644 index 0000000..7638de6 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/entity/UserStats.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/handler/EmotionMetaObjectHandler.java b/backend-single/src/main/java/com/emotion/handler/EmotionMetaObjectHandler.java new file mode 100644 index 0000000..1730fc9 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/handler/EmotionMetaObjectHandler.java @@ -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; + } + } +} diff --git a/backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java b/backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java new file mode 100644 index 0000000..16d6def --- /dev/null +++ b/backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java @@ -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; + } +} diff --git a/backend-single/src/main/java/com/emotion/interceptor/UserContextInterceptor.java b/backend-single/src/main/java/com/emotion/interceptor/UserContextInterceptor.java new file mode 100644 index 0000000..60a424e --- /dev/null +++ b/backend-single/src/main/java/com/emotion/interceptor/UserContextInterceptor.java @@ -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"; + } +} diff --git a/backend-single/src/main/java/com/emotion/mapper/AchievementMapper.java b/backend-single/src/main/java/com/emotion/mapper/AchievementMapper.java new file mode 100644 index 0000000..8be6b1a --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/AchievementMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/CommentMapper.java b/backend-single/src/main/java/com/emotion/mapper/CommentMapper.java new file mode 100644 index 0000000..b20c992 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/CommentMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/CommunityPostMapper.java b/backend-single/src/main/java/com/emotion/mapper/CommunityPostMapper.java new file mode 100644 index 0000000..262c8ef --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/CommunityPostMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/ConversationMapper.java b/backend-single/src/main/java/com/emotion/mapper/ConversationMapper.java new file mode 100644 index 0000000..be83bc5 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/ConversationMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/CozeApiCallMapper.java b/backend-single/src/main/java/com/emotion/mapper/CozeApiCallMapper.java new file mode 100644 index 0000000..96bdc3e --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/CozeApiCallMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/EmotionAnalysisMapper.java b/backend-single/src/main/java/com/emotion/mapper/EmotionAnalysisMapper.java new file mode 100644 index 0000000..5ec3f55 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/EmotionAnalysisMapper.java @@ -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 { +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/mapper/EmotionRecordMapper.java b/backend-single/src/main/java/com/emotion/mapper/EmotionRecordMapper.java new file mode 100644 index 0000000..0f487bf --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/EmotionRecordMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/GrowthTopicMapper.java b/backend-single/src/main/java/com/emotion/mapper/GrowthTopicMapper.java new file mode 100644 index 0000000..5c1b3aa --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/GrowthTopicMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/GuestUserMapper.java b/backend-single/src/main/java/com/emotion/mapper/GuestUserMapper.java new file mode 100644 index 0000000..dea5258 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/GuestUserMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/LocationPinMapper.java b/backend-single/src/main/java/com/emotion/mapper/LocationPinMapper.java new file mode 100644 index 0000000..6ed3c17 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/LocationPinMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/MessageMapper.java b/backend-single/src/main/java/com/emotion/mapper/MessageMapper.java new file mode 100644 index 0000000..8321c7d --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/MessageMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/RewardMapper.java b/backend-single/src/main/java/com/emotion/mapper/RewardMapper.java new file mode 100644 index 0000000..2b3e297 Binary files /dev/null and b/backend-single/src/main/java/com/emotion/mapper/RewardMapper.java differ diff --git a/backend-single/src/main/java/com/emotion/mapper/TopicInteractionMapper.java b/backend-single/src/main/java/com/emotion/mapper/TopicInteractionMapper.java new file mode 100644 index 0000000..dff4bbf --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/TopicInteractionMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/UserMapper.java b/backend-single/src/main/java/com/emotion/mapper/UserMapper.java new file mode 100644 index 0000000..8b14437 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/UserMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/mapper/UserStatsMapper.java b/backend-single/src/main/java/com/emotion/mapper/UserStatsMapper.java new file mode 100644 index 0000000..8142641 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/mapper/UserStatsMapper.java @@ -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 { +} diff --git a/backend-single/src/main/java/com/emotion/service/AiService.java b/backend-single/src/main/java/com/emotion/service/AiService.java index af652ea..077a32c 100644 --- a/backend-single/src/main/java/com/emotion/service/AiService.java +++ b/backend-single/src/main/java/com/emotion/service/AiService.java @@ -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"; diff --git a/backend-single/src/main/java/com/emotion/service/ConversationService.java b/backend-single/src/main/java/com/emotion/service/ConversationService.java index f15fff2..43358d7 100644 --- a/backend-single/src/main/java/com/emotion/service/ConversationService.java +++ b/backend-single/src/main/java/com/emotion/service/ConversationService.java @@ -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 { /** - * 对话行映射器 + * 分页查询会话 */ - private static class ConversationRowMapper implements RowMapper { - @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 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 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 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 getByUserId(String userId); /** - * 根据用户ID查询对话列表 + * 根据用户ID查询活跃会话列表 */ - public List 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 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 getForArchive(Integer days); + + /** + * 批量归档会话 + */ + boolean batchArchive(List conversationIds); + + /** + * 创建会话 + */ + Conversation createConversation(String userId, String title, String cozeConversationId); + + /** + * 结束会话 + */ + boolean endConversation(String conversationId); +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/IAiService.java b/backend-single/src/main/java/com/emotion/service/IAiService.java new file mode 100644 index 0000000..0764a84 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/IAiService.java @@ -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 messages, String userId); + + /** + * 检查AI服务是否可用 + * + * @return 是否可用 + */ + boolean isServiceAvailable(); + + /** + * 获取AI服务状态信息 + * + * @return 状态信息 + */ + String getServiceStatus(); +} diff --git a/backend-single/src/main/java/com/emotion/service/IConversationService.java b/backend-single/src/main/java/com/emotion/service/IConversationService.java new file mode 100644 index 0000000..68b1bbb --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/IConversationService.java @@ -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 { + + /** + * 创建新会话 + * + * @param userId 用户ID + * @param title 会话标题 + * @param type 会话类型 + * @return 会话信息 + */ + Conversation createConversation(String userId, String title, String type); + + /** + * 根据用户ID分页查询会话列表 + * + * @param page 分页参数 + * @param userId 用户ID + * @return 会话分页数据 + */ + IPage getByUserId(Page page, String userId); + + /** + * 根据用户ID查询活跃会话列表 + * + * @param userId 用户ID + * @return 活跃会话列表 + */ + List 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 conversationIds); + + /** + * 获取或创建会话 + * + * @param userId 用户ID + * @param cozeConversationId Coze会话ID + * @param title 会话标题 + * @return 会话信息 + */ + Conversation getOrCreateConversation(String userId, String cozeConversationId, String title); +} diff --git a/backend-single/src/main/java/com/emotion/service/ICozeApiCallService.java b/backend-single/src/main/java/com/emotion/service/ICozeApiCallService.java new file mode 100644 index 0000000..6188dd5 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/ICozeApiCallService.java @@ -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 { + + /** + * 根据会话ID分页查询API调用记录 + */ + IPage getByConversationId(Page page, String conversationId); + + /** + * 根据用户ID分页查询API调用记录 + */ + IPage getByUserId(Page page, String userId); + + /** + * 根据Bot ID查询API调用记录 + */ + List getByBotId(String botId); + + /** + * 根据状态查询API调用记录 + */ + List getByStatus(String status); + + /** + * 根据请求类型查询API调用记录 + */ + List getByRequestType(String requestType); + + /** + * 根据时间范围查询API调用记录 + */ + List 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 getFailedCalls(); + + /** + * 查询超时的API调用记录 + */ + List getTimeoutCalls(); + + /** + * 根据追踪ID查询API调用记录 + */ + CozeApiCall getByTraceId(String traceId); + + /** + * 根据会话ID和请求类型查询API调用记录 + */ + List 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); +} diff --git a/backend-single/src/main/java/com/emotion/service/IMessageService.java b/backend-single/src/main/java/com/emotion/service/IMessageService.java new file mode 100644 index 0000000..42331c7 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/IMessageService.java @@ -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 { + + /** + * 保存消息 + * + * @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 getByConversationId(Page page, String conversationId); + + /** + * 根据发送者分页查询消息 + * + * @param page 分页参数 + * @param sender 发送者 + * @return 消息分页数据 + */ + IPage getBySender(Page page, String sender); + + /** + * 根据会话ID查询消息列表(用于总结) + * + * @param conversationId 会话ID + * @param limit 限制数量 + * @return 消息列表 + */ + List getByConversationIdForSummary(String conversationId, Integer limit); + + /** + * 根据时间范围查询消息 + * + * @param conversationId 会话ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 消息列表 + */ + List 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 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 messages); + + /** + * 删除会话的所有消息 + * + * @param conversationId 会话ID + * @return 是否成功 + */ + boolean deleteByConversationId(String conversationId); +} diff --git a/backend-single/src/main/java/com/emotion/service/MessageService.java b/backend-single/src/main/java/com/emotion/service/MessageService.java index 1e058b0..bbd6eec 100644 --- a/backend-single/src/main/java/com/emotion/service/MessageService.java +++ b/backend-single/src/main/java/com/emotion/service/MessageService.java @@ -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 { + /** - * 消息行映射器 + * 分页查询消息 */ - private static class MessageRowMapper implements RowMapper { - @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 getPage(BasePageRequest request); + + /** + * 根据会话ID分页查询消息 + */ + IPage getPageByConversationId(BasePageRequest request, String conversationId); + + /** + * 根据会话ID查询消息列表 + */ + List getByConversationId(String conversationId); + + /** + * 根据发送者查询消息列表 + */ + List getBySender(String sender); + + /** + * 根据时间范围查询消息 + */ + List getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 查询会话的最后一条消息 + */ + Message getLastMessageByConversationId(String conversationId); + + /** + * 根据父消息ID查询回复消息 + */ + List 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 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 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); } diff --git a/backend-single/src/main/java/com/emotion/service/UserService.java b/backend-single/src/main/java/com/emotion/service/UserService.java index 35c1e0b..86da18c 100644 --- a/backend-single/src/main/java/com/emotion/service/UserService.java +++ b/backend-single/src/main/java/com/emotion/service/UserService.java @@ -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 { + /** - * 用户行映射器 + * 分页查询用户 */ - private static class UserRowMapper implements RowMapper { - @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 getPage(BasePageRequest request); + /** * 根据账号查询用户 */ - public User findByAccount(String account) { - try { - String sql = "SELECT * FROM user WHERE account = ? AND is_deleted = 0"; - List 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 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 getByStatus(Integer status); + + /** + * 根据会员等级查询用户列表 + */ + List getByMemberLevel(String memberLevel); + + /** + * 查询活跃用户(最近N天有活动) + */ + List getActiveUsers(Integer days); + + /** + * 查询新注册用户(最近N天注册) + */ + List 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); } diff --git a/backend-single/src/main/java/com/emotion/service/WebSocketService.java b/backend-single/src/main/java/com/emotion/service/WebSocketService.java new file mode 100644 index 0000000..4da3d94 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/WebSocketService.java @@ -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 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(); + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/AiServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/AiServiceImpl.java new file mode 100644 index 0000000..ec39d78 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/AiServiceImpl.java @@ -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 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 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 messages, String userId) { + try { + if (messages.isEmpty()) { + return "暂无对话记录可供总结。"; + } + + // 构建对话历史文本 + String conversationText = buildConversationText(messages); + + // 构建总结请求数据 + Map 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 buildChatRequestData(String conversationId, String message, String userId) { + Map 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 buildSummaryRequestData(String conversationText, String userId) { + Map 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 requestData, String botId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(cozeApiToken); + + HttpEntity> entity = new HttpEntity<>(requestData, headers); + + String url = cozeBaseUrl + "/v3/chat"; + ResponseEntity 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 messages) { + return messages.stream() + .map(message -> { + String senderName = "user".equals(message.getSender()) ? "用户" : "AI助手"; + return senderName + ": " + message.getContent(); + }) + .collect(Collectors.joining("\n")); + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/CozeApiCallServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/CozeApiCallServiceImpl.java new file mode 100644 index 0000000..6fe19e6 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/CozeApiCallServiceImpl.java @@ -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 implements ICozeApiCallService { + + @Override + public IPage getByConversationId(Page page, String conversationId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getConversationId, conversationId) + .eq(CozeApiCall::getIsDeleted, 0) + .orderByDesc(CozeApiCall::getStartTime); + return this.page(page, wrapper); + } + + @Override + public IPage getByUserId(Page page, String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getUserId, userId) + .eq(CozeApiCall::getIsDeleted, 0) + .orderByDesc(CozeApiCall::getStartTime); + return this.page(page, wrapper); + } + + @Override + public List getByBotId(String botId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getBotId, botId) + .eq(CozeApiCall::getIsDeleted, 0) + .orderByDesc(CozeApiCall::getStartTime); + return this.list(wrapper); + } + + @Override + public List getByStatus(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getStatus, status) + .eq(CozeApiCall::getIsDeleted, 0) + .orderByDesc(CozeApiCall::getStartTime); + return this.list(wrapper); + } + + @Override + public List getByRequestType(String requestType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getRequestType, requestType) + .eq(CozeApiCall::getIsDeleted, 0) + .orderByDesc(CozeApiCall::getStartTime); + return this.list(wrapper); + } + + @Override + public List getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getUserId, userId) + .eq(CozeApiCall::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByBotId(String botId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getBotId, botId) + .eq(CozeApiCall::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByStatus(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getStatus, status) + .eq(CozeApiCall::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long sumTokensByUserId(String userId) { + // 使用原生SQL或者查询后计算 + List calls = this.list(new LambdaQueryWrapper() + .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 calls = this.list(new LambdaQueryWrapper() + .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 getFailedCalls() { + LambdaQueryWrapper 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 getTimeoutCalls() { + LambdaQueryWrapper 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CozeApiCall::getTraceId, traceId) + .eq(CozeApiCall::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public List getByConversationIdAndRequestType(String conversationId, String requestType) { + LambdaQueryWrapper 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 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 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; + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java new file mode 100644 index 0000000..3e3f474 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java @@ -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 implements MessageService { + + private static final Logger log = LoggerFactory.getLogger(MessageServiceImpl.class); + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper 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 getPageByConversationId(BasePageRequest request, String conversationId) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper 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 getByConversationId(String conversationId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Message::getConversationId, conversationId) + .eq(Message::getIsDeleted, 0) + .orderByAsc(Message::getTimestamp); + return this.list(wrapper); + } + + @Override + public List getBySender(String sender) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Message::getSender, sender) + .eq(Message::getIsDeleted, 0) + .orderByDesc(Message::getTimestamp); + return this.list(wrapper); + } + + @Override + public List getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime) { + LambdaQueryWrapper 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 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 getRepliesByParentId(String parentMessageId) { + LambdaQueryWrapper 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Message::getConversationId, conversationId) + .eq(Message::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countBySender(String sender) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Message::getSender, sender) + .eq(Message::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countUnreadMessages(String conversationId) { + LambdaQueryWrapper 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 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 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 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); + } +} diff --git a/backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..5d32bad --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java @@ -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 implements UserService { + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public IPage getPage(BasePageRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + LambdaQueryWrapper 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getAccount, account) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public User getByUsername(String username) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getUsername, username) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public User getByEmail(String email) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getEmail, email) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public User getByPhone(String phone) { + LambdaQueryWrapper 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getThirdPartyId, thirdPartyId) + .eq(User::getThirdPartyType, thirdPartyType) + .eq(User::getIsDeleted, 0); + return this.getOne(wrapper); + } + + @Override + public List getByStatus(Integer status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getStatus, status) + .eq(User::getIsDeleted, 0) + .orderByDesc(User::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getByMemberLevel(String memberLevel) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getMemberLevel, memberLevel) + .eq(User::getIsDeleted, 0) + .orderByDesc(User::getCreateTime); + return this.list(wrapper); + } + + @Override + public List getActiveUsers(Integer days) { + LambdaQueryWrapper 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 getNewUsers(Integer days) { + LambdaQueryWrapper 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 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 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 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getStatus, status) + .eq(User::getIsDeleted, 0); + return this.count(wrapper); + } + + @Override + public Long countByMemberLevel(String memberLevel) { + LambdaQueryWrapper 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 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; + } + } +} diff --git a/backend-single/src/main/java/com/emotion/util/JwtUtil.java b/backend-single/src/main/java/com/emotion/util/JwtUtil.java new file mode 100644 index 0000000..d1514fe --- /dev/null +++ b/backend-single/src/main/java/com/emotion/util/JwtUtil.java @@ -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; + } + } +} diff --git a/backend-single/src/main/java/com/emotion/util/SnowflakeIdGenerator.java b/backend-single/src/main/java/com/emotion/util/SnowflakeIdGenerator.java new file mode 100644 index 0000000..6887bc6 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/util/SnowflakeIdGenerator.java @@ -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; + } +} diff --git a/backend-single/src/main/java/com/emotion/util/UserContextHolder.java b/backend-single/src/main/java/com/emotion/util/UserContextHolder.java new file mode 100644 index 0000000..c7a6c8a --- /dev/null +++ b/backend-single/src/main/java/com/emotion/util/UserContextHolder.java @@ -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 USER_ID_HOLDER = new ThreadLocal<>(); + + /** + * 用户名线程本地变量 + */ + private static final ThreadLocal USERNAME_HOLDER = new ThreadLocal<>(); + + /** + * 用户类型线程本地变量 + */ + private static final ThreadLocal USER_TYPE_HOLDER = new ThreadLocal<>(); + + /** + * 客户端IP线程本地变量 + */ + private static final ThreadLocal CLIENT_IP_HOLDER = new ThreadLocal<>(); + + /** + * 请求ID线程本地变量 + */ + private static final ThreadLocal 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; + } +} diff --git a/backend-single/src/main/java/com/emotion/util/UserContextUtils.java b/backend-single/src/main/java/com/emotion/util/UserContextUtils.java new file mode 100644 index 0000000..64a7247 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/util/UserContextUtils.java @@ -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(); + } + } + } +} diff --git a/backend-single/src/main/resources/application-local.yml b/backend-single/src/main/resources/application-local.yml index 1ba7c79..dc1cb73 100644 --- a/backend-single/src/main/resources/application-local.yml +++ b/backend-single/src/main/resources/application-local.yml @@ -8,9 +8,9 @@ spring: # 数据库配置 - 本地MySQL datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/emotion?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + url: jdbc:mysql://47.111.10.27:3306/emotion?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root - password: 123456 + password: EmotionMuseum2025*# hikari: minimum-idle: 5 maximum-pool-size: 20 @@ -52,9 +52,13 @@ emotion: # 文件上传路径 - 本地开发 upload: path: ./uploads/emotion-museum - + # 开发模式配置 dev: mock-enabled: true debug-mode: true hot-reload: true + + # 雪花算法配置 + snowflake: + machine-id: 1 diff --git a/backend-single/src/main/resources/application-prod.yml b/backend-single/src/main/resources/application-prod.yml index 145e5e7..3d47dc2 100644 --- a/backend-single/src/main/resources/application-prod.yml +++ b/backend-single/src/main/resources/application-prod.yml @@ -8,7 +8,7 @@ spring: # 数据库配置 - 生产MySQL datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/emotion?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + url: jdbc:mysql://47.111.10.27:3306/emotion?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: emotion password: EmotionDB2024! hikari: diff --git a/backend-single/src/main/resources/application.yml b/backend-single/src/main/resources/application.yml index 2940aec..bd47f13 100644 --- a/backend-single/src/main/resources/application.yml +++ b/backend-single/src/main/resources/application.yml @@ -63,7 +63,7 @@ management: emotion: # JWT配置 jwt: - secret: EmotionMuseumJWTSecretKey2025ForAuthenticationAndAuthorization + secret: EmotionMuseumJWTSecretKey2025ForAuthenticationAndAuthorizationSecureEnoughForHS512Algorithm expiration: 86400000 # 24小时 header: Authorization prefix: "Bearer " @@ -71,9 +71,14 @@ emotion: # Coze API配置 - 所有环境统一 coze: api: - token: pat_7523042446285439016_emotion_museum_2025 + token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO base-url: https://api.coze.cn - bot-id: 7523042446285439016 + # 对话聊天 + chat: + bot-id: 7523042446285439016 + # 聊天记录总结 + summary: + bot-id: 7529062814150295595 workflow-id: 7523047462895796287 timeout: 30000 retry-count: 3 diff --git a/backend-single/src/main/resources/sql/init.sql b/backend-single/src/main/resources/sql/init.sql deleted file mode 100644 index d2b2199..0000000 --- a/backend-single/src/main/resources/sql/init.sql +++ /dev/null @@ -1,183 +0,0 @@ --- 情感博物馆数据库初始化脚本 --- 作者: emotion-museum --- 日期: 2025-07-22 - --- 创建数据库 -CREATE DATABASE IF NOT EXISTS emotion DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -USE emotion; - --- 删除已存在的表(开发阶段) -DROP TABLE IF EXISTS message; -DROP TABLE IF EXISTS conversation; -DROP TABLE IF EXISTS coze_api_call; -DROP TABLE IF EXISTS emotion_record; -DROP TABLE IF EXISTS user; - --- 创建用户表 -CREATE TABLE user ( - id VARCHAR(32) NOT NULL PRIMARY KEY COMMENT '用户ID', - username VARCHAR(50) NOT NULL COMMENT '用户名', - account VARCHAR(50) NOT NULL UNIQUE COMMENT '账号', - password VARCHAR(255) NOT NULL COMMENT '密码', - email VARCHAR(100) COMMENT '邮箱', - phone VARCHAR(20) COMMENT '手机号', - nickname VARCHAR(50) COMMENT '昵称', - avatar VARCHAR(500) COMMENT '头像URL', - gender TINYINT DEFAULT 0 COMMENT '性别:0-未知,1-男,2-女', - bio TEXT COMMENT '个人简介', - member_level VARCHAR(20) DEFAULT 'free' COMMENT '会员等级', - total_days INT DEFAULT 0 COMMENT '总天数', - status TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-正常', - is_verified TINYINT DEFAULT 0 COMMENT '是否验证:0-未验证,1-已验证', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - last_active_time DATETIME COMMENT '最后活跃时间', - create_by VARCHAR(32) COMMENT '创建人', - update_by VARCHAR(32) COMMENT '更新人', - is_deleted TINYINT DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除', - remarks TEXT COMMENT '备注', - INDEX idx_account (account), - INDEX idx_email (email), - INDEX idx_phone (phone), - INDEX idx_status (status), - INDEX idx_create_time (create_time) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; - --- 创建对话表 -CREATE TABLE conversation ( - id VARCHAR(32) NOT NULL PRIMARY KEY COMMENT '对话ID', - user_id VARCHAR(32) COMMENT '用户ID', - title VARCHAR(200) NOT NULL COMMENT '对话标题', - type VARCHAR(20) DEFAULT 'user' COMMENT '对话类型:user-用户对话,guest-访客对话', - start_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间', - end_time DATETIME COMMENT '结束时间', - message_count INT DEFAULT 0 COMMENT '消息数量', - status TINYINT DEFAULT 1 COMMENT '状态:0-结束,1-进行中', - client_ip VARCHAR(50) COMMENT '客户端IP(访客模式)', - user_agent TEXT COMMENT '用户代理(访客模式)', - coze_conversation_id VARCHAR(100) COMMENT 'Coze对话ID', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - create_by VARCHAR(32) COMMENT '创建人', - update_by VARCHAR(32) COMMENT '更新人', - is_deleted TINYINT DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除', - remarks TEXT COMMENT '备注', - INDEX idx_user_id (user_id), - INDEX idx_type (type), - INDEX idx_status (status), - INDEX idx_create_time (create_time), - INDEX idx_client_ip (client_ip) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='对话表'; - --- 创建消息表 -CREATE TABLE message ( - id VARCHAR(32) NOT NULL PRIMARY KEY COMMENT '消息ID', - conversation_id VARCHAR(32) NOT NULL COMMENT '对话ID', - user_id VARCHAR(32) COMMENT '用户ID', - content TEXT NOT NULL COMMENT '消息内容', - content_type VARCHAR(20) DEFAULT 'text' COMMENT '消息类型:text-文本,image-图片,file-文件', - sender_type VARCHAR(20) NOT NULL COMMENT '发送者类型:user-用户,ai-AI,system-系统', - sender_id VARCHAR(32) COMMENT '发送者ID', - status VARCHAR(20) DEFAULT 'sent' COMMENT '消息状态:sending-发送中,sent-已发送,delivered-已送达,read-已读,failed-失败', - send_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间', - is_read TINYINT DEFAULT 0 COMMENT '是否已读:0-未读,1-已读', - parent_message_id VARCHAR(32) COMMENT '父消息ID(用于回复链)', - coze_role VARCHAR(20) COMMENT 'Coze消息角色 (user/assistant/system)', - coze_message_id VARCHAR(100) COMMENT 'Coze消息ID', - error_message TEXT COMMENT '错误信息', - retry_count INT DEFAULT 0 COMMENT '重试次数', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - create_by VARCHAR(32) COMMENT '创建人', - update_by VARCHAR(32) COMMENT '更新人', - is_deleted TINYINT DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除', - remarks TEXT COMMENT '备注', - INDEX idx_conversation_id (conversation_id), - INDEX idx_user_id (user_id), - INDEX idx_sender_type (sender_type), - INDEX idx_send_time (send_time), - INDEX idx_status (status) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息表'; - --- 创建Coze API调用记录表 -CREATE TABLE coze_api_call ( - id VARCHAR(32) NOT NULL PRIMARY KEY COMMENT '记录ID', - conversation_id VARCHAR(32) COMMENT '对话ID', - user_id VARCHAR(32) COMMENT '用户ID', - user_message TEXT COMMENT '用户消息', - ai_reply TEXT COMMENT 'AI回复', - api_request_data TEXT COMMENT 'API请求数据', - api_response_data TEXT COMMENT 'API响应数据', - call_status TINYINT DEFAULT 1 COMMENT '调用状态:1-成功,0-失败', - error_message TEXT COMMENT '错误信息', - response_time_ms INT COMMENT '响应时间(毫秒)', - api_type VARCHAR(20) DEFAULT 'chat' COMMENT 'API类型:chat-聊天,emotion-情绪分析', - client_ip VARCHAR(50) COMMENT '客户端IP', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - create_by VARCHAR(32) COMMENT '创建人', - update_by VARCHAR(32) COMMENT '更新人', - is_deleted TINYINT DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除', - remarks TEXT COMMENT '备注', - INDEX idx_conversation_id (conversation_id), - INDEX idx_user_id (user_id), - INDEX idx_call_status (call_status), - INDEX idx_create_time (create_time), - INDEX idx_api_type (api_type) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Coze API调用记录表'; - --- 创建情绪记录表 -CREATE TABLE emotion_record ( - id VARCHAR(32) NOT NULL PRIMARY KEY COMMENT '记录ID', - user_id VARCHAR(32) NOT NULL COMMENT '用户ID', - record_date DATE NOT NULL COMMENT '记录日期', - emotion_type VARCHAR(20) NOT NULL COMMENT '情绪类型:joy-喜悦,sadness-悲伤,anger-愤怒,fear-恐惧,surprise-惊讶,neutral-平静', - intensity DECIMAL(3,2) DEFAULT 0.50 COMMENT '情绪强度 (0.00-1.00)', - triggers TEXT COMMENT '触发因素', - description TEXT COMMENT '描述', - tags JSON COMMENT '标签', - weather VARCHAR(50) COMMENT '天气', - location VARCHAR(100) COMMENT '地点', - activity VARCHAR(100) COMMENT '活动', - people VARCHAR(200) COMMENT '相关人物', - notes TEXT COMMENT '备注', - analysis_result TEXT COMMENT '情绪分析结果', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - create_by VARCHAR(32) COMMENT '创建人', - update_by VARCHAR(32) COMMENT '更新人', - is_deleted TINYINT DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除', - remarks TEXT COMMENT '备注', - INDEX idx_user_id (user_id), - INDEX idx_record_date (record_date), - INDEX idx_emotion_type (emotion_type), - INDEX idx_create_time (create_time), - UNIQUE KEY uk_user_date (user_id, record_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='情绪记录表'; - --- 插入测试数据 -INSERT INTO user (id, username, account, password, email, nickname, member_level, total_days, status, is_verified, last_active_time) VALUES -('admin001', 'admin', 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXgwkOBbYbqnhHGGGKTAiYOUFlW', 'admin@emotion.com', '管理员', 'premium', 365, 1, 1, NOW()), -('test001', 'testuser', 'test', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXgwkOBbYbqnhHGGGKTAiYOUFlW', 'test@emotion.com', '测试用户', 'free', 30, 1, 1, NOW()); - --- 插入测试对话 -INSERT INTO conversation (id, user_id, title, type, start_time, message_count, status, client_ip) VALUES -('conv001', 'admin001', '我的第一次对话', 'user', NOW(), 2, 1, '127.0.0.1'), -('conv002', 'test001', '情绪咨询', 'user', NOW(), 1, 1, '127.0.0.1'); - --- 插入测试消息 -INSERT INTO message (id, conversation_id, user_id, content, sender_type, sender_id, status, send_time) VALUES -('msg001', 'conv001', 'admin001', '你好,我想了解一下情绪管理', 'user', 'admin001', 'sent', NOW()), -('msg002', 'conv001', 'admin001', '你好!我很高兴为你介绍情绪管理的相关知识。情绪管理是一项重要的生活技能...', 'ai', 'ai-assistant', 'sent', NOW()), -('msg003', 'conv002', 'test001', '我最近感觉压力很大', 'user', 'test001', 'sent', NOW()); - --- 插入测试情绪记录 -INSERT INTO emotion_record (id, user_id, record_date, emotion_type, intensity, triggers, description, tags, weather, location, activity) VALUES -('record001', 'admin001', CURDATE(), 'joy', 0.80, '完成了重要项目', '今天心情很好,完成了一个重要的项目', '["工作", "成就感"]', '晴天', '办公室', '工作'), -('record002', 'test001', CURDATE(), 'sadness', 0.60, '工作压力', '感觉有些压力和焦虑', '["工作", "压力"]', '阴天', '家里', '思考'); - -COMMIT; - --- 显示创建结果 -SELECT 'Database initialization completed successfully!' as status; diff --git a/backend-single/src/main/resources/sql/mysql_emotion_museum_final.sql b/backend-single/src/main/resources/sql/mysql_emotion_museum_final.sql new file mode 100644 index 0000000..7c41677 --- /dev/null +++ b/backend-single/src/main/resources/sql/mysql_emotion_museum_final.sql @@ -0,0 +1,854 @@ +-- ============================================================================ +-- 情绪博物馆数据库完整部署脚本 +-- 版本: v3.0 Final (雪花算法主键版本) - 开发版本 +-- 创建时间: 2025-07-13 +-- 数据库类型: MySQL 8.0+ +-- 说明: 包含完整表结构、索引、初始数据的一体化部署脚本 +-- 主键类型: VARCHAR(36) 使用雪花算法生成,避免前端精度丢失问题 +-- 关联策略: 不使用外键约束,通过代码中的ID字段关联 +-- 特性: 开发阶段 - 先删除表再重新创建,确保表结构是最新的 +-- 警告: 此脚本会删除现有表和数据,仅适用于开发环境! +-- ============================================================================ +-- 设置SQL模式和字符集 +SET + SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO'; + +SET + AUTOCOMMIT = 0; + +START TRANSACTION; + +SET + time_zone = "+00:00"; + +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS emotion_museum DEFAULT CHARACTER +SET + utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE emotion_museum; + +-- ============================================================================ +-- 数据库设计原则 +-- ============================================================================ +-- 1. 主键策略: 使用VARCHAR(36)雪花算法ID,避免前端精度丢失 +-- 2. 关联策略: 不使用外键约束,通过代码中的ID字段维护关联关系 +-- 3. 公共字段: 所有表继承BaseEntity的公共字段 +-- 4. 索引优化: 为查询频繁的字段创建合适的索引 +-- 5. 字符集: 统一使用utf8mb4支持emoji和特殊字符 +-- ============================================================================ +-- 删除现有表(开发阶段确保表结构最新) +-- 警告: 这会删除所有数据! +-- ============================================================================ +DROP TABLE IF EXISTS user_stats; + +DROP TABLE IF EXISTS guest_user; + +DROP TABLE IF EXISTS reward; + +DROP TABLE IF EXISTS achievement; + +DROP TABLE IF EXISTS comment; + +DROP TABLE IF EXISTS community_post; + +DROP TABLE IF EXISTS location_pin; + +DROP TABLE IF EXISTS topic_interaction; + +DROP TABLE IF EXISTS growth_topic; + +DROP TABLE IF EXISTS emotion_record; + +DROP TABLE IF EXISTS emotion_analysis; + +DROP TABLE IF EXISTS coze_api_call; + +DROP TABLE IF EXISTS message; + +DROP TABLE IF EXISTS conversation; + +DROP TABLE IF EXISTS user; + +-- ============================================================================ +-- 1. 用户表 (user) +-- ============================================================================ +CREATE TABLE user ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + account VARCHAR(50) NOT NULL UNIQUE, -- 账号 + password VARCHAR(255) NOT NULL, -- 密码(加密后) + username VARCHAR(50) NOT NULL UNIQUE, -- 用户名 + email VARCHAR(100) NOT NULL UNIQUE, -- 邮箱 + phone VARCHAR(20) UNIQUE, -- 手机号 + avatar VARCHAR(500), -- 头像URL + nickname VARCHAR(50) NOT NULL, -- 昵称 + birth_date DATE, -- 生日 + location VARCHAR(100), -- 所在地 + bio TEXT, -- 个人简介 + member_level VARCHAR(20) NOT NULL DEFAULT 'free', -- 会员等级 + total_days INT NOT NULL DEFAULT 0, -- 使用天数 + -- 成长数据 + self_awareness DECIMAL(5, 2) NOT NULL DEFAULT 50.00, -- 自我感知 + emotional_resilience DECIMAL(5, 2) NOT NULL DEFAULT 50.00, -- 情绪韧性 + action_power DECIMAL(5, 2) NOT NULL DEFAULT 50.00, -- 行动力 + empathy DECIMAL(5, 2) NOT NULL DEFAULT 50.00, -- 共情力 + life_enthusiasm DECIMAL(5, 2) NOT NULL DEFAULT 50.00, -- 生活热度 + -- 状态字段 + status TINYINT NOT NULL DEFAULT 1, -- 状态: 0-禁用, 1-正常 + is_verified TINYINT NOT NULL DEFAULT 0, -- 是否已验证: 0-未验证, 1-已验证 + last_active_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 最后活跃时间 + -- 第三方登录字段 + third_party_id VARCHAR(128), -- 第三方平台ID + third_party_type VARCHAR(32), -- 第三方平台类型: wechat, qq, wechat-mp + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表'; + +-- ============================================================================ +-- 2. 对话表 (conversation) +-- 关联说明: user_id 关联 user.id,通过代码逻辑维护关联关系 +-- ============================================================================ +CREATE TABLE conversation ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + user_id VARCHAR(36) NOT NULL, -- 用户ID (关联user.id) + user_type VARCHAR(20) NOT NULL DEFAULT 'registered', -- 用户类型: registered-注册用户, guest-访客用户 + title VARCHAR(200), -- 对话标题 + type VARCHAR(50) NOT NULL DEFAULT 'emotion_chat', -- 对话类型 + status VARCHAR(20) NOT NULL DEFAULT 'active', -- 状态: active-活跃, ended-结束, archived-归档 + coze_conversation_id VARCHAR(100), -- Coze对话ID + bot_id VARCHAR(50), -- 使用的Bot ID + workflow_id VARCHAR(50), -- 使用的Workflow ID + initial_message TEXT, -- 初始消息 + context TEXT, -- 上下文信息 + primary_emotion VARCHAR(50), -- 主要情绪 + emotion_intensity DECIMAL(3, 2), -- 情绪强度 + emotion_trend VARCHAR(50), -- 情绪趋势 + keywords JSON, -- 关键词 + ai_insights TEXT, -- AI洞察 + confidence DECIMAL(3, 2), -- 分析置信度 + start_time DATETIME, -- 开始时间 + end_time DATETIME, -- 结束时间 + last_active_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 最后活跃时间 + message_count INT NOT NULL DEFAULT 0, -- 消息数量 + total_tokens INT DEFAULT 0, -- 总Token使用量 + total_cost DECIMAL(10, 4) DEFAULT 0.0000, -- 总费用 + client_ip VARCHAR(45), -- 客户端IP地址 (支持IPv6) + user_agent TEXT, -- 用户代理信息 + summary TEXT, -- 对话摘要 + tags JSON, -- 标签 + metadata JSON, -- 扩展元数据 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '对话表'; + +-- ============================================================================ +-- 3. 消息表 (message) +-- 关联说明: conversation_id 关联 conversation.id,通过代码逻辑维护关联关系 +-- ============================================================================ +CREATE TABLE message ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + conversation_id VARCHAR(36) NOT NULL, -- 对话ID (关联conversation.id) + content TEXT NOT NULL, -- 消息内容 + type VARCHAR(50) NOT NULL DEFAULT 'text', -- 消息类型 + sender VARCHAR(20) NOT NULL, -- 发送者: user-用户, assistant-AI助手 + timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 消息时间戳 + coze_chat_id VARCHAR(50), -- Coze平台的聊天ID + coze_message_id VARCHAR(50), -- Coze平台的消息ID + status VARCHAR(20) DEFAULT 'sent', -- 消息状态: sending/sent/failed/processing + error_message TEXT, -- 错误信息 + emotion_score DECIMAL(3, 2), -- 情绪评分 + emotion_type VARCHAR(50), -- 情绪类型 + emotion_confidence DECIMAL(3, 2), -- 情绪分析置信度 + prompt_tokens INT DEFAULT 0, -- 输入Token数 + completion_tokens INT DEFAULT 0, -- 输出Token数 + total_tokens INT DEFAULT 0, -- 总Token数 + api_cost DECIMAL(10, 6) DEFAULT 0.000000, -- API调用费用 + is_read TINYINT NOT NULL DEFAULT 0, -- 是否已读: 0-未读, 1-已读 + parent_message_id VARCHAR(36), -- 父消息ID(用于回复链) + emotion_analysis JSON, -- 情绪分析结果 + metadata JSON, -- 扩展元数据 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息表'; + +-- ============================================================================ +-- 4. Coze API调用记录表 (coze_api_call) - 优化版本 +-- ============================================================================ +CREATE TABLE coze_api_call ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + conversation_id VARCHAR(36), -- 对话ID + message_id VARCHAR(36), -- 消息ID + -- Coze API 信息 + coze_chat_id VARCHAR(50), -- Coze聊天ID + coze_conversation_id VARCHAR(50), -- Coze对话ID + bot_id VARCHAR(50) NOT NULL, -- Bot ID + workflow_id VARCHAR(50), -- Workflow ID + user_id VARCHAR(36) NOT NULL, -- 用户ID + -- 请求信息 + request_type VARCHAR(20) NOT NULL, -- 请求类型: chat/stream/retrieve/messages + request_url VARCHAR(500), -- 请求URL + request_body JSON, -- 请求体 + request_headers JSON, -- 请求头 + -- 用户消息内容 + user_message TEXT, -- 用户输入的消息内容 + user_message_type VARCHAR(20) DEFAULT 'text', -- 用户消息类型: text/image/file + -- AI回复内容 + ai_reply TEXT, -- AI回复的消息内容 + ai_reply_type VARCHAR(20) DEFAULT 'text', -- AI回复类型: text/image/file + -- 响应信息 + response_status INT, -- HTTP状态码 + response_body JSON, -- 响应体 + response_headers JSON, -- 响应头 + -- 轮询信息 + poll_count INT DEFAULT 0, -- 轮询次数 + poll_start_time DATETIME, -- 轮询开始时间 + poll_end_time DATETIME, -- 轮询结束时间 + final_status VARCHAR(20), -- 最终状态: completed/failed/timeout + -- 状态和时间 + status VARCHAR(20) NOT NULL, -- 调用状态: pending/success/failed/timeout + start_time DATETIME NOT NULL, -- 开始时间 + end_time DATETIME, -- 结束时间 + duration_ms INT, -- 耗时(毫秒) + -- 使用统计 + prompt_tokens INT DEFAULT 0, -- 输入Token数 + completion_tokens INT DEFAULT 0, -- 输出Token数 + total_tokens INT DEFAULT 0, -- 总Token数 + cost DECIMAL(10, 6) DEFAULT 0.000000, -- 费用 + -- 功能调用信息 + function_calls JSON, -- 函数调用记录 + function_results JSON, -- 函数调用结果 + -- 错误信息 + error_code VARCHAR(50), -- 错误代码 + error_message TEXT, -- 错误信息 + -- 扩展信息 + client_ip VARCHAR(45), -- 客户端IP + user_agent TEXT, -- 用户代理 + session_id VARCHAR(100), -- 会话ID + trace_id VARCHAR(100), -- 追踪ID + metadata JSON, -- 扩展元数据 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Coze API调用记录表 - 完整版本'; + +-- ============================================================================ +-- 5. 情绪分析表 (emotion_analysis) +-- ============================================================================ +CREATE TABLE emotion_analysis ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + user_id VARCHAR(36) NOT NULL, -- 用户ID + message_id VARCHAR(36), -- 关联消息ID + text TEXT NOT NULL, -- 分析文本 + primary_emotion VARCHAR(50), -- 主要情绪 + intensity DECIMAL(3, 2), -- 情绪强度 + polarity VARCHAR(20), -- 情绪极性: positive-积极, negative-消极, neutral-中性 + confidence DECIMAL(3, 2), -- 置信度 + emotions JSON, -- 情绪分布详情 + keywords JSON, -- 关键词列表 + suggestion TEXT, -- 建议 + analysis_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 分析时间 + metadata JSON, -- 扩展元数据 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '情绪分析表'; + +-- ============================================================================ +-- 6. 情绪记录表 (emotion_record) +-- ============================================================================ +CREATE TABLE emotion_record ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + user_id VARCHAR(36) NOT NULL, -- 用户ID + record_date DATE NOT NULL, -- 记录日期 + emotion_type VARCHAR(50) NOT NULL, -- 情绪类型 + intensity DECIMAL(3, 2) NOT NULL, -- 情绪强度 + triggers TEXT, -- 触发因素 + description TEXT, -- 描述 + tags JSON, -- 标签 + weather VARCHAR(50), -- 天气 + location VARCHAR(100), -- 地点 + activity VARCHAR(100), -- 活动 + people VARCHAR(200), -- 相关人物 + notes TEXT, -- 备注 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '情绪记录表'; + +-- ============================================================================ +-- 7. 成长课题表 (growth_topic) +-- ============================================================================ +CREATE TABLE growth_topic ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + title VARCHAR(100) NOT NULL, -- 课题标题 + category VARCHAR(50) NOT NULL, -- 分类 + difficulty VARCHAR(20) NOT NULL, -- 难度: easy-简单, medium-中等, hard-困难 + description TEXT, -- 描述 + content TEXT, -- 内容 + duration_days INT, -- 持续天数 + unlock_conditions JSON, -- 解锁条件 + is_unlocked TINYINT NOT NULL DEFAULT 1, -- 是否解锁 + progress DECIMAL(5, 2) NOT NULL DEFAULT 0.00, -- 进度百分比 + completed_time DATETIME, -- 完成时间 + rewards JSON, -- 奖励 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '成长课题表'; + +-- ============================================================================ +-- 8. 课题互动表 (topic_interaction) +-- ============================================================================ +CREATE TABLE topic_interaction ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + topic_id VARCHAR(36) NOT NULL, -- 课题ID + type VARCHAR(50) NOT NULL, -- 互动类型 + content TEXT, -- 内容 + user_input TEXT, -- 用户输入 + ai_response TEXT, -- AI回应 + rating INT, -- 评分 + feedback TEXT, -- 反馈 + completed_time DATETIME, -- 完成时间 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '课题互动表'; + +-- ============================================================================ +-- 9. 地点标记表 (location_pin) +-- ============================================================================ +CREATE TABLE location_pin ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + name VARCHAR(100) NOT NULL, -- 地点名称 + type VARCHAR(50) NOT NULL, -- 地点类型 + category VARCHAR(50), -- 地点分类 + latitude DECIMAL(10, 8) NOT NULL, -- 纬度 + longitude DECIMAL(11, 8) NOT NULL, -- 经度 + address VARCHAR(200), -- 地址 + description TEXT, -- 描述 + created_by VARCHAR(36), -- 创建者 + likes INT NOT NULL DEFAULT 0, -- 点赞数 + visits INT NOT NULL DEFAULT 0, -- 访问数 + is_bookmarked TINYINT NOT NULL DEFAULT 0, -- 是否收藏 + last_visit_time DATETIME, -- 最后访问时间 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '地点标记表'; + +-- ============================================================================ +-- 10. 社区帖子表 (community_post) +-- ============================================================================ +CREATE TABLE community_post ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + user_id VARCHAR(36) NOT NULL, -- 用户ID + location_id VARCHAR(36), -- 地点ID + title VARCHAR(200), -- 标题 + content TEXT NOT NULL, -- 内容 + type VARCHAR(50) NOT NULL, -- 帖子类型 + images JSON, -- 图片列表 + tags JSON, -- 标签 + likes INT NOT NULL DEFAULT 0, -- 点赞数 + view_count INT NOT NULL DEFAULT 0, -- 浏览数 + comment_count INT NOT NULL DEFAULT 0, -- 评论数 + is_private TINYINT NOT NULL DEFAULT 0, -- 是否私密 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社区帖子表'; + +-- ============================================================================ +-- 11. 评论表 (comment) +-- ============================================================================ +CREATE TABLE comment ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + post_id VARCHAR(36) NOT NULL, -- 帖子ID + user_id VARCHAR(36) NOT NULL, -- 用户ID + content TEXT NOT NULL, -- 评论内容 + reply_to_id VARCHAR(36), -- 回复的评论ID + likes INT NOT NULL DEFAULT 0, -- 点赞数 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '评论表'; + +-- ============================================================================ +-- 12. 成就表 (achievement) +-- ============================================================================ +CREATE TABLE achievement ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + title VARCHAR(100) NOT NULL, -- 成就标题 + description TEXT, -- 描述 + category VARCHAR(50) NOT NULL, -- 分类 + icon VARCHAR(200), -- 图标 + rarity VARCHAR(20) NOT NULL, -- 稀有度 + condition_type VARCHAR(50), -- 条件类型 + condition_value JSON, -- 条件值 + rewards JSON, -- 奖励 + unlocked_time DATETIME, -- 解锁时间 + progress DECIMAL(5, 2) NOT NULL DEFAULT 0.00, -- 进度 + is_hidden TINYINT NOT NULL DEFAULT 0, -- 是否隐藏 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '成就表'; + +-- ============================================================================ +-- 13. 奖励表 (reward) +-- ============================================================================ +CREATE TABLE reward ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + topic_id VARCHAR(36), -- 课题ID + achievement_id VARCHAR(36), -- 成就ID + type VARCHAR(50) NOT NULL, -- 奖励类型 + name VARCHAR(100) NOT NULL, -- 奖励名称 + description TEXT, -- 描述 + icon VARCHAR(200), -- 图标 + rarity VARCHAR(20), -- 稀有度 + value JSON, -- 奖励值 + earned_time DATETIME, -- 获得时间 + is_new TINYINT NOT NULL DEFAULT 1, -- 是否新获得 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '奖励表'; + +-- ============================================================================ +-- 14. 访客用户表 (guest_user) +-- ============================================================================ +CREATE TABLE guest_user ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + guest_user_id VARCHAR(50) NOT NULL UNIQUE, -- 访客用户ID (格式: guest_xxx) + ip_address VARCHAR(45) NOT NULL, -- 客户端IP地址 (支持IPv6) + user_agent TEXT, -- 用户代理信息 + nickname VARCHAR(50), -- 访客昵称 + avatar VARCHAR(500), -- 访客头像 + last_active_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 最后活跃时间 + conversation_count INT NOT NULL DEFAULT 0, -- 会话数量 + message_count INT NOT NULL DEFAULT 0, -- 消息数量 + location VARCHAR(100), -- IP地址的地理位置信息 + device_info VARCHAR(200), -- 设备信息 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '访客用户表'; + +-- ============================================================================ +-- 15. 用户统计表 (user_stats) +-- ============================================================================ +CREATE TABLE user_stats ( + id VARCHAR(36) PRIMARY KEY, -- UUID主键 + user_id VARCHAR(36) NOT NULL UNIQUE, -- 用户ID + total_conversations INT NOT NULL DEFAULT 0, -- 总对话数 + total_messages INT NOT NULL DEFAULT 0, -- 总消息数 + total_emotions_recorded INT NOT NULL DEFAULT 0, -- 总情绪记录数 + topics_completed INT NOT NULL DEFAULT 0, -- 完成的课题数 + achievements_unlocked INT NOT NULL DEFAULT 0, -- 解锁的成就数 + total_points INT NOT NULL DEFAULT 0, -- 总积分 + consecutive_days INT NOT NULL DEFAULT 0, -- 连续使用天数 + max_consecutive_days INT NOT NULL DEFAULT 0, -- 最大连续天数 + locations_visited INT NOT NULL DEFAULT 0, -- 访问的地点数 + posts_created INT NOT NULL DEFAULT 0, -- 创建的帖子数 + comments_made INT NOT NULL DEFAULT 0, -- 评论数 + likes_received INT NOT NULL DEFAULT 0, -- 收到的点赞数 + social_interactions INT NOT NULL DEFAULT 0, -- 社交互动数 + -- 公共字段 + create_by VARCHAR(36), -- 创建人ID + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + update_by VARCHAR(36), -- 更新人ID + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 + is_deleted TINYINT NOT NULL DEFAULT 0, -- 是否删除: 0-未删除, 1-已删除 + remarks VARCHAR(500) -- 备注 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户统计表'; + +-- ============================================================================ +-- 创建索引以提高查询性能 +-- 注意: MySQL的CREATE INDEX不支持IF NOT EXISTS +-- 如果索引已存在,重复执行会产生警告但不会中断脚本执行 +-- ============================================================================ +-- user表索引 +CREATE INDEX idx_user_account ON user (account); + +CREATE INDEX idx_user_username ON user (username); + +CREATE INDEX idx_user_email ON user (email); + +CREATE INDEX idx_user_phone ON user (phone); + +CREATE INDEX idx_user_last_active_time ON user (last_active_time); + +CREATE INDEX idx_user_create_time ON user (create_time); + +CREATE INDEX idx_user_member_level ON user (member_level); + +CREATE INDEX idx_user_status ON user (status); + +CREATE INDEX idx_user_is_verified ON user (is_verified); + +CREATE INDEX idx_user_create_by ON user (create_by); + +CREATE INDEX idx_user_update_by ON user (update_by); + +CREATE INDEX idx_user_is_deleted ON user (is_deleted); + +CREATE INDEX idx_user_third_party_id ON user (third_party_id); + +CREATE INDEX idx_user_third_party_type ON user (third_party_type); + +-- conversation表索引 +CREATE INDEX idx_conversation_user_id ON conversation (user_id); + +CREATE INDEX idx_conversation_start_time ON conversation (start_time); + +CREATE INDEX idx_conversation_user_id_start_time ON conversation (user_id, start_time); + +CREATE INDEX idx_conversation_primary_emotion ON conversation (primary_emotion); + +CREATE INDEX idx_conversation_end_time ON conversation (end_time); + +CREATE INDEX idx_conversation_create_time ON conversation (create_time); + +CREATE INDEX idx_conversation_coze_conversation_id ON conversation (coze_conversation_id); + +CREATE INDEX idx_conversation_status ON conversation (status); + +CREATE INDEX idx_conversation_last_active_time ON conversation (last_active_time); + +CREATE INDEX idx_conversation_create_by ON conversation (create_by); + +CREATE INDEX idx_conversation_update_by ON conversation (update_by); + +CREATE INDEX idx_conversation_is_deleted ON conversation (is_deleted); + +CREATE INDEX idx_conversation_user_type ON conversation (user_type); + +CREATE INDEX idx_conversation_emotion_trend ON conversation (emotion_trend); + +CREATE INDEX idx_conversation_confidence ON conversation (confidence); + +CREATE INDEX idx_conversation_client_ip ON conversation (client_ip); + +-- message表索引 +CREATE INDEX idx_message_conversation_id ON message (conversation_id); + +CREATE INDEX idx_message_timestamp ON message (timestamp); + +CREATE INDEX idx_message_conversation_id_timestamp ON message (conversation_id, timestamp); + +CREATE INDEX idx_message_sender ON message (sender); + +CREATE INDEX idx_message_type ON message (type); + +CREATE INDEX idx_message_is_read ON message (is_read); + +CREATE INDEX idx_message_create_time ON message (create_time); + +CREATE INDEX idx_message_coze_chat_id ON message (coze_chat_id); + +CREATE INDEX idx_message_status ON message (status); + +CREATE INDEX idx_message_parent_message_id ON message (parent_message_id); + +CREATE INDEX idx_message_create_by ON message (create_by); + +CREATE INDEX idx_message_update_by ON message (update_by); + +CREATE INDEX idx_message_is_deleted ON message (is_deleted); + +-- coze_api_call表索引 +CREATE INDEX idx_coze_api_call_conversation_id ON coze_api_call (conversation_id); + +CREATE INDEX idx_coze_api_call_message_id ON coze_api_call (message_id); + +CREATE INDEX idx_coze_api_call_coze_chat_id ON coze_api_call (coze_chat_id); + +CREATE INDEX idx_coze_api_call_bot_id ON coze_api_call (bot_id); + +CREATE INDEX idx_coze_api_call_user_id ON coze_api_call (user_id); + +CREATE INDEX idx_coze_api_call_status ON coze_api_call (status); + +CREATE INDEX idx_coze_api_call_final_status ON coze_api_call (final_status); + +CREATE INDEX idx_coze_api_call_start_time ON coze_api_call (start_time); + +CREATE INDEX idx_coze_api_call_request_type ON coze_api_call (request_type); + +CREATE INDEX idx_coze_api_call_client_ip ON coze_api_call (client_ip); + +CREATE INDEX idx_coze_api_call_session_id ON coze_api_call (session_id); + +CREATE INDEX idx_coze_api_call_trace_id ON coze_api_call (trace_id); + +CREATE INDEX idx_coze_api_call_user_status ON coze_api_call (user_id, status); + +CREATE INDEX idx_coze_api_call_conversation_time ON coze_api_call (conversation_id, start_time); + +-- emotion_analysis表索引 +CREATE INDEX idx_emotion_analysis_user_id ON emotion_analysis (user_id); + +CREATE INDEX idx_emotion_analysis_message_id ON emotion_analysis (message_id); + +CREATE INDEX idx_emotion_analysis_primary_emotion ON emotion_analysis (primary_emotion); + +CREATE INDEX idx_emotion_analysis_analysis_time ON emotion_analysis (analysis_time); + +CREATE INDEX idx_emotion_analysis_create_time ON emotion_analysis (create_time); + +CREATE INDEX idx_emotion_analysis_create_by ON emotion_analysis (create_by); + +CREATE INDEX idx_emotion_analysis_update_by ON emotion_analysis (update_by); + +CREATE INDEX idx_emotion_analysis_is_deleted ON emotion_analysis (is_deleted); + +-- emotion_record表索引 +CREATE INDEX idx_emotion_record_user_id ON emotion_record (user_id); + +CREATE INDEX idx_emotion_record_date ON emotion_record (record_date); + +CREATE INDEX idx_emotion_record_emotion_type ON emotion_record (emotion_type); + +CREATE INDEX idx_emotion_record_user_id_date ON emotion_record (user_id, record_date); + +CREATE INDEX idx_emotion_record_user_id_emotion_type ON emotion_record (user_id, emotion_type); + +CREATE INDEX idx_emotion_record_intensity ON emotion_record (intensity); + +CREATE INDEX idx_emotion_record_create_time ON emotion_record (create_time); + +CREATE INDEX idx_emotion_record_create_by ON emotion_record (create_by); + +CREATE INDEX idx_emotion_record_update_by ON emotion_record (update_by); + +CREATE INDEX idx_emotion_record_is_deleted ON emotion_record (is_deleted); + +-- growth_topic表索引 +CREATE INDEX idx_growth_topic_category ON growth_topic (category); + +CREATE INDEX idx_growth_topic_difficulty ON growth_topic (difficulty); + +CREATE INDEX idx_growth_topic_is_unlocked ON growth_topic (is_unlocked); + +CREATE INDEX idx_growth_topic_progress ON growth_topic (progress); + +CREATE INDEX idx_growth_topic_completed_time ON growth_topic (completed_time); + +CREATE INDEX idx_growth_topic_category_difficulty ON growth_topic (category, difficulty); + +CREATE INDEX idx_growth_topic_create_time ON growth_topic (create_time); + +-- topic_interaction表索引 +CREATE INDEX idx_topic_interaction_topic_id ON topic_interaction (topic_id); + +CREATE INDEX idx_topic_interaction_type ON topic_interaction (type); + +CREATE INDEX idx_topic_interaction_completed_time ON topic_interaction (completed_time); + +CREATE INDEX idx_topic_interaction_rating ON topic_interaction (rating); + +CREATE INDEX idx_topic_interaction_topic_id_type ON topic_interaction (topic_id, type); + +CREATE INDEX idx_topic_interaction_create_time ON topic_interaction (create_time); + +-- location_pin表索引 +CREATE INDEX idx_location_pin_latitude_longitude ON location_pin (latitude, longitude); + +CREATE INDEX idx_location_pin_type ON location_pin (type); + +CREATE INDEX idx_location_pin_category ON location_pin (category); + +CREATE INDEX idx_location_pin_created_by ON location_pin (created_by); + +CREATE INDEX idx_location_pin_likes ON location_pin (likes); + +CREATE INDEX idx_location_pin_visits ON location_pin (visits); + +CREATE INDEX idx_location_pin_is_bookmarked ON location_pin (is_bookmarked); + +CREATE INDEX idx_location_pin_type_category ON location_pin (type, category); + +CREATE INDEX idx_location_pin_create_time ON location_pin (create_time); + +CREATE INDEX idx_location_pin_last_visit_time ON location_pin (last_visit_time); + +-- community_post表索引 +CREATE INDEX idx_community_post_user_id ON community_post (user_id); + +CREATE INDEX idx_community_post_location_id ON community_post (location_id); + +CREATE INDEX idx_community_post_create_time ON community_post (create_time); + +CREATE INDEX idx_community_post_type ON community_post (type); + +CREATE INDEX idx_community_post_likes ON community_post (likes); + +CREATE INDEX idx_community_post_view_count ON community_post (view_count); + +CREATE INDEX idx_community_post_is_private ON community_post (is_private); + +CREATE INDEX idx_community_post_user_id_create_time ON community_post (user_id, create_time); + +CREATE INDEX idx_community_post_type_create_time ON community_post (type, create_time); + +-- comment表索引 +CREATE INDEX idx_comment_post_id ON comment (post_id); + +CREATE INDEX idx_comment_user_id ON comment (user_id); + +CREATE INDEX idx_comment_reply_to_id ON comment (reply_to_id); + +CREATE INDEX idx_comment_create_time ON comment (create_time); + +CREATE INDEX idx_comment_likes ON comment (likes); + +CREATE INDEX idx_comment_post_id_create_time ON comment (post_id, create_time); + +-- achievement表索引 +CREATE INDEX idx_achievement_category ON achievement (category); + +CREATE INDEX idx_achievement_rarity ON achievement (rarity); + +CREATE INDEX idx_achievement_unlocked_time ON achievement (unlocked_time); + +CREATE INDEX idx_achievement_is_hidden ON achievement (is_hidden); + +CREATE INDEX idx_achievement_progress ON achievement (progress); + +CREATE INDEX idx_achievement_category_rarity ON achievement (category, rarity); + +CREATE INDEX idx_achievement_create_time ON achievement (create_time); + +-- reward表索引 +CREATE INDEX idx_reward_topic_id ON reward (topic_id); + +CREATE INDEX idx_reward_achievement_id ON reward (achievement_id); + +CREATE INDEX idx_reward_type ON reward (type); + +CREATE INDEX idx_reward_earned_time ON reward (earned_time); + +CREATE INDEX idx_reward_rarity ON reward (rarity); + +CREATE INDEX idx_reward_is_new ON reward (is_new); + +CREATE INDEX idx_reward_type_earned_time ON reward (type, earned_time); + +CREATE INDEX idx_reward_create_time ON reward (create_time); + +-- user_stats表索引 +CREATE INDEX idx_user_stats_user_id ON user_stats (user_id); + +CREATE INDEX idx_user_stats_total_points ON user_stats (total_points); + +CREATE INDEX idx_user_stats_consecutive_days ON user_stats (consecutive_days); + +CREATE INDEX idx_user_stats_max_consecutive_days ON user_stats (max_consecutive_days); + +CREATE INDEX idx_user_stats_social_interactions ON user_stats (social_interactions); + +CREATE INDEX idx_user_stats_update_time ON user_stats (update_time); + +CREATE INDEX idx_user_stats_create_time ON user_stats (create_time); + +-- guest_user表索引 +CREATE INDEX idx_guest_user_guest_user_id ON guest_user (guest_user_id); + +CREATE INDEX idx_guest_user_ip_address ON guest_user (ip_address); + +CREATE INDEX idx_guest_user_last_active_time ON guest_user (last_active_time); + +CREATE INDEX idx_guest_user_conversation_count ON guest_user (conversation_count); + +CREATE INDEX idx_guest_user_message_count ON guest_user (message_count); + +CREATE INDEX idx_guest_user_create_time ON guest_user (create_time); + +CREATE INDEX idx_guest_user_create_by ON guest_user (create_by); + +CREATE INDEX idx_guest_user_update_by ON guest_user (update_by); + +CREATE INDEX idx_guest_user_is_deleted ON guest_user (is_deleted); + +-- ============================================================================ +-- 数据库统计信息 +-- ============================================================================ +SELECT + COUNT(*) as total_tables +FROM + INFORMATION_SCHEMA.TABLES +WHERE + TABLE_SCHEMA = 'emotion_museum'; + +-- 显示创建的表 +SELECT + TABLE_NAME as table_name, + TABLE_COMMENT as comment, + ENGINE as engine +FROM + INFORMATION_SCHEMA.TABLES +WHERE + TABLE_SCHEMA = 'emotion_museum' +ORDER BY + TABLE_NAME; + +-- 提交事务 +COMMIT; \ No newline at end of file diff --git a/web-flowith/.env.development b/web-flowith/.env.development index cdfe7e3..cf6ff38 100644 --- a/web-flowith/.env.development +++ b/web-flowith/.env.development @@ -4,10 +4,10 @@ VITE_APP_TITLE=开心APP - 开发环境 VITE_APP_DESCRIPTION=你的情绪陪伴使者 -# API配置 - 通过网关访问 -VITE_API_BASE_URL=http://localhost:19000 -VITE_UPLOAD_URL=http://localhost:19000/api/upload -VITE_WS_URL=http://localhost:19000/ws/chat +# API配置 - 直接访问backend-single +VITE_API_BASE_URL=http://localhost:8080/api +VITE_UPLOAD_URL=http://localhost:8080/api/upload +VITE_WS_URL=http://localhost:8080/ws/chat # WebSocket配置 VITE_WS_RECONNECT_ATTEMPTS=5 diff --git a/web-flowith/.env.production b/web-flowith/.env.production index 3cb51b3..a73dda5 100644 --- a/web-flowith/.env.production +++ b/web-flowith/.env.production @@ -3,7 +3,7 @@ VITE_APP_TITLE=开心APP VITE_APP_DESCRIPTION=你的情绪陪伴使者 # API配置 - 生产环境通过网关访问 -VITE_API_BASE_URL=http://47.111.10.27:19000 +VITE_API_BASE_URL=http://47.111.10.27:19000/api VITE_UPLOAD_URL=http://47.111.10.27:19000/api/upload VITE_WS_URL=http://47.111.10.27:19000/ws/chat diff --git a/web-flowith/index.html b/web-flowith/index.html index 73f134c..527d57a 100644 --- a/web-flowith/index.html +++ b/web-flowith/index.html @@ -4,6 +4,7 @@ + 开心APP - 你的情绪陪伴使者 diff --git a/web-flowith/src/components/layout/AppHeader.vue b/web-flowith/src/components/layout/AppHeader.vue index 87aac26..563c1a7 100644 --- a/web-flowith/src/components/layout/AppHeader.vue +++ b/web-flowith/src/components/layout/AppHeader.vue @@ -28,11 +28,21 @@