服务层重构与优化:补全所有ServiceImpl实现类,修复RestTemplate注入,完善DTO与配置,保证编译与启动通过

This commit is contained in:
2025-07-24 14:15:31 +08:00
parent 873b8e55da
commit cf4d73ceff
95 changed files with 5889 additions and 2282 deletions
@@ -0,0 +1,148 @@
# Controller层重构总结
## 重构概述
本次重构主要完成了以下工作:
1. **创建统一的request和response包结构**
2. **建立全局异常处理机制**
3. **重构所有Controller层代码**
4. **优化接口入参和出参规范**
## 完成的工作
### 1. 创建统一的基础类
#### 在emotion-common模块中创建:
- `BaseRequest` - 基础请求类,包含通用字段如requestId、clientIp、userAgent等
- `BaseResponse` - 基础响应类,包含通用字段如timestamp、requestId、processingTime等
- `BasePageRequest` - 基础分页请求类,继承BaseRequest,包含分页参数
- `BasePageResponse` - 基础分页响应类,继承BaseResponse,包含分页信息
#### 异常处理类:
- `BusinessException` - 业务异常
- `AuthException` - 认证异常
- `CaptchaException` - 验证码异常
- `TokenException` - Token异常
- `GlobalExceptionHandler` - 全局异常处理器
### 2. 各模块request和response类
#### emotion-ai模块:
- **Request类**
- `AiChatRequest` - AI聊天请求
- `CreateConversationRequest` - 创建会话请求
- `EmotionAnalysisRequest` - 情绪分析请求
- `GuestChatRequest` - 访客聊天请求
- `ConversationListRequest` - 会话列表请求
- **Response类**
- `AiChatResponse` - AI聊天响应
- `CreateConversationResponse` - 创建会话响应
- `EmotionAnalysisResponse` - 情绪分析响应
- `GuestChatResponse` - 访客聊天响应
- `ConversationListResponse` - 会话列表响应
#### emotion-auth模块:
- **Request类**
- `LoginRequest` - 登录请求
- `RegisterRequest` - 注册请求
- `OAuthLoginRequest` - 第三方登录请求
- `SliderCaptchaVerifyRequest` - 滑块验证码验证请求
- **Response类**
- `LoginResponse` - 登录响应
- `UserInfoResponse` - 用户信息响应
- `CaptchaResponse` - 验证码响应
- `SliderCaptchaResponse` - 滑块验证码响应
#### emotion-user模块:
- **Request类**
- `UserUpdateRequest` - 用户更新请求
- **Response类**
- `UserInfoResponse` - 用户信息响应
#### emotion-record模块:
- **Request类**
- `CreateEmotionRecordRequest` - 创建情绪记录请求
- **Response类**
- `EmotionRecordResponse` - 情绪记录响应
### 3. Controller层重构
#### 重构原则:
1. **移除业务逻辑** - 所有业务逻辑移至Service层
2. **统一入参出参** - 使用新的request/response格式
3. **移除try-catch** - 使用全局异常处理机制
4. **统一返回格式** - 使用Result包装返回结果
#### 已重构的Controller
- `AiChatController` - AI聊天控制器
- `GuestChatController` - 访客聊天控制器
- `AuthController` - 认证控制器
- `CaptchaController` - 验证码控制器
- `UserController` - 用户控制器
### 4. 全局异常处理
#### 异常处理机制:
- 统一异常处理器 `GlobalExceptionHandler`
- 支持多种异常类型处理
- 自动参数校验异常处理
- 统一错误响应格式
#### 支持的异常类型:
- 业务异常 `BusinessException`
- 认证异常 `AuthException`
- 验证码异常 `CaptchaException`
- Token异常 `TokenException`
- 参数校验异常 `MethodArgumentNotValidException`
- 系统异常 `RuntimeException``Exception`
## 代码规范
### 1. 命名规范
- Request类以`Request`结尾
- Response类以`Response`结尾
- 包名使用`request``response`
### 2. 继承关系
- 所有Request类继承`BaseRequest``BasePageRequest`
- 所有Response类继承`BaseResponse``BasePageResponse`
### 3. 注解规范
- 使用`@Schema`注解描述字段
- 使用`@Valid`注解进行参数校验
- 使用`@NotBlank``@NotNull`等校验注解
### 4. Controller规范
- 不包含业务逻辑
- 统一使用Result包装返回结果
- 不使用try-catch,依赖全局异常处理
- 接口文档完整
## 优势
1. **代码结构清晰** - 职责分离明确
2. **异常处理统一** - 全局异常处理机制
3. **接口规范统一** - 统一的入参出参格式
4. **维护性提升** - 代码更易维护和扩展
5. **开发效率提升** - 减少重复代码
## 后续工作
1. **Service层接口更新** - 确保Service层使用新的request/response格式
2. **单元测试编写** - 为重构后的代码编写测试用例
3. **接口文档更新** - 更新API文档
4. **性能测试** - 验证重构后的性能表现
## 注意事项
1. 所有Controller层不再包含业务逻辑
2. 异常处理统一由GlobalExceptionHandler处理
3. 新的request/response类需要在Service层中使用
4. 需要更新相关的单元测试和集成测试
@@ -1,11 +1,10 @@
package com.emotionmuseum.ai.controller;
import com.emotionmuseum.ai.dto.*;
import com.emotionmuseum.ai.entity.Conversation;
import com.emotionmuseum.ai.entity.Message;
import com.emotionmuseum.ai.request.*;
import com.emotionmuseum.ai.response.*;
import com.emotionmuseum.ai.service.AiChatService;
import com.emotionmuseum.ai.service.ConversationDbService;
import com.emotionmuseum.common.dto.PageQuery;
import com.emotionmuseum.common.response.BasePageResponse;
import com.emotionmuseum.common.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -16,7 +15,6 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
/**
* AI聊天控制器
@@ -40,17 +38,15 @@ public class AiChatController {
public Result<CreateConversationResponse> createConversation(
@Valid @RequestBody CreateConversationRequest request) {
log.info("收到创建会话请求: userId={}, title={}", request.getUserId(), request.getTitle());
CreateConversationResponse response = aiChatService.createConversation(request);
return Result.success(response);
}
@Operation(summary = "发送聊天消息")
@PostMapping("/send")
public Result<ChatResponse> sendMessage(@Valid @RequestBody ChatRequest request) {
public Result<AiChatResponse> sendMessage(@Valid @RequestBody AiChatRequest request) {
log.info("收到聊天请求: userId={}, message={}", request.getUserId(), request.getMessage());
ChatResponse response = aiChatService.chat(request);
AiChatResponse response = aiChatService.chat(request);
return Result.success(response);
}
@@ -58,16 +54,14 @@ public class AiChatController {
@PostMapping("/emotion/analyze")
public Result<EmotionAnalysisResponse> analyzeEmotion(@Valid @RequestBody EmotionAnalysisRequest request) {
log.info("收到情绪分析请求: userId={}, text={}", request.getUserId(), request.getText());
EmotionAnalysisResponse response = aiChatService.analyzeEmotion(request);
return Result.success(response);
}
@Operation(summary = "流式聊天")
@PostMapping("/stream")
public Result<String> streamChat(@Valid @RequestBody ChatRequest request) {
public Result<String> streamChat(@Valid @RequestBody AiChatRequest request) {
log.info("收到流式聊天请求: userId={}", request.getUserId());
String response = aiChatService.streamChat(request);
return Result.success(response);
}
@@ -76,16 +70,14 @@ public class AiChatController {
@GetMapping("/health")
public Result<Boolean> healthCheck() {
log.info("AI服务健康检查");
boolean isHealthy = aiChatService.healthCheck();
return Result.success(isHealthy);
}
@Operation(summary = "获取AI服务信息")
@GetMapping("/info")
public Result<Object> getServiceInfo() {
public Result<String> getServiceInfo() {
log.info("获取AI服务信息");
return Result.success("Emotion Museum AI Service - Powered by Spring AI & Coze");
}
@@ -1,6 +1,7 @@
package com.emotionmuseum.ai.controller;
import com.emotionmuseum.ai.dto.*;
import com.emotionmuseum.ai.request.*;
import com.emotionmuseum.ai.response.*;
import com.emotionmuseum.ai.service.GuestChatService;
import com.emotionmuseum.common.result.Result;
import io.swagger.v3.oas.annotations.Operation;
@@ -0,0 +1,64 @@
package com.emotionmuseum.ai.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.List;
/**
* AI聊天请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "AI聊天请求")
public class AiChatRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID", example = "user_123")
@NotBlank(message = "用户ID不能为空")
private String userId;
@Schema(description = "消息内容", example = "我今天感觉有点焦虑,不知道该怎么办")
@NotBlank(message = "消息内容不能为空")
@Size(max = 2000, message = "消息内容不能超过2000字符")
private String message;
@Schema(description = "对话ID(可选)", example = "conv_123456")
private String conversationId;
@Schema(description = "消息类型", example = "text")
private String type = "text";
@Schema(description = "聊天历史(可选)")
private List<ChatMessage> history;
@Schema(description = "是否需要情绪分析", example = "true")
private Boolean needEmotionAnalysis = true;
@Schema(description = "上下文信息")
private String context;
/**
* 聊天消息
*/
@Data
@Schema(description = "聊天消息")
public static class ChatMessage {
@Schema(description = "角色", example = "user")
private String role; // user, assistant
@Schema(description = "消息内容")
private String content;
@Schema(description = "时间戳")
private Long timestamp;
}
}
@@ -0,0 +1,38 @@
package com.emotionmuseum.ai.request;
import com.emotionmuseum.common.request.BasePageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 会话列表请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "会话列表请求")
public class ConversationListRequest extends BasePageRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID", example = "user_123")
private String userId;
@Schema(description = "会话类型", example = "emotion_chat")
private String type;
@Schema(description = "会话状态", example = "active")
private String status;
@Schema(description = "用户类型", example = "guest")
private String userType;
@Schema(description = "开始时间", example = "2025-01-01 00:00:00")
private String startTime;
@Schema(description = "结束时间", example = "2025-12-31 23:59:59")
private String endTime;
}
@@ -0,0 +1,38 @@
package com.emotionmuseum.ai.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.NotBlank;
/**
* 创建会话请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "创建会话请求")
public class CreateConversationRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID", example = "user_123")
@NotBlank(message = "用户ID不能为空")
private String userId;
@Schema(description = "会话标题", example = "今日心情分享")
private String title;
@Schema(description = "会话类型", example = "emotion_chat")
private String type = "emotion_chat";
@Schema(description = "初始消息", example = "你好,我想聊聊今天的心情")
private String initialMessage;
@Schema(description = "上下文信息")
private String context;
}
@@ -0,0 +1,41 @@
package com.emotionmuseum.ai.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
/**
* 情绪分析请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "情绪分析请求")
public class EmotionAnalysisRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID", example = "user_123")
@NotBlank(message = "用户ID不能为空")
private String userId;
@Schema(description = "待分析文本", example = "我今天感觉很沮丧,工作压力很大")
@NotBlank(message = "待分析文本不能为空")
@Size(max = 1000, message = "待分析文本不能超过1000字符")
private String text;
@Schema(description = "分析类型", example = "detailed")
private String analysisType = "detailed"; // simple, detailed
@Schema(description = "语言", example = "zh")
private String language = "zh";
@Schema(description = "上下文信息")
private String context;
}
@@ -0,0 +1,43 @@
package com.emotionmuseum.ai.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
/**
* 访客聊天请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "访客聊天请求")
public class GuestChatRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "消息内容", example = "你好,我想聊聊今天的心情")
@NotBlank(message = "消息内容不能为空")
@Size(max = 2000, message = "消息内容不能超过2000字符")
private String message;
@Schema(description = "会话ID(可选,如果不提供则创建新会话)", example = "conv_123456")
private String conversationId;
@Schema(description = "会话标题(创建新会话时使用)", example = "今日心情分享")
private String title;
@Schema(description = "消息类型", example = "text")
private String messageType = "text";
@Schema(description = "是否流式响应", example = "false")
private Boolean stream = false;
@Schema(description = "附加上下文信息")
private String context;
}
@@ -0,0 +1,100 @@
package com.emotionmuseum.ai.response;
import com.emotionmuseum.common.response.BaseResponse;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* AI聊天响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "AI聊天响应")
public class AiChatResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "消息ID")
private String messageId;
@Schema(description = "对话ID")
private String conversationId;
@Schema(description = "AI回复内容")
private String content;
@Schema(description = "消息类型", example = "text")
private String type = "text";
@Schema(description = "发送者", example = "assistant")
private String sender = "assistant";
@Schema(description = "响应时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime responseTime;
@Schema(description = "情绪分析结果")
private EmotionAnalysisResponse emotionAnalysis;
@Schema(description = "使用情况")
private Usage usage;
@Schema(description = "元数据")
private Map<String, Object> metadata;
@Schema(description = "是否为多条消息")
private Boolean multipleMessages = false;
@Schema(description = "消息数量")
private Integer messageCount = 1;
@Schema(description = "所有消息ID列表(当拆分为多条消息时)")
private List<String> messageIds;
/**
* 使用情况
*/
@Data
@Schema(description = "使用情况")
public static class Usage {
@Schema(description = "输入Token数")
private Integer promptTokens;
@Schema(description = "输出Token数")
private Integer completionTokens;
@Schema(description = "总Token数")
private Integer totalTokens;
}
/**
* 情绪分析响应
*/
@Data
@Schema(description = "情绪分析响应")
public static class EmotionAnalysisResponse {
@Schema(description = "情绪类型")
private String emotionType;
@Schema(description = "情绪强度")
private Double intensity;
@Schema(description = "情绪描述")
private String description;
@Schema(description = "建议")
private String suggestion;
@Schema(description = "置信度")
private Double confidence;
}
}
@@ -0,0 +1,61 @@
package com.emotionmuseum.ai.response;
import com.emotionmuseum.common.response.BaseResponse;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
/**
* 会话列表响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "会话列表响应")
public class ConversationListResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "会话ID")
private String conversationId;
@Schema(description = "会话标题")
private String title;
@Schema(description = "会话类型")
private String type;
@Schema(description = "会话状态")
private String status;
@Schema(description = "用户ID")
private String userId;
@Schema(description = "用户类型")
private String userType;
@Schema(description = "消息数量")
private Integer messageCount;
@Schema(description = "最后活跃时间")
private LocalDateTime lastActiveTime;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "主要情绪")
private String primaryEmotion;
@Schema(description = "情绪强度")
private Double emotionIntensity;
@Schema(description = "Coze会话ID")
private String cozeConversationId;
}
@@ -0,0 +1,53 @@
package com.emotionmuseum.ai.response;
import com.emotionmuseum.common.response.BaseResponse;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 创建会话响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "创建会话响应")
public class CreateConversationResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "会话ID")
private String conversationId;
@Schema(description = "用户ID")
private String userId;
@Schema(description = "会话标题")
private String title;
@Schema(description = "会话类型")
private String type;
@Schema(description = "会话状态", example = "active")
private String status = "active";
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "Coze会话ID")
private String cozeConversationId;
@Schema(description = "元数据")
private Map<String, Object> metadata;
}
@@ -0,0 +1,69 @@
package com.emotionmuseum.ai.response;
import com.emotionmuseum.common.response.BaseResponse;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 情绪分析响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "情绪分析响应")
public class EmotionAnalysisResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "主要情绪", example = "焦虑")
private String primaryEmotion;
@Schema(description = "情绪强度", example = "0.75")
private Double intensity;
@Schema(description = "情绪极性", example = "negative")
private String polarity; // positive, negative, neutral
@Schema(description = "置信度", example = "0.85")
private Double confidence;
@Schema(description = "情绪分布")
private List<EmotionScore> emotions;
@Schema(description = "关键词")
private List<String> keywords;
@Schema(description = "建议")
private String suggestion;
@Schema(description = "分析时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime analysisTime;
@Schema(description = "额外信息")
private Map<String, Object> metadata;
/**
* 情绪得分
*/
@Data
@Schema(description = "情绪得分")
public static class EmotionScore {
@Schema(description = "情绪名称")
private String emotion;
@Schema(description = "得分")
private Double score;
@Schema(description = "描述")
private String description;
}
}
@@ -0,0 +1,111 @@
package com.emotionmuseum.ai.response;
import com.emotionmuseum.common.response.BaseResponse;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
/**
* 访客聊天响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "访客聊天响应")
public class GuestChatResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "访客用户ID")
private String guestUserId;
@Schema(description = "访客昵称")
private String guestNickname;
@Schema(description = "会话ID")
private String conversationId;
@Schema(description = "会话标题")
private String conversationTitle;
@Schema(description = "用户消息ID")
private String userMessageId;
@Schema(description = "AI回复消息ID")
private String aiMessageId;
@Schema(description = "用户消息内容")
private String userMessage;
@Schema(description = "AI回复内容")
private String aiReply;
@Schema(description = "消息时间戳")
private LocalDateTime messageTimestamp;
@Schema(description = "会话状态")
private String conversationStatus;
@Schema(description = "是否为新会话")
private Boolean isNewConversation;
@Schema(description = "Coze聊天ID")
private String cozeChatId;
@Schema(description = "情绪分析结果")
private EmotionAnalysisResult emotionAnalysis;
@Schema(description = "Token使用情况")
private TokenUsage tokenUsage;
@Schema(description = "错误信息(如果有)")
private String errorMessage;
/**
* 情绪分析结果内部类
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "情绪分析结果")
public static class EmotionAnalysisResult {
@Schema(description = "主要情绪")
private String primaryEmotion;
@Schema(description = "情绪得分")
private Double emotionScore;
@Schema(description = "置信度")
private Double confidence;
@Schema(description = "情绪趋势")
private String emotionTrend;
}
/**
* Token使用情况内部类
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Token使用情况")
public static class TokenUsage {
@Schema(description = "输入Token数")
private Integer promptTokens;
@Schema(description = "输出Token数")
private Integer completionTokens;
@Schema(description = "总Token数")
private Integer totalTokens;
}
}
@@ -1,11 +1,11 @@
package com.emotionmuseum.auth.controller;
import com.emotionmuseum.common.result.Result;
import com.emotionmuseum.auth.dto.LoginRequest;
import com.emotionmuseum.auth.dto.RegisterRequest;
import com.emotionmuseum.auth.request.LoginRequest;
import com.emotionmuseum.auth.request.RegisterRequest;
import com.emotionmuseum.auth.service.AuthService;
import com.emotionmuseum.auth.vo.LoginResponse;
import com.emotionmuseum.auth.vo.UserInfoResponse;
import com.emotionmuseum.auth.response.LoginResponse;
import com.emotionmuseum.auth.response.UserInfoResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -1,9 +1,9 @@
package com.emotionmuseum.auth.controller;
import com.emotionmuseum.common.result.Result;
import com.emotionmuseum.auth.dto.CaptchaResponse;
import com.emotionmuseum.auth.dto.SliderCaptchaResponse;
import com.emotionmuseum.auth.dto.SliderCaptchaVerifyRequest;
import com.emotionmuseum.auth.response.CaptchaResponse;
import com.emotionmuseum.auth.response.SliderCaptchaResponse;
import com.emotionmuseum.auth.request.SliderCaptchaVerifyRequest;
import com.emotionmuseum.auth.service.CaptchaService;
import com.emotionmuseum.auth.service.SliderCaptchaService;
import io.swagger.v3.oas.annotations.Operation;
@@ -0,0 +1,41 @@
package com.emotionmuseum.auth.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.NotBlank;
/**
* 用户登录请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "用户登录请求")
public class LoginRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "账号(支持账号/邮箱/手机号)", example = "test_user")
@NotBlank(message = "账号不能为空")
private String account;
@Schema(description = "密码", example = "123456")
@NotBlank(message = "密码不能为空")
private String password;
@Schema(description = "验证码ID", example = "captcha_123")
@NotBlank(message = "验证码ID不能为空")
private String captchaId;
@Schema(description = "验证码", example = "1234")
@NotBlank(message = "验证码不能为空")
private String captcha;
@Schema(description = "记住我", example = "false")
private Boolean rememberMe = false;
}
@@ -0,0 +1,41 @@
package com.emotionmuseum.auth.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.NotBlank;
/**
* 第三方登录请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "第三方登录请求")
public class OAuthLoginRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "第三方平台类型", example = "wechat")
@NotBlank(message = "平台类型不能为空")
private String platform;
@Schema(description = "授权码", example = "auth_code_123")
@NotBlank(message = "授权码不能为空")
private String code;
@Schema(description = "状态码", example = "state_123")
private String state;
@Schema(description = "验证码ID", example = "captcha_123")
@NotBlank(message = "验证码ID不能为空")
private String captchaId;
@Schema(description = "验证码", example = "1234")
@NotBlank(message = "验证码不能为空")
private String captcha;
}
@@ -0,0 +1,83 @@
package com.emotionmuseum.auth.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import java.time.LocalDate;
/**
* 用户注册请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "用户注册请求")
public class RegisterRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "账号", example = "test_user")
@NotBlank(message = "账号不能为空")
@Size(min = 4, max = 20, message = "账号长度必须在4-20位之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "账号只能包含字母、数字和下划线")
private String account;
@Schema(description = "密码", example = "123456")
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private String password;
@Schema(description = "确认密码", example = "123456")
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
@Schema(description = "验证码ID", example = "captcha_123")
@NotBlank(message = "验证码ID不能为空")
private String captchaId;
@Schema(description = "验证码", example = "1234")
@NotBlank(message = "验证码不能为空")
private String captcha;
@Schema(description = "用户名", example = "测试用户")
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20位之间")
private String username;
@Schema(description = "邮箱", example = "test@example.com")
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Schema(description = "手机号", example = "13800138000")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Schema(description = "昵称", example = "小测试")
@NotBlank(message = "昵称不能为空")
@Size(min = 1, max = 20, message = "昵称长度必须在1-20位之间")
private String nickname;
@Schema(description = "生日", example = "1990-01-01")
private LocalDate birthDate;
@Schema(description = "所在地", example = "北京市")
@Size(max = 50, message = "所在地长度不能超过50位")
private String location;
@Schema(description = "个人简介", example = "这是一个测试用户")
@Size(max = 200, message = "个人简介长度不能超过200位")
private String bio;
/**
* 验证密码一致性
*/
public boolean isPasswordMatch() {
return password != null && password.equals(confirmPassword);
}
}
@@ -0,0 +1,38 @@
package com.emotionmuseum.auth.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 滑块验证码验证请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "滑块验证码验证请求")
public class SliderCaptchaVerifyRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "验证码ID")
@NotBlank(message = "验证码ID不能为空")
private String captchaId;
@Schema(description = "滑块X坐标")
@NotNull(message = "滑块X坐标不能为空")
private Integer x;
@Schema(description = "滑块Y坐标")
@NotNull(message = "滑块Y坐标不能为空")
private Integer y;
@Schema(description = "滑动轨迹")
private String track;
}
@@ -0,0 +1,36 @@
package com.emotionmuseum.auth.response;
import com.emotionmuseum.common.response.BaseResponse;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 验证码响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Schema(description = "验证码响应")
public class CaptchaResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "验证码ID")
private String captchaId;
@Schema(description = "验证码图片Base64")
private String captchaImage;
@Schema(description = "验证码类型", example = "arithmetic")
private String captchaType;
@Schema(description = "过期时间(秒)", example = "300")
private Long expireTime;
}
@@ -0,0 +1,77 @@
package com.emotionmuseum.auth.response;
import com.emotionmuseum.common.response.BaseResponse;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 登录响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "登录响应")
public class LoginResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "访问Token")
private String accessToken;
@Schema(description = "刷新Token")
private String refreshToken;
@Schema(description = "Token类型", example = "Bearer")
private String tokenType = "Bearer";
@Schema(description = "Token过期时间(秒)", example = "86400")
private Long expiresIn;
@Schema(description = "用户信息")
private UserInfoResponse userInfo;
@Schema(description = "登录时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime loginTime;
/**
* 用户信息响应
*/
@Data
@Schema(description = "用户信息响应")
public static class UserInfoResponse {
@Schema(description = "用户ID")
private String userId;
@Schema(description = "账号")
private String account;
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机号")
private String phone;
@Schema(description = "头像URL")
private String avatar;
@Schema(description = "用户状态")
private String status;
@Schema(description = "最后登录时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime lastLoginTime;
}
}
@@ -0,0 +1,42 @@
package com.emotionmuseum.auth.response;
import com.emotionmuseum.common.response.BaseResponse;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 滑块验证码响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Schema(description = "滑块验证码响应")
public class SliderCaptchaResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "验证码ID")
private String captchaId;
@Schema(description = "背景图片Base64")
private String backgroundImage;
@Schema(description = "滑块图片Base64")
private String sliderImage;
@Schema(description = "滑块X坐标")
private Integer sliderX;
@Schema(description = "滑块Y坐标")
private Integer sliderY;
@Schema(description = "过期时间(秒)")
private Long expireTime;
}
@@ -0,0 +1,102 @@
package com.emotionmuseum.auth.response;
import com.emotionmuseum.common.response.BaseResponse;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 用户信息响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "用户信息响应")
public class UserInfoResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private String id;
@Schema(description = "账号")
private String account;
@Schema(description = "用户名")
private String username;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机号")
private String phone;
@Schema(description = "头像URL")
private String avatar;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "生日")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate birthDate;
@Schema(description = "所在地")
private String location;
@Schema(description = "个人简介")
private String bio;
@Schema(description = "会员等级")
private String memberLevel;
@Schema(description = "使用天数")
private Integer totalDays;
@Schema(description = "成长数据")
private GrowthStatsVO growthStats;
@Schema(description = "状态")
private Integer status;
@Schema(description = "是否已验证")
private Integer isVerified;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "最后活跃时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime lastActiveTime;
/**
* 成长数据VO
*/
@Data
@Schema(description = "成长数据")
public static class GrowthStatsVO {
@Schema(description = "自我感知")
private BigDecimal selfAwareness;
@Schema(description = "情绪韧性")
private BigDecimal emotionalResilience;
@Schema(description = "行动力")
private BigDecimal actionPower;
@Schema(description = "共情力")
private BigDecimal empathy;
@Schema(description = "生活热度")
private BigDecimal lifeEnthusiasm;
}
}
@@ -0,0 +1,62 @@
package com.emotionmuseum.common.exception;
import com.emotionmuseum.common.result.ResultCode;
import lombok.Getter;
/**
* 认证异常
*
* @author emotion-museum
* @since 2025-07-24
*/
@Getter
public class AuthException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private final Integer code;
/**
* 错误消息
*/
private final String message;
public AuthException(String message) {
super(message);
this.code = ResultCode.UNAUTHORIZED.getCode();
this.message = message;
}
public AuthException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public AuthException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
public AuthException(String message, Throwable cause) {
super(message, cause);
this.code = ResultCode.UNAUTHORIZED.getCode();
this.message = message;
}
public AuthException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.message = message;
}
public AuthException(ResultCode resultCode, Throwable cause) {
super(resultCode.getMessage(), cause);
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
}
@@ -0,0 +1,68 @@
package com.emotionmuseum.common.exception;
import com.emotionmuseum.common.result.ResultCode;
import lombok.Getter;
/**
* 业务异常
*
* @author emotion-museum
* @since 2025-07-24
*/
@Getter
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private final Integer code;
/**
* 错误消息
*/
private final String message;
public BusinessException(String message) {
super(message);
this.code = ResultCode.BUSINESS_ERROR.getCode();
this.message = message;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
public BusinessException(ResultCode resultCode, String message) {
super(message);
this.code = resultCode.getCode();
this.message = message;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.code = ResultCode.BUSINESS_ERROR.getCode();
this.message = message;
}
public BusinessException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.message = message;
}
public BusinessException(ResultCode resultCode, Throwable cause) {
super(resultCode.getMessage(), cause);
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
}
@@ -0,0 +1,62 @@
package com.emotionmuseum.common.exception;
import com.emotionmuseum.common.result.ResultCode;
import lombok.Getter;
/**
* 验证码异常
*
* @author emotion-museum
* @since 2025-07-24
*/
@Getter
public class CaptchaException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private final Integer code;
/**
* 错误消息
*/
private final String message;
public CaptchaException(String message) {
super(message);
this.code = ResultCode.CAPTCHA_ERROR.getCode();
this.message = message;
}
public CaptchaException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public CaptchaException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
public CaptchaException(String message, Throwable cause) {
super(message, cause);
this.code = ResultCode.CAPTCHA_ERROR.getCode();
this.message = message;
}
public CaptchaException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.message = message;
}
public CaptchaException(ResultCode resultCode, Throwable cause) {
super(resultCode.getMessage(), cause);
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
}
@@ -0,0 +1,153 @@
package com.emotionmuseum.common.exception;
import com.emotionmuseum.common.result.Result;
import com.emotionmuseum.common.result.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.util.Set;
/**
* 全局异常处理器
*
* @author emotion-museum
* @since 2025-07-24
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理认证异常
*/
@ExceptionHandler(AuthException.class)
public Result<Void> handleAuthException(AuthException e, HttpServletRequest request) {
log.warn("认证异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理令牌异常
*/
@ExceptionHandler(TokenException.class)
public Result<Void> handleTokenException(TokenException e, HttpServletRequest request) {
log.warn("令牌异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理验证码异常
*/
@ExceptionHandler(CaptchaException.class)
public Result<Void> handleCaptchaException(CaptchaException e, HttpServletRequest request) {
log.warn("验证码异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.warn("业务异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
log.warn("参数校验失败: {} {}", request.getMethod(), request.getRequestURI(), e);
StringBuilder message = new StringBuilder("参数校验失败: ");
for (FieldError error : e.getBindingResult().getFieldErrors()) {
message.append(error.getField()).append(" ").append(error.getDefaultMessage()).append("; ");
}
return Result.error(ResultCode.PARAM_VALIDATION_ERROR.getCode(), message.toString());
}
/**
* 处理Bean校验异常
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleBindException(BindException e, HttpServletRequest request) {
log.warn("参数绑定失败: {} {}", request.getMethod(), request.getRequestURI(), e);
StringBuilder message = new StringBuilder("参数绑定失败: ");
for (FieldError error : e.getBindingResult().getFieldErrors()) {
message.append(error.getField()).append(" ").append(error.getDefaultMessage()).append("; ");
}
return Result.error(ResultCode.PARAM_VALIDATION_ERROR.getCode(), message.toString());
}
/**
* 处理约束校验异常
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
log.warn("约束校验失败: {} {}", request.getMethod(), request.getRequestURI(), e);
StringBuilder message = new StringBuilder("约束校验失败: ");
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
for (ConstraintViolation<?> violation : violations) {
message.append(violation.getPropertyPath()).append(" ").append(violation.getMessage()).append("; ");
}
return Result.error(ResultCode.PARAM_VALIDATION_ERROR.getCode(), message.toString());
}
/**
* 处理非法参数异常
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
log.warn("非法参数: {} {}", request.getMethod(), request.getRequestURI(), e);
return Result.error(ResultCode.BAD_REQUEST.getCode(), "参数错误: " + e.getMessage());
}
/**
* 处理空指针异常
*/
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
log.error("空指针异常: {} {}", request.getMethod(), request.getRequestURI(), e);
return Result.error(ResultCode.INTERNAL_SERVER_ERROR.getCode(), "系统内部错误");
}
/**
* 处理运行时异常
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.error("运行时异常: {} {}", request.getMethod(), request.getRequestURI(), e);
return Result.error(ResultCode.INTERNAL_SERVER_ERROR.getCode(), "系统运行异常: " + e.getMessage());
}
/**
* 处理所有其他异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleException(Exception e, HttpServletRequest request) {
log.error("未知异常: {} {}", request.getMethod(), request.getRequestURI(), e);
return Result.error(ResultCode.INTERNAL_SERVER_ERROR.getCode(), "系统异常,请联系管理员");
}
}
@@ -0,0 +1,62 @@
package com.emotionmuseum.common.exception;
import com.emotionmuseum.common.result.ResultCode;
import lombok.Getter;
/**
* Token异常
*
* @author emotion-museum
* @since 2025-07-24
*/
@Getter
public class TokenException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private final Integer code;
/**
* 错误消息
*/
private final String message;
public TokenException(String message) {
super(message);
this.code = ResultCode.TOKEN_INVALID.getCode();
this.message = message;
}
public TokenException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public TokenException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
public TokenException(String message, Throwable cause) {
super(message, cause);
this.code = ResultCode.TOKEN_INVALID.getCode();
this.message = message;
}
public TokenException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.message = message;
}
public TokenException(ResultCode resultCode, Throwable cause) {
super(resultCode.getMessage(), cause);
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
}
@@ -0,0 +1,55 @@
package com.emotionmuseum.common.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
/**
* 基础分页请求类
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "基础分页请求")
public abstract class BasePageRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
/**
* 页码,从1开始
*/
@Schema(description = "页码,从1开始", example = "1", minimum = "1")
@Min(value = 1, message = "页码必须大于0")
private Integer pageNum = 1;
/**
* 每页大小
*/
@Schema(description = "每页大小", example = "20", minimum = "1", maximum = "100")
@Min(value = 1, message = "每页大小必须大于0")
@Max(value = 100, message = "每页大小不能超过100")
private Integer pageSize = 20;
/**
* 排序字段
*/
@Schema(description = "排序字段", example = "create_time")
private String sortField;
/**
* 排序方向
*/
@Schema(description = "排序方向", example = "DESC", allowableValues = {"ASC", "DESC"})
private String sortOrder = "DESC";
/**
* 搜索关键词
*/
@Schema(description = "搜索关键词", example = "关键词")
private String keyword;
}
@@ -0,0 +1,60 @@
package com.emotionmuseum.common.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* 基础请求类
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@Schema(description = "基础请求")
public abstract class BaseRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 请求ID,用于链路追踪
*/
@Schema(description = "请求ID", example = "req_123456789")
private String requestId;
/**
* 客户端IP地址
*/
@Schema(description = "客户端IP地址", example = "192.168.1.100")
private String clientIp;
/**
* 用户代理信息
*/
@Schema(description = "用户代理信息", example = "Mozilla/5.0...")
private String userAgent;
/**
* 请求时间戳
*/
@Schema(description = "请求时间戳", example = "1721808000000")
private Long timestamp;
/**
* 设备类型
*/
@Schema(description = "设备类型", example = "WEB", allowableValues = {"WEB", "MOBILE", "APP"})
private String deviceType;
/**
* 应用版本
*/
@Schema(description = "应用版本", example = "1.0.0")
private String appVersion;
public BaseRequest() {
this.timestamp = System.currentTimeMillis();
}
}
@@ -0,0 +1,101 @@
package com.emotionmuseum.common.response;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 基础分页响应类
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "基础分页响应")
public class BasePageResponse<T> extends BaseResponse {
private static final long serialVersionUID = 1L;
/**
* 当前页码
*/
@Schema(description = "当前页码", example = "1")
private Long current;
/**
* 每页大小
*/
@Schema(description = "每页大小", example = "20")
private Long size;
/**
* 总记录数
*/
@Schema(description = "总记录数", example = "100")
private Long total;
/**
* 总页数
*/
@Schema(description = "总页数", example = "5")
private Long pages;
/**
* 数据列表
*/
@Schema(description = "数据列表")
private List<T> records;
/**
* 是否有下一页
*/
@Schema(description = "是否有下一页", example = "true")
private Boolean hasNext;
/**
* 是否有上一页
*/
@Schema(description = "是否有上一页", example = "false")
private Boolean hasPrevious;
public BasePageResponse() {
super();
}
public BasePageResponse(IPage<T> page) {
super();
this.current = page.getCurrent();
this.size = page.getSize();
this.total = page.getTotal();
this.pages = page.getPages();
this.records = page.getRecords();
this.hasNext = page.hasNext();
this.hasPrevious = page.hasPrevious();
}
/**
* 静态工厂方法
*/
public static <T> BasePageResponse<T> of(IPage<T> page) {
return new BasePageResponse<>(page);
}
/**
* 静态工厂方法
*/
public static <T> BasePageResponse<T> of(List<T> records, Long current, Long size, Long total) {
BasePageResponse<T> response = new BasePageResponse<>();
response.setRecords(records);
response.setCurrent(current);
response.setSize(size);
response.setTotal(total);
response.setPages((total + size - 1) / size);
response.setHasNext(current < response.getPages());
response.setHasPrevious(current > 1);
return response;
}
}
@@ -0,0 +1,73 @@
package com.emotionmuseum.common.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 基础响应类
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "基础响应")
public abstract class BaseResponse implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 响应时间戳
*/
@Schema(description = "响应时间戳", example = "1721808000000")
private Long timestamp;
/**
* 请求ID,用于链路追踪
*/
@Schema(description = "请求ID", example = "req_123456789")
private String requestId;
/**
* 服务器处理时间(毫秒)
*/
@Schema(description = "服务器处理时间(毫秒)", example = "150")
private Long processingTime;
/**
* 服务节点标识
*/
@Schema(description = "服务节点标识", example = "node-001")
private String serverNode;
public BaseResponse() {
this.timestamp = System.currentTimeMillis();
}
/**
* 设置请求ID
*/
public BaseResponse requestId(String requestId) {
this.requestId = requestId;
return this;
}
/**
* 设置处理时间
*/
public BaseResponse processingTime(Long processingTime) {
this.processingTime = processingTime;
return this;
}
/**
* 设置服务节点
*/
public BaseResponse serverNode(String serverNode) {
this.serverNode = serverNode;
return this;
}
}
@@ -127,4 +127,60 @@ public class Result<T> implements Serializable {
this.requestId = requestId;
return this;
}
/**
* 未授权响应
*/
public static <T> Result<T> unauthorized() {
return new Result<>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage());
}
/**
* 未授权响应(自定义消息)
*/
public static <T> Result<T> unauthorized(String message) {
return new Result<>(ResultCode.UNAUTHORIZED.getCode(), message);
}
/**
* 禁止访问响应
*/
public static <T> Result<T> forbidden() {
return new Result<>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage());
}
/**
* 禁止访问响应(自定义消息)
*/
public static <T> Result<T> forbidden(String message) {
return new Result<>(ResultCode.FORBIDDEN.getCode(), message);
}
/**
* 请求参数错误响应
*/
public static <T> Result<T> badRequest() {
return new Result<>(ResultCode.BAD_REQUEST.getCode(), ResultCode.BAD_REQUEST.getMessage());
}
/**
* 请求参数错误响应(自定义消息)
*/
public static <T> Result<T> badRequest(String message) {
return new Result<>(ResultCode.BAD_REQUEST.getCode(), message);
}
/**
* 资源不存在响应
*/
public static <T> Result<T> notFound() {
return new Result<>(ResultCode.NOT_FOUND.getCode(), ResultCode.NOT_FOUND.getMessage());
}
/**
* 资源不存在响应(自定义消息)
*/
public static <T> Result<T> notFound(String message) {
return new Result<>(ResultCode.NOT_FOUND.getCode(), message);
}
}
@@ -0,0 +1,63 @@
package com.emotionmuseum.record.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import java.time.LocalDate;
import java.util.List;
/**
* 创建情绪记录请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "创建情绪记录请求")
public class CreateEmotionRecordRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID", example = "user_123")
@NotBlank(message = "用户ID不能为空")
private String userId;
@Schema(description = "记录日期", example = "2025-07-24")
@NotNull(message = "记录日期不能为空")
private LocalDate recordDate;
@Schema(description = "情绪类型", example = "joy")
@NotBlank(message = "情绪类型不能为空")
private String emotionType;
@Schema(description = "情绪强度", example = "0.8")
@NotNull(message = "情绪强度不能为空")
@DecimalMin(value = "0.0", message = "情绪强度不能小于0")
@DecimalMax(value = "1.0", message = "情绪强度不能大于1")
private Double intensity;
@Schema(description = "触发因素", example = "完成了重要项目")
private String triggers;
@Schema(description = "详细描述", example = "今天心情很好,完成了一个重要的项目")
private String description;
@Schema(description = "标签列表", example = "[\"工作\", \"成就感\"]")
private List<String> tags;
@Schema(description = "天气情况", example = "晴天")
private String weather;
@Schema(description = "所在位置", example = "办公室")
private String location;
@Schema(description = "当时活动", example = "工作")
private String activity;
}
@@ -0,0 +1,67 @@
package com.emotionmuseum.record.response;
import com.emotionmuseum.common.response.BaseResponse;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 情绪记录响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "情绪记录响应")
public class EmotionRecordResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "记录ID")
private String id;
@Schema(description = "用户ID")
private String userId;
@Schema(description = "记录日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate recordDate;
@Schema(description = "情绪类型")
private String emotionType;
@Schema(description = "情绪强度")
private Double intensity;
@Schema(description = "触发因素")
private String triggers;
@Schema(description = "详细描述")
private String description;
@Schema(description = "标签列表")
private List<String> tags;
@Schema(description = "天气情况")
private String weather;
@Schema(description = "所在位置")
private String location;
@Schema(description = "当时活动")
private String activity;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}
@@ -1,9 +1,9 @@
package com.emotionmuseum.user.controller;
import com.emotionmuseum.common.result.Result;
import com.emotionmuseum.user.dto.UserUpdateRequest;
import com.emotionmuseum.user.request.UserUpdateRequest;
import com.emotionmuseum.user.service.UserService;
import com.emotionmuseum.user.vo.UserInfoResponse;
import com.emotionmuseum.user.response.UserInfoResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -0,0 +1,55 @@
package com.emotionmuseum.user.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
/**
* 用户信息更新请求
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "用户信息更新请求")
public class UserUpdateRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
@Schema(description = "用户名", example = "新用户名")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20位之间")
private String username;
@Schema(description = "邮箱", example = "new@example.com")
@Email(message = "邮箱格式不正确")
private String email;
@Schema(description = "手机号", example = "13900139000")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Schema(description = "头像URL", example = "https://example.com/avatar.jpg")
private String avatar;
@Schema(description = "昵称", example = "新昵称")
@Size(min = 1, max = 20, message = "昵称长度必须在1-20位之间")
private String nickname;
@Schema(description = "生日", example = "1990-01-01")
private LocalDate birthDate;
@Schema(description = "所在地", example = "上海市")
@Size(max = 50, message = "所在地长度不能超过50位")
private String location;
@Schema(description = "个人简介", example = "更新后的个人简介")
@Size(max = 200, message = "个人简介长度不能超过200位")
private String bio;
}
@@ -0,0 +1,102 @@
package com.emotionmuseum.user.response;
import com.emotionmuseum.common.response.BaseResponse;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 用户信息响应
*
* @author emotion-museum
* @since 2025-07-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "用户信息响应")
public class UserInfoResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private String id;
@Schema(description = "账号")
private String account;
@Schema(description = "用户名")
private String username;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机号")
private String phone;
@Schema(description = "头像URL")
private String avatar;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "生日")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate birthDate;
@Schema(description = "所在地")
private String location;
@Schema(description = "个人简介")
private String bio;
@Schema(description = "会员等级")
private String memberLevel;
@Schema(description = "使用天数")
private Integer totalDays;
@Schema(description = "成长数据")
private GrowthStatsVO growthStats;
@Schema(description = "状态")
private Integer status;
@Schema(description = "是否已验证")
private Integer isVerified;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "最后活跃时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime lastActiveTime;
/**
* 成长数据VO
*/
@Data
@Schema(description = "成长数据")
public static class GrowthStatsVO {
@Schema(description = "自我感知")
private BigDecimal selfAwareness;
@Schema(description = "情绪韧性")
private BigDecimal emotionalResilience;
@Schema(description = "行动力")
private BigDecimal actionPower;
@Schema(description = "共情力")
private BigDecimal empathy;
@Schema(description = "生活热度")
private BigDecimal lifeEnthusiasm;
}
}