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