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

- 创建完整的entity实体类体系,包括所有业务实体
- 实现BaseEntity基类,统一管理公共字段
- 创建雪花算法ID生成器和自动填充处理器
- 简化所有mapper接口,只继承BaseMapper
- 重构service层,使用LambdaQueryWrapper进行数据库操作
- 创建BasePageRequest分页查询基类
- 完善用户上下文管理和JWT认证
- 新增WebSocket聊天功能和相关控制器
- 更新前端配置和组件,完善用户认证流程
- 同步数据库建表脚本
This commit is contained in:
2025-07-24 00:37:23 +08:00
parent 645036fcd2
commit 880e0e3c88
87 changed files with 8114 additions and 1106 deletions
@@ -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_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_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_session_id ON coze_api_call (session_id);
@@ -853,10 +851,4 @@ ORDER BY
TABLE_NAME; TABLE_NAME;
-- 提交事务 -- 提交事务
COMMIT; 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;
@@ -1,15 +1,17 @@
package com.emotion; package com.emotion;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
/** /**
* 情感博物馆简化版启动类 * 情感博物馆简化版启动类
* *
* @author emotion-museum * @author emotion-museum
* @date 2025-07-21 * @date 2025-07-21
*/ */
@SpringBootApplication @SpringBootApplication
@MapperScan("com.emotion.mapper")
public class EmotionSimpleApplication { public class EmotionSimpleApplication {
public static void main(String[] args) { public static void main(String[] args) {
@@ -21,7 +21,7 @@ public abstract class BaseEntity implements Serializable {
/** /**
* 主键ID * 主键ID
*/ */
@TableId(type = IdType.ASSIGN_ID) @TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id; private String id;
/** /**
@@ -0,0 +1,44 @@
package com.emotion.common;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
/**
* 分页查询基类请求参数
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
public class BasePageRequest {
/**
* 当前页码,从1开始
*/
@Min(value = 1, message = "页码不能小于1")
private Long current = 1L;
/**
* 每页大小
*/
@Min(value = 1, message = "每页大小不能小于1")
@Max(value = 100, message = "每页大小不能超过100")
private Long size = 10L;
/**
* 排序字段
*/
private String orderBy;
/**
* 排序方式:asc-升序,desc-降序
*/
private String orderDirection = "desc";
/**
* 搜索关键词
*/
private String keyword;
}
@@ -0,0 +1,91 @@
package com.emotion.config;
import com.emotion.util.SnowflakeIdGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.InetAddress;
import java.net.NetworkInterface;
/**
* ID生成器配置类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
@Configuration
public class IdGeneratorConfig {
/**
* 机器ID配置(可通过配置文件指定)
*/
@Value("${emotion.snowflake.machine-id:#{null}}")
private Long machineId;
/**
* 配置雪花算法ID生成器
*
* @return SnowflakeIdGenerator实例
*/
@Bean
public SnowflakeIdGenerator snowflakeIdGenerator() {
long finalMachineId;
if (machineId != null) {
// 使用配置文件中指定的机器ID
finalMachineId = machineId;
log.info("使用配置文件指定的机器ID: {}", finalMachineId);
} else {
// 自动生成机器ID
finalMachineId = generateMachineId();
log.info("自动生成机器ID: {}", finalMachineId);
}
return new SnowflakeIdGenerator(finalMachineId);
}
/**
* 自动生成机器ID
* 基于MAC地址和IP地址生成唯一的机器ID
*
* @return 机器ID (0-1023)
*/
private long generateMachineId() {
try {
// 获取本机IP地址
InetAddress localHost = InetAddress.getLocalHost();
// 获取网络接口
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(localHost);
if (networkInterface != null) {
// 获取MAC地址
byte[] hardwareAddress = networkInterface.getHardwareAddress();
if (hardwareAddress != null && hardwareAddress.length >= 6) {
// 使用MAC地址的后两个字节生成机器ID
long machineId = ((hardwareAddress[4] & 0xFF) << 8) | (hardwareAddress[5] & 0xFF);
// 确保机器ID在有效范围内 (0-1023)
return machineId & 0x3FF;
}
}
// 如果无法获取MAC地址,使用IP地址生成
byte[] address = localHost.getAddress();
if (address.length >= 4) {
// 使用IP地址的后两个字节生成机器ID
long machineId = ((address[2] & 0xFF) << 8) | (address[3] & 0xFF);
return machineId & 0x3FF;
}
} catch (Exception e) {
log.warn("自动生成机器ID失败,使用默认策略: {}", e.getMessage());
}
// 如果所有方法都失败,使用当前时间戳生成
return System.currentTimeMillis() % 1024;
}
}
@@ -0,0 +1,28 @@
package com.emotion.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus配置类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Configuration
public class MybatisPlusConfig {
/**
* 分页插件配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
@@ -0,0 +1,57 @@
package com.emotion.config;
import com.emotion.interceptor.UserContextInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置类
* 配置拦截器、跨域等
*
* @author emotion-museum
* @date 2025-07-23
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserContextInterceptor userContextInterceptor;
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userContextInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/error",
"/favicon.ico",
"/actuator/**",
"/swagger-ui/**",
"/swagger-resources/**",
"/v2/api-docs",
"/v3/api-docs",
"/webjars/**",
"/doc.html",
"/static/**",
"/public/**"
);
}
/**
* 跨域配置
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
@@ -0,0 +1,37 @@
package com.emotion.config;
import com.emotion.interceptor.JwtAuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC配置
*
* @author emotion-museum
* @date 2025-07-23
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private JwtAuthInterceptor jwtAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtAuthInterceptor)
.addPathPatterns("/api/**") // 拦截所有API请求
.excludePathPatterns(
"/api/auth/login", // 登录接口
"/api/auth/register", // 注册接口
"/api/auth/captcha", // 验证码接口
"/api/auth/refresh-token", // 刷新token接口
"/api/health", // 健康检查接口
"/api/ws/**", // WebSocket接口
"/swagger-ui/**", // Swagger UI
"/v3/api-docs/**", // API文档
"/actuator/**" // 监控端点
);
}
}
@@ -0,0 +1,162 @@
package com.emotion.controller;
import com.emotion.common.Result;
import com.emotion.service.IAiService;
import com.emotion.service.IMessageService;
import com.emotion.service.IConversationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* AI聊天控制器
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
@RestController
@RequestMapping("/api/ai")
public class AiChatController {
@Autowired
private IAiService aiService;
@Autowired
private IMessageService messageService;
@Autowired
private IConversationService conversationService;
/**
* 发送聊天消息
*/
@PostMapping("/chat")
public Result<Map<String, Object>> sendChatMessage(@RequestBody Map<String, String> request) {
try {
String conversationId = request.get("conversationId");
String message = request.get("message");
String userId = request.get("userId");
if (message == null || message.trim().isEmpty()) {
return Result.error("消息内容不能为空");
}
if (userId == null || userId.trim().isEmpty()) {
userId = "guest_" + System.currentTimeMillis();
}
log.info("收到AI聊天请求: conversationId={}, userId={}, message={}",
conversationId, userId, message);
// 调用AI服务
String aiReply = aiService.sendChatMessage(conversationId, message, userId);
Map<String, Object> response = new HashMap<>();
response.put("conversationId", conversationId);
response.put("userMessage", message);
response.put("aiReply", aiReply);
response.put("userId", userId);
response.put("timestamp", System.currentTimeMillis());
return Result.success(response);
} catch (Exception e) {
log.error("AI聊天请求处理失败", e);
return Result.error("AI聊天服务暂时不可用,请稍后再试");
}
}
/**
* 生成对话总结
*/
@PostMapping("/summary")
public Result<Map<String, Object>> generateSummary(@RequestBody Map<String, String> request) {
try {
String conversationId = request.get("conversationId");
String userId = request.get("userId");
if (conversationId == null || conversationId.trim().isEmpty()) {
return Result.error("会话ID不能为空");
}
if (userId == null || userId.trim().isEmpty()) {
userId = "guest_" + System.currentTimeMillis();
}
log.info("收到对话总结请求: conversationId={}, userId={}", conversationId, userId);
// 调用AI总结服务
String summary = aiService.generateConversationSummary(conversationId, userId);
Map<String, Object> response = new HashMap<>();
response.put("conversationId", conversationId);
response.put("summary", summary);
response.put("userId", userId);
response.put("timestamp", System.currentTimeMillis());
return Result.success(response);
} catch (Exception e) {
log.error("对话总结请求处理失败", e);
return Result.error("对话总结服务暂时不可用,请稍后再试");
}
}
/**
* 获取AI服务状态
*/
@GetMapping("/status")
public Result<Map<String, Object>> getServiceStatus() {
try {
boolean available = aiService.isServiceAvailable();
String status = aiService.getServiceStatus();
Map<String, Object> response = new HashMap<>();
response.put("available", available);
response.put("status", status);
response.put("timestamp", System.currentTimeMillis());
return Result.success(response);
} catch (Exception e) {
log.error("获取AI服务状态失败", e);
return Result.error("无法获取AI服务状态");
}
}
/**
* 获取聊天记录统计
*/
@GetMapping("/stats")
public Result<Map<String, Object>> getChatStats(@RequestParam(required = false) String userId,
@RequestParam(required = false) String conversationId) {
try {
Map<String, Object> stats = new HashMap<>();
if (userId != null && !userId.trim().isEmpty()) {
Long userConversationCount = conversationService.countByUserId(userId);
Long activeConversationCount = conversationService.countActiveByUserId(userId);
stats.put("userConversationCount", userConversationCount);
stats.put("activeConversationCount", activeConversationCount);
}
if (conversationId != null && !conversationId.trim().isEmpty()) {
Long conversationMessageCount = messageService.countByConversationId(conversationId);
stats.put("conversationMessageCount", conversationMessageCount);
}
stats.put("timestamp", System.currentTimeMillis());
return Result.success(stats);
} catch (Exception e) {
log.error("获取聊天统计失败", e);
return Result.error("无法获取聊天统计信息");
}
}
}
@@ -11,6 +11,11 @@ import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; 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); private static final Logger log = LoggerFactory.getLogger(AuthController.class);
// 验证码存储(生产环境应使用Redis)
private static final Map<String, String> captchaStore = new ConcurrentHashMap<>();
@Autowired @Autowired
private UserService userService; private UserService userService;
@Autowired
private JwtUtil jwtUtil;
/** /**
* 用户登录 * 用户登录
*/ */
@@ -37,10 +48,30 @@ public class AuthController {
try { try {
String account = request.get("account"); String account = request.get("account");
String password = request.get("password"); String password = request.get("password");
String captcha = request.get("captcha");
String captchaKey = request.get("captchaKey");
if (account == null || password == null) { if (account == null || password == null) {
return Result.error("账号和密码不能为空"); 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); User user = userService.findByAccount(account);
@@ -56,9 +87,14 @@ public class AuthController {
// 更新最后活跃时间 // 更新最后活跃时间
userService.updateLastActiveTime(user.getId()); userService.updateLastActiveTime(user.getId());
// 生成JWT token
String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());
// 构建响应 // 构建响应
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("accessToken", "token-" + user.getId() + "-" + System.currentTimeMillis()); response.put("accessToken", accessToken);
response.put("refreshToken", refreshToken);
response.put("expiresIn", 86400L); response.put("expiresIn", 86400L);
Map<String, Object> userInfo = new HashMap<>(); Map<String, Object> userInfo = new HashMap<>();
@@ -85,7 +121,7 @@ public class AuthController {
@PostMapping("/register") @PostMapping("/register")
public Result<Map<String, Object>> register(@RequestBody Map<String, String> request) { public Result<Map<String, Object>> register(@RequestBody Map<String, String> request) {
log.info("用户注册请求: {}", request.get("account")); log.info("用户注册请求: {}", request.get("account"));
try { try {
String account = request.get("account"); String account = request.get("account");
String password = request.get("password"); String password = request.get("password");
@@ -93,11 +129,31 @@ public class AuthController {
String email = request.get("email"); String email = request.get("email");
String phone = request.get("phone"); String phone = request.get("phone");
String nickname = request.get("nickname"); String nickname = request.get("nickname");
String captcha = request.get("captcha");
String captchaKey = request.get("captchaKey");
if (account == null || password == null) { if (account == null || password == null) {
return Result.error("账号和密码不能为空"); 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)) { if (userService.accountExists(account)) {
return Result.error("账号已存在"); return Result.error("账号已存在");
@@ -113,37 +169,112 @@ public class AuthController {
user.setNickname(nickname != null ? nickname : username != null ? username : account); user.setNickname(nickname != null ? nickname : username != null ? username : account);
User createdUser = userService.createUser(user); User createdUser = userService.createUser(user);
// 构建响应 // 生成JWT token(注册成功后自动登录)
String accessToken = jwtUtil.generateToken(createdUser.getId(), createdUser.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(createdUser.getId(), createdUser.getUsername());
// 构建用户信息
Map<String, Object> userInfo = new HashMap<>(); Map<String, Object> userInfo = new HashMap<>();
userInfo.put("id", createdUser.getId()); userInfo.put("id", createdUser.getId());
userInfo.put("username", createdUser.getUsername()); userInfo.put("username", createdUser.getUsername());
userInfo.put("account", createdUser.getAccount()); userInfo.put("account", createdUser.getAccount());
userInfo.put("nickname", createdUser.getNickname()); userInfo.put("nickname", createdUser.getNickname());
userInfo.put("avatar", createdUser.getAvatar());
userInfo.put("status", createdUser.getStatus()); userInfo.put("status", createdUser.getStatus());
userInfo.put("createTime", createdUser.getCreateTime()); userInfo.put("createTime", createdUser.getCreateTime());
return Result.success("注册成功", userInfo); // 构建完整响应(包含token信息)
Map<String, Object> response = new HashMap<>();
response.put("accessToken", accessToken);
response.put("refreshToken", refreshToken);
response.put("expiresIn", 86400L); // 24小时
response.put("userInfo", userInfo);
response.put("loginTime", LocalDateTime.now());
log.info("用户注册并自动登录成功: {}", createdUser.getAccount());
return Result.success("注册成功,已自动登录", response);
} catch (Exception e) { } catch (Exception e) {
log.error("用户注册失败: {}", e.getMessage()); log.error("用户注册失败: {}", e.getMessage());
return Result.error("注册失败: " + e.getMessage()); return Result.error("注册失败: " + e.getMessage());
} }
} }
/**
* 获取当前用户信息
*/
@GetMapping("/user-info")
public Result<Map<String, Object>> getCurrentUserInfo(HttpServletRequest request) {
try {
// 从请求属性中获取用户信息(由JWT拦截器设置)
String userId = (String) request.getAttribute("userId");
String username = (String) request.getAttribute("username");
if (userId == null) {
return Result.error("用户未登录");
}
// 根据用户ID获取完整用户信息
User user = userService.findById(userId);
if (user == null) {
return Result.error("用户不存在");
}
// 构建用户信息响应
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("id", user.getId());
userInfo.put("username", user.getUsername());
userInfo.put("account", user.getAccount());
userInfo.put("nickname", user.getNickname());
userInfo.put("avatar", user.getAvatar());
userInfo.put("email", user.getEmail());
userInfo.put("phone", user.getPhone());
userInfo.put("status", user.getStatus());
userInfo.put("createTime", user.getCreateTime());
return Result.success("获取用户信息成功", userInfo);
} catch (Exception e) {
log.error("获取用户信息失败: {}", e.getMessage());
return Result.error("获取用户信息失败");
}
}
/** /**
* 获取验证码 * 获取验证码
*/ */
@GetMapping("/captcha") @GetMapping("/captcha")
public Result<Map<String, Object>> getCaptcha() { public Result<Map<String, Object>> getCaptcha() {
log.info("获取验证码请求"); log.info("获取验证码请求");
try { try {
// 生成验证码
SpecCaptcha captcha = new SpecCaptcha(130, 48, 4);
captcha.setCharType(Captcha.TYPE_DEFAULT);
// 生成验证码key
String captchaKey = "captcha_" + System.currentTimeMillis();
String captchaText = captcha.text().toLowerCase();
// 存储验证码(5分钟过期)
captchaStore.put(captchaKey, captchaText);
// 5分钟后清理验证码
new Thread(() -> {
try {
Thread.sleep(300000); // 5分钟
captchaStore.remove(captchaKey);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("captchaId", "captcha-" + System.currentTimeMillis()); response.put("key", captchaKey);
response.put("captchaImage", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="); response.put("image", captcha.toBase64().replace("data:image/png;base64,", ""));
response.put("type", "spec");
response.put("expireTime", 300); response.put("expireTime", 300);
log.info("生成验证码成功,key: {}, text: {}", captchaKey, captchaText);
return Result.success("获取验证码成功", response); return Result.success("获取验证码成功", response);
} catch (Exception e) { } catch (Exception e) {
log.error("获取验证码失败: {}", e.getMessage()); log.error("获取验证码失败: {}", e.getMessage());
@@ -0,0 +1,139 @@
package com.emotion.controller;
import com.emotion.dto.websocket.ChatRequest;
import com.emotion.dto.websocket.ConnectRequest;
import com.emotion.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;
import javax.validation.Valid;
import java.security.Principal;
/**
* 优化的WebSocket聊天控制器
* 使用规范的请求对象封装参数
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
@Controller
public class ChatWebSocketController {
@Autowired
private WebSocketService webSocketService;
/**
* 处理聊天消息
* @param chatRequest 聊天请求对象
* @param headerAccessor 消息头访问器
* @param principal 用户主体
*/
@MessageMapping("/chat.send")
public void handleChatMessage(@Valid @Payload ChatRequest chatRequest,
SimpMessageHeaderAccessor headerAccessor,
Principal principal) {
try {
log.info("收到WebSocket聊天消息: {}", chatRequest);
// 获取会话ID
String sessionId = headerAccessor.getSessionId();
// 如果请求中没有发送者ID,尝试从Principal获取
if (chatRequest.getSenderId() == null && principal != null) {
chatRequest.setSenderId(principal.getName());
}
// 如果还是没有发送者ID,使用会话ID作为访客ID
if (chatRequest.getSenderId() == null) {
chatRequest.setSenderId("guest_" + sessionId);
chatRequest.setSenderType(ChatRequest.SenderType.GUEST);
}
// 设置时间戳
if (chatRequest.getTimestamp() == null) {
chatRequest.setTimestamp(System.currentTimeMillis());
}
// 处理聊天消息
webSocketService.handleChatMessage(chatRequest, sessionId, principal);
} catch (Exception e) {
log.error("处理WebSocket聊天消息失败", e);
}
}
/**
* 处理用户连接
* @param connectRequest 连接请求对象
* @param headerAccessor 消息头访问器
* @param principal 用户主体
*/
@MessageMapping("/chat.connect")
public void connectUser(@Payload ConnectRequest connectRequest,
SimpMessageHeaderAccessor headerAccessor,
Principal principal) {
try {
String sessionId = headerAccessor.getSessionId();
log.info("用户连接WebSocket: connectRequest={}, sessionId={}, principal={}",
connectRequest, sessionId, principal);
// 如果请求中没有用户ID,尝试从Principal获取
if (connectRequest.getUserId() == null && principal != null) {
connectRequest.setUserId(principal.getName());
}
// 设置连接时间戳
if (connectRequest.getTimestamp() == null) {
connectRequest.setTimestamp(System.currentTimeMillis());
}
// 处理用户连接
webSocketService.handleUserConnect(connectRequest, sessionId, principal);
} catch (Exception e) {
log.error("处理用户WebSocket连接失败", e);
}
}
/**
* 处理用户断开连接
* @param headerAccessor 消息头访问器
* @param principal 用户主体
*/
@MessageMapping("/chat.disconnect")
public void disconnectUser(SimpMessageHeaderAccessor headerAccessor, Principal principal) {
try {
String sessionId = headerAccessor.getSessionId();
log.info("用户断开WebSocket连接: sessionId={}, principal={}", sessionId, principal);
// 处理用户断开连接
webSocketService.handleUserDisconnect(sessionId, principal);
} catch (Exception e) {
log.error("处理用户WebSocket断开连接失败", e);
}
}
/**
* 处理心跳消息
* @param headerAccessor 消息头访问器
* @param principal 用户主体
*/
@MessageMapping("/chat.heartbeat")
public void heartbeat(SimpMessageHeaderAccessor headerAccessor, Principal principal) {
try {
String sessionId = headerAccessor.getSessionId();
// 处理心跳消息
webSocketService.handleHeartbeat(sessionId, principal);
} catch (Exception e) {
log.error("处理WebSocket心跳失败", e);
}
}
}
@@ -0,0 +1,220 @@
package com.emotion.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.emotion.common.Result;
import com.emotion.entity.Message;
import com.emotion.service.IMessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 消息控制器
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
@RestController
@RequestMapping("/api/message")
public class MessageController {
@Autowired
private IMessageService messageService;
/**
* 保存消息
*/
@PostMapping("/save")
public Result<Message> saveMessage(@RequestBody Map<String, String> request) {
try {
String conversationId = request.get("conversationId");
String content = request.get("content");
String type = request.get("type");
String sender = request.get("sender");
if (content == null || content.trim().isEmpty()) {
return Result.error("消息内容不能为空");
}
if (sender == null || sender.trim().isEmpty()) {
return Result.error("发送者不能为空");
}
Message message = messageService.saveMessage(conversationId, content, type, sender);
if (message != null) {
return Result.success(message);
} else {
return Result.error("保存消息失败");
}
} catch (Exception e) {
log.error("保存消息失败", e);
return Result.error("保存消息失败:" + e.getMessage());
}
}
/**
* 根据会话ID分页查询消息
*/
@GetMapping("/conversation/{conversationId}")
public Result<IPage<Message>> getMessagesByConversationId(
@PathVariable String conversationId,
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "20") Integer size) {
try {
Page<Message> page = new Page<>(current, size);
IPage<Message> result = messageService.getByConversationId(page, conversationId);
return Result.success(result);
} catch (Exception e) {
log.error("查询会话消息失败", e);
return Result.error("查询会话消息失败:" + e.getMessage());
}
}
/**
* 根据发送者分页查询消息
*/
@GetMapping("/sender/{sender}")
public Result<IPage<Message>> getMessagesBySender(
@PathVariable String sender,
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "20") Integer size) {
try {
Page<Message> page = new Page<>(current, size);
IPage<Message> result = messageService.getBySender(page, sender);
return Result.success(result);
} catch (Exception e) {
log.error("查询发送者消息失败", e);
return Result.error("查询发送者消息失败:" + e.getMessage());
}
}
/**
* 查询会话的最后一条消息
*/
@GetMapping("/last/{conversationId}")
public Result<Message> getLastMessage(@PathVariable String conversationId) {
try {
Message message = messageService.getLastMessageByConversationId(conversationId);
return Result.success(message);
} catch (Exception e) {
log.error("查询最后一条消息失败", e);
return Result.error("查询最后一条消息失败:" + e.getMessage());
}
}
/**
* 统计会话消息数量
*/
@GetMapping("/count/{conversationId}")
public Result<Long> countMessages(@PathVariable String conversationId) {
try {
Long count = messageService.countByConversationId(conversationId);
return Result.success(count);
} catch (Exception e) {
log.error("统计消息数量失败", e);
return Result.error("统计消息数量失败:" + e.getMessage());
}
}
/**
* 更新消息状态
*/
@PutMapping("/{messageId}/status")
public Result<Boolean> updateStatus(@PathVariable String messageId,
@RequestBody Map<String, String> request) {
try {
String status = request.get("status");
if (status == null || status.trim().isEmpty()) {
return Result.error("状态不能为空");
}
boolean success = messageService.updateStatus(messageId, status);
return Result.success(success);
} catch (Exception e) {
log.error("更新消息状态失败", e);
return Result.error("更新消息状态失败:" + e.getMessage());
}
}
/**
* 标记消息为已读
*/
@PutMapping("/{messageId}/read")
public Result<Boolean> markAsRead(@PathVariable String messageId) {
try {
boolean success = messageService.updateReadStatus(messageId, 1);
return Result.success(success);
} catch (Exception e) {
log.error("标记消息已读失败", e);
return Result.error("标记消息已读失败:" + e.getMessage());
}
}
/**
* 批量标记会话消息为已读
*/
@PutMapping("/conversation/{conversationId}/read")
public Result<Boolean> markConversationAsRead(@PathVariable String conversationId) {
try {
boolean success = messageService.markConversationMessagesAsRead(conversationId);
return Result.success(success);
} catch (Exception e) {
log.error("批量标记消息已读失败", e);
return Result.error("批量标记消息已读失败:" + e.getMessage());
}
}
/**
* 获取消息统计信息
*/
@GetMapping("/stats")
public Result<Map<String, Object>> getMessageStats(
@RequestParam(required = false) String conversationId,
@RequestParam(required = false) String sender) {
try {
Map<String, Object> stats = new HashMap<>();
if (conversationId != null && !conversationId.trim().isEmpty()) {
Long conversationCount = messageService.countByConversationId(conversationId);
stats.put("conversationMessageCount", conversationCount);
Message lastMessage = messageService.getLastMessageByConversationId(conversationId);
stats.put("lastMessage", lastMessage);
}
if (sender != null && !sender.trim().isEmpty()) {
Long senderCount = messageService.countBySender(sender);
stats.put("senderMessageCount", senderCount);
}
stats.put("timestamp", System.currentTimeMillis());
return Result.success(stats);
} catch (Exception e) {
log.error("获取消息统计失败", e);
return Result.error("获取消息统计失败:" + e.getMessage());
}
}
/**
* 删除会话的所有消息
*/
@DeleteMapping("/conversation/{conversationId}")
public Result<Boolean> deleteConversationMessages(@PathVariable String conversationId) {
try {
boolean success = messageService.deleteByConversationId(conversationId);
return Result.success(success);
} catch (Exception e) {
log.error("删除会话消息失败", e);
return Result.error("删除会话消息失败:" + e.getMessage());
}
}
}
@@ -1,7 +1,7 @@
package com.emotion.controller; package com.emotion.controller;
import com.emotion.common.Result; import com.emotion.common.Result;
import com.emotion.entity.SimpleUser; import com.emotion.entity.User;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -10,6 +10,8 @@ import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller; 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.HashMap;
import java.util.Map; import java.util.Map;
@@ -31,59 +33,9 @@ public class WebSocketController {
@Autowired @Autowired
private AiService aiService; private AiService aiService;
/** // 已移除旧的WebSocket消息处理方法,使用新的ChatWebSocketController
* 处理聊天消息
*/
@MessageMapping("/chat.send")
@SendTo("/topic/public")
public Map<String, Object> sendMessage(@Payload Map<String, Object> chatMessage) {
log.info("收到WebSocket消息: {}", chatMessage);
try {
String content = (String) chatMessage.get("content");
String sender = (String) chatMessage.get("sender");
String type = (String) chatMessage.get("type");
// 构建响应消息
Map<String, Object> response = new HashMap<>();
response.put("content", content);
response.put("sender", sender);
response.put("type", type);
response.put("timestamp", System.currentTimeMillis());
return response;
} catch (Exception e) {
log.error("处理WebSocket消息失败", e);
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("content", "消息处理失败");
errorResponse.put("type", "ERROR");
errorResponse.put("timestamp", System.currentTimeMillis());
return errorResponse;
}
}
/** // 已移除旧的用户连接处理方法,使用新的ChatWebSocketController
* 处理用户连接
*/
@MessageMapping("/chat.connect")
@SendTo("/topic/public")
public Map<String, Object> connectUser(@Payload Map<String, Object> chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
String username = (String) chatMessage.get("sender");
// 在WebSocket会话中添加用户名
headerAccessor.getSessionAttributes().put("username", username);
log.info("用户连接: {}", username);
Map<String, Object> response = new HashMap<>();
response.put("content", username + " 加入了聊天");
response.put("type", "JOIN");
response.put("sender", "System");
response.put("timestamp", System.currentTimeMillis());
return response;
}
/** /**
* 处理AI聊天消息 * 处理AI聊天消息
@@ -142,7 +94,22 @@ public class WebSocketController {
systemMessage.put("sender", "System"); systemMessage.put("sender", "System");
systemMessage.put("type", "SYSTEM"); systemMessage.put("type", "SYSTEM");
systemMessage.put("timestamp", System.currentTimeMillis()); systemMessage.put("timestamp", System.currentTimeMillis());
messagingTemplate.convertAndSend(destination, systemMessage); messagingTemplate.convertAndSend(destination, systemMessage);
} }
/**
* WebSocket状态监控接口
*/
@GetMapping("/api/ws/status")
@ResponseBody
public Map<String, Object> getWebSocketStatus() {
Map<String, Object> status = new HashMap<>();
status.put("status", "active");
status.put("timestamp", System.currentTimeMillis());
status.put("endpoint", "/ws/chat");
status.put("protocols", new String[]{"websocket", "sockjs"});
status.put("message", "WebSocket服务正常运行");
return status;
}
} }
@@ -0,0 +1,97 @@
package com.emotion.dto.websocket;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* WebSocket聊天请求对象
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatRequest {
/**
* 消息内容
*/
@NotBlank(message = "消息内容不能为空")
private String content;
/**
* 发送者ID
*/
@NotBlank(message = "发送者ID不能为空")
private String senderId;
/**
* 发送者类型
*/
@NotNull(message = "发送者类型不能为空")
private SenderType senderType;
/**
* 消息类型
*/
@NotNull(message = "消息类型不能为空")
private MessageType messageType;
/**
* 会话ID(可选)
*/
private String conversationId;
/**
* 发送时间戳
*/
private Long timestamp;
/**
* 发送者类型枚举
*/
public enum SenderType {
USER("用户"),
GUEST("访客"),
AI("AI助手"),
SYSTEM("系统");
private final String description;
SenderType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 消息类型枚举
*/
public enum MessageType {
TEXT("文本消息"),
IMAGE("图片消息"),
FILE("文件消息"),
SYSTEM("系统消息"),
HEARTBEAT("心跳消息");
private final String description;
MessageType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
}
@@ -0,0 +1,46 @@
package com.emotion.dto.websocket;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import javax.validation.constraints.NotBlank;
/**
* WebSocket连接请求对象
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ConnectRequest {
/**
* 用户ID
*/
private String userId;
/**
* 用户名
*/
private String username;
/**
* 客户端类型
*/
private String clientType;
/**
* 客户端版本
*/
private String clientVersion;
/**
* 连接时间戳
*/
private Long timestamp;
}
@@ -0,0 +1,130 @@
package com.emotion.dto.websocket;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
/**
* WebSocket消息对象
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WebSocketMessage {
/**
* 消息ID
*/
private String messageId;
/**
* 会话ID
*/
private String conversationId;
/**
* 消息类型
*/
private MessageType type;
/**
* 消息内容
*/
private String content;
/**
* 发送者ID
*/
private String senderId;
/**
* 发送者类型
*/
private SenderType senderType;
/**
* 消息状态
*/
private MessageStatus status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 扩展数据
*/
private Object data;
/**
* 消息类型枚举
*/
public enum MessageType {
TEXT("文本消息"),
TYPING("正在输入"),
SYSTEM("系统消息"),
ERROR("错误消息"),
HEARTBEAT("心跳消息"),
CONNECTION("连接消息"),
AI_THINKING("AI思考中");
private final String description;
MessageType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 发送者类型枚举
*/
public enum SenderType {
USER("用户"),
GUEST("访客"),
AI("AI助手"),
SYSTEM("系统");
private final String description;
SenderType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 消息状态枚举
*/
public enum MessageStatus {
SENDING("发送中"),
SENT("已发送"),
DELIVERED("已送达"),
READ("已读"),
FAILED("发送失败");
private final String description;
MessageStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
}
@@ -0,0 +1,93 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 成就实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("achievement")
public class Achievement extends BaseEntity {
/**
* 成就标题
*/
@TableField("title")
private String title;
/**
* 描述
*/
@TableField("description")
private String description;
/**
* 分类
*/
@TableField("category")
private String category;
/**
* 图标
*/
@TableField("icon")
private String icon;
/**
* 稀有度
*/
@TableField("rarity")
private String rarity;
/**
* 条件类型
*/
@TableField("condition_type")
private String conditionType;
/**
* 条件值
*/
@TableField("condition_value")
private String conditionValue;
/**
* 奖励
*/
@TableField("rewards")
private String rewards;
/**
* 解锁时间
*/
@TableField("unlocked_time")
private LocalDateTime unlockedTime;
/**
* 进度
*/
@TableField("progress")
private BigDecimal progress;
/**
* 是否隐藏
*/
@TableField("is_hidden")
private Integer isHidden;
}
@@ -0,0 +1,54 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 评论实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("comment")
public class Comment extends BaseEntity {
/**
* 帖子ID
*/
@TableField("post_id")
private String postId;
/**
* 用户ID
*/
@TableField("user_id")
private String userId;
/**
* 评论内容
*/
@TableField("content")
private String content;
/**
* 回复的评论ID
*/
@TableField("reply_to_id")
private String replyToId;
/**
* 点赞数
*/
@TableField("likes")
private Integer likes;
}
@@ -0,0 +1,90 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 社区帖子实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("community_post")
public class CommunityPost extends BaseEntity {
/**
* 用户ID
*/
@TableField("user_id")
private String userId;
/**
* 地点ID
*/
@TableField("location_id")
private String locationId;
/**
* 标题
*/
@TableField("title")
private String title;
/**
* 内容
*/
@TableField("content")
private String content;
/**
* 帖子类型
*/
@TableField("type")
private String type;
/**
* 图片列表
*/
@TableField("images")
private String images;
/**
* 标签
*/
@TableField("tags")
private String tags;
/**
* 点赞数
*/
@TableField("likes")
private Integer likes;
/**
* 浏览数
*/
@TableField("view_count")
private Integer viewCount;
/**
* 评论数
*/
@TableField("comment_count")
private Integer commentCount;
/**
* 是否私密
*/
@TableField("is_private")
private Integer isPrivate;
}
@@ -1,91 +1,191 @@
package com.emotion.entity; 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; import java.time.LocalDateTime;
/** /**
* 对话实体 * 对话实体
* *
* @author emotion-museum * @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; private String userId;
/**
* 用户类型: registered-注册用户, guest-访客用户
*/
@TableField("user_type")
private String userType;
/**
* 对话标题
*/
@TableField("title")
private String title; private String title;
/**
* 对话类型
*/
@TableField("type")
private String type; private String type;
private LocalDateTime startTime;
private LocalDateTime endTime; /**
private Integer messageCount; * 状态: active-活跃, ended-结束, archived-归档
private Integer status; */
private String clientIp; @TableField("status")
private String userAgent; private String conversationStatus;
/**
* Coze对话ID
*/
@TableField("coze_conversation_id")
private String cozeConversationId; private String cozeConversationId;
private LocalDateTime createTime;
private LocalDateTime updateTime; /**
private String createBy; * 使用的Bot ID
private String updateBy; */
private Integer isDeleted; @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; private String remarks;
// 构造函数
public Conversation() {
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
this.status = 1;
this.isDeleted = 0;
this.messageCount = 0;
}
// Getter和Setter方法
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public LocalDateTime getStartTime() { return startTime; }
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
public LocalDateTime getEndTime() { return endTime; }
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
public Integer getMessageCount() { return messageCount; }
public void setMessageCount(Integer messageCount) { this.messageCount = messageCount; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getClientIp() { return clientIp; }
public void setClientIp(String clientIp) { this.clientIp = clientIp; }
public String getUserAgent() { return userAgent; }
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
public String getCozeConversationId() { return cozeConversationId; }
public void setCozeConversationId(String cozeConversationId) { this.cozeConversationId = cozeConversationId; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
public String getCreateBy() { return createBy; }
public void setCreateBy(String createBy) { this.createBy = createBy; }
public String getUpdateBy() { return updateBy; }
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public Integer getIsDeleted() { return isDeleted; }
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
public String getRemarks() { return remarks; }
public void setRemarks(String remarks) { this.remarks = remarks; }
} }
@@ -0,0 +1,301 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* Coze API调用记录实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("coze_api_call")
public class CozeApiCall {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
/**
* 对话ID
*/
@TableField("conversation_id")
private String conversationId;
/**
* 消息ID
*/
@TableField("message_id")
private String messageId;
/**
* Coze聊天ID
*/
@TableField("coze_chat_id")
private String cozeChatId;
/**
* Coze对话ID
*/
@TableField("coze_conversation_id")
private String cozeConversationId;
/**
* Bot ID
*/
@TableField("bot_id")
private String botId;
/**
* Workflow ID
*/
@TableField("workflow_id")
private String workflowId;
/**
* 用户ID
*/
@TableField("user_id")
private String userId;
/**
* 请求类型: chat/stream/retrieve/messages
*/
@TableField("request_type")
private String requestType;
/**
* 请求URL
*/
@TableField("request_url")
private String requestUrl;
/**
* 请求体
*/
@TableField("request_body")
private String requestBody;
/**
* 请求头
*/
@TableField("request_headers")
private String requestHeaders;
/**
* 用户输入的消息内容
*/
@TableField("user_message")
private String userMessage;
/**
* 用户消息类型: text/image/file
*/
@TableField("user_message_type")
private String userMessageType;
/**
* AI回复的消息内容
*/
@TableField("ai_reply")
private String aiReply;
/**
* AI回复类型: text/image/file
*/
@TableField("ai_reply_type")
private String aiReplyType;
/**
* HTTP状态码
*/
@TableField("response_status")
private Integer responseStatus;
/**
* 响应体
*/
@TableField("response_body")
private String responseBody;
/**
* 响应头
*/
@TableField("response_headers")
private String responseHeaders;
/**
* 轮询次数
*/
@TableField("poll_count")
private Integer pollCount;
/**
* 轮询开始时间
*/
@TableField("poll_start_time")
private LocalDateTime pollStartTime;
/**
* 轮询结束时间
*/
@TableField("poll_end_time")
private LocalDateTime pollEndTime;
/**
* 最终状态: completed/failed/timeout
*/
@TableField("final_status")
private String finalStatus;
/**
* 调用状态: pending/success/failed/timeout
*/
@TableField("status")
private String status;
/**
* 开始时间
*/
@TableField("start_time")
private LocalDateTime startTime;
/**
* 结束时间
*/
@TableField("end_time")
private LocalDateTime endTime;
/**
* 耗时(毫秒)
*/
@TableField("duration_ms")
private Integer durationMs;
/**
* 输入Token数
*/
@TableField("prompt_tokens")
private Integer promptTokens;
/**
* 输出Token数
*/
@TableField("completion_tokens")
private Integer completionTokens;
/**
* 总Token数
*/
@TableField("total_tokens")
private Integer totalTokens;
/**
* 费用
*/
@TableField("cost")
private BigDecimal cost;
/**
* 函数调用记录
*/
@TableField("function_calls")
private String functionCalls;
/**
* 函数调用结果
*/
@TableField("function_results")
private String functionResults;
/**
* 错误代码
*/
@TableField("error_code")
private String errorCode;
/**
* 错误信息
*/
@TableField("error_message")
private String errorMessage;
/**
* 客户端IP
*/
@TableField("client_ip")
private String clientIp;
/**
* 用户代理
*/
@TableField("user_agent")
private String userAgent;
/**
* 会话ID
*/
@TableField("session_id")
private String sessionId;
/**
* 追踪ID
*/
@TableField("trace_id")
private String traceId;
/**
* 扩展元数据
*/
@TableField("metadata")
private String metadata;
/**
* 创建人ID
*/
@TableField("create_by")
private String createBy;
/**
* 创建时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新人ID
*/
@TableField("update_by")
private String updateBy;
/**
* 更新时间
*/
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 是否删除: 0-未删除, 1-已删除
*/
@TableField("is_deleted")
@TableLogic
private Integer isDeleted;
/**
* 备注
*/
@TableField("remarks")
private String remarks;
}
@@ -0,0 +1,99 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 情绪分析实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("emotion_analysis")
public class EmotionAnalysis extends BaseEntity {
/**
* 用户ID
*/
@TableField("user_id")
private String userId;
/**
* 关联消息ID
*/
@TableField("message_id")
private String messageId;
/**
* 分析文本
*/
@TableField("text")
private String text;
/**
* 主要情绪
*/
@TableField("primary_emotion")
private String primaryEmotion;
/**
* 情绪强度
*/
@TableField("intensity")
private BigDecimal intensity;
/**
* 情绪极性: positive-积极, negative-消极, neutral-中性
*/
@TableField("polarity")
private String polarity;
/**
* 置信度
*/
@TableField("confidence")
private BigDecimal confidence;
/**
* 情绪分布详情
*/
@TableField("emotions")
private String emotions;
/**
* 关键词列表
*/
@TableField("keywords")
private String keywords;
/**
* 建议
*/
@TableField("suggestion")
private String suggestion;
/**
* 分析时间
*/
@TableField("analysis_time")
private LocalDateTime analysisTime;
/**
* 扩展元数据
*/
@TableField("metadata")
private String metadata;
}
@@ -0,0 +1,99 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 情绪记录实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("emotion_record")
public class EmotionRecord extends BaseEntity {
/**
* 用户ID
*/
@TableField("user_id")
private String userId;
/**
* 记录日期
*/
@TableField("record_date")
private LocalDate recordDate;
/**
* 情绪类型
*/
@TableField("emotion_type")
private String emotionType;
/**
* 情绪强度
*/
@TableField("intensity")
private BigDecimal intensity;
/**
* 触发因素
*/
@TableField("triggers")
private String triggers;
/**
* 描述
*/
@TableField("description")
private String description;
/**
* 标签
*/
@TableField("tags")
private String tags;
/**
* 天气
*/
@TableField("weather")
private String weather;
/**
* 地点
*/
@TableField("location")
private String location;
/**
* 活动
*/
@TableField("activity")
private String activity;
/**
* 相关人物
*/
@TableField("people")
private String people;
/**
* 备注
*/
@TableField("notes")
private String notes;
}
@@ -0,0 +1,93 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 成长课题实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("growth_topic")
public class GrowthTopic extends BaseEntity {
/**
* 课题标题
*/
@TableField("title")
private String title;
/**
* 分类
*/
@TableField("category")
private String category;
/**
* 难度: easy-简单, medium-中等, hard-困难
*/
@TableField("difficulty")
private String difficulty;
/**
* 描述
*/
@TableField("description")
private String description;
/**
* 内容
*/
@TableField("content")
private String content;
/**
* 持续天数
*/
@TableField("duration_days")
private Integer durationDays;
/**
* 解锁条件
*/
@TableField("unlock_conditions")
private String unlockConditions;
/**
* 是否解锁
*/
@TableField("is_unlocked")
private Integer isUnlocked;
/**
* 进度百分比
*/
@TableField("progress")
private BigDecimal progress;
/**
* 完成时间
*/
@TableField("completed_time")
private LocalDateTime completedTime;
/**
* 奖励
*/
@TableField("rewards")
private String rewards;
}
@@ -0,0 +1,86 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 访客用户实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("guest_user")
public class GuestUser extends BaseEntity {
/**
* 访客用户ID (格式: guest_xxx)
*/
@TableField("guest_user_id")
private String guestUserId;
/**
* 客户端IP地址 (支持IPv6)
*/
@TableField("ip_address")
private String ipAddress;
/**
* 用户代理信息
*/
@TableField("user_agent")
private String userAgent;
/**
* 访客昵称
*/
@TableField("nickname")
private String nickname;
/**
* 访客头像
*/
@TableField("avatar")
private String avatar;
/**
* 最后活跃时间
*/
@TableField("last_active_time")
private LocalDateTime lastActiveTime;
/**
* 会话数量
*/
@TableField("conversation_count")
private Integer conversationCount;
/**
* 消息数量
*/
@TableField("message_count")
private Integer messageCount;
/**
* IP地址的地理位置信息
*/
@TableField("location")
private String location;
/**
* 设备信息
*/
@TableField("device_info")
private String deviceInfo;
}
@@ -0,0 +1,99 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 地点标记实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("location_pin")
public class LocationPin extends BaseEntity {
/**
* 地点名称
*/
@TableField("name")
private String name;
/**
* 地点类型
*/
@TableField("type")
private String type;
/**
* 地点分类
*/
@TableField("category")
private String category;
/**
* 纬度
*/
@TableField("latitude")
private BigDecimal latitude;
/**
* 经度
*/
@TableField("longitude")
private BigDecimal longitude;
/**
* 地址
*/
@TableField("address")
private String address;
/**
* 描述
*/
@TableField("description")
private String description;
/**
* 创建者
*/
@TableField("created_by")
private String createdBy;
/**
* 点赞数
*/
@TableField("likes")
private Integer likes;
/**
* 访问数
*/
@TableField("visits")
private Integer visits;
/**
* 是否收藏
*/
@TableField("is_bookmarked")
private Integer isBookmarked;
/**
* 最后访问时间
*/
@TableField("last_visit_time")
private LocalDateTime lastVisitTime;
}
@@ -1,108 +1,149 @@
package com.emotion.entity; 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; import java.time.LocalDateTime;
/** /**
* 消息实体 * 消息实体
* *
* @author emotion-museum * @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 conversationId;
private String userId;
/**
* 消息内容
*/
@TableField("content")
private String content; private String content;
private String contentType;
private String senderType; /**
private String senderId; * 消息类型
private String status; */
private LocalDateTime sendTime; @TableField("type")
private Integer isRead; private String type;
private String parentMessageId;
private String cozeRole; /**
* 发送者: 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; private String cozeMessageId;
/**
* 消息状态: sending/sent/failed/processing
*/
@TableField("status")
private String status;
/**
* 错误信息
*/
@TableField("error_message")
private String errorMessage; 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(); @TableField("emotion_score")
this.sendTime = LocalDateTime.now(); private BigDecimal emotionScore;
this.isDeleted = 0;
this.isRead = 0;
this.retryCount = 0;
}
// Getter和Setter方法 /**
public String getId() { return id; } * 情绪类型
public void setId(String id) { this.id = id; } */
@TableField("emotion_type")
private String emotionType;
public String getConversationId() { return conversationId; } /**
public void setConversationId(String conversationId) { this.conversationId = conversationId; } * 情绪分析置信度
*/
@TableField("emotion_confidence")
private BigDecimal emotionConfidence;
public String getUserId() { return userId; } /**
public void setUserId(String userId) { this.userId = userId; } * 输入Token数
*/
@TableField("prompt_tokens")
private Integer promptTokens;
public String getContent() { return content; } /**
public void setContent(String content) { this.content = content; } * 输出Token数
*/
@TableField("completion_tokens")
private Integer completionTokens;
public String getContentType() { return contentType; } /**
public void setContentType(String contentType) { this.contentType = contentType; } * 总Token数
*/
@TableField("total_tokens")
private Integer totalTokens;
public String getSenderType() { return senderType; } /**
public void setSenderType(String senderType) { this.senderType = senderType; } * API调用费用
*/
@TableField("api_cost")
private BigDecimal apiCost;
public String getSenderId() { return senderId; } /**
public void setSenderId(String senderId) { this.senderId = senderId; } * 是否已读: 0-未读, 1-已读
*/
@TableField("is_read")
private Integer isRead;
public String getStatus() { return status; } /**
public void setStatus(String status) { this.status = status; } * 父消息ID(用于回复链)
*/
@TableField("parent_message_id")
private String parentMessageId;
public LocalDateTime getSendTime() { return sendTime; } /**
public void setSendTime(LocalDateTime sendTime) { this.sendTime = sendTime; } * 情绪分析结果
*/
@TableField("emotion_analysis")
private String emotionAnalysis;
public Integer getIsRead() { return isRead; } /**
public void setIsRead(Integer isRead) { this.isRead = isRead; } * 扩展元数据
*/
@TableField("metadata")
private String metadata;
public String getParentMessageId() { return parentMessageId; }
public void setParentMessageId(String parentMessageId) { this.parentMessageId = parentMessageId; }
public String getCozeRole() { return cozeRole; }
public void setCozeRole(String cozeRole) { this.cozeRole = cozeRole; }
public String getCozeMessageId() { return cozeMessageId; }
public void setCozeMessageId(String cozeMessageId) { this.cozeMessageId = cozeMessageId; }
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public Integer getRetryCount() { return retryCount; }
public void setRetryCount(Integer retryCount) { this.retryCount = retryCount; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
public String getCreateBy() { return createBy; }
public void setCreateBy(String createBy) { this.createBy = createBy; }
public String getUpdateBy() { return updateBy; }
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public Integer getIsDeleted() { return isDeleted; }
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
public String getRemarks() { return remarks; }
public void setRemarks(String remarks) { this.remarks = remarks; }
} }
@@ -0,0 +1,86 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 奖励实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("reward")
public class Reward extends BaseEntity {
/**
* 课题ID
*/
@TableField("topic_id")
private String topicId;
/**
* 成就ID
*/
@TableField("achievement_id")
private String achievementId;
/**
* 奖励类型
*/
@TableField("type")
private String type;
/**
* 奖励名称
*/
@TableField("name")
private String name;
/**
* 描述
*/
@TableField("description")
private String description;
/**
* 图标
*/
@TableField("icon")
private String icon;
/**
* 稀有度
*/
@TableField("rarity")
private String rarity;
/**
* 奖励值
*/
@TableField("value")
private String value;
/**
* 获得时间
*/
@TableField("earned_time")
private LocalDateTime earnedTime;
/**
* 是否新获得
*/
@TableField("is_new")
private Integer isNew;
}
@@ -1,70 +0,0 @@
package com.emotion.entity;
import java.time.LocalDateTime;
/**
* 简化用户实体(不使用Lombok)
*
* @author emotion-museum
* @date 2025-07-22
*/
public class SimpleUser {
private String id;
private String username;
private String account;
private String password;
private String email;
private String phone;
private String nickname;
private String avatar;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
// 构造函数
public SimpleUser() {}
public SimpleUser(String id, String username, String account) {
this.id = id;
this.username = username;
this.account = account;
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
this.status = 1;
}
// Getter和Setter方法
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getAccount() { return account; }
public void setAccount(String account) { this.account = account; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getNickname() { return nickname; }
public void setNickname(String nickname) { this.nickname = nickname; }
public String getAvatar() { return avatar; }
public void setAvatar(String avatar) { this.avatar = avatar; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
}
@@ -0,0 +1,74 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 课题互动实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("topic_interaction")
public class TopicInteraction extends BaseEntity {
/**
* 课题ID
*/
@TableField("topic_id")
private String topicId;
/**
* 互动类型
*/
@TableField("type")
private String type;
/**
* 内容
*/
@TableField("content")
private String content;
/**
* 用户输入
*/
@TableField("user_input")
private String userInput;
/**
* AI回应
*/
@TableField("ai_response")
private String aiResponse;
/**
* 评分
*/
@TableField("rating")
private Integer rating;
/**
* 反馈
*/
@TableField("feedback")
private String feedback;
/**
* 完成时间
*/
@TableField("completed_time")
private LocalDateTime completedTime;
}
@@ -1,106 +1,162 @@
package com.emotion.entity; 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; import java.time.LocalDateTime;
/** /**
* 用户实体 * 用户实体
* *
* @author emotion-museum * @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; private String account;
/**
* 密码(加密后)
*/
@TableField("password")
private String password; private String password;
/**
* 用户名
*/
@TableField("username")
private String username;
/**
* 邮箱
*/
@TableField("email")
private String email; private String email;
/**
* 手机号
*/
@TableField("phone")
private String phone; private String phone;
private String nickname;
/**
* 头像URL
*/
@TableField("avatar")
private String 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; private String bio;
/**
* 会员等级
*/
@TableField("member_level")
private String memberLevel; private String memberLevel;
/**
* 使用天数
*/
@TableField("total_days")
private Integer totalDays; 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; private Integer status;
/**
* 是否已验证: 0-未验证, 1-已验证
*/
@TableField("is_verified")
private Integer isVerified; private Integer isVerified;
private LocalDateTime createTime;
private LocalDateTime updateTime; /**
* 最后活跃时间
*/
@TableField("last_active_time")
private LocalDateTime lastActiveTime; private LocalDateTime lastActiveTime;
private String createBy;
private String updateBy;
private Integer isDeleted;
private String remarks;
// 构造函数 /**
public User() { * 第三方平台ID
this.createTime = LocalDateTime.now(); */
this.updateTime = LocalDateTime.now(); @TableField("third_party_id")
this.status = 1; private String thirdPartyId;
this.isDeleted = 0;
}
// Getter和Setter方法 /**
public String getId() { return id; } * 第三方平台类型
public void setId(String id) { this.id = id; } */
@TableField("third_party_type")
private String thirdPartyType;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getAccount() { return account; }
public void setAccount(String account) { this.account = account; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getNickname() { return nickname; }
public void setNickname(String nickname) { this.nickname = nickname; }
public String getAvatar() { return avatar; }
public void setAvatar(String avatar) { this.avatar = avatar; }
public Integer getGender() { return gender; }
public void setGender(Integer gender) { this.gender = gender; }
public String getBio() { return bio; }
public void setBio(String bio) { this.bio = bio; }
public String getMemberLevel() { return memberLevel; }
public void setMemberLevel(String memberLevel) { this.memberLevel = memberLevel; }
public Integer getTotalDays() { return totalDays; }
public void setTotalDays(Integer totalDays) { this.totalDays = totalDays; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public Integer getIsVerified() { return isVerified; }
public void setIsVerified(Integer isVerified) { this.isVerified = isVerified; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
public LocalDateTime getLastActiveTime() { return lastActiveTime; }
public void setLastActiveTime(LocalDateTime lastActiveTime) { this.lastActiveTime = lastActiveTime; }
public String getCreateBy() { return createBy; }
public void setCreateBy(String createBy) { this.createBy = createBy; }
public String getUpdateBy() { return updateBy; }
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public Integer getIsDeleted() { return isDeleted; }
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
public String getRemarks() { return remarks; }
public void setRemarks(String remarks) { this.remarks = remarks; }
} }
@@ -0,0 +1,108 @@
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 用户统计实体类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("user_stats")
public class UserStats extends BaseEntity {
/**
* 用户ID
*/
@TableField("user_id")
private String userId;
/**
* 总对话数
*/
@TableField("total_conversations")
private Integer totalConversations;
/**
* 总消息数
*/
@TableField("total_messages")
private Integer totalMessages;
/**
* 总情绪记录数
*/
@TableField("total_emotions_recorded")
private Integer totalEmotionsRecorded;
/**
* 完成的课题数
*/
@TableField("topics_completed")
private Integer topicsCompleted;
/**
* 解锁的成就数
*/
@TableField("achievements_unlocked")
private Integer achievementsUnlocked;
/**
* 总积分
*/
@TableField("total_points")
private Integer totalPoints;
/**
* 连续使用天数
*/
@TableField("consecutive_days")
private Integer consecutiveDays;
/**
* 最大连续天数
*/
@TableField("max_consecutive_days")
private Integer maxConsecutiveDays;
/**
* 访问的地点数
*/
@TableField("locations_visited")
private Integer locationsVisited;
/**
* 创建的帖子数
*/
@TableField("posts_created")
private Integer postsCreated;
/**
* 评论数
*/
@TableField("comments_made")
private Integer commentsMade;
/**
* 收到的点赞数
*/
@TableField("likes_received")
private Integer likesReceived;
/**
* 社交互动数
*/
@TableField("social_interactions")
private Integer socialInteractions;
}
@@ -0,0 +1,190 @@
package com.emotion.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.emotion.util.SnowflakeIdGenerator;
import com.emotion.util.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatis-Plus 自动填充处理器
* 自动填充公共字段:id, create_by, create_time, update_by, update_time
* 支持雪花算法自动生成主键ID
*
* @author emotion-museum
* @since 2025-07-23
*/
@Slf4j
@Component
public class EmotionMetaObjectHandler implements MetaObjectHandler {
/**
* 雪花算法ID生成器
*/
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
/**
* 插入时自动填充
*/
@Override
public void insertFill(MetaObject metaObject) {
try {
LocalDateTime now = LocalDateTime.now();
String currentUserId = getCurrentUserId();
// 填充主键ID(如果为空)
fillPrimaryKey(metaObject);
// 填充创建时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
// 填充更新时间
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);
// 填充创建人ID
if (currentUserId != null) {
this.strictInsertFill(metaObject, "createBy", String.class, currentUserId);
}
// 填充更新人ID
if (currentUserId != null) {
this.strictInsertFill(metaObject, "updateBy", String.class, currentUserId);
}
// 填充逻辑删除字段默认值
this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0);
log.debug("插入时自动填充完成: createTime={}, updateTime={}, createBy={}, updateBy={}",
now, now, currentUserId, currentUserId);
} catch (Exception e) {
// 自动填充失败不应该影响业务逻辑
log.warn("插入时自动填充失败,但不影响业务逻辑: {}", e.getMessage());
}
}
/**
* 更新时自动填充
*/
@Override
public void updateFill(MetaObject metaObject) {
try {
LocalDateTime now = LocalDateTime.now();
String currentUserId = getCurrentUserId();
// 填充更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, now);
// 填充更新人ID
if (currentUserId != null) {
this.strictUpdateFill(metaObject, "updateBy", String.class, currentUserId);
}
log.debug("更新时自动填充完成: updateTime={}, updateBy={}", now, currentUserId);
} catch (Exception e) {
// 自动填充失败不应该影响业务逻辑
log.warn("更新时自动填充失败,但不影响业务逻辑: {}", e.getMessage());
}
}
/**
* 填充主键ID
* 如果主键ID为空,则使用雪花算法生成
*
* @param metaObject 元对象
*/
private void fillPrimaryKey(MetaObject metaObject) {
try {
// 检查是否有id字段
if (metaObject.hasSetter("id")) {
Object idValue = metaObject.getValue("id");
// 如果ID为空,则生成新的ID
if (idValue == null || (idValue instanceof String && ((String) idValue).isEmpty())) {
String newId = snowflakeIdGenerator.nextIdAsString();
this.strictInsertFill(metaObject, "id", String.class, newId);
log.debug("自动生成主键ID: {}", newId);
}
}
} catch (Exception e) {
log.warn("主键ID自动填充失败,但不影响业务逻辑: {}", e.getMessage());
}
}
/**
* 获取当前用户ID
* 优先级:
* 1. 从ThreadLocal获取(如果有用户上下文)
* 2. 从Spring Security获取(如果有认证信息)
* 3. 返回系统默认值
*
* @return 当前用户ID,如果获取失败返回null
*/
private String getCurrentUserId() {
try {
// 1. 尝试从ThreadLocal获取用户ID(如果有用户上下文工具类)
String userIdFromContext = getUserIdFromContext();
if (userIdFromContext != null) {
return userIdFromContext;
}
// 2. 尝试从Spring Security获取用户ID
String userIdFromSecurity = getUserIdFromSecurity();
if (userIdFromSecurity != null) {
return userIdFromSecurity;
}
// 3. 返回系统默认值(用于系统操作或未登录用户)
return "system";
} catch (Exception e) {
log.debug("获取当前用户ID失败: {}", e.getMessage());
return "system";
}
}
/**
* 从用户上下文获取用户ID
* 这里可以集成自定义的用户上下文工具类
*
* @return 用户ID或null
*/
private String getUserIdFromContext() {
try {
// 从线程变量获取
return UserContextHolder.getCurrentUserId();
} catch (Exception e) {
log.debug("从用户上下文获取用户ID失败: {}", e.getMessage());
return null;
}
}
/**
* 从Spring Security获取用户ID
*
* @return 用户ID或null
*/
private String getUserIdFromSecurity() {
try {
// TODO: 集成Spring Security
// Authentication authentication =
// SecurityContextHolder.getContext().getAuthentication();
// if (authentication != null && authentication.isAuthenticated()
// && !"anonymousUser".equals(authentication.getPrincipal())) {
// UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// return userDetails.getUsername(); // 或者从UserDetails中获取用户ID
// }
return null;
} catch (Exception e) {
log.debug("从Spring Security获取用户ID失败: {}", e.getMessage());
return null;
}
}
}
@@ -0,0 +1,105 @@
package com.emotion.interceptor;
import com.emotion.util.JwtUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* JWT认证拦截器
*
* @author emotion-museum
* @date 2025-07-23
*/
@Component
public class JwtAuthInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(JwtAuthInterceptor.class);
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String method = request.getMethod();
log.debug("JWT拦截器处理请求: {} {}", method, requestURI);
// 跨域预检请求直接放行
if ("OPTIONS".equals(method)) {
return true;
}
// 不需要认证的接口
if (isPublicEndpoint(requestURI)) {
log.debug("公开接口,无需认证: {}", requestURI);
return true;
}
// 获取Authorization头
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
log.warn("请求缺少Authorization头或格式错误: {}", requestURI);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\",\"data\":null}");
return false;
}
// 提取token
String token = authHeader.substring(7);
// 验证token
if (!jwtUtil.validateToken(token)) {
log.warn("Token验证失败: {}", requestURI);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期\",\"data\":null}");
return false;
}
// 从token中获取用户信息并设置到请求属性中
String userId = jwtUtil.getUserIdFromToken(token);
String username = jwtUtil.getUsernameFromToken(token);
request.setAttribute("userId", userId);
request.setAttribute("username", username);
request.setAttribute("token", token);
log.debug("Token验证成功,用户: {} ({})", username, userId);
return true;
}
/**
* 判断是否为公开接口(不需要认证)
*/
private boolean isPublicEndpoint(String requestURI) {
// 公开接口列表
String[] publicEndpoints = {
"/api/auth/login",
"/api/auth/register",
"/api/auth/captcha",
"/api/auth/refresh-token",
"/api/health",
"/api/ws/chat",
"/swagger-ui",
"/v3/api-docs",
"/actuator"
};
for (String endpoint : publicEndpoints) {
if (requestURI.startsWith(endpoint)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,211 @@
package com.emotion.interceptor;
import com.emotion.util.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* 用户上下文拦截器
* 用于在请求处理前设置用户上下文信息,请求处理后清理上下文
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
@Component
public class UserContextInterceptor implements HandlerInterceptor {
/**
* 请求处理前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
try {
// 生成请求ID
String requestId = UUID.randomUUID().toString().replace("-", "");
// 获取用户信息
String userId = getUserIdFromRequest(request);
String username = getUsernameFromRequest(request);
String userType = getUserTypeFromRequest(request);
String clientIp = getClientIpFromRequest(request);
// 设置用户上下文
UserContextHolder.setUserContext(userId, username, userType, clientIp, requestId);
// 设置响应头中的请求ID,便于追踪
response.setHeader("X-Request-Id", requestId);
log.debug("设置用户上下文: {}", UserContextHolder.getContextSummary());
return true;
} catch (Exception e) {
log.warn("设置用户上下文失败: {}", e.getMessage());
// 即使设置失败也不影响请求处理
return true;
}
}
/**
* 请求处理后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
try {
log.debug("清理用户上下文: {}", UserContextHolder.getContextSummary());
// 清理用户上下文
UserContextHolder.clear();
} catch (Exception e) {
log.warn("清理用户上下文失败: {}", e.getMessage());
}
}
/**
* 从请求中获取用户ID
*
* @param request HTTP请求
* @return 用户ID
*/
private String getUserIdFromRequest(HttpServletRequest request) {
// 1. 从请求头获取
String userId = request.getHeader("X-User-Id");
if (StringUtils.hasText(userId)) {
return userId;
}
// 2. 从请求参数获取
userId = request.getParameter("userId");
if (StringUtils.hasText(userId)) {
return userId;
}
// 3. 从Session获取
Object sessionUserId = request.getSession().getAttribute("userId");
if (sessionUserId != null) {
return sessionUserId.toString();
}
// 4. 生成访客ID
return "guest_" + System.currentTimeMillis();
}
/**
* 从请求中获取用户名
*
* @param request HTTP请求
* @return 用户名
*/
private String getUsernameFromRequest(HttpServletRequest request) {
// 1. 从请求头获取
String username = request.getHeader("X-Username");
if (StringUtils.hasText(username)) {
return username;
}
// 2. 从请求参数获取
username = request.getParameter("username");
if (StringUtils.hasText(username)) {
return username;
}
// 3. 从Session获取
Object sessionUsername = request.getSession().getAttribute("username");
if (sessionUsername != null) {
return sessionUsername.toString();
}
return "guest";
}
/**
* 从请求中获取用户类型
*
* @param request HTTP请求
* @return 用户类型
*/
private String getUserTypeFromRequest(HttpServletRequest request) {
// 1. 从请求头获取
String userType = request.getHeader("X-User-Type");
if (StringUtils.hasText(userType)) {
return userType;
}
// 2. 从请求参数获取
userType = request.getParameter("userType");
if (StringUtils.hasText(userType)) {
return userType;
}
// 3. 从Session获取
Object sessionUserType = request.getSession().getAttribute("userType");
if (sessionUserType != null) {
return sessionUserType.toString();
}
return "GUEST";
}
/**
* 从请求中获取客户端IP
*
* @param request HTTP请求
* @return 客户端IP
*/
private String getClientIpFromRequest(HttpServletRequest request) {
String ip = null;
// 1. 从X-Forwarded-For获取(经过代理的情况)
ip = request.getHeader("X-Forwarded-For");
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
// 多个IP的情况,取第一个
int index = ip.indexOf(',');
if (index != -1) {
ip = ip.substring(0, index);
}
return ip.trim();
}
// 2. 从X-Real-IP获取
ip = request.getHeader("X-Real-IP");
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
// 3. 从Proxy-Client-IP获取
ip = request.getHeader("Proxy-Client-IP");
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
// 4. 从WL-Proxy-Client-IP获取
ip = request.getHeader("WL-Proxy-Client-IP");
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
// 5. 从HTTP_CLIENT_IP获取
ip = request.getHeader("HTTP_CLIENT_IP");
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
// 6. 从HTTP_X_FORWARDED_FOR获取
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
// 7. 最后从getRemoteAddr获取
ip = request.getRemoteAddr();
return StringUtils.hasText(ip) ? ip.trim() : "unknown";
}
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.Achievement;
import org.apache.ibatis.annotations.Mapper;
/**
* 成就Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface AchievementMapper extends BaseMapper<Achievement> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.Comment;
import org.apache.ibatis.annotations.Mapper;
/**
* 评论Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface CommentMapper extends BaseMapper<Comment> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.CommunityPost;
import org.apache.ibatis.annotations.Mapper;
/**
* 社区帖子Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface CommunityPostMapper extends BaseMapper<CommunityPost> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.Conversation;
import org.apache.ibatis.annotations.Mapper;
/**
* 会话Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface ConversationMapper extends BaseMapper<Conversation> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.CozeApiCall;
import org.apache.ibatis.annotations.Mapper;
/**
* Coze API调用记录Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface CozeApiCallMapper extends BaseMapper<CozeApiCall> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.EmotionAnalysis;
import org.apache.ibatis.annotations.Mapper;
/**
* 情绪分析Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface EmotionAnalysisMapper extends BaseMapper<EmotionAnalysis> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.EmotionRecord;
import org.apache.ibatis.annotations.Mapper;
/**
* 情绪记录Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface EmotionRecordMapper extends BaseMapper<EmotionRecord> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.GrowthTopic;
import org.apache.ibatis.annotations.Mapper;
/**
* 成长课题Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface GrowthTopicMapper extends BaseMapper<GrowthTopic> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.GuestUser;
import org.apache.ibatis.annotations.Mapper;
/**
* 访客用户Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface GuestUserMapper extends BaseMapper<GuestUser> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.LocationPin;
import org.apache.ibatis.annotations.Mapper;
/**
* 地点标记Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface LocationPinMapper extends BaseMapper<LocationPin> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.Message;
import org.apache.ibatis.annotations.Mapper;
/**
* 消息Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface MessageMapper extends BaseMapper<Message> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.TopicInteraction;
import org.apache.ibatis.annotations.Mapper;
/**
* 课题互动Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface TopicInteractionMapper extends BaseMapper<TopicInteraction> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
@@ -0,0 +1,15 @@
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.UserStats;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户统计Mapper接口
*
* @author emotion-museum
* @date 2025-07-23
*/
@Mapper
public interface UserStatsMapper extends BaseMapper<UserStats> {
}
@@ -26,6 +26,7 @@ public class AiService {
private static final Logger log = LoggerFactory.getLogger(AiService.class); private static final Logger log = LoggerFactory.getLogger(AiService.class);
private String cozeApiToken = "your-coze-api-token"; private String cozeApiToken = "your-coze-api-token";
private String cozeBaseUrl = "https://api.coze.cn"; private String cozeBaseUrl = "https://api.coze.cn";
private String botId = "7523042446285439016"; private String botId = "7523042446285439016";
@@ -1,149 +1,88 @@
package com.emotion.service; 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 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.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.UUID;
/** /**
* 话服务 * 话服务接口
* *
* @author emotion-museum * @author emotion-museum
* @date 2025-07-22 * @date 2025-07-23
*/ */
@Service public interface ConversationService extends IService<Conversation> {
public class ConversationService {
private static final Logger log = LoggerFactory.getLogger(ConversationService.class);
@Autowired
private JdbcTemplate jdbcTemplate;
/** /**
* 对话行映射器 * 分页查询会话
*/ */
private static class ConversationRowMapper implements RowMapper<Conversation> { IPage<Conversation> getPage(BasePageRequest request);
@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;
}
}
/** /**
* 创建对 * 根据用户ID分页查询会
*/ */
public Conversation createConversation(String userId, String title, String type, String clientIp) { IPage<Conversation> getPageByUserId(BasePageRequest request, String userId);
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());
}
}
/** /**
* 根据ID查询对话 * 根据用户ID查询会话列表
*/ */
public Conversation findById(String id) { List<Conversation> getByUserId(String userId);
try {
String sql = "SELECT * FROM conversation WHERE id = ? AND is_deleted = 0";
List<Conversation> conversations = jdbcTemplate.query(sql, new ConversationRowMapper(), id);
return conversations.isEmpty() ? null : conversations.get(0);
} catch (Exception e) {
log.error("根据ID查询对话失败: {}", e.getMessage());
return null;
}
}
/** /**
* 根据用户ID查询话列表 * 根据用户ID查询活跃会话列表
*/ */
public List<Conversation> findByUserId(String userId) { List<Conversation> getActiveByUserId(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();
}
}
/** /**
* 更新消息数量 * 根据Coze会话ID查询会话
*/ */
public boolean updateMessageCount(String conversationId, int messageCount) { Conversation getByCozeConversationId(String cozeConversationId);
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;
}
}
/** /**
* 结束对话 * 更新会话消息数量
*/ */
public boolean endConversation(String conversationId) { boolean updateMessageCount(String conversationId, Integer messageCount);
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) { boolean updateStatus(String conversationId, Integer status);
log.error("结束对话失败: {}", e.getMessage());
return false; /**
} * 更新会话结束时间
} */
} boolean updateEndTime(String conversationId, LocalDateTime endTime);
/**
* 统计用户的会话数量
*/
Long countByUserId(String userId);
/**
* 统计用户的活跃会话数量
*/
Long countActiveByUserId(String userId);
/**
* 查询需要归档的会话(超过指定天数未活跃)
*/
List<Conversation> getForArchive(Integer days);
/**
* 批量归档会话
*/
boolean batchArchive(List<String> conversationIds);
/**
* 创建会话
*/
Conversation createConversation(String userId, String title, String cozeConversationId);
/**
* 结束会话
*/
boolean endConversation(String conversationId);
}
@@ -0,0 +1,56 @@
package com.emotion.service;
import com.emotion.entity.Message;
import java.util.List;
/**
* AI服务接口
*
* @author emotion-museum
* @date 2025-07-23
*/
public interface IAiService {
/**
* 发送聊天消息到AI
*
* @param conversationId 会话ID
* @param message 用户消息
* @param userId 用户ID
* @return AI回复内容
*/
String sendChatMessage(String conversationId, String message, String userId);
/**
* 根据聊天记录生成对话总结
*
* @param conversationId 会话ID
* @param userId 用户ID
* @return 对话总结
*/
String generateConversationSummary(String conversationId, String userId);
/**
* 根据消息列表生成总结
*
* @param messages 消息列表
* @param userId 用户ID
* @return 对话总结
*/
String generateSummaryFromRecords(List<Message> messages, String userId);
/**
* 检查AI服务是否可用
*
* @return 是否可用
*/
boolean isServiceAvailable();
/**
* 获取AI服务状态信息
*
* @return 状态信息
*/
String getServiceStatus();
}
@@ -0,0 +1,121 @@
package com.emotion.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.entity.Conversation;
import java.time.LocalDateTime;
import java.util.List;
/**
* 会话服务接口
*
* @author emotion-museum
* @date 2025-07-23
*/
public interface IConversationService extends IService<Conversation> {
/**
* 创建新会话
*
* @param userId 用户ID
* @param title 会话标题
* @param type 会话类型
* @return 会话信息
*/
Conversation createConversation(String userId, String title, String type);
/**
* 根据用户ID分页查询会话列表
*
* @param page 分页参数
* @param userId 用户ID
* @return 会话分页数据
*/
IPage<Conversation> getByUserId(Page<Conversation> page, String userId);
/**
* 根据用户ID查询活跃会话列表
*
* @param userId 用户ID
* @return 活跃会话列表
*/
List<Conversation> getActiveByUserId(String userId);
/**
* 根据Coze会话ID查询会话
*
* @param cozeConversationId Coze会话ID
* @return 会话信息
*/
Conversation getByCozeConversationId(String cozeConversationId);
/**
* 更新会话消息数量
*
* @param conversationId 会话ID
* @param messageCount 消息数量
* @return 是否成功
*/
boolean updateMessageCount(String conversationId, Integer messageCount);
/**
* 更新会话状态
*
* @param conversationId 会话ID
* @param status 状态
* @return 是否成功
*/
boolean updateStatus(String conversationId, Integer status);
/**
* 结束会话
*
* @param conversationId 会话ID
* @return 是否成功
*/
boolean endConversation(String conversationId);
/**
* 统计用户的会话数量
*
* @param userId 用户ID
* @return 会话数量
*/
Long countByUserId(String userId);
/**
* 统计用户的活跃会话数量
*
* @param userId 用户ID
* @return 活跃会话数量
*/
Long countActiveByUserId(String userId);
/**
* 归档超时会话
*
* @param days 超时天数
* @return 归档的会话数量
*/
int archiveTimeoutConversations(Integer days);
/**
* 批量归档会话
*
* @param conversationIds 会话ID列表
* @return 是否成功
*/
boolean batchArchive(List<String> conversationIds);
/**
* 获取或创建会话
*
* @param userId 用户ID
* @param cozeConversationId Coze会话ID
* @param title 会话标题
* @return 会话信息
*/
Conversation getOrCreateConversation(String userId, String cozeConversationId, String title);
}
@@ -0,0 +1,112 @@
package com.emotion.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.entity.CozeApiCall;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* Coze API调用记录服务接口
*
* @author emotion-museum
* @date 2025-07-23
*/
public interface ICozeApiCallService extends IService<CozeApiCall> {
/**
* 根据会话ID分页查询API调用记录
*/
IPage<CozeApiCall> getByConversationId(Page<CozeApiCall> page, String conversationId);
/**
* 根据用户ID分页查询API调用记录
*/
IPage<CozeApiCall> getByUserId(Page<CozeApiCall> page, String userId);
/**
* 根据Bot ID查询API调用记录
*/
List<CozeApiCall> getByBotId(String botId);
/**
* 根据状态查询API调用记录
*/
List<CozeApiCall> getByStatus(String status);
/**
* 根据请求类型查询API调用记录
*/
List<CozeApiCall> getByRequestType(String requestType);
/**
* 根据时间范围查询API调用记录
*/
List<CozeApiCall> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime);
/**
* 统计用户的API调用次数
*/
Long countByUserId(String userId);
/**
* 统计Bot的API调用次数
*/
Long countByBotId(String botId);
/**
* 统计指定状态的API调用次数
*/
Long countByStatus(String status);
/**
* 统计用户的Token使用量
*/
Long sumTokensByUserId(String userId);
/**
* 统计用户的API调用费用
*/
BigDecimal sumCostByUserId(String userId);
/**
* 查询失败的API调用记录
*/
List<CozeApiCall> getFailedCalls();
/**
* 查询超时的API调用记录
*/
List<CozeApiCall> getTimeoutCalls();
/**
* 根据追踪ID查询API调用记录
*/
CozeApiCall getByTraceId(String traceId);
/**
* 根据会话ID和请求类型查询API调用记录
*/
List<CozeApiCall> getByConversationIdAndRequestType(String conversationId, String requestType);
/**
* 更新API调用状态
*/
boolean updateStatus(String id, String status, String finalStatus, LocalDateTime endTime);
/**
* 更新API调用结果
*/
boolean updateResult(String id, Integer responseStatus, String responseBody, String aiReply,
Integer totalTokens, BigDecimal cost, String status, String finalStatus,
LocalDateTime endTime);
/**
* 创建API调用记录
*/
CozeApiCall createApiCall(String conversationId, String messageId, String userId,
String requestType, String requestUrl, String requestBody);
}
@@ -0,0 +1,141 @@
package com.emotion.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.entity.Message;
import java.time.LocalDateTime;
import java.util.List;
/**
* 消息服务接口
*
* @author emotion-museum
* @date 2025-07-23
*/
public interface IMessageService extends IService<Message> {
/**
* 保存消息
*
* @param conversationId 会话ID
* @param content 消息内容
* @param type 消息类型
* @param sender 发送者
* @return 消息
*/
Message saveMessage(String conversationId, String content, String type, String sender);
/**
* 根据会话ID分页查询消息
*
* @param page 分页参数
* @param conversationId 会话ID
* @return 消息分页数据
*/
IPage<Message> getByConversationId(Page<Message> page, String conversationId);
/**
* 根据发送者分页查询消息
*
* @param page 分页参数
* @param sender 发送者
* @return 消息分页数据
*/
IPage<Message> getBySender(Page<Message> page, String sender);
/**
* 根据会话ID查询消息列表(用于总结)
*
* @param conversationId 会话ID
* @param limit 限制数量
* @return 消息列表
*/
List<Message> getByConversationIdForSummary(String conversationId, Integer limit);
/**
* 根据时间范围查询消息
*
* @param conversationId 会话ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return 消息列表
*/
List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime);
/**
* 统计会话的消息数量
*
* @param conversationId 会话ID
* @return 消息数量
*/
Long countByConversationId(String conversationId);
/**
* 统计发送者的消息数量
*
* @param sender 发送者
* @return 消息数量
*/
Long countBySender(String sender);
/**
* 查询会话的最后一条消息
*
* @param conversationId 会话ID
* @return 最后一条消息
*/
Message getLastMessageByConversationId(String conversationId);
/**
* 根据发送者查询消息
*
* @param conversationId 会话ID
* @param sender 发送者
* @return 消息列表
*/
List<Message> getByConversationIdAndSender(String conversationId, String sender);
/**
* 更新消息状态
*
* @param messageId 消息ID
* @param status 状态
* @return 是否成功
*/
boolean updateStatus(String messageId, String status);
/**
* 更新消息已读状态
*
* @param messageId 消息ID
* @param isRead 是否已读
* @return 是否成功
*/
boolean updateReadStatus(String messageId, Integer isRead);
/**
* 批量更新会话消息为已读
*
* @param conversationId 会话ID
* @return 是否成功
*/
boolean markConversationMessagesAsRead(String conversationId);
/**
* 批量保存消息
*
* @param messages 消息列表
* @return 是否成功
*/
boolean saveBatch(List<Message> messages);
/**
* 删除会话的所有消息
*
* @param conversationId 会话ID
* @return 是否成功
*/
boolean deleteByConversationId(String conversationId);
}
@@ -1,165 +1,94 @@
package com.emotion.service; 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 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.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.UUID;
/** /**
* 消息服务 * 消息服务接口
* *
* @author emotion-museum * @author emotion-museum
* @date 2025-07-22 * @date 2025-07-23
*/ */
@Service public interface MessageService extends IService<Message> {
public class MessageService {
private static final Logger log = LoggerFactory.getLogger(MessageService.class);
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ConversationService conversationService;
/** /**
* 消息行映射器 * 分页查询消息
*/ */
private static class MessageRowMapper implements RowMapper<Message> { IPage<Message> getPage(BasePageRequest request);
@Override
public Message mapRow(ResultSet rs, int rowNum) throws SQLException { /**
Message message = new Message(); * 根据会话ID分页查询消息
message.setId(rs.getString("id")); */
message.setConversationId(rs.getString("conversation_id")); IPage<Message> getPageByConversationId(BasePageRequest request, String conversationId);
message.setUserId(rs.getString("user_id"));
message.setContent(rs.getString("content")); /**
message.setContentType(rs.getString("content_type")); * 根据会话ID查询消息列表
message.setSenderType(rs.getString("sender_type")); */
message.setSenderId(rs.getString("sender_id")); List<Message> getByConversationId(String conversationId);
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")); List<Message> getBySender(String sender);
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 ? List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime);
rs.getTimestamp("create_time").toLocalDateTime() : null);
message.setUpdateTime(rs.getTimestamp("update_time") != null ? /**
rs.getTimestamp("update_time").toLocalDateTime() : null); * 查询会话的最后一条消息
return message; */
} Message getLastMessageByConversationId(String conversationId);
}
/**
* 根据父消息ID查询回复消息
*/
List<Message> getRepliesByParentId(String parentMessageId);
/**
* 统计会话的消息数量
*/
Long countByConversationId(String conversationId);
/**
* 统计发送者的消息数量
*/
Long countBySender(String sender);
/**
* 查询未读消息数量
*/
Long countUnreadMessages(String conversationId);
/**
* 更新消息状态
*/
boolean updateStatus(String messageId, String status);
/**
* 更新消息已读状态
*/
boolean updateReadStatus(String messageId, Integer isRead);
/**
* 批量更新会话消息为已读
*/
boolean markConversationMessagesAsRead(String conversationId);
/** /**
* 创建消息 * 创建消息
*/ */
public Message createMessage(String conversationId, String userId, String content, Message createMessage(String conversationId, String userId, String content,
String senderType, String senderId) { String contentType, String senderType, String senderId);
try {
Message message = new Message();
message.setId(UUID.randomUUID().toString().replace("-", ""));
message.setConversationId(conversationId);
message.setUserId(userId);
message.setContent(content);
message.setContentType("text");
message.setSenderType(senderType);
message.setSenderId(senderId);
message.setStatus("sent");
message.setSendTime(LocalDateTime.now());
message.setIsRead(0);
message.setRetryCount(0);
message.setCreateTime(LocalDateTime.now());
message.setUpdateTime(LocalDateTime.now());
message.setIsDeleted(0);
String sql = "INSERT INTO message (id, conversation_id, user_id, content, content_type, " +
"sender_type, sender_id, status, send_time, is_read, retry_count, " +
"create_time, update_time, is_deleted) VALUES " +
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sql,
message.getId(), message.getConversationId(), message.getUserId(),
message.getContent(), message.getContentType(), message.getSenderType(),
message.getSenderId(), message.getStatus(), message.getSendTime(),
message.getIsRead(), message.getRetryCount(), message.getCreateTime(),
message.getUpdateTime(), message.getIsDeleted());
// 更新对话的消息数量
updateConversationMessageCount(conversationId);
log.info("消息创建成功: {}", message.getId());
return message;
} catch (Exception e) {
log.error("创建消息失败: {}", e.getMessage());
throw new RuntimeException("创建消息失败: " + e.getMessage());
}
}
/**
* 根据对话ID查询消息列表
*/
public List<Message> findByConversationId(String conversationId) {
try {
String sql = "SELECT * FROM message WHERE conversation_id = ? AND is_deleted = 0 ORDER BY send_time ASC";
return jdbcTemplate.query(sql, new MessageRowMapper(), conversationId);
} catch (Exception e) {
log.error("根据对话ID查询消息列表失败: {}", e.getMessage());
return List.of();
}
}
/**
* 根据ID查询消息
*/
public Message findById(String id) {
try {
String sql = "SELECT * FROM message WHERE id = ? AND is_deleted = 0";
List<Message> messages = jdbcTemplate.query(sql, new MessageRowMapper(), id);
return messages.isEmpty() ? null : messages.get(0);
} catch (Exception e) {
log.error("根据ID查询消息失败: {}", e.getMessage());
return null;
}
}
/** /**
* 标记消息为已读 * 标记消息为已读
*/ */
public boolean markAsRead(String messageId) { 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());
}
}
} }
@@ -1,193 +1,108 @@
package com.emotion.service; 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 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.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.UUID;
/** /**
* 用户服务 * 用户服务接口
* *
* @author emotion-museum * @author emotion-museum
* @date 2025-07-22 * @date 2025-07-23
*/ */
@Service public interface UserService extends IService<User> {
public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private PasswordEncoder passwordEncoder;
/** /**
* 用户行映射器 * 分页查询用户
*/ */
private static class UserRowMapper implements RowMapper<User> { IPage<User> getPage(BasePageRequest request);
@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;
}
}
/** /**
* 根据账号查询用户 * 根据账号查询用户
*/ */
public User findByAccount(String account) { User getByAccount(String account);
try {
String sql = "SELECT * FROM user WHERE account = ? AND is_deleted = 0";
List<User> users = jdbcTemplate.query(sql, new UserRowMapper(), account);
return users.isEmpty() ? null : users.get(0);
} catch (Exception e) {
log.error("根据账号查询用户失败: {}", e.getMessage());
return null;
}
}
/** /**
* 根据ID查询用户 * 根据用户名查询用户
*/ */
public User findById(String id) { User getByUsername(String username);
try {
String sql = "SELECT * FROM user WHERE id = ? AND is_deleted = 0"; /**
List<User> users = jdbcTemplate.query(sql, new UserRowMapper(), id); * 根据邮箱查询用户
return users.isEmpty() ? null : users.get(0); */
} catch (Exception e) { User getByEmail(String email);
log.error("根据ID查询用户失败: {}", e.getMessage());
return null; /**
} * 根据手机号查询用户
} */
User getByPhone(String phone);
/**
* 根据第三方平台信息查询用户
*/
User getByThirdParty(String thirdPartyId, String thirdPartyType);
/**
* 根据状态查询用户列表
*/
List<User> getByStatus(Integer status);
/**
* 根据会员等级查询用户列表
*/
List<User> getByMemberLevel(String memberLevel);
/**
* 查询活跃用户(最近N天有活动)
*/
List<User> getActiveUsers(Integer days);
/**
* 查询新注册用户(最近N天注册)
*/
List<User> getNewUsers(Integer days);
/**
* 更新用户最后活跃时间
*/
boolean updateLastActiveTime(String userId, LocalDateTime lastActiveTime);
/**
* 更新用户状态
*/
boolean updateStatus(String userId, Integer status);
/**
* 更新用户使用天数
*/
boolean updateTotalDays(String userId, Integer totalDays);
/**
* 统计指定状态的用户数量
*/
Long countByStatus(Integer status);
/**
* 统计指定会员等级的用户数量
*/
Long countByMemberLevel(String memberLevel);
/** /**
* 创建用户 * 创建用户
*/ */
public User createUser(User user) { User createUser(String account, String username, String password, String email, String phone);
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());
}
}
/** /**
* 更新用户 * 验证用户密码
*/ */
public boolean updateUser(User user) { boolean validatePassword(String userId, String password);
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;
}
}
/** /**
* 更新最后活跃时间 * 更新用户密码
*/ */
public boolean updateLastActiveTime(String userId) { boolean updatePassword(String userId, String newPassword);
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;
}
}
} }
@@ -0,0 +1,266 @@
package com.emotion.service;
import com.emotion.dto.websocket.ChatRequest;
import com.emotion.dto.websocket.ConnectRequest;
import com.emotion.dto.websocket.WebSocketMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket服务
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
@Service
public class WebSocketService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private IAiService aiService;
@Autowired
private IMessageService messageService;
@Autowired
private IConversationService conversationService;
// 在线用户管理
private final ConcurrentHashMap<String, String> onlineUsers = new ConcurrentHashMap<>();
/**
* 处理聊天消息
*/
public void handleChatMessage(ChatRequest request, String sessionId, Principal principal) {
try {
log.info("处理聊天消息: {}", request);
// 验证请求参数
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
sendErrorMessage(request.getSenderId(), "消息内容不能为空");
return;
}
// 构建用户消息
WebSocketMessage userMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(request.getConversationId())
.type(WebSocketMessage.MessageType.TEXT)
.content(request.getContent())
.senderId(request.getSenderId())
.senderType(WebSocketMessage.SenderType.valueOf(request.getSenderType().name()))
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
// 发送用户消息到会话
if (request.getConversationId() != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + request.getConversationId(), userMessage);
}
// 发送给用户私有队列
messagingTemplate.convertAndSendToUser(request.getSenderId(), "/queue/messages", userMessage);
// 发送AI思考状态
sendAiThinkingMessage(request.getSenderId(), request.getConversationId());
// 异步调用AI服务
processAiResponse(request);
} catch (Exception e) {
log.error("处理聊天消息失败", e);
sendErrorMessage(request.getSenderId(), "消息处理失败,请稍后重试");
}
}
/**
* 处理用户连接
*/
public void handleUserConnect(ConnectRequest request, String sessionId, Principal principal) {
try {
String userId = request.getUserId();
if (userId == null && principal != null) {
userId = principal.getName();
}
if (userId == null) {
userId = "guest_" + sessionId;
}
log.info("用户连接WebSocket: userId={}, sessionId={}", userId, sessionId);
// 记录在线用户
onlineUsers.put(sessionId, userId);
// 发送连接成功消息
WebSocketMessage connectMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type(WebSocketMessage.MessageType.CONNECTION)
.content("连接成功")
.senderId("system")
.senderType(WebSocketMessage.SenderType.SYSTEM)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", connectMessage);
} catch (Exception e) {
log.error("处理用户连接失败", e);
}
}
/**
* 处理用户断开连接
*/
public void handleUserDisconnect(String sessionId, Principal principal) {
try {
String userId = onlineUsers.remove(sessionId);
log.info("用户断开WebSocket连接: userId={}, sessionId={}", userId, sessionId);
} catch (Exception e) {
log.error("处理用户断开连接失败", e);
}
}
/**
* 处理心跳消息
*/
public void handleHeartbeat(String sessionId, Principal principal) {
try {
String userId = onlineUsers.get(sessionId);
if (userId == null && principal != null) {
userId = principal.getName();
}
// 发送心跳响应
WebSocketMessage heartbeatMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type(WebSocketMessage.MessageType.HEARTBEAT)
.content("pong")
.senderId("system")
.senderType(WebSocketMessage.SenderType.SYSTEM)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
if (userId != null) {
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", heartbeatMessage);
}
} catch (Exception e) {
log.error("处理心跳消息失败", e);
}
}
/**
* 发送AI思考状态消息
*/
private void sendAiThinkingMessage(String userId, String conversationId) {
WebSocketMessage thinkingMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type(WebSocketMessage.MessageType.AI_THINKING)
.content("AI正在思考中...")
.senderId("ai")
.senderType(WebSocketMessage.SenderType.AI)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", thinkingMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, thinkingMessage);
}
}
/**
* 异步处理AI响应
*/
private void processAiResponse(ChatRequest request) {
// 使用线程池异步处理AI响应
new Thread(() -> {
try {
// 保存用户消息到数据库
messageService.saveMessage(
request.getConversationId(),
request.getContent(),
request.getMessageType().name(),
request.getSenderType().name()
);
// 调用AI服务
String aiReply = aiService.sendChatMessage(
request.getConversationId(),
request.getContent(),
request.getSenderId()
);
// 构建AI回复消息
WebSocketMessage aiMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(request.getConversationId())
.type(WebSocketMessage.MessageType.TEXT)
.content(aiReply)
.senderId("ai")
.senderType(WebSocketMessage.SenderType.AI)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
// 保存AI回复到数据库
messageService.saveMessage(
request.getConversationId(),
aiReply,
"text",
"assistant"
);
// 发送AI回复
messagingTemplate.convertAndSendToUser(request.getSenderId(), "/queue/messages", aiMessage);
if (request.getConversationId() != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + request.getConversationId(), aiMessage);
}
} catch (Exception e) {
log.error("AI响应处理失败", e);
sendErrorMessage(request.getSenderId(), "AI服务暂时不可用,请稍后重试");
}
}).start();
}
/**
* 发送错误消息
*/
private void sendErrorMessage(String userId, String errorContent) {
WebSocketMessage errorMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type(WebSocketMessage.MessageType.ERROR)
.content(errorContent)
.senderId("system")
.senderType(WebSocketMessage.SenderType.SYSTEM)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", errorMessage);
}
/**
* 获取在线用户数量
*/
public int getOnlineUserCount() {
return onlineUsers.size();
}
}
@@ -0,0 +1,242 @@
package com.emotion.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.emotion.entity.Message;
import com.emotion.service.IAiService;
import com.emotion.service.IMessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* AI服务实现类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
@Service
public class AiServiceImpl implements IAiService {
@Autowired
private IMessageService messageService;
private final RestTemplate restTemplate;
// Coze平台配置 - 对话聊天
@Value("${coze.api.token}")
private String cozeApiToken;
@Value("${coze.api.base-url:https://api.coze.cn}")
private String cozeBaseUrl;
@Value("${coze.api.chat.bot-id}")
private String chatBotId;
// Coze平台配置 - 聊天记录总结
@Value("${coze.api.summary.bot-id}")
private String summaryBotId;
public AiServiceImpl() {
this.restTemplate = new RestTemplate();
}
@Override
public String sendChatMessage(String conversationId, String message, String userId) {
long startTime = System.currentTimeMillis();
try {
log.info("发送聊天消息到AI: conversationId={}, userId={}, message={}",
conversationId, userId, message);
// 构建聊天请求数据
Map<String, Object> requestData = buildChatRequestData(conversationId, message, userId);
// 发送请求到Coze API
String response = sendToCozeApi(requestData, chatBotId);
// 解析响应
String aiReply = parseCozeResponse(response);
log.info("AI聊天回复成功: conversationId={}, 耗时={}ms, 回复长度={}",
conversationId, System.currentTimeMillis() - startTime, aiReply.length());
return aiReply;
} catch (Exception e) {
log.error("AI聊天服务调用失败: conversationId={}, error={}", conversationId, e.getMessage(), e);
return "抱歉,我现在无法回复您的消息,请稍后再试。";
}
}
@Override
public String generateConversationSummary(String conversationId, String userId) {
try {
log.info("生成对话总结: conversationId={}, userId={}", conversationId, userId);
// 获取消息记录(限制数量避免token过多)
List<Message> messages = messageService.getByConversationIdForSummary(conversationId, 100);
if (messages.isEmpty()) {
return "暂无对话记录可供总结。";
}
return generateSummaryFromRecords(messages, userId);
} catch (Exception e) {
log.error("生成对话总结失败: conversationId={}, error={}", conversationId, e.getMessage(), e);
return "对话总结生成失败,请稍后再试。";
}
}
@Override
public String generateSummaryFromRecords(List<Message> messages, String userId) {
try {
if (messages.isEmpty()) {
return "暂无对话记录可供总结。";
}
// 构建对话历史文本
String conversationText = buildConversationText(messages);
// 构建总结请求数据
Map<String, Object> requestData = buildSummaryRequestData(conversationText, userId);
// 发送请求到Coze API
String response = sendToCozeApi(requestData, summaryBotId);
// 解析响应
String summary = parseCozeResponse(response);
log.info("对话总结生成成功: userId={}, 记录数量={}, 总结长度={}",
userId, messages.size(), summary.length());
return summary;
} catch (Exception e) {
log.error("根据记录生成总结失败: userId={}, error={}", userId, e.getMessage(), e);
return "对话总结生成失败,请稍后再试。";
}
}
@Override
public boolean isServiceAvailable() {
try {
// 简单的健康检查
return StringUtils.hasText(cozeApiToken) &&
StringUtils.hasText(chatBotId) &&
StringUtils.hasText(summaryBotId);
} catch (Exception e) {
log.error("AI服务可用性检查失败", e);
return false;
}
}
@Override
public String getServiceStatus() {
try {
boolean available = isServiceAvailable();
return String.format("AI服务状态: %s, 聊天Bot: %s, 总结Bot: %s",
available ? "可用" : "不可用", chatBotId, summaryBotId);
} catch (Exception e) {
return "AI服务状态检查失败: " + e.getMessage();
}
}
/**
* 构建聊天请求数据
*/
private Map<String, Object> buildChatRequestData(String conversationId, String message, String userId) {
Map<String, Object> requestData = new HashMap<>();
requestData.put("bot_id", chatBotId);
requestData.put("user_id", userId);
requestData.put("query", message);
requestData.put("stream", false);
if (StringUtils.hasText(conversationId)) {
requestData.put("conversation_id", conversationId);
}
return requestData;
}
/**
* 构建总结请求数据
*/
private Map<String, Object> buildSummaryRequestData(String conversationText, String userId) {
Map<String, Object> requestData = new HashMap<>();
requestData.put("bot_id", summaryBotId);
requestData.put("user_id", userId);
requestData.put("query", "请对以下对话内容进行总结,提取关键信息和主要话题:\n\n" + conversationText);
requestData.put("stream", false);
return requestData;
}
/**
* 发送请求到Coze API
*/
private String sendToCozeApi(Map<String, Object> requestData, String botId) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(cozeApiToken);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestData, headers);
String url = cozeBaseUrl + "/v3/chat";
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
return response.getBody();
} else {
throw new RuntimeException("Coze API调用失败: " + response.getStatusCode());
}
}
/**
* 解析Coze响应
*/
private String parseCozeResponse(String response) {
try {
JSONObject jsonResponse = JSON.parseObject(response);
if (jsonResponse.getInteger("code") == 0) {
JSONArray messages = jsonResponse.getJSONArray("messages");
if (messages != null && !messages.isEmpty()) {
JSONObject lastMessage = messages.getJSONObject(messages.size() - 1);
return lastMessage.getString("content");
}
}
log.warn("Coze响应解析异常: {}", response);
return "AI服务响应异常,请稍后再试。";
} catch (Exception e) {
log.error("解析Coze响应失败", e);
return "AI服务响应解析失败,请稍后再试。";
}
}
/**
* 构建对话历史文本
*/
private String buildConversationText(List<Message> messages) {
return messages.stream()
.map(message -> {
String senderName = "user".equals(message.getSender()) ? "用户" : "AI助手";
return senderName + ": " + message.getContent();
})
.collect(Collectors.joining("\n"));
}
}
@@ -0,0 +1,206 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.entity.CozeApiCall;
import com.emotion.mapper.CozeApiCallMapper;
import com.emotion.service.ICozeApiCallService;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* Coze API调用记录服务实现类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Service
public class CozeApiCallServiceImpl extends ServiceImpl<CozeApiCallMapper, CozeApiCall> implements ICozeApiCallService {
@Override
public IPage<CozeApiCall> getByConversationId(Page<CozeApiCall> page, String conversationId) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getConversationId, conversationId)
.eq(CozeApiCall::getIsDeleted, 0)
.orderByDesc(CozeApiCall::getStartTime);
return this.page(page, wrapper);
}
@Override
public IPage<CozeApiCall> getByUserId(Page<CozeApiCall> page, String userId) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getUserId, userId)
.eq(CozeApiCall::getIsDeleted, 0)
.orderByDesc(CozeApiCall::getStartTime);
return this.page(page, wrapper);
}
@Override
public List<CozeApiCall> getByBotId(String botId) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getBotId, botId)
.eq(CozeApiCall::getIsDeleted, 0)
.orderByDesc(CozeApiCall::getStartTime);
return this.list(wrapper);
}
@Override
public List<CozeApiCall> getByStatus(String status) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getStatus, status)
.eq(CozeApiCall::getIsDeleted, 0)
.orderByDesc(CozeApiCall::getStartTime);
return this.list(wrapper);
}
@Override
public List<CozeApiCall> getByRequestType(String requestType) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getRequestType, requestType)
.eq(CozeApiCall::getIsDeleted, 0)
.orderByDesc(CozeApiCall::getStartTime);
return this.list(wrapper);
}
@Override
public List<CozeApiCall> getByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.between(CozeApiCall::getStartTime, startTime, endTime)
.eq(CozeApiCall::getIsDeleted, 0)
.orderByDesc(CozeApiCall::getStartTime);
return this.list(wrapper);
}
@Override
public Long countByUserId(String userId) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getUserId, userId)
.eq(CozeApiCall::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByBotId(String botId) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getBotId, botId)
.eq(CozeApiCall::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByStatus(String status) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getStatus, status)
.eq(CozeApiCall::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long sumTokensByUserId(String userId) {
// 使用原生SQL或者查询后计算
List<CozeApiCall> calls = this.list(new LambdaQueryWrapper<CozeApiCall>()
.eq(CozeApiCall::getUserId, userId)
.eq(CozeApiCall::getIsDeleted, 0)
.isNotNull(CozeApiCall::getTotalTokens));
return calls.stream().mapToLong(call -> call.getTotalTokens() != null ? call.getTotalTokens() : 0).sum();
}
@Override
public BigDecimal sumCostByUserId(String userId) {
List<CozeApiCall> calls = this.list(new LambdaQueryWrapper<CozeApiCall>()
.eq(CozeApiCall::getUserId, userId)
.eq(CozeApiCall::getIsDeleted, 0)
.isNotNull(CozeApiCall::getCost));
return calls.stream()
.map(call -> call.getCost() != null ? call.getCost() : BigDecimal.ZERO)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@Override
public List<CozeApiCall> getFailedCalls() {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.and(w -> w.eq(CozeApiCall::getStatus, "failed").or().eq(CozeApiCall::getFinalStatus, "failed"))
.eq(CozeApiCall::getIsDeleted, 0)
.orderByDesc(CozeApiCall::getStartTime);
return this.list(wrapper);
}
@Override
public List<CozeApiCall> getTimeoutCalls() {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.and(w -> w.eq(CozeApiCall::getStatus, "timeout").or().eq(CozeApiCall::getFinalStatus, "timeout"))
.eq(CozeApiCall::getIsDeleted, 0)
.orderByDesc(CozeApiCall::getStartTime);
return this.list(wrapper);
}
@Override
public CozeApiCall getByTraceId(String traceId) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getTraceId, traceId)
.eq(CozeApiCall::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public List<CozeApiCall> getByConversationIdAndRequestType(String conversationId, String requestType) {
LambdaQueryWrapper<CozeApiCall> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CozeApiCall::getConversationId, conversationId)
.eq(CozeApiCall::getRequestType, requestType)
.eq(CozeApiCall::getIsDeleted, 0)
.orderByDesc(CozeApiCall::getStartTime);
return this.list(wrapper);
}
@Override
public boolean updateStatus(String id, String status, String finalStatus, LocalDateTime endTime) {
LambdaUpdateWrapper<CozeApiCall> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(CozeApiCall::getId, id)
.set(CozeApiCall::getStatus, status)
.set(CozeApiCall::getFinalStatus, finalStatus)
.set(CozeApiCall::getEndTime, endTime)
.set(CozeApiCall::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean updateResult(String id, Integer responseStatus, String responseBody, String aiReply,
Integer totalTokens, BigDecimal cost, String status, String finalStatus,
LocalDateTime endTime) {
LambdaUpdateWrapper<CozeApiCall> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(CozeApiCall::getId, id)
.set(CozeApiCall::getResponseStatus, responseStatus)
.set(CozeApiCall::getResponseBody, responseBody)
.set(CozeApiCall::getAiReply, aiReply)
.set(CozeApiCall::getTotalTokens, totalTokens)
.set(CozeApiCall::getCost, cost)
.set(CozeApiCall::getStatus, status)
.set(CozeApiCall::getFinalStatus, finalStatus)
.set(CozeApiCall::getEndTime, endTime)
.set(CozeApiCall::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public CozeApiCall createApiCall(String conversationId, String messageId, String userId,
String requestType, String requestUrl, String requestBody) {
CozeApiCall apiCall = CozeApiCall.builder()
.conversationId(conversationId)
.messageId(messageId)
.userId(userId)
.requestType(requestType)
.requestUrl(requestUrl)
.requestBody(requestBody)
.status("pending")
.startTime(LocalDateTime.now())
.build();
this.save(apiCall);
return apiCall;
}
}
@@ -0,0 +1,210 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.entity.Message;
import com.emotion.mapper.MessageMapper;
import com.emotion.service.MessageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 消息服务实现类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Service
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements MessageService {
private static final Logger log = LoggerFactory.getLogger(MessageServiceImpl.class);
@Override
public IPage<Message> getPage(BasePageRequest request) {
Page<Message> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.like(Message::getContent, request.getKeyword());
}
wrapper.eq(Message::getIsDeleted, 0);
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(Message::getTimestamp);
} else {
wrapper.orderByDesc(Message::getTimestamp);
}
} else {
wrapper.orderByDesc(Message::getTimestamp);
}
return this.page(page, wrapper);
}
@Override
public IPage<Message> getPageByConversationId(BasePageRequest request, String conversationId) {
Page<Message> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsDeleted, 0);
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.like(Message::getContent, request.getKeyword());
}
wrapper.orderByAsc(Message::getTimestamp);
return this.page(page, wrapper);
}
@Override
public List<Message> getByConversationId(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getTimestamp);
return this.list(wrapper);
}
@Override
public List<Message> getBySender(String sender) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getSender, sender)
.eq(Message::getIsDeleted, 0)
.orderByDesc(Message::getTimestamp);
return this.list(wrapper);
}
@Override
public List<Message> getByTimeRange(String conversationId, LocalDateTime startTime, LocalDateTime endTime) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.between(Message::getTimestamp, startTime, endTime)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getTimestamp);
return this.list(wrapper);
}
@Override
public Message getLastMessageByConversationId(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsDeleted, 0)
.orderByDesc(Message::getTimestamp)
.last("LIMIT 1");
return this.getOne(wrapper);
}
@Override
public List<Message> getRepliesByParentId(String parentMessageId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getParentMessageId, parentMessageId)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getTimestamp);
return this.list(wrapper);
}
@Override
public Long countByConversationId(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countBySender(String sender) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getSender, sender)
.eq(Message::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countUnreadMessages(String conversationId) {
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsRead, 0)
.eq(Message::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public boolean updateStatus(String messageId, String status) {
LambdaUpdateWrapper<Message> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Message::getId, messageId)
.set(Message::getStatus, status)
.set(Message::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean updateReadStatus(String messageId, Integer isRead) {
LambdaUpdateWrapper<Message> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Message::getId, messageId)
.set(Message::getIsRead, isRead)
.set(Message::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean markConversationMessagesAsRead(String conversationId) {
LambdaUpdateWrapper<Message> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Message::getConversationId, conversationId)
.eq(Message::getIsRead, 0)
.set(Message::getIsRead, 1)
.set(Message::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public Message createMessage(String conversationId, String userId, String content,
String contentType, String senderType, String senderId) {
try {
Message message = Message.builder()
.id(UUID.randomUUID().toString())
.conversationId(conversationId)
.userId(userId)
.content(content)
.contentType(StringUtils.hasText(contentType) ? contentType : "text")
.senderType(senderType)
.sender(senderId)
.timestamp(LocalDateTime.now())
.status("sent")
.isRead(0)
.build();
boolean saved = this.save(message);
if (saved) {
log.info("保存消息成功: id={}, conversationId={}, sender={}",
message.getId(), conversationId, senderId);
return message;
} else {
log.error("保存消息失败: conversationId={}, sender={}", conversationId, senderId);
return null;
}
} catch (Exception e) {
log.error("保存消息异常: conversationId={}, error={}", conversationId, e.getMessage(), e);
return null;
}
}
@Override
public boolean markAsRead(String messageId) {
return updateReadStatus(messageId, 1);
}
}
@@ -0,0 +1,232 @@
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.entity.User;
import com.emotion.mapper.UserMapper;
import com.emotion.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户服务实现类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public IPage<User> getPage(BasePageRequest request) {
Page<User> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(User::getUsername, request.getKeyword())
.or().like(User::getAccount, request.getKeyword())
.or().like(User::getEmail, request.getKeyword())
.or().like(User::getPhone, request.getKeyword()));
}
wrapper.eq(User::getIsDeleted, 0);
// 排序
if (StringUtils.hasText(request.getOrderBy())) {
if ("asc".equalsIgnoreCase(request.getOrderDirection())) {
wrapper.orderByAsc(getColumnByField(request.getOrderBy()));
} else {
wrapper.orderByDesc(getColumnByField(request.getOrderBy()));
}
} else {
wrapper.orderByDesc(User::getCreateTime);
}
return this.page(page, wrapper);
}
@Override
public User getByAccount(String account) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getAccount, account)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public User getByUsername(String username) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public User getByEmail(String email) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getEmail, email)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public User getByPhone(String phone) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getPhone, phone)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public User getByThirdParty(String thirdPartyId, String thirdPartyType) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getThirdPartyId, thirdPartyId)
.eq(User::getThirdPartyType, thirdPartyType)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
@Override
public List<User> getByStatus(Integer status) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, status)
.eq(User::getIsDeleted, 0)
.orderByDesc(User::getCreateTime);
return this.list(wrapper);
}
@Override
public List<User> getByMemberLevel(String memberLevel) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getMemberLevel, memberLevel)
.eq(User::getIsDeleted, 0)
.orderByDesc(User::getCreateTime);
return this.list(wrapper);
}
@Override
public List<User> getActiveUsers(Integer days) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(User::getLastActiveTime, LocalDateTime.now().minusDays(days))
.eq(User::getIsDeleted, 0)
.orderByDesc(User::getLastActiveTime);
return this.list(wrapper);
}
@Override
public List<User> getNewUsers(Integer days) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(User::getCreateTime, LocalDateTime.now().minusDays(days))
.eq(User::getIsDeleted, 0)
.orderByDesc(User::getCreateTime);
return this.list(wrapper);
}
@Override
public boolean updateLastActiveTime(String userId, LocalDateTime lastActiveTime) {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, userId)
.set(User::getLastActiveTime, lastActiveTime)
.set(User::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean updateStatus(String userId, Integer status) {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, userId)
.set(User::getStatus, status)
.set(User::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public boolean updateTotalDays(String userId, Integer totalDays) {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, userId)
.set(User::getTotalDays, totalDays)
.set(User::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
@Override
public Long countByStatus(Integer status) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, status)
.eq(User::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public Long countByMemberLevel(String memberLevel) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getMemberLevel, memberLevel)
.eq(User::getIsDeleted, 0);
return this.count(wrapper);
}
@Override
public User createUser(String account, String username, String password, String email, String phone) {
User user = User.builder()
.account(account)
.username(username)
.password(passwordEncoder.encode(password))
.email(email)
.phone(phone)
.status(1)
.memberLevel("basic")
.totalDays(0)
.lastActiveTime(LocalDateTime.now())
.build();
this.save(user);
return user;
}
@Override
public boolean validatePassword(String userId, String password) {
User user = this.getById(userId);
if (user == null) {
return false;
}
return passwordEncoder.matches(password, user.getPassword());
}
@Override
public boolean updatePassword(String userId, String newPassword) {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, userId)
.set(User::getPassword, passwordEncoder.encode(newPassword))
.set(User::getUpdateTime, LocalDateTime.now());
return this.update(wrapper);
}
/**
* 根据字段名获取对应的数据库列
*/
private String getColumnByField(String field) {
// 这里可以根据需要扩展更多字段映射
switch (field) {
case "createTime":
return "create_time";
case "updateTime":
return "update_time";
case "lastActiveTime":
return "last_active_time";
default:
return field;
}
}
}
@@ -0,0 +1,203 @@
package com.emotion.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
/**
* JWT工具类
*
* @author emotion-museum
* @date 2025-07-23
*/
@Component
public class JwtUtil {
private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);
/**
* JWT密钥
*/
@Value("${emotion.jwt.secret:emotion-museum-secret-key-2025}")
private String secret;
/**
* JWT过期时间(毫秒)
*/
@Value("${emotion.jwt.expiration:86400000}")
private Long expiration;
/**
* 刷新Token过期时间(毫秒)
*/
@Value("${emotion.jwt.refresh-expiration:604800000}")
private Long refreshExpiration;
/**
* 获取密钥
*/
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* 生成Token
*
* @param userId 用户ID
* @param username 用户名
* @return Token
*/
public String generateToken(String userId, String username) {
return generateToken(userId, username, expiration);
}
/**
* 生成刷新Token
*
* @param userId 用户ID
* @param username 用户名
* @return 刷新Token
*/
public String generateRefreshToken(String userId, String username) {
return generateToken(userId, username, refreshExpiration);
}
/**
* 生成Token
*
* @param userId 用户ID
* @param username 用户名
* @param expiration 过期时间(毫秒)
* @return Token
*/
private String generateToken(String userId, String username, Long expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userId)
.claim("username", username)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSecretKey(), SignatureAlgorithm.HS512)
.compact();
}
/**
* 从Token中获取用户ID
*
* @param token Token
* @return 用户ID
*/
public String getUserIdFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims != null ? claims.getSubject() : null;
}
/**
* 从Token中获取用户名
*
* @param token Token
* @return 用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims != null ? claims.get("username", String.class) : null;
}
/**
* 从Token中获取过期时间
*
* @param token Token
* @return 过期时间
*/
public Date getExpirationDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims != null ? claims.getExpiration() : null;
}
/**
* 从Token中获取Claims
*
* @param token Token
* @return Claims
*/
private Claims getClaimsFromToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
log.warn("解析Token失败: {}", e.getMessage());
return null;
}
}
/**
* 验证Token是否有效
*
* @param token Token
* @return 是否有效
*/
public boolean validateToken(String token) {
if (token == null || token.trim().isEmpty()) {
return false;
}
try {
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return false;
}
// 检查是否过期
Date expiration = claims.getExpiration();
return expiration != null && expiration.after(new Date());
} catch (Exception e) {
log.warn("Token验证失败: {}", e.getMessage());
return false;
}
}
/**
* 检查Token是否过期
*
* @param token Token
* @return 是否过期
*/
public boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration != null && expiration.before(new Date());
}
/**
* 刷新Token
*
* @param token 原Token
* @return 新Token
*/
public String refreshToken(String token) {
try {
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return null;
}
String userId = claims.getSubject();
String username = claims.get("username", String.class);
return generateToken(userId, username);
} catch (Exception e) {
log.warn("刷新Token失败: {}", e.getMessage());
return null;
}
}
}
@@ -0,0 +1,232 @@
package com.emotion.util;
import lombok.extern.slf4j.Slf4j;
/**
* 雪花算法ID生成器
* 生成64位长整型ID,转换为字符串避免前端精度丢失问题
*
* 雪花算法结构:
* 1位符号位(固定为0) + 41位时间戳 + 10位机器ID + 12位序列号
*
* @author emotion-museum
* @since 2025-07-23
*/
@Slf4j
public class SnowflakeIdGenerator {
/**
* 起始时间戳 (2024-01-01 00:00:00)
*/
private static final long START_TIMESTAMP = 1704067200000L;
/**
* 机器ID位数
*/
private static final long MACHINE_ID_BITS = 10L;
/**
* 序列号位数
*/
private static final long SEQUENCE_BITS = 12L;
/**
* 机器ID最大值
*/
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
/**
* 序列号最大值
*/
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
/**
* 机器ID左移位数
*/
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
/**
* 时间戳左移位数
*/
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
/**
* 机器ID
*/
private final long machineId;
/**
* 序列号
*/
private long sequence = 0L;
/**
* 上次生成ID的时间戳
*/
private long lastTimestamp = -1L;
/**
* 构造函数
*
* @param machineId 机器ID (0-1023)
*/
public SnowflakeIdGenerator(long machineId) {
if (machineId > MAX_MACHINE_ID || machineId < 0) {
throw new IllegalArgumentException(
String.format("机器ID必须在0到%d之间", MAX_MACHINE_ID));
}
this.machineId = machineId;
log.info("雪花算法ID生成器初始化完成,机器ID: {}", machineId);
}
/**
* 默认构造函数,使用默认机器ID
*/
public SnowflakeIdGenerator() {
// 使用当前时间戳的后10位作为默认机器ID
this(System.currentTimeMillis() % (MAX_MACHINE_ID + 1));
}
/**
* 生成下一个ID
*
* @return 生成的ID
*/
public synchronized long nextId() {
long timestamp = getCurrentTimestamp();
// 如果当前时间小于上次ID生成的时间戳,说明系统时钟回退过,抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("系统时钟回退,拒绝生成ID。当前时间戳: %d, 上次时间戳: %d",
timestamp, lastTimestamp));
}
// 如果是同一时间戳,则在序列号上自增
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
// 如果序列号溢出,则等待下一个毫秒
if (sequence == 0) {
timestamp = getNextTimestamp(lastTimestamp);
}
} else {
// 如果是新的时间戳,则序列号重置为0
sequence = 0L;
}
// 更新上次生成ID的时间戳
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (machineId << MACHINE_ID_SHIFT)
| sequence;
}
/**
* 生成字符串格式的ID
*
* @return 字符串格式的ID
*/
public String nextIdAsString() {
return String.valueOf(nextId());
}
/**
* 获取当前时间戳
*
* @return 当前时间戳
*/
private long getCurrentTimestamp() {
return System.currentTimeMillis();
}
/**
* 获取下一个时间戳
*
* @param lastTimestamp 上次时间戳
* @return 下一个时间戳
*/
private long getNextTimestamp(long lastTimestamp) {
long timestamp = getCurrentTimestamp();
while (timestamp <= lastTimestamp) {
timestamp = getCurrentTimestamp();
}
return timestamp;
}
/**
* 解析ID获取时间戳
*
* @param id 雪花算法生成的ID
* @return 时间戳
*/
public long parseTimestamp(long id) {
return (id >> TIMESTAMP_SHIFT) + START_TIMESTAMP;
}
/**
* 解析ID获取机器ID
*
* @param id 雪花算法生成的ID
* @return 机器ID
*/
public long parseMachineId(long id) {
return (id >> MACHINE_ID_SHIFT) & MAX_MACHINE_ID;
}
/**
* 解析ID获取序列号
*
* @param id 雪花算法生成的ID
* @return 序列号
*/
public long parseSequence(long id) {
return id & MAX_SEQUENCE;
}
/**
* 获取机器ID
*
* @return 机器ID
*/
public long getMachineId() {
return machineId;
}
/**
* 批量生成ID
*
* @param count 生成数量
* @return ID数组
*/
public long[] nextIds(int count) {
if (count <= 0) {
throw new IllegalArgumentException("生成数量必须大于0");
}
long[] ids = new long[count];
for (int i = 0; i < count; i++) {
ids[i] = nextId();
}
return ids;
}
/**
* 批量生成字符串格式的ID
*
* @param count 生成数量
* @return 字符串ID数组
*/
public String[] nextIdsAsString(int count) {
if (count <= 0) {
throw new IllegalArgumentException("生成数量必须大于0");
}
String[] ids = new String[count];
for (int i = 0; i < count; i++) {
ids[i] = nextIdAsString();
}
return ids;
}
}
@@ -0,0 +1,222 @@
package com.emotion.util;
import lombok.extern.slf4j.Slf4j;
/**
* 用户上下文持有者
* 用于在当前线程中存储用户信息
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
public class UserContextHolder {
/**
* 用户ID线程本地变量
*/
private static final ThreadLocal<String> USER_ID_HOLDER = new ThreadLocal<>();
/**
* 用户名线程本地变量
*/
private static final ThreadLocal<String> USERNAME_HOLDER = new ThreadLocal<>();
/**
* 用户类型线程本地变量
*/
private static final ThreadLocal<String> USER_TYPE_HOLDER = new ThreadLocal<>();
/**
* 客户端IP线程本地变量
*/
private static final ThreadLocal<String> CLIENT_IP_HOLDER = new ThreadLocal<>();
/**
* 请求ID线程本地变量
*/
private static final ThreadLocal<String> REQUEST_ID_HOLDER = new ThreadLocal<>();
/**
* 设置当前用户ID
*
* @param userId 用户ID
*/
public static void setCurrentUserId(String userId) {
USER_ID_HOLDER.set(userId);
log.debug("设置当前用户ID: {}", userId);
}
/**
* 获取当前用户ID
*
* @return 用户ID
*/
public static String getCurrentUserId() {
return USER_ID_HOLDER.get();
}
/**
* 设置当前用户名
*
* @param username 用户名
*/
public static void setCurrentUsername(String username) {
USERNAME_HOLDER.set(username);
log.debug("设置当前用户名: {}", username);
}
/**
* 获取当前用户名
*
* @return 用户名
*/
public static String getCurrentUsername() {
return USERNAME_HOLDER.get();
}
/**
* 设置当前用户类型
*
* @param userType 用户类型
*/
public static void setCurrentUserType(String userType) {
USER_TYPE_HOLDER.set(userType);
log.debug("设置当前用户类型: {}", userType);
}
/**
* 获取当前用户类型
*
* @return 用户类型
*/
public static String getCurrentUserType() {
return USER_TYPE_HOLDER.get();
}
/**
* 设置客户端IP
*
* @param clientIp 客户端IP
*/
public static void setClientIp(String clientIp) {
CLIENT_IP_HOLDER.set(clientIp);
log.debug("设置客户端IP: {}", clientIp);
}
/**
* 获取客户端IP
*
* @return 客户端IP
*/
public static String getClientIp() {
return CLIENT_IP_HOLDER.get();
}
/**
* 设置请求ID
*
* @param requestId 请求ID
*/
public static void setRequestId(String requestId) {
REQUEST_ID_HOLDER.set(requestId);
log.debug("设置请求ID: {}", requestId);
}
/**
* 获取请求ID
*
* @return 请求ID
*/
public static String getRequestId() {
return REQUEST_ID_HOLDER.get();
}
/**
* 设置用户上下文信息
*
* @param userId 用户ID
* @param username 用户名
* @param userType 用户类型
* @param clientIp 客户端IP
* @param requestId 请求ID
*/
public static void setUserContext(String userId, String username, String userType,
String clientIp, String requestId) {
setCurrentUserId(userId);
setCurrentUsername(username);
setCurrentUserType(userType);
setClientIp(clientIp);
setRequestId(requestId);
log.debug("设置用户上下文: userId={}, username={}, userType={}, clientIp={}, requestId={}",
userId, username, userType, clientIp, requestId);
}
/**
* 清除当前用户ID
*/
public static void clearUserId() {
USER_ID_HOLDER.remove();
}
/**
* 清除当前用户名
*/
public static void clearUsername() {
USERNAME_HOLDER.remove();
}
/**
* 清除当前用户类型
*/
public static void clearUserType() {
USER_TYPE_HOLDER.remove();
}
/**
* 清除客户端IP
*/
public static void clearClientIp() {
CLIENT_IP_HOLDER.remove();
}
/**
* 清除请求ID
*/
public static void clearRequestId() {
REQUEST_ID_HOLDER.remove();
}
/**
* 清除所有用户上下文信息
*/
public static void clear() {
clearUserId();
clearUsername();
clearUserType();
clearClientIp();
clearRequestId();
log.debug("清除所有用户上下文信息");
}
/**
* 获取当前用户上下文摘要信息
*
* @return 用户上下文摘要
*/
public static String getContextSummary() {
return String.format("UserContext[userId=%s, username=%s, userType=%s, clientIp=%s, requestId=%s]",
getCurrentUserId(), getCurrentUsername(), getCurrentUserType(),
getClientIp(), getRequestId());
}
/**
* 检查是否有用户上下文
*
* @return 是否有用户上下文
*/
public static boolean hasUserContext() {
return getCurrentUserId() != null || getCurrentUsername() != null;
}
}
@@ -0,0 +1,214 @@
package com.emotion.util;
import lombok.extern.slf4j.Slf4j;
/**
* 用户上下文工具类
* 提供便捷的用户上下文操作方法
*
* @author emotion-museum
* @date 2025-07-23
*/
@Slf4j
public class UserContextUtils {
/**
* 获取当前用户ID,如果为空则返回默认值
*
* @param defaultValue 默认值
* @return 用户ID
*/
public static String getCurrentUserIdOrDefault(String defaultValue) {
String userId = UserContextHolder.getCurrentUserId();
return userId != null ? userId : defaultValue;
}
/**
* 获取当前用户ID,如果为空则返回"system"
*
* @return 用户ID
*/
public static String getCurrentUserIdOrSystem() {
return getCurrentUserIdOrDefault("system");
}
/**
* 获取当前用户名,如果为空则返回默认值
*
* @param defaultValue 默认值
* @return 用户名
*/
public static String getCurrentUsernameOrDefault(String defaultValue) {
String username = UserContextHolder.getCurrentUsername();
return username != null ? username : defaultValue;
}
/**
* 获取当前用户名,如果为空则返回"guest"
*
* @return 用户名
*/
public static String getCurrentUsernameOrGuest() {
return getCurrentUsernameOrDefault("guest");
}
/**
* 获取当前用户类型,如果为空则返回默认值
*
* @param defaultValue 默认值
* @return 用户类型
*/
public static String getCurrentUserTypeOrDefault(String defaultValue) {
String userType = UserContextHolder.getCurrentUserType();
return userType != null ? userType : defaultValue;
}
/**
* 获取当前用户类型,如果为空则返回"GUEST"
*
* @return 用户类型
*/
public static String getCurrentUserTypeOrGuest() {
return getCurrentUserTypeOrDefault("GUEST");
}
/**
* 获取客户端IP,如果为空则返回默认值
*
* @param defaultValue 默认值
* @return 客户端IP
*/
public static String getClientIpOrDefault(String defaultValue) {
String clientIp = UserContextHolder.getClientIp();
return clientIp != null ? clientIp : defaultValue;
}
/**
* 获取客户端IP,如果为空则返回"unknown"
*
* @return 客户端IP
*/
public static String getClientIpOrUnknown() {
return getClientIpOrDefault("unknown");
}
/**
* 获取请求ID,如果为空则返回默认值
*
* @param defaultValue 默认值
* @return 请求ID
*/
public static String getRequestIdOrDefault(String defaultValue) {
String requestId = UserContextHolder.getRequestId();
return requestId != null ? requestId : defaultValue;
}
/**
* 获取请求ID,如果为空则返回"unknown"
*
* @return 请求ID
*/
public static String getRequestIdOrUnknown() {
return getRequestIdOrDefault("unknown");
}
/**
* 检查当前用户是否为访客
*
* @return 是否为访客
*/
public static boolean isGuest() {
String userId = UserContextHolder.getCurrentUserId();
String userType = UserContextHolder.getCurrentUserType();
return userId == null ||
userId.startsWith("guest_") ||
"GUEST".equalsIgnoreCase(userType);
}
/**
* 检查当前用户是否为系统用户
*
* @return 是否为系统用户
*/
public static boolean isSystem() {
String userId = UserContextHolder.getCurrentUserId();
String userType = UserContextHolder.getCurrentUserType();
return "system".equals(userId) ||
"SYSTEM".equalsIgnoreCase(userType);
}
/**
* 检查当前用户是否为注册用户
*
* @return 是否为注册用户
*/
public static boolean isRegisteredUser() {
return !isGuest() && !isSystem();
}
/**
* 安全地执行需要用户上下文的操作
* 如果没有用户上下文,会设置默认的系统上下文
*
* @param operation 操作
*/
public static void executeWithContext(Runnable operation) {
boolean hasContext = UserContextHolder.hasUserContext();
try {
// 如果没有用户上下文,设置默认的系统上下文
if (!hasContext) {
UserContextHolder.setUserContext("system", "system", "SYSTEM", "127.0.0.1", "system");
log.debug("设置默认系统用户上下文");
}
// 执行操作
operation.run();
} finally {
// 如果是我们设置的默认上下文,执行后清理
if (!hasContext) {
UserContextHolder.clear();
log.debug("清理默认系统用户上下文");
}
}
}
/**
* 临时设置用户上下文执行操作
*
* @param userId 用户ID
* @param username 用户名
* @param userType 用户类型
* @param operation 操作
*/
public static void executeWithTempContext(String userId, String username, String userType, Runnable operation) {
// 保存当前上下文
String originalUserId = UserContextHolder.getCurrentUserId();
String originalUsername = UserContextHolder.getCurrentUsername();
String originalUserType = UserContextHolder.getCurrentUserType();
String originalClientIp = UserContextHolder.getClientIp();
String originalRequestId = UserContextHolder.getRequestId();
try {
// 设置临时上下文
UserContextHolder.setUserContext(userId, username, userType,
originalClientIp != null ? originalClientIp : "127.0.0.1",
originalRequestId != null ? originalRequestId : "temp");
// 执行操作
operation.run();
} finally {
// 恢复原始上下文
if (originalUserId != null || originalUsername != null) {
UserContextHolder.setUserContext(originalUserId, originalUsername, originalUserType,
originalClientIp, originalRequestId);
} else {
UserContextHolder.clear();
}
}
}
}
@@ -8,9 +8,9 @@ spring:
# 数据库配置 - 本地MySQL # 数据库配置 - 本地MySQL
datasource: datasource:
driver-class-name: com.mysql.cj.jdbc.Driver 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 username: root
password: 123456 password: EmotionMuseum2025*#
hikari: hikari:
minimum-idle: 5 minimum-idle: 5
maximum-pool-size: 20 maximum-pool-size: 20
@@ -52,9 +52,13 @@ emotion:
# 文件上传路径 - 本地开发 # 文件上传路径 - 本地开发
upload: upload:
path: ./uploads/emotion-museum path: ./uploads/emotion-museum
# 开发模式配置 # 开发模式配置
dev: dev:
mock-enabled: true mock-enabled: true
debug-mode: true debug-mode: true
hot-reload: true hot-reload: true
# 雪花算法配置
snowflake:
machine-id: 1
@@ -8,7 +8,7 @@ spring:
# 数据库配置 - 生产MySQL # 数据库配置 - 生产MySQL
datasource: datasource:
driver-class-name: com.mysql.cj.jdbc.Driver 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 username: emotion
password: EmotionDB2024! password: EmotionDB2024!
hikari: hikari:
@@ -63,7 +63,7 @@ management:
emotion: emotion:
# JWT配置 # JWT配置
jwt: jwt:
secret: EmotionMuseumJWTSecretKey2025ForAuthenticationAndAuthorization secret: EmotionMuseumJWTSecretKey2025ForAuthenticationAndAuthorizationSecureEnoughForHS512Algorithm
expiration: 86400000 # 24小时 expiration: 86400000 # 24小时
header: Authorization header: Authorization
prefix: "Bearer " prefix: "Bearer "
@@ -71,9 +71,14 @@ emotion:
# Coze API配置 - 所有环境统一 # Coze API配置 - 所有环境统一
coze: coze:
api: api:
token: pat_7523042446285439016_emotion_museum_2025 token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
base-url: https://api.coze.cn base-url: https://api.coze.cn
bot-id: 7523042446285439016 # 对话聊天
chat:
bot-id: 7523042446285439016
# 聊天记录总结
summary:
bot-id: 7529062814150295595
workflow-id: 7523047462895796287 workflow-id: 7523047462895796287
timeout: 30000 timeout: 30000
retry-count: 3 retry-count: 3
@@ -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-AIsystem-系统',
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;
@@ -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;
+4 -4
View File
@@ -4,10 +4,10 @@
VITE_APP_TITLE=开心APP - 开发环境 VITE_APP_TITLE=开心APP - 开发环境
VITE_APP_DESCRIPTION=你的情绪陪伴使者 VITE_APP_DESCRIPTION=你的情绪陪伴使者
# API配置 - 通过网关访问 # API配置 - 直接访问backend-single
VITE_API_BASE_URL=http://localhost:19000 VITE_API_BASE_URL=http://localhost:8080/api
VITE_UPLOAD_URL=http://localhost:19000/api/upload VITE_UPLOAD_URL=http://localhost:8080/api/upload
VITE_WS_URL=http://localhost:19000/ws/chat VITE_WS_URL=http://localhost:8080/ws/chat
# WebSocket配置 # WebSocket配置
VITE_WS_RECONNECT_ATTEMPTS=5 VITE_WS_RECONNECT_ATTEMPTS=5
+1 -1
View File
@@ -3,7 +3,7 @@ VITE_APP_TITLE=开心APP
VITE_APP_DESCRIPTION=你的情绪陪伴使者 VITE_APP_DESCRIPTION=你的情绪陪伴使者
# API配置 - 生产环境通过网关访问 # 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_UPLOAD_URL=http://47.111.10.27:19000/api/upload
VITE_WS_URL=http://47.111.10.27:19000/ws/chat VITE_WS_URL=http://47.111.10.27:19000/ws/chat
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png" /> <link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Permissions-Policy" content="unload=()">
<title>开心APP - 你的情绪陪伴使者</title> <title>开心APP - 你的情绪陪伴使者</title>
<meta name="description" content="开心APP是一款AI情绪陪伴应用,提供智能对话、情绪日记、个人展板等功能,陪伴你的每一个情绪时刻。" /> <meta name="description" content="开心APP是一款AI情绪陪伴应用,提供智能对话、情绪日记、个人展板等功能,陪伴你的每一个情绪时刻。" />
<meta name="keywords" content="AI助手,情绪陪伴,智能对话,情绪日记,心理健康" /> <meta name="keywords" content="AI助手,情绪陪伴,智能对话,情绪日记,心理健康" />
@@ -28,11 +28,21 @@
<!-- 已登录状态 --> <!-- 已登录状态 -->
<template v-else> <template v-else>
<a-dropdown> <a-dropdown>
<a-button type="text" class="user-btn"> <div class="user-info-section">
<UserOutlined /> <a-avatar
{{ userStore.userInfo?.nickname || userStore.userInfo?.account || '用户' }} :size="32"
<DownOutlined /> :src="userStore.userInfo?.avatar"
</a-button> class="user-avatar"
>
<template #icon v-if="!userStore.userInfo?.avatar">
<UserOutlined />
</template>
</a-avatar>
<span class="user-nickname">
{{ userStore.userInfo?.nickname || userStore.userInfo?.username || '用户' }}
</span>
<DownOutlined class="dropdown-icon" />
</div>
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item key="profile" @click="$router.push('/dashboard')"> <a-menu-item key="profile" @click="$router.push('/dashboard')">
@@ -150,6 +160,39 @@
} }
} }
.user-info-section {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(74, 144, 226, 0.1);
}
.user-avatar {
border: 2px solid #f0f0f0;
}
.user-nickname {
font-weight: 500;
color: #4A90E2;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdown-icon {
font-size: 12px;
color: #8c8c8c;
margin-left: 4px;
}
}
.user-btn { .user-btn {
color: #4A90E2; color: #4A90E2;
font-weight: 500; font-weight: 500;
@@ -164,4 +207,25 @@
border-radius: 20px; border-radius: 20px;
font-weight: 600; font-weight: 600;
} }
// 响应式设计
@media (max-width: 768px) {
.header-content {
padding: 0 16px;
}
.nav-menu {
display: none;
}
.user-info-section {
.user-nickname {
display: none;
}
}
.header-actions {
gap: 8px;
}
}
</style> </style>
+17
View File
@@ -38,6 +38,15 @@ const routes: RouteRecordRaw[] = [
requiresAuth: false requiresAuth: false
} }
}, },
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/Profile/index.vue'),
meta: {
title: '个人中心',
requiresAuth: true
}
},
{ {
path: '/topic-tracker', path: '/topic-tracker',
name: 'TopicTracker', name: 'TopicTracker',
@@ -151,7 +160,15 @@ router.beforeEach(async (to, from, next) => {
const { useUserStore } = await import('@/stores/user') const { useUserStore } = await import('@/stores/user')
const userStore = useUserStore() const userStore = useUserStore()
console.log('路由守卫检查登录状态:', {
path: to.path,
isLoggedIn: userStore.isLoggedIn,
token: !!userStore.token,
userInfo: !!userStore.userInfo
})
if (userStore.isLoggedIn) { if (userStore.isLoggedIn) {
console.log('用户已登录,重定向到首页')
next('/') next('/')
return return
} }
+16 -49
View File
@@ -1,4 +1,4 @@
import axios from 'axios' import request from '@/utils/request'
import type { import type {
LoginRequest, LoginRequest,
LoginResponse, LoginResponse,
@@ -12,80 +12,47 @@ import type {
UserInfo UserInfo
} from '@/types/auth' } from '@/types/auth'
// 创建axios实例
const authApi = axios.create({
baseURL: '/api/auth',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
authApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
authApi.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
if (error.response?.status === 401) {
// token过期,清除本地存储并跳转到登录页
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
window.location.href = '/login'
}
return Promise.reject(error.response?.data || error)
}
)
export const authService = { export const authService = {
// 获取验证码 // 获取验证码
async getCaptcha(): Promise<CaptchaResponse> { async getCaptcha(): Promise<CaptchaResponse> {
const response: ApiResponse<CaptchaResponse> = await authApi.get('/captcha') const response = await request.get('/auth/captcha')
return response.data return response.data.data
}, },
// 用户登录 // 用户登录
async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> { async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> {
return await authApi.post('/login', data) const response = await request.post('/auth/login', data)
return response.data
}, },
// 用户注册 // 用户注册
async register(data: RegisterRequest): Promise<ApiResponse<UserInfo>> { async register(data: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
return await authApi.post('/register', data) const response = await request.post('/auth/register', data)
return response.data
}, },
// 刷新token // 刷新token
async refreshToken(data: RefreshTokenRequest): Promise<ApiResponse<LoginResponse>> { async refreshToken(data: RefreshTokenRequest): Promise<ApiResponse<LoginResponse>> {
return await authApi.post('/refresh-token', data) const response = await request.post('/auth/refresh-token', data)
return response.data
}, },
// 用户登出 // 用户登出
async logout(): Promise<ApiResponse<void>> { async logout(): Promise<ApiResponse<void>> {
return await authApi.post('/logout') const response = await request.post('/auth/logout')
return response.data
}, },
// 获取用户信息 // 获取用户信息
async getUserInfo(): Promise<ApiResponse<UserInfo>> { async getUserInfo(): Promise<ApiResponse<UserInfo>> {
return await authApi.get('/user-info') const response = await request.get('/auth/user-info')
return response.data
}, },
// 修改密码 // 修改密码
async changePassword(data: ChangePasswordRequest): Promise<ApiResponse<void>> { async changePassword(data: ChangePasswordRequest): Promise<ApiResponse<void>> {
return await authApi.post('/change-password', data) const response = await request.post('/auth/change-password', data)
return response.data
}, },
// 忘记密码 // 忘记密码
+41 -9
View File
@@ -1,5 +1,5 @@
import SockJS from 'sockjs-client' import SockJS from 'sockjs-client'
import { Stomp, Client } from 'stompjs' import * as Stomp from 'stompjs'
import type { ChatMessage } from '@/types' import type { ChatMessage } from '@/types'
// WebSocket消息类型 // WebSocket消息类型
@@ -17,11 +17,21 @@ export interface WebSocketMessage {
// 聊天请求类型 // 聊天请求类型
export interface ChatRequest { export interface ChatRequest {
conversationId?: string
content: string content: string
senderId: string senderId: string
senderType: 'USER' | 'GUEST' senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM'
messageType: 'TEXT' messageType: 'TEXT' | 'IMAGE' | 'FILE' | 'SYSTEM' | 'HEARTBEAT'
conversationId?: string
timestamp?: number
}
// 连接请求类型
export interface ConnectRequest {
userId?: string
username?: string
clientType?: string
clientVersion?: string
timestamp?: number
} }
// WebSocket连接状态 // WebSocket连接状态
@@ -37,7 +47,7 @@ export interface WebSocketCallbacks {
} }
export class WebSocketService { export class WebSocketService {
private client: Client | null = null private client: Stomp.Client | null = null
private callbacks: WebSocketCallbacks = {} private callbacks: WebSocketCallbacks = {}
private status: ConnectionStatus = 'DISCONNECTED' private status: ConnectionStatus = 'DISCONNECTED'
private reconnectAttempts = 0 private reconnectAttempts = 0
@@ -60,12 +70,18 @@ export class WebSocketService {
this.setStatus('CONNECTING') this.setStatus('CONNECTING')
// 创建SockJS连接 // 创建SockJS连接
const socket = new SockJS(this.wsUrl) const socket = new SockJS(this.wsUrl, null, {
transports: ['websocket', 'xhr-streaming', 'xhr-polling']
})
this.client = Stomp.over(socket) this.client = Stomp.over(socket)
// 禁用调试日志 // 禁用调试日志
this.client.debug = () => {} this.client.debug = () => {}
// 设置心跳
this.client.heartbeat.outgoing = 20000
this.client.heartbeat.incoming = 20000
// 连接配置 // 连接配置
const connectHeaders = { const connectHeaders = {
'X-User-Id': this.userId 'X-User-Id': this.userId
@@ -94,7 +110,12 @@ export class WebSocketService {
console.error('WebSocket连接失败:', error) console.error('WebSocket连接失败:', error)
this.setStatus('ERROR') this.setStatus('ERROR')
this.callbacks.onError?.(error) this.callbacks.onError?.(error)
// 检查是否是网络错误
if (error && error.type === 'close' && error.code === 1006) {
console.log('WebSocket连接被异常关闭,尝试重连...')
}
// 尝试重连 // 尝试重连
this.scheduleReconnect() this.scheduleReconnect()
reject(error) reject(error)
@@ -133,12 +154,14 @@ export class WebSocketService {
return return
} }
// 使用新的后端接口格式
const chatRequest: ChatRequest = { const chatRequest: ChatRequest = {
content, content,
senderId: this.userId!, senderId: this.userId!,
senderType: this.userId?.startsWith('guest_') ? 'GUEST' : 'USER', senderType: this.userId?.startsWith('guest_') ? 'GUEST' : 'USER',
messageType: 'TEXT', messageType: 'TEXT',
conversationId: conversationId || this.conversationId || undefined conversationId: conversationId || this.conversationId || undefined,
timestamp: Date.now()
} }
try { try {
@@ -206,8 +229,17 @@ export class WebSocketService {
private sendConnectMessage(): void { private sendConnectMessage(): void {
if (!this.client?.connected) return if (!this.client?.connected) return
const connectRequest: ConnectRequest = {
userId: this.userId!,
username: this.userId!,
clientType: 'web',
clientVersion: '1.0.0',
timestamp: Date.now()
}
try { try {
this.client.send('/app/chat.connect', {}, JSON.stringify({})) this.client.send('/app/chat.connect', {}, JSON.stringify(connectRequest))
console.log('发送连接消息:', connectRequest)
} catch (error) { } catch (error) {
console.error('发送连接消息失败:', error) console.error('发送连接消息失败:', error)
} }
+32 -19
View File
@@ -27,26 +27,40 @@ export const useUserStore = defineStore('user', () => {
} }
} }
const setUserInfo = (userInfoData: UserInfo | null) => {
userInfo.value = userInfoData
// 存储到localStorage
if (userInfoData) {
localStorage.setItem('userInfo', JSON.stringify(userInfoData))
} else {
localStorage.removeItem('userInfo')
}
}
// 新的登录方法,支持认证服务 // 新的登录方法,支持认证服务
const loginWithAuth = async (loginData: LoginRequest) => { const loginWithAuth = async (loginData: LoginRequest) => {
isLoading.value = true isLoading.value = true
try { try {
const response = await authService.login(loginData) const response = await authService.login(loginData)
if (response.success) { console.log('登录API响应:', response)
token.value = response.data.token
userInfo.value = response.data.userInfo
// 保存到本地存储 // 修复:直接处理后端返回的数据格式 {code: 200, data: {...}}
authUtils.setToken(response.data.token) if (response.code === 200 && response.data) {
authUtils.setUserInfo(response.data.userInfo) // 使用store的方法来设置token和用户信息,确保响应式更新
setToken(response.data.accessToken)
setUserInfo(response.data.userInfo)
return { success: true, data: response.data } console.log('登录成功,用户信息已保存:', response.data.userInfo)
console.log('Token已保存:', response.data.accessToken.substring(0, 20) + '...')
return { code: 200, data: response.data, message: response.message }
} else { } else {
return { success: false, message: response.message } return { code: response.code || 500, message: response.message || '登录失败' }
} }
} catch (error: any) { } catch (error: any) {
return { success: false, message: error.message || '登录失败' } console.error('登录请求失败:', error)
return { code: 500, message: error.message || '登录失败' }
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
@@ -102,24 +116,22 @@ export const useUserStore = defineStore('user', () => {
const initUser = () => { const initUser = () => {
const savedToken = authUtils.getToken() const savedToken = authUtils.getToken()
const savedUserInfo = authUtils.getUserInfo() const savedUserInfo = authUtils.getUserInfo()
const savedUser = localStorage.getItem('user')
console.log('初始化用户状态:', { savedToken: !!savedToken, savedUserInfo })
if (savedToken) { if (savedToken) {
setToken(savedToken) setToken(savedToken)
} }
if (savedUserInfo) { if (savedUserInfo) {
userInfo.value = savedUserInfo setUserInfo(savedUserInfo)
} }
if (savedUser) { console.log('用户状态初始化完成:', {
try { token: !!token.value,
setUser(JSON.parse(savedUser)) userInfo: userInfo.value,
} catch (error) { isLoggedIn: isLoggedIn.value
console.error('Failed to parse saved user data:', error) })
localStorage.removeItem('user')
}
}
} }
// 刷新用户信息 // 刷新用户信息
@@ -148,6 +160,7 @@ export const useUserStore = defineStore('user', () => {
// 方法 // 方法
setUser, setUser,
setToken, setToken,
setUserInfo,
login, login,
loginWithAuth, loginWithAuth,
logout, logout,
+2 -1
View File
@@ -32,10 +32,11 @@ export interface UserInfo {
// 登录响应 // 登录响应
export interface LoginResponse { export interface LoginResponse {
token: string accessToken: string
refreshToken: string refreshToken: string
userInfo: UserInfo userInfo: UserInfo
expiresIn: number expiresIn: number
loginTime: string
} }
// 验证码响应 // 验证码响应
+113
View File
@@ -0,0 +1,113 @@
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
import { message } from 'ant-design-vue'
import { useUserStore } from '@/stores/user'
import router from '@/router'
// 获取API基础URL
const getApiBaseUrl = () => {
// 开发环境使用代理
if (import.meta.env.DEV) {
return '/api'
}
// 生产环境使用环境变量
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api'
}
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: getApiBaseUrl(),
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
console.log('API Base URL:', getApiBaseUrl())
// 请求拦截器
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 从localStorage获取token
const token = localStorage.getItem('token')
if (token && config.headers) {
// 在请求头中添加Authorization
config.headers.Authorization = `Bearer ${token}`
}
console.log('发送请求:', {
url: config.url,
method: config.method,
hasToken: !!token,
headers: config.headers
})
return config
},
(error) => {
console.error('请求拦截器错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
console.log('收到响应:', {
url: response.config.url,
status: response.status,
data: response.data
})
return response
},
(error) => {
console.error('响应拦截器错误:', error)
if (error.response) {
const { status, data } = error.response
switch (status) {
case 401:
// token过期或无效
message.error('登录已过期,请重新登录')
// 清除本地存储的用户信息
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
// 清除store中的用户信息
const userStore = useUserStore()
userStore.setToken('')
userStore.setUserInfo(null)
// 跳转到登录页
router.push('/login')
break
case 403:
message.error('没有权限访问该资源')
break
case 404:
message.error('请求的资源不存在')
break
case 500:
message.error('服务器内部错误')
break
default:
message.error(data?.message || '请求失败')
}
} else if (error.request) {
message.error('网络连接失败,请检查网络')
} else {
message.error('请求配置错误')
}
return Promise.reject(error)
}
)
export default request
+35 -3
View File
@@ -94,7 +94,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, h } from 'vue' import { ref, reactive, onMounted, nextTick, h } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue' import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
@@ -138,9 +138,12 @@
const getCaptcha = async () => { const getCaptcha = async () => {
try { try {
const response = await authService.getCaptcha() const response = await authService.getCaptcha()
console.log('验证码响应:', response)
captchaImage.value = `data:image/png;base64,${response.image}` captchaImage.value = `data:image/png;base64,${response.image}`
captchaKey.value = response.key captchaKey.value = response.key
console.log('验证码图片URL:', captchaImage.value.substring(0, 50) + '...')
} catch (error) { } catch (error) {
console.error('获取验证码失败:', error)
message.error('获取验证码失败') message.error('获取验证码失败')
} }
} }
@@ -161,12 +164,41 @@
const result = await userStore.loginWithAuth(loginData) const result = await userStore.loginWithAuth(loginData)
if (result.success) { console.log('登录结果:', result)
console.log('用户store状态:', {
token: userStore.token,
userInfo: userStore.userInfo,
isLoggedIn: userStore.isLoggedIn
})
// 修复:检查 result.code === 200 而不是 result.success
if (result.code === 200) {
message.success('登录成功') message.success('登录成功')
// 等待状态更新后再跳转
await nextTick()
// 跳转到首页或之前的页面 // 跳转到首页或之前的页面
const redirect = router.currentRoute.value.query.redirect as string const redirect = router.currentRoute.value.query.redirect as string
router.push(redirect || '/') const targetPath = redirect || '/'
console.log('准备跳转到:', targetPath)
// 延迟一下确保状态完全更新
setTimeout(() => {
try {
// 使用replace而不是push,避免路由守卫问题
router.replace(targetPath).then(() => {
console.log('路由跳转完成')
}).catch((error) => {
console.error('路由跳转失败,使用window.location:', error)
// 如果路由跳转失败,使用window.location作为备选
window.location.href = targetPath
})
} catch (error) {
console.error('路由跳转异常,使用window.location:', error)
window.location.href = targetPath
}
}, 100)
} else { } else {
message.error(result.message || '登录失败') message.error(result.message || '登录失败')
refreshCaptcha() // 刷新验证码 refreshCaptcha() // 刷新验证码
+562
View File
@@ -0,0 +1,562 @@
<template>
<div class="profile-page">
<!-- 头部 -->
<header class="page-header">
<div class="header-content">
<div class="header-left">
<a-button type="text" @click="$router.back()" class="back-btn">
<ArrowLeftOutlined />
</a-button>
<h1 class="page-title">个人中心</h1>
</div>
<a-button type="text" @click="handleLogout" class="logout-btn">
<LogoutOutlined />
退出登录
</a-button>
</div>
</header>
<!-- 主要内容 -->
<main class="page-main">
<div class="container">
<!-- 用户信息卡片 -->
<a-card class="user-info-card" :loading="loading">
<div class="user-header">
<div class="avatar-section">
<a-avatar :size="80" :src="userInfo?.avatar" class="user-avatar">
<template #icon><UserOutlined /></template>
</a-avatar>
<a-button type="link" size="small" @click="showAvatarModal = true">
更换头像
</a-button>
</div>
<div class="user-details">
<h2 class="username">{{ userInfo?.nickname || userInfo?.username || '未设置昵称' }}</h2>
<p class="user-account">账号{{ userInfo?.account }}</p>
<p class="user-status">
<a-tag :color="userInfo?.status === 'ACTIVE' ? 'green' : 'red'">
{{ userInfo?.status === 'ACTIVE' ? '正常' : '禁用' }}
</a-tag>
</p>
</div>
</div>
</a-card>
<!-- 功能菜单 -->
<div class="menu-section">
<a-card title="账户管理" class="menu-card">
<div class="menu-list">
<div class="menu-item" @click="showEditProfileModal = true">
<EditOutlined class="menu-icon" />
<span class="menu-text">编辑个人信息</span>
<RightOutlined class="menu-arrow" />
</div>
<div class="menu-item" @click="showChangePasswordModal = true">
<LockOutlined class="menu-icon" />
<span class="menu-text">修改密码</span>
<RightOutlined class="menu-arrow" />
</div>
</div>
</a-card>
<a-card title="应用设置" class="menu-card">
<div class="menu-list">
<div class="menu-item" @click="$router.push('/settings')">
<SettingOutlined class="menu-icon" />
<span class="menu-text">系统设置</span>
<RightOutlined class="menu-arrow" />
</div>
<div class="menu-item" @click="showAboutModal = true">
<InfoCircleOutlined class="menu-icon" />
<span class="menu-text">关于应用</span>
<RightOutlined class="menu-arrow" />
</div>
</div>
</a-card>
</div>
<!-- 统计信息 -->
<a-card title="使用统计" class="stats-card">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{{ stats.loginCount || 0 }}</div>
<div class="stat-label">登录次数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ stats.chatCount || 0 }}</div>
<div class="stat-label">聊天次数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ stats.diaryCount || 0 }}</div>
<div class="stat-label">日记数量</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ formatDate(userInfo?.createTime) }}</div>
<div class="stat-label">注册时间</div>
</div>
</div>
</a-card>
</div>
</main>
<!-- 编辑个人信息模态框 -->
<a-modal
v-model:open="showEditProfileModal"
title="编辑个人信息"
@ok="handleUpdateProfile"
:confirm-loading="updateLoading"
>
<a-form :model="profileForm" layout="vertical">
<a-form-item label="昵称">
<a-input v-model:value="profileForm.nickname" placeholder="请输入昵称" />
</a-form-item>
<a-form-item label="邮箱">
<a-input v-model:value="profileForm.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item label="手机号">
<a-input v-model:value="profileForm.phone" placeholder="请输入手机号" />
</a-form-item>
</a-form>
</a-modal>
<!-- 修改密码模态框 -->
<a-modal
v-model:open="showChangePasswordModal"
title="修改密码"
@ok="handleChangePassword"
:confirm-loading="passwordLoading"
>
<a-form :model="passwordForm" layout="vertical">
<a-form-item label="当前密码">
<a-input-password v-model:value="passwordForm.oldPassword" placeholder="请输入当前密码" />
</a-form-item>
<a-form-item label="新密码">
<a-input-password v-model:value="passwordForm.newPassword" placeholder="请输入新密码" />
</a-form-item>
<a-form-item label="确认新密码">
<a-input-password v-model:value="passwordForm.confirmPassword" placeholder="请再次输入新密码" />
</a-form-item>
</a-form>
</a-modal>
<!-- 更换头像模态框 -->
<a-modal
v-model:open="showAvatarModal"
title="更换头像"
@ok="handleUpdateAvatar"
:confirm-loading="avatarLoading"
>
<div class="avatar-upload">
<a-upload
v-model:file-list="avatarFileList"
:before-upload="beforeAvatarUpload"
list-type="picture-card"
:show-upload-list="false"
>
<div v-if="avatarUrl">
<img :src="avatarUrl" alt="avatar" style="width: 100%; height: 100%; object-fit: cover;" />
</div>
<div v-else>
<PlusOutlined />
<div style="margin-top: 8px">上传头像</div>
</div>
</a-upload>
</div>
</a-modal>
<!-- 关于应用模态框 -->
<a-modal
v-model:open="showAboutModal"
title="关于应用"
:footer="null"
>
<div class="about-content">
<div class="app-info">
<h3>情感博物馆</h3>
<p>版本v1.0.0</p>
<p>一个专注于情感记录与分析的智能应用</p>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import {
ArrowLeftOutlined,
UserOutlined,
EditOutlined,
LockOutlined,
SettingOutlined,
InfoCircleOutlined,
RightOutlined,
LogoutOutlined,
PlusOutlined
} from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/user'
import { authService } from '@/services/auth'
const router = useRouter()
const userStore = useUserStore()
// 响应式数据
const loading = ref(false)
const updateLoading = ref(false)
const passwordLoading = ref(false)
const avatarLoading = ref(false)
// 模态框显示状态
const showEditProfileModal = ref(false)
const showChangePasswordModal = ref(false)
const showAvatarModal = ref(false)
const showAboutModal = ref(false)
// 表单数据
const profileForm = reactive({
nickname: '',
email: '',
phone: ''
})
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
// 头像相关
const avatarFileList = ref([])
const avatarUrl = ref('')
// 统计数据
const stats = reactive({
loginCount: 0,
chatCount: 0,
diaryCount: 0
})
// 计算属性
const userInfo = computed(() => userStore.userInfo)
// 方法
const formatDate = (dateString: string) => {
if (!dateString) return '未知'
return new Date(dateString).toLocaleDateString()
}
const handleLogout = async () => {
try {
await userStore.logout()
message.success('退出登录成功')
router.push('/login')
} catch (error) {
message.error('退出登录失败')
}
}
const handleUpdateProfile = async () => {
updateLoading.value = true
try {
// TODO: 调用更新个人信息API
message.success('个人信息更新成功')
showEditProfileModal.value = false
} catch (error) {
message.error('更新失败')
} finally {
updateLoading.value = false
}
}
const handleChangePassword = async () => {
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
message.error('两次输入的密码不一致')
return
}
passwordLoading.value = true
try {
// TODO: 调用修改密码API
message.success('密码修改成功')
showChangePasswordModal.value = false
// 清空表单
Object.assign(passwordForm, {
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
} catch (error) {
message.error('密码修改失败')
} finally {
passwordLoading.value = false
}
}
const beforeAvatarUpload = (file: File) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
message.error('只能上传 JPG/PNG 格式的图片!')
return false
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error('图片大小不能超过 2MB!')
return false
}
// 预览图片
const reader = new FileReader()
reader.onload = (e) => {
avatarUrl.value = e.target?.result as string
}
reader.readAsDataURL(file)
return false // 阻止自动上传
}
const handleUpdateAvatar = async () => {
avatarLoading.value = true
try {
// TODO: 调用上传头像API
message.success('头像更新成功')
showAvatarModal.value = false
} catch (error) {
message.error('头像更新失败')
} finally {
avatarLoading.value = false
}
}
// 初始化数据
const initData = () => {
if (userInfo.value) {
profileForm.nickname = userInfo.value.nickname || ''
profileForm.email = userInfo.value.email || ''
profileForm.phone = userInfo.value.phone || ''
avatarUrl.value = userInfo.value.avatar || ''
}
}
onMounted(() => {
initData()
// TODO: 加载统计数据
})
</script>
<style lang="scss" scoped>
.profile-page {
min-height: 100vh;
background: #f5f5f5;
}
.page-header {
background: white;
border-bottom: 1px solid #e8e8e8;
padding: 0 16px;
position: sticky;
top: 0;
z-index: 100;
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 56px;
max-width: 1200px;
margin: 0 auto;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #262626;
}
.back-btn, .logout-btn {
display: flex;
align-items: center;
gap: 4px;
color: #666;
&:hover {
color: #1890ff;
}
}
}
.page-main {
padding: 24px 16px;
.container {
max-width: 800px;
margin: 0 auto;
}
}
.user-info-card {
margin-bottom: 24px;
.user-header {
display: flex;
gap: 20px;
align-items: flex-start;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
.user-avatar {
border: 2px solid #f0f0f0;
}
}
.user-details {
flex: 1;
.username {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: #262626;
}
.user-account {
margin: 0 0 8px 0;
color: #666;
font-size: 14px;
}
.user-status {
margin: 0;
}
}
}
.menu-section {
margin-bottom: 24px;
.menu-card {
margin-bottom: 16px;
.menu-list {
.menu-item {
display: flex;
align-items: center;
padding: 12px 0;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: all 0.2s;
&:last-child {
border-bottom: none;
}
&:hover {
background: #fafafa;
margin: 0 -16px;
padding: 12px 16px;
}
.menu-icon {
width: 20px;
color: #666;
margin-right: 12px;
}
.menu-text {
flex: 1;
color: #262626;
}
.menu-arrow {
color: #bfbfbf;
font-size: 12px;
}
}
}
}
}
.stats-card {
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 16px;
.stat-item {
text-align: center;
padding: 16px;
background: #fafafa;
border-radius: 8px;
.stat-value {
font-size: 24px;
font-weight: 600;
color: #1890ff;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
}
}
}
.avatar-upload {
display: flex;
justify-content: center;
:deep(.ant-upload-select) {
width: 120px !important;
height: 120px !important;
}
}
.about-content {
text-align: center;
padding: 20px;
.app-info {
h3 {
color: #1890ff;
margin-bottom: 16px;
}
p {
margin-bottom: 8px;
color: #666;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-main {
padding: 16px 12px;
}
.user-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
}
</style>
+19 -2
View File
@@ -26,6 +26,7 @@
placeholder="请输入手机号或邮箱" placeholder="请输入手机号或邮箱"
size="large" size="large"
:prefix="h(UserOutlined)" :prefix="h(UserOutlined)"
autocomplete="off"
/> />
</a-form-item> </a-form-item>
@@ -35,6 +36,7 @@
placeholder="请输入密码" placeholder="请输入密码"
size="large" size="large"
:prefix="h(LockOutlined)" :prefix="h(LockOutlined)"
autocomplete="new-password"
/> />
</a-form-item> </a-form-item>
@@ -44,6 +46,7 @@
placeholder="请再次输入密码" placeholder="请再次输入密码"
size="large" size="large"
:prefix="h(LockOutlined)" :prefix="h(LockOutlined)"
autocomplete="new-password"
/> />
</a-form-item> </a-form-item>
@@ -101,9 +104,11 @@
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue' import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { authService } from '@/services/auth' import { authService } from '@/services/auth'
import { useUserStore } from '@/stores/user'
import type { RegisterRequest } from '@/types/auth' import type { RegisterRequest } from '@/types/auth'
const router = useRouter() const router = useRouter()
const userStore = useUserStore()
// 表单数据 // 表单数据
const registerForm = reactive<RegisterRequest>({ const registerForm = reactive<RegisterRequest>({
@@ -150,9 +155,12 @@
const getCaptcha = async () => { const getCaptcha = async () => {
try { try {
const response = await authService.getCaptcha() const response = await authService.getCaptcha()
console.log('验证码响应:', response)
captchaImage.value = `data:image/png;base64,${response.image}` captchaImage.value = `data:image/png;base64,${response.image}`
captchaKey.value = response.key captchaKey.value = response.key
console.log('验证码图片URL:', captchaImage.value.substring(0, 50) + '...')
} catch (error) { } catch (error) {
console.error('获取验证码失败:', error)
message.error('获取验证码失败') message.error('获取验证码失败')
} }
} }
@@ -174,8 +182,17 @@
const response = await authService.register(registerData) const response = await authService.register(registerData)
if (response.success) { if (response.success) {
message.success('注册成功,登录') message.success('注册成功,已自动登录')
router.push('/login')
// 使用userStore的方法保存用户信息和token
userStore.setToken(response.data.accessToken)
userStore.setUserInfo(response.data.userInfo)
console.log('注册成功,用户信息:', response.data.userInfo)
console.log('Token已保存:', response.data.accessToken.substring(0, 20) + '...')
// 跳转到首页
router.push('/')
} else { } else {
message.error(response.message || '注册失败') message.error(response.message || '注册失败')
refreshCaptcha() // 刷新验证码 refreshCaptcha() // 刷新验证码
+1 -1
View File
@@ -4,7 +4,7 @@ import { resolve } from 'path'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: '/emotion/happy/', base: '/',
plugins: [vue()], plugins: [vue()],
resolve: { resolve: {
alias: { alias: {