first
demo
This commit is contained in:
13
src/main/java/com/lxy/hsend/HsEndApplication.java
Normal file
13
src/main/java/com/lxy/hsend/HsEndApplication.java
Normal file
@ -0,0 +1,13 @@
|
||||
package com.lxy.hsend;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class HsEndApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HsEndApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
71
src/main/java/com/lxy/hsend/annotation/RateLimit.java
Normal file
71
src/main/java/com/lxy/hsend/annotation/RateLimit.java
Normal file
@ -0,0 +1,71 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 限流注解
|
||||
* 用于对接口进行访问频率限制
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RateLimit {
|
||||
|
||||
/**
|
||||
* 限流key的前缀,默认为方法的全限定名
|
||||
*/
|
||||
String key() default "";
|
||||
|
||||
/**
|
||||
* 时间窗口内允许的最大请求次数
|
||||
*/
|
||||
int count() default 100;
|
||||
|
||||
/**
|
||||
* 时间窗口大小
|
||||
*/
|
||||
int period() default 60;
|
||||
|
||||
/**
|
||||
* 时间单位
|
||||
*/
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* 限流类型
|
||||
*/
|
||||
LimitType limitType() default LimitType.IP;
|
||||
|
||||
/**
|
||||
* 超出限制时的提示信息
|
||||
*/
|
||||
String message() default "访问频率过高,请稍后再试";
|
||||
|
||||
/**
|
||||
* 限流类型枚举
|
||||
*/
|
||||
enum LimitType {
|
||||
/**
|
||||
* 根据IP地址限流
|
||||
*/
|
||||
IP,
|
||||
|
||||
/**
|
||||
* 根据用户ID限流
|
||||
*/
|
||||
USER,
|
||||
|
||||
/**
|
||||
* 根据自定义key限流
|
||||
*/
|
||||
CUSTOM,
|
||||
|
||||
/**
|
||||
* 全局限流
|
||||
*/
|
||||
GLOBAL
|
||||
}
|
||||
}
|
||||
90
src/main/java/com/lxy/hsend/annotation/RequestLog.java
Normal file
90
src/main/java/com/lxy/hsend/annotation/RequestLog.java
Normal file
@ -0,0 +1,90 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 请求日志注解
|
||||
* 用于标记需要记录请求日志的接口
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RequestLog {
|
||||
|
||||
/**
|
||||
* 操作描述
|
||||
*/
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* 是否记录请求参数
|
||||
*/
|
||||
boolean logParams() default true;
|
||||
|
||||
/**
|
||||
* 是否记录响应结果
|
||||
*/
|
||||
boolean logResult() default true;
|
||||
|
||||
/**
|
||||
* 是否记录执行时间
|
||||
*/
|
||||
boolean logTime() default true;
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
*/
|
||||
OperationType type() default OperationType.OTHER;
|
||||
|
||||
/**
|
||||
* 操作类型枚举
|
||||
*/
|
||||
enum OperationType {
|
||||
/**
|
||||
* 查询操作
|
||||
*/
|
||||
QUERY,
|
||||
|
||||
/**
|
||||
* 新增操作
|
||||
*/
|
||||
CREATE,
|
||||
|
||||
/**
|
||||
* 更新操作
|
||||
*/
|
||||
UPDATE,
|
||||
|
||||
/**
|
||||
* 删除操作
|
||||
*/
|
||||
DELETE,
|
||||
|
||||
/**
|
||||
* 登录操作
|
||||
*/
|
||||
LOGIN,
|
||||
|
||||
/**
|
||||
* 登出操作
|
||||
*/
|
||||
LOGOUT,
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
*/
|
||||
UPLOAD,
|
||||
|
||||
/**
|
||||
* 文件下载
|
||||
*/
|
||||
DOWNLOAD,
|
||||
|
||||
/**
|
||||
* 其他操作
|
||||
*/
|
||||
OTHER
|
||||
}
|
||||
}
|
||||
25
src/main/java/com/lxy/hsend/annotation/RequireAuth.java
Normal file
25
src/main/java/com/lxy/hsend/annotation/RequireAuth.java
Normal file
@ -0,0 +1,25 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 需要认证的注解
|
||||
* 用于标记需要用户登录才能访问的接口
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RequireAuth {
|
||||
|
||||
/**
|
||||
* 是否可选认证(如果有token则验证,没有token则不验证)
|
||||
*/
|
||||
boolean optional() default false;
|
||||
|
||||
/**
|
||||
* 描述信息
|
||||
*/
|
||||
String value() default "";
|
||||
}
|
||||
25
src/main/java/com/lxy/hsend/annotation/ValidEmail.java
Normal file
25
src/main/java/com/lxy/hsend/annotation/ValidEmail.java
Normal file
@ -0,0 +1,25 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import com.lxy.hsend.annotation.validator.EmailValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 邮箱格式验证注解
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = EmailValidator.class)
|
||||
@Documented
|
||||
public @interface ValidEmail {
|
||||
|
||||
String message() default "邮箱格式不正确";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
25
src/main/java/com/lxy/hsend/annotation/ValidPassword.java
Normal file
25
src/main/java/com/lxy/hsend/annotation/ValidPassword.java
Normal file
@ -0,0 +1,25 @@
|
||||
package com.lxy.hsend.annotation;
|
||||
|
||||
import com.lxy.hsend.annotation.validator.PasswordValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 密码格式验证注解
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = PasswordValidator.class)
|
||||
@Documented
|
||||
public @interface ValidPassword {
|
||||
|
||||
String message() default "密码格式不正确,密码长度应为6-20位,包含字母和数字";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.lxy.hsend.annotation.validator;
|
||||
|
||||
import com.lxy.hsend.annotation.ValidEmail;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
/**
|
||||
* 邮箱格式验证器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class EmailValidator implements ConstraintValidator<ValidEmail, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(ValidEmail constraintAnnotation) {
|
||||
// 初始化方法,可以获取注解参数
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String email, ConstraintValidatorContext context) {
|
||||
// 空值交给@NotNull等注解处理
|
||||
if (email == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return StringUtil.isValidEmail(email);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.lxy.hsend.annotation.validator;
|
||||
|
||||
import com.lxy.hsend.annotation.ValidPassword;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
/**
|
||||
* 密码格式验证器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(ValidPassword constraintAnnotation) {
|
||||
// 初始化方法,可以获取注解参数
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String password, ConstraintValidatorContext context) {
|
||||
// 空值交给@NotNull等注解处理
|
||||
if (password == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return StringUtil.isValidPassword(password);
|
||||
}
|
||||
}
|
||||
123
src/main/java/com/lxy/hsend/common/ApiResponse.java
Normal file
123
src/main/java/com/lxy/hsend/common/ApiResponse.java
Normal file
@ -0,0 +1,123 @@
|
||||
package com.lxy.hsend.common;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* 统一API响应格式
|
||||
*
|
||||
* @author lxy
|
||||
* @param <T> 响应数据类型
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ApiResponse<T> {
|
||||
|
||||
/**
|
||||
* 错误码,0表示成功
|
||||
*/
|
||||
private Integer error;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
private T body;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private Boolean success;
|
||||
|
||||
/**
|
||||
* 成功响应(无数据)
|
||||
*/
|
||||
public static <T> ApiResponse<T> success() {
|
||||
return new ApiResponse<T>(0, null, "操作成功", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(带数据)
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return new ApiResponse<T>(0, data, "操作成功", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(带数据和消息)
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(T data, String message) {
|
||||
return new ApiResponse<T>(0, data, message, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应
|
||||
*/
|
||||
public static <T> ApiResponse<T> error(Integer errorCode, String message) {
|
||||
return new ApiResponse<T>(errorCode, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应(带数据)
|
||||
*/
|
||||
public static <T> ApiResponse<T> error(Integer errorCode, String message, T data) {
|
||||
return new ApiResponse<T>(errorCode, data, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数验证错误
|
||||
*/
|
||||
public static <T> ApiResponse<T> validationError(String message) {
|
||||
return new ApiResponse<T>(1001, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证失败
|
||||
*/
|
||||
public static <T> ApiResponse<T> authError(String message) {
|
||||
return new ApiResponse<T>(1002, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限不足
|
||||
*/
|
||||
public static <T> ApiResponse<T> forbiddenError(String message) {
|
||||
return new ApiResponse<T>(1003, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源不存在
|
||||
*/
|
||||
public static <T> ApiResponse<T> notFoundError(String message) {
|
||||
return new ApiResponse<T>(1004, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务逻辑错误
|
||||
*/
|
||||
public static <T> ApiResponse<T> businessError(String message) {
|
||||
return new ApiResponse<T>(2001, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统错误
|
||||
*/
|
||||
public static <T> ApiResponse<T> systemError(String message) {
|
||||
return new ApiResponse<T>(5001, null, message, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未知错误
|
||||
*/
|
||||
public static <T> ApiResponse<T> unknownError() {
|
||||
return new ApiResponse<T>(5002, null, "系统出现未知错误", false);
|
||||
}
|
||||
}
|
||||
86
src/main/java/com/lxy/hsend/common/Constants.java
Normal file
86
src/main/java/com/lxy/hsend/common/Constants.java
Normal file
@ -0,0 +1,86 @@
|
||||
package com.lxy.hsend.common;
|
||||
|
||||
/**
|
||||
* 应用常量
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class Constants {
|
||||
|
||||
// JWT相关常量
|
||||
public static final String JWT_SECRET_KEY = "hs-end-jwt-secret-key-2024-secure-256bit-hmac-sha256";
|
||||
public static final String JWT_TOKEN_PREFIX = "Bearer ";
|
||||
public static final String JWT_HEADER_NAME = "Authorization";
|
||||
public static final long JWT_EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000L; // 7天
|
||||
public static final long JWT_REFRESH_TIME = 30 * 24 * 60 * 60 * 1000L; // 30天
|
||||
|
||||
// Redis相关常量
|
||||
public static final String REDIS_USER_SESSION_PREFIX = "user:session:";
|
||||
public static final String REDIS_JWT_BLACKLIST_PREFIX = "jwt:blacklist:";
|
||||
public static final String REDIS_USER_INFO_PREFIX = "user:info:";
|
||||
public static final String REDIS_RATE_LIMIT_PREFIX = "rate:limit:";
|
||||
public static final long REDIS_SESSION_EXPIRE_TIME = 7 * 24 * 60 * 60; // 7天(秒)
|
||||
public static final long REDIS_USER_INFO_EXPIRE_TIME = 30 * 60; // 30分钟(秒)
|
||||
|
||||
// 分页相关常量
|
||||
public static final int DEFAULT_PAGE_SIZE = 10;
|
||||
public static final int MAX_PAGE_SIZE = 100;
|
||||
public static final int DEFAULT_PAGE_NUM = 1;
|
||||
|
||||
// 文件上传相关常量
|
||||
public static final long MAX_FILE_SIZE = 10 * 1024 * 1024L; // 10MB
|
||||
public static final String[] ALLOWED_FILE_TYPES = {
|
||||
"image/jpeg", "image/png", "image/gif", "image/webp",
|
||||
"application/pdf", "text/plain", "text/markdown",
|
||||
"application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
};
|
||||
public static final String FILE_UPLOAD_PATH = "uploads/";
|
||||
|
||||
// 用户相关常量
|
||||
public static final int PASSWORD_MIN_LENGTH = 6;
|
||||
public static final int PASSWORD_MAX_LENGTH = 20;
|
||||
public static final int NICKNAME_MIN_LENGTH = 1;
|
||||
public static final int NICKNAME_MAX_LENGTH = 50;
|
||||
|
||||
// 会话相关常量
|
||||
public static final int SESSION_TITLE_MAX_LENGTH = 100;
|
||||
public static final int MESSAGE_CONTENT_MAX_LENGTH = 10000;
|
||||
|
||||
// API相关常量
|
||||
public static final String API_PREFIX = "/api";
|
||||
public static final String API_VERSION = "v1";
|
||||
|
||||
// 时间格式常量
|
||||
public static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||
public static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
public static final String TIMEZONE = "Asia/Shanghai";
|
||||
|
||||
// 请求限流常量
|
||||
public static final int RATE_LIMIT_REQUESTS = 100; // 每分钟最大请求数
|
||||
public static final int RATE_LIMIT_WINDOW = 60; // 时间窗口(秒)
|
||||
|
||||
// 邮箱验证正则
|
||||
public static final String EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$";
|
||||
|
||||
// 密码验证正则(至少包含字母和数字)
|
||||
public static final String PASSWORD_REGEX = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{6,20}$";
|
||||
|
||||
// 用户状态
|
||||
public static final byte USER_STATUS_NORMAL = 1;
|
||||
public static final byte USER_STATUS_LOCKED = 2;
|
||||
public static final byte USER_STATUS_DELETED = 3;
|
||||
|
||||
// 消息角色
|
||||
public static final String MESSAGE_ROLE_USER = "user";
|
||||
public static final String MESSAGE_ROLE_ASSISTANT = "assistant";
|
||||
public static final String MESSAGE_ROLE_SYSTEM = "system";
|
||||
|
||||
// AI模型
|
||||
public static final String AI_MODEL_GPT35 = "gpt-3.5-turbo";
|
||||
public static final String AI_MODEL_GPT4 = "gpt-4";
|
||||
public static final String AI_MODEL_CLAUDE = "claude-3";
|
||||
|
||||
private Constants() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
74
src/main/java/com/lxy/hsend/common/ErrorCode.java
Normal file
74
src/main/java/com/lxy/hsend/common/ErrorCode.java
Normal file
@ -0,0 +1,74 @@
|
||||
package com.lxy.hsend.common;
|
||||
|
||||
/**
|
||||
* 错误码枚举
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public enum ErrorCode {
|
||||
|
||||
// 成功
|
||||
SUCCESS(0, "操作成功"),
|
||||
|
||||
// 客户端错误 1xxx
|
||||
VALIDATION_ERROR(1001, "参数验证失败"),
|
||||
AUTH_ERROR(1002, "认证失败"),
|
||||
FORBIDDEN_ERROR(1003, "权限不足"),
|
||||
NOT_FOUND_ERROR(1004, "资源不存在"),
|
||||
METHOD_NOT_ALLOWED(1005, "请求方法不支持"),
|
||||
RATE_LIMIT_ERROR(1006, "请求频率超限"),
|
||||
|
||||
// 业务逻辑错误 2xxx
|
||||
BUSINESS_ERROR(2001, "业务逻辑错误"),
|
||||
USER_NOT_FOUND(2002, "用户不存在"),
|
||||
USER_ALREADY_EXISTS(2003, "用户已存在"),
|
||||
RESOURCE_NOT_FOUND(2005, "资源不存在"),
|
||||
PASSWORD_ERROR(2004, "密码错误"),
|
||||
SESSION_NOT_FOUND(2005, "会话不存在"),
|
||||
MESSAGE_NOT_FOUND(2006, "消息不存在"),
|
||||
FILE_NOT_FOUND(2007, "文件不存在"),
|
||||
FILE_UPLOAD_ERROR(2008, "文件上传失败"),
|
||||
AI_SERVICE_ERROR(2009, "AI服务调用失败"),
|
||||
|
||||
// 第三方服务错误 3xxx
|
||||
EXTERNAL_SERVICE_ERROR(3001, "第三方服务异常"),
|
||||
AI_API_ERROR(3002, "AI接口调用失败"),
|
||||
REDIS_ERROR(3003, "Redis服务异常"),
|
||||
|
||||
// 数据访问错误 4xxx
|
||||
DATABASE_ERROR(4001, "数据库操作失败"),
|
||||
DATA_INTEGRITY_ERROR(4002, "数据完整性约束违反"),
|
||||
TRANSACTION_ERROR(4003, "事务处理失败"),
|
||||
|
||||
// 系统错误 5xxx
|
||||
SYSTEM_ERROR(5001, "系统内部错误"),
|
||||
UNKNOWN_ERROR(5002, "未知错误");
|
||||
|
||||
private final Integer code;
|
||||
private final String message;
|
||||
|
||||
ErrorCode(Integer code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据错误码获取枚举
|
||||
*/
|
||||
public static ErrorCode getByCode(Integer code) {
|
||||
for (ErrorCode errorCode : values()) {
|
||||
if (errorCode.getCode().equals(code)) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
return UNKNOWN_ERROR;
|
||||
}
|
||||
}
|
||||
159
src/main/java/com/lxy/hsend/config/JwtAuthenticationFilter.java
Normal file
159
src/main/java/com/lxy/hsend/config/JwtAuthenticationFilter.java
Normal file
@ -0,0 +1,159 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JWT认证拦截器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// 不需要认证的路径
|
||||
private static final List<String> EXCLUDE_PATHS = Arrays.asList(
|
||||
"/api/auth/",
|
||||
"/api/health/",
|
||||
"/swagger-ui/",
|
||||
"/v3/api-docs/",
|
||||
"/actuator/",
|
||||
"/static/",
|
||||
"/uploads/"
|
||||
);
|
||||
|
||||
// 可选认证的路径(如果有token则验证,没有token也放行)
|
||||
private static final List<String> OPTIONAL_AUTH_PATHS = Arrays.asList(
|
||||
"/api/user/\\d+" // 匹配 /api/user/{userId} 格式
|
||||
);
|
||||
|
||||
@Autowired
|
||||
public JwtAuthenticationFilter(JwtUtil jwtUtil, ObjectMapper objectMapper) {
|
||||
this.jwtUtil = jwtUtil;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
String method = request.getMethod();
|
||||
|
||||
log.debug("JWT过滤器处理请求: {} {}", method, requestURI);
|
||||
|
||||
// 跳过OPTIONS请求
|
||||
if ("OPTIONS".equals(method)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否在排除列表中
|
||||
if (isExcludePath(requestURI)) {
|
||||
log.debug("路径在排除列表中,跳过认证: {}", requestURI);
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否为可选认证路径
|
||||
boolean isOptionalAuth = isOptionalAuthPath(requestURI);
|
||||
|
||||
// 获取Token
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
|
||||
// 如果没有Token
|
||||
if (StringUtil.isEmpty(token)) {
|
||||
if (isOptionalAuth) {
|
||||
log.debug("可选认证路径,没有token,继续执行: {}", requestURI);
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
} else {
|
||||
log.warn("缺少认证Token: {}", requestURI);
|
||||
writeErrorResponse(response, ErrorCode.AUTH_ERROR, "请提供有效的认证Token");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证Token
|
||||
if (!jwtUtil.validateToken(token)) {
|
||||
log.warn("Token验证失败: {}", requestURI);
|
||||
writeErrorResponse(response, ErrorCode.AUTH_ERROR, "Token无效或已过期,请重新登录");
|
||||
return;
|
||||
}
|
||||
|
||||
// Token验证通过,设置用户信息到请求属性
|
||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||
String email = jwtUtil.getEmailFromToken(token);
|
||||
|
||||
if (userId != null && email != null) {
|
||||
request.setAttribute("currentUserId", userId);
|
||||
request.setAttribute("currentUserEmail", email);
|
||||
log.debug("JWT认证成功: userId={}, email={}, uri={}", userId, email, requestURI);
|
||||
} else {
|
||||
log.warn("Token中用户信息不完整: {}", requestURI);
|
||||
writeErrorResponse(response, ErrorCode.AUTH_ERROR, "Token信息不完整");
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("JWT认证异常: {} - {}", requestURI, e.getMessage(), e);
|
||||
writeErrorResponse(response, ErrorCode.SYSTEM_ERROR, "认证系统异常");
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否在排除列表中
|
||||
*/
|
||||
private boolean isExcludePath(String requestURI) {
|
||||
return EXCLUDE_PATHS.stream().anyMatch(requestURI::startsWith);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为可选认证路径
|
||||
*/
|
||||
private boolean isOptionalAuthPath(String requestURI) {
|
||||
return OPTIONAL_AUTH_PATHS.stream()
|
||||
.anyMatch(pattern -> requestURI.matches("/api/user/\\d+"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入错误响应
|
||||
*/
|
||||
private void writeErrorResponse(HttpServletResponse response, ErrorCode errorCode, String message) throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
response.setHeader("Access-Control-Allow-Headers", "*");
|
||||
|
||||
ApiResponse<Object> errorResponse = ApiResponse.error(errorCode.getCode(), message);
|
||||
String json = objectMapper.writeValueAsString(errorResponse);
|
||||
|
||||
response.getWriter().write(json);
|
||||
response.getWriter().flush();
|
||||
}
|
||||
}
|
||||
88
src/main/java/com/lxy/hsend/config/RedisConfig.java
Normal file
88
src/main/java/com/lxy/hsend/config/RedisConfig.java
Normal file
@ -0,0 +1,88 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Redis配置类
|
||||
* 配置Redis连接和序列化方式
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 创建ObjectMapper实例并配置
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
|
||||
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
|
||||
jackson2JsonRedisSerializer.setObjectMapper(mapper);
|
||||
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
|
||||
// key采用String的序列化方式
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
// hash的key也采用String的序列化方式
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
// value序列化方式采用jackson
|
||||
template.setValueSerializer(jackson2JsonRedisSerializer);
|
||||
// hash的value序列化方式采用jackson
|
||||
template.setHashValueSerializer(jackson2JsonRedisSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存管理器配置
|
||||
*/
|
||||
@Bean
|
||||
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
|
||||
// 创建ObjectMapper实例并配置
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
|
||||
// JSON序列化配置
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
|
||||
jackson2JsonRedisSerializer.setObjectMapper(mapper);
|
||||
|
||||
// 配置序列化
|
||||
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
||||
.entryTtl(Duration.ofSeconds(Constants.REDIS_USER_INFO_EXPIRE_TIME)) // 默认缓存时间30分钟
|
||||
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
|
||||
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
|
||||
.disableCachingNullValues(); // 不缓存null值
|
||||
|
||||
return RedisCacheManager.builder(connectionFactory)
|
||||
.cacheDefaults(config)
|
||||
.transactionAware()
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
70
src/main/java/com/lxy/hsend/config/SecurityConfig.java
Normal file
70
src/main/java/com/lxy/hsend/config/SecurityConfig.java
Normal file
@ -0,0 +1,70 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* Spring Security配置类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Autowired
|
||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
|
||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用CSRF保护(使用JWT,不需要CSRF保护)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 禁用默认的Session管理,使用无状态JWT
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
|
||||
// 禁用默认的登录表单
|
||||
.formLogin(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 禁用HTTP Basic认证
|
||||
.httpBasic(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 配置请求授权
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
// 公开接口,无需认证
|
||||
.requestMatchers("/api/health/**").permitAll()
|
||||
.requestMatchers("/api/auth/**").permitAll()
|
||||
.requestMatchers("/api/user/{userId}").permitAll() // 公开的用户信息接口
|
||||
|
||||
// Swagger相关接口
|
||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
|
||||
|
||||
// Actuator监控接口
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
|
||||
// 静态资源
|
||||
.requestMatchers("/static/**", "/uploads/**").permitAll()
|
||||
|
||||
// 其他所有请求需要认证
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
// 添加JWT认证拦截器(在Spring Security的UsernamePasswordAuthenticationFilter之前执行)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
42
src/main/java/com/lxy/hsend/config/SwaggerConfig.java
Normal file
42
src/main/java/com/lxy/hsend/config/SwaggerConfig.java
Normal file
@ -0,0 +1,42 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger配置类
|
||||
* 配置API文档生成
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("海角AI - API接口文档")
|
||||
.version("1.0.0")
|
||||
.description("海角AI后端服务API接口文档,提供用户认证、聊天会话、消息处理、收藏管理、文件上传等功能。")
|
||||
.contact(new Contact()
|
||||
.name("海角AI团队")
|
||||
.email("contact@haijiao.ai"))
|
||||
.license(new License()
|
||||
.name("MIT License")
|
||||
.url("https://opensource.org/licenses/MIT")))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("bearerAuth",
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.description("JWT认证令牌,格式:Bearer <token>")))
|
||||
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
|
||||
}
|
||||
}
|
||||
36
src/main/java/com/lxy/hsend/config/WebConfig.java
Normal file
36
src/main/java/com/lxy/hsend/config/WebConfig.java
Normal file
@ -0,0 +1,36 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web配置类
|
||||
* 配置跨域访问等Web相关设置
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Value("${cors.allowed-origins}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Value("${cors.allowed-methods}")
|
||||
private String allowedMethods;
|
||||
|
||||
@Value("${cors.allowed-headers}")
|
||||
private String allowedHeaders;
|
||||
|
||||
@Value("${cors.allow-credentials}")
|
||||
private boolean allowCredentials;
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOriginPatterns(allowedOrigins.split(","))
|
||||
.allowedMethods(allowedMethods.split(","))
|
||||
.allowedHeaders(allowedHeaders)
|
||||
.allowCredentials(allowCredentials)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
95
src/main/java/com/lxy/hsend/config/WebMvcConfig.java
Normal file
95
src/main/java/com/lxy/hsend/config/WebMvcConfig.java
Normal file
@ -0,0 +1,95 @@
|
||||
package com.lxy.hsend.config;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.*;
|
||||
|
||||
/**
|
||||
* Web MVC配置
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 配置CORS跨域
|
||||
*/
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置静态资源处理
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 文件上传资源
|
||||
registry.addResourceHandler("/uploads/**")
|
||||
.addResourceLocations("file:" + Constants.FILE_UPLOAD_PATH);
|
||||
|
||||
// Swagger UI资源
|
||||
registry.addResourceHandler("/swagger-ui/**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/webjars/springdoc-openapi-ui/");
|
||||
|
||||
// 静态资源
|
||||
registry.addResourceHandler("/static/**")
|
||||
.addResourceLocations("classpath:/static/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置路径匹配
|
||||
*/
|
||||
@Override
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
// 设置路径匹配策略
|
||||
configurer.setUseTrailingSlashMatch(true);
|
||||
// 设置后缀模式匹配
|
||||
configurer.setUseSuffixPatternMatch(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置内容协商
|
||||
*/
|
||||
@Override
|
||||
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
|
||||
configurer
|
||||
.defaultContentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.favorParameter(false)
|
||||
.ignoreAcceptHeader(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加拦截器
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 这里可以添加自定义拦截器
|
||||
// 例如:认证拦截器、日志拦截器等
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置视图解析器
|
||||
*/
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
// 配置JSON视图解析器
|
||||
registry.enableContentNegotiation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置消息转换器
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageConverters(java.util.List<org.springframework.http.converter.HttpMessageConverter<?>> converters) {
|
||||
// Spring Boot会自动配置Jackson消息转换器
|
||||
WebMvcConfigurer.super.configureMessageConverters(converters);
|
||||
}
|
||||
}
|
||||
130
src/main/java/com/lxy/hsend/controller/AuthController.java
Normal file
130
src/main/java/com/lxy/hsend/controller/AuthController.java
Normal file
@ -0,0 +1,130 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.auth.LoginRequest;
|
||||
import com.lxy.hsend.dto.auth.LoginResponse;
|
||||
import com.lxy.hsend.dto.auth.RegisterRequest;
|
||||
import com.lxy.hsend.dto.user.UserInfoResponse;
|
||||
import com.lxy.hsend.service.UserService;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@Tag(name = "认证管理", description = "用户认证相关接口")
|
||||
public class AuthController {
|
||||
|
||||
private final UserService userService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
public AuthController(UserService userService, JwtUtil jwtUtil) {
|
||||
this.userService = userService;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "用户注册", description = "通过邮箱和密码注册新用户")
|
||||
@RequestLog(value = "用户注册", type = RequestLog.OperationType.CREATE)
|
||||
public ApiResponse<UserInfoResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||
UserInfoResponse userInfo = userService.register(request);
|
||||
return ApiResponse.success(userInfo, "注册成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "用户登录", description = "通过邮箱和密码登录")
|
||||
@RequestLog(value = "用户登录", type = RequestLog.OperationType.LOGIN)
|
||||
public ApiResponse<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
LoginResponse loginResponse = userService.login(request);
|
||||
return ApiResponse.success(loginResponse, "登录成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@RequireAuth
|
||||
@Operation(summary = "用户登出", description = "用户登出,将token加入黑名单")
|
||||
@RequestLog(value = "用户登出", type = RequestLog.OperationType.LOGOUT)
|
||||
public ApiResponse<Void> logout(HttpServletRequest request) {
|
||||
// 从请求头获取token
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
|
||||
if (token != null) {
|
||||
// 从token获取用户ID
|
||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||
if (userId != null) {
|
||||
userService.logout(userId, token);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse.success(null, "登出成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
@Operation(summary = "刷新Token", description = "使用刷新Token获取新的访问Token")
|
||||
@RequestLog(value = "刷新Token", type = RequestLog.OperationType.OTHER)
|
||||
public ApiResponse<LoginResponse> refreshToken(@RequestParam String refreshToken) {
|
||||
try {
|
||||
// 验证刷新Token
|
||||
if (!jwtUtil.validateToken(refreshToken)) {
|
||||
return ApiResponse.authError("刷新Token无效");
|
||||
}
|
||||
|
||||
String tokenType = jwtUtil.getTokenTypeFromToken(refreshToken);
|
||||
if (!"refresh".equals(tokenType)) {
|
||||
return ApiResponse.authError("Token类型错误");
|
||||
}
|
||||
|
||||
// 生成新的访问Token
|
||||
String newAccessToken = jwtUtil.refreshToken(refreshToken);
|
||||
Long userId = jwtUtil.getUserIdFromToken(refreshToken);
|
||||
|
||||
// 获取用户信息
|
||||
UserInfoResponse userInfo = userService.getUserById(userId);
|
||||
|
||||
LoginResponse loginResponse = new LoginResponse(newAccessToken, refreshToken,
|
||||
com.lxy.hsend.common.Constants.JWT_EXPIRATION_TIME / 1000, userInfo);
|
||||
|
||||
return ApiResponse.success(loginResponse, "Token刷新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("Token刷新失败: {}", e.getMessage());
|
||||
return ApiResponse.authError("Token刷新失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*/
|
||||
@GetMapping("/check-email")
|
||||
@Operation(summary = "检查邮箱", description = "检查邮箱是否已被注册")
|
||||
public ApiResponse<Boolean> checkEmail(@RequestParam String email) {
|
||||
boolean exists = userService.existsByEmail(email);
|
||||
return ApiResponse.success(exists, exists ? "邮箱已存在" : "邮箱可用");
|
||||
}
|
||||
}
|
||||
128
src/main/java/com/lxy/hsend/controller/FavoriteController.java
Normal file
128
src/main/java/com/lxy/hsend/controller/FavoriteController.java
Normal file
@ -0,0 +1,128 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RateLimit;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.favorite.AddFavoriteRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteListRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteResponse;
|
||||
import com.lxy.hsend.dto.favorite.RemoveFavoriteRequest;
|
||||
import com.lxy.hsend.service.FavoriteService;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 收藏控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/favorites")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "收藏管理", description = "消息收藏相关接口")
|
||||
public class FavoriteController {
|
||||
|
||||
private final FavoriteService favoriteService;
|
||||
|
||||
/**
|
||||
* 添加收藏
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@RateLimit(count = 60, period = 60) // 每分钟最多60次收藏操作
|
||||
@Operation(summary = "添加收藏", description = "将指定消息添加到收藏列表")
|
||||
public ApiResponse<FavoriteResponse> addFavorite(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestBody @Valid AddFavoriteRequest request) {
|
||||
|
||||
log.info("用户添加收藏: user={}, messageId={}, sessionId={}",
|
||||
userDetails.getUsername(), request.getMessageId(), request.getSessionId());
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return favoriteService.addFavorite(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消收藏
|
||||
*/
|
||||
@PostMapping("/remove")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@RateLimit(count = 60, period = 60) // 每分钟最多60次取消收藏操作
|
||||
@Operation(summary = "取消收藏", description = "从收藏列表中移除指定消息")
|
||||
public ApiResponse<Void> removeFavorite(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestBody @Valid RemoveFavoriteRequest request) {
|
||||
|
||||
log.info("用户取消收藏: user={}, messageId={}",
|
||||
userDetails.getUsername(), request.getMessageId());
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return favoriteService.removeFavorite(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收藏列表
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "获取收藏列表", description = "分页获取用户的收藏消息列表,支持关键词搜索")
|
||||
public ApiResponse<PageUtil.PageResponse<FavoriteResponse>> getFavoriteList(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize,
|
||||
@Parameter(description = "消息角色过滤") @RequestParam(defaultValue = "all") String messageRole) {
|
||||
|
||||
log.info("获取收藏列表: user={}, keyword={}, page={}, pageSize={}, messageRole={}",
|
||||
userDetails.getUsername(), keyword, page, pageSize, messageRole);
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
FavoriteListRequest request = new FavoriteListRequest(keyword, page, pageSize, messageRole);
|
||||
return favoriteService.getFavoriteList(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否被收藏
|
||||
*/
|
||||
@GetMapping("/check/{messageId}")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "检查收藏状态", description = "检查指定消息是否已被当前用户收藏")
|
||||
public ApiResponse<Boolean> checkFavoriteStatus(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Parameter(description = "消息ID") @PathVariable String messageId) {
|
||||
|
||||
log.info("检查收藏状态: user={}, messageId={}", userDetails.getUsername(), messageId);
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return favoriteService.isFavorited(userId, messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户收藏数量
|
||||
*/
|
||||
@GetMapping("/count")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "获取收藏数量", description = "获取当前用户的总收藏数量")
|
||||
public ApiResponse<Long> getFavoriteCount(
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
|
||||
log.info("获取收藏数量: user={}", userDetails.getUsername());
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return favoriteService.getUserFavoriteCount(userId);
|
||||
}
|
||||
}
|
||||
47
src/main/java/com/lxy/hsend/controller/HealthController.java
Normal file
47
src/main/java/com/lxy/hsend/controller/HealthController.java
Normal file
@ -0,0 +1,47 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 健康检查控制器
|
||||
* 提供系统健康状态检查接口
|
||||
*/
|
||||
@Tag(name = "健康检查", description = "系统健康状态检查相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/health")
|
||||
public class HealthController {
|
||||
|
||||
@Operation(summary = "系统健康检查", description = "检查系统运行状态")
|
||||
@GetMapping("/check")
|
||||
public ResponseEntity<Map<String, Object>> healthCheck() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("status", "UP");
|
||||
response.put("timestamp", LocalDateTime.now());
|
||||
response.put("service", "hs-end");
|
||||
response.put("version", "1.0.0");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "系统信息", description = "获取系统基本信息")
|
||||
@GetMapping("/info")
|
||||
public ResponseEntity<Map<String, Object>> systemInfo() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("application", "海角AI后端服务");
|
||||
response.put("version", "1.0.0");
|
||||
response.put("java.version", System.getProperty("java.version"));
|
||||
response.put("os.name", System.getProperty("os.name"));
|
||||
response.put("startup.time", LocalDateTime.now());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RateLimit;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.message.MessageListRequest;
|
||||
import com.lxy.hsend.dto.message.MessageResponse;
|
||||
import com.lxy.hsend.dto.message.SendMessageRequest;
|
||||
import com.lxy.hsend.service.MessageService;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 消息控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "消息管理", description = "聊天消息相关接口")
|
||||
public class MessageController {
|
||||
|
||||
private final MessageService messageService;
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
@PostMapping("/send")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@RateLimit(count = 30, period = 60) // 每分钟最多30条消息
|
||||
@Operation(summary = "发送消息", description = "发送聊天消息并获取AI响应")
|
||||
public ApiResponse<MessageResponse> sendMessage(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestBody @Valid SendMessageRequest request) {
|
||||
|
||||
log.info("用户发送消息: user={}, sessionId={}, contentLength={}",
|
||||
userDetails.getUsername(), request.getSessionId(), request.getContent().length());
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return messageService.sendMessage(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息列表
|
||||
*/
|
||||
@GetMapping("/messages")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "获取消息列表", description = "分页获取指定会话的消息列表")
|
||||
public ApiResponse<PageUtil.PageResponse<MessageResponse>> getMessages(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Parameter(description = "会话ID") @RequestParam String sessionId,
|
||||
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize,
|
||||
@Parameter(description = "角色过滤") @RequestParam(defaultValue = "all") String role) {
|
||||
|
||||
log.info("获取消息列表: user={}, sessionId={}, page={}, pageSize={}, role={}",
|
||||
userDetails.getUsername(), sessionId, page, pageSize, role);
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
MessageListRequest request = new MessageListRequest(sessionId, page, pageSize, role);
|
||||
return messageService.getMessages(userId, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息详情
|
||||
*/
|
||||
@GetMapping("/message/{messageId}")
|
||||
@RequireAuth
|
||||
@RequestLog
|
||||
@Operation(summary = "获取消息详情", description = "根据消息ID获取消息详细信息")
|
||||
public ApiResponse<MessageResponse> getMessageById(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Parameter(description = "消息ID") @PathVariable String messageId) {
|
||||
|
||||
log.info("获取消息详情: user={}, messageId={}", userDetails.getUsername(), messageId);
|
||||
|
||||
Long userId = Long.parseLong(userDetails.getUsername());
|
||||
return messageService.getMessageById(userId, messageId);
|
||||
}
|
||||
}
|
||||
166
src/main/java/com/lxy/hsend/controller/SessionController.java
Normal file
166
src/main/java/com/lxy/hsend/controller/SessionController.java
Normal file
@ -0,0 +1,166 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.session.*;
|
||||
import com.lxy.hsend.service.SessionService;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 会话控制器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
@RequireAuth
|
||||
@Tag(name = "会话管理", description = "聊天会话管理相关接口")
|
||||
public class SessionController {
|
||||
|
||||
private final SessionService sessionService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
public SessionController(SessionService sessionService, JwtUtil jwtUtil) {
|
||||
this.sessionService = sessionService;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*/
|
||||
@PostMapping("/session")
|
||||
@Operation(summary = "创建会话", description = "创建一个新的聊天会话")
|
||||
@RequestLog(value = "创建会话", type = RequestLog.OperationType.CREATE)
|
||||
public ApiResponse<SessionDetailResponse> createSession(
|
||||
@Valid @RequestBody CreateSessionRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
SessionDetailResponse response = sessionService.createSession(userId, request);
|
||||
|
||||
return ApiResponse.success(response, "会话创建成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*/
|
||||
@GetMapping("/sessions")
|
||||
@Operation(summary = "获取会话列表", description = "获取用户的会话列表,支持搜索和分页")
|
||||
@RequestLog(value = "获取会话列表", type = RequestLog.OperationType.QUERY)
|
||||
public ApiResponse<Page<SessionListResponse>> getSessionList(
|
||||
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") Integer page,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer pageSize,
|
||||
@Parameter(description = "排序字段") @RequestParam(defaultValue = "last_message_time") String sortBy,
|
||||
@Parameter(description = "排序方向") @RequestParam(defaultValue = "desc") String sortOrder,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
|
||||
// 构建搜索请求
|
||||
SessionSearchRequest searchRequest = new SessionSearchRequest();
|
||||
searchRequest.setKeyword(keyword);
|
||||
searchRequest.setPage(page);
|
||||
searchRequest.setPageSize(pageSize);
|
||||
searchRequest.setSortBy(sortBy);
|
||||
searchRequest.setSortOrder(sortOrder);
|
||||
|
||||
Page<SessionListResponse> response = sessionService.getSessionList(userId, searchRequest);
|
||||
|
||||
return ApiResponse.success(response, "获取会话列表成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话详情
|
||||
*/
|
||||
@GetMapping("/session/{sessionId}")
|
||||
@Operation(summary = "获取会话详情", description = "根据会话ID获取会话的详细信息")
|
||||
@RequestLog(value = "获取会话详情", type = RequestLog.OperationType.QUERY)
|
||||
public ApiResponse<SessionDetailResponse> getSessionDetail(
|
||||
@Parameter(description = "会话ID") @PathVariable String sessionId,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
SessionDetailResponse response = sessionService.getSessionDetail(userId, sessionId);
|
||||
|
||||
return ApiResponse.success(response, "获取会话详情成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话
|
||||
*/
|
||||
@PutMapping("/session/{sessionId}")
|
||||
@Operation(summary = "更新会话", description = "更新会话信息,如标题等")
|
||||
@RequestLog(value = "更新会话", type = RequestLog.OperationType.UPDATE)
|
||||
public ApiResponse<SessionDetailResponse> updateSession(
|
||||
@Parameter(description = "会话ID") @PathVariable String sessionId,
|
||||
@Valid @RequestBody UpdateSessionRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
SessionDetailResponse response = sessionService.updateSession(userId, sessionId, request);
|
||||
|
||||
return ApiResponse.success(response, "会话更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*/
|
||||
@DeleteMapping("/session/{sessionId}")
|
||||
@Operation(summary = "删除会话", description = "删除指定的会话及其所有消息和收藏记录")
|
||||
@RequestLog(value = "删除会话", type = RequestLog.OperationType.DELETE)
|
||||
public ApiResponse<Void> deleteSession(
|
||||
@Parameter(description = "会话ID") @PathVariable String sessionId,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
sessionService.deleteSession(userId, sessionId);
|
||||
|
||||
return ApiResponse.success(null, "会话删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空会话
|
||||
*/
|
||||
@PostMapping("/session/{sessionId}/clear")
|
||||
@Operation(summary = "清空会话", description = "清空会话中的所有消息,但保留会话本身")
|
||||
@RequestLog(value = "清空会话", type = RequestLog.OperationType.DELETE)
|
||||
public ApiResponse<Void> clearSession(
|
||||
@Parameter(description = "会话ID") @PathVariable String sessionId,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
sessionService.clearSession(userId, sessionId);
|
||||
|
||||
return ApiResponse.success(null, "会话清空成功");
|
||||
}
|
||||
|
||||
// 获取会话消息列表的功能现在由 MessageController 处理
|
||||
|
||||
/**
|
||||
* 从请求中获取当前用户ID
|
||||
*/
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
|
||||
if (token != null && jwtUtil.validateToken(token)) {
|
||||
return jwtUtil.getUserIdFromToken(token);
|
||||
}
|
||||
|
||||
throw new RuntimeException("无法获取用户信息");
|
||||
}
|
||||
}
|
||||
98
src/main/java/com/lxy/hsend/controller/UserController.java
Normal file
98
src/main/java/com/lxy/hsend/controller/UserController.java
Normal file
@ -0,0 +1,98 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.lxy.hsend.annotation.RequestLog;
|
||||
import com.lxy.hsend.annotation.RequireAuth;
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.user.UpdateUserRequest;
|
||||
import com.lxy.hsend.dto.user.UserInfoResponse;
|
||||
import com.lxy.hsend.service.UserService;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
@Tag(name = "用户管理", description = "用户信息相关接口")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
public UserController(UserService userService, JwtUtil jwtUtil) {
|
||||
this.userService = userService;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
@RequireAuth
|
||||
@Operation(summary = "获取用户信息", description = "获取当前登录用户的基本信息")
|
||||
@RequestLog(value = "获取用户信息", type = RequestLog.OperationType.QUERY)
|
||||
public ApiResponse<UserInfoResponse> getUserInfo(HttpServletRequest request) {
|
||||
Long userId = getCurrentUserId(request);
|
||||
UserInfoResponse userInfo = userService.getUserById(userId);
|
||||
return ApiResponse.success(userInfo, "获取用户信息成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前用户信息
|
||||
*/
|
||||
@PutMapping("/info")
|
||||
@RequireAuth
|
||||
@Operation(summary = "更新用户信息", description = "更新当前登录用户的基本信息")
|
||||
@RequestLog(value = "更新用户信息", type = RequestLog.OperationType.UPDATE)
|
||||
public ApiResponse<UserInfoResponse> updateUserInfo(@Valid @RequestBody UpdateUserRequest request, HttpServletRequest httpRequest) {
|
||||
Long userId = getCurrentUserId(httpRequest);
|
||||
UserInfoResponse userInfo = userService.updateUser(userId, request);
|
||||
return ApiResponse.success(userInfo, "更新用户信息成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户信息(公开接口,用于获取其他用户的基本信息)
|
||||
*/
|
||||
@GetMapping("/{userId}")
|
||||
@Operation(summary = "根据ID获取用户信息", description = "获取指定用户的基本信息(公开信息)")
|
||||
@RequestLog(value = "根据ID获取用户信息", type = RequestLog.OperationType.QUERY)
|
||||
public ApiResponse<UserInfoResponse> getUserById(@PathVariable Long userId) {
|
||||
UserInfoResponse userInfo = userService.getUserById(userId);
|
||||
|
||||
// 对于公开接口,隐藏敏感信息
|
||||
UserInfoResponse publicInfo = new UserInfoResponse();
|
||||
publicInfo.setUserId(userInfo.getUserId());
|
||||
publicInfo.setNickname(userInfo.getNickname());
|
||||
publicInfo.setAvatarUrl(userInfo.getAvatarUrl());
|
||||
publicInfo.setCreatedAt(userInfo.getCreatedAt());
|
||||
// 不返回邮箱、状态、最后登录时间等敏感信息
|
||||
|
||||
return ApiResponse.success(publicInfo, "获取用户信息成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取当前用户ID
|
||||
*/
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
|
||||
if (token != null && jwtUtil.validateToken(token)) {
|
||||
return jwtUtil.getUserIdFromToken(token);
|
||||
}
|
||||
|
||||
throw new RuntimeException("无法获取用户信息");
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/lxy/hsend/dto/auth/LoginRequest.java
Normal file
33
src/main/java/com/lxy/hsend/dto/auth/LoginRequest.java
Normal file
@ -0,0 +1,33 @@
|
||||
package com.lxy.hsend.dto.auth;
|
||||
|
||||
import com.lxy.hsend.annotation.ValidEmail;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 用户登录请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@ValidEmail(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 记住登录状态(可选,暂时保留)
|
||||
*/
|
||||
private Boolean rememberMe = false;
|
||||
}
|
||||
53
src/main/java/com/lxy/hsend/dto/auth/LoginResponse.java
Normal file
53
src/main/java/com/lxy/hsend/dto/auth/LoginResponse.java
Normal file
@ -0,0 +1,53 @@
|
||||
package com.lxy.hsend.dto.auth;
|
||||
|
||||
import com.lxy.hsend.dto.user.UserInfoResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 用户登录响应DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
|
||||
/**
|
||||
* JWT访问令牌
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* JWT刷新令牌
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 令牌类型
|
||||
*/
|
||||
private String tokenType = "Bearer";
|
||||
|
||||
/**
|
||||
* 令牌过期时间(秒)
|
||||
*/
|
||||
private Long expiresIn;
|
||||
|
||||
/**
|
||||
* 用户基本信息
|
||||
*/
|
||||
private UserInfoResponse userInfo;
|
||||
|
||||
/**
|
||||
* 构造方法(不包含tokenType)
|
||||
*/
|
||||
public LoginResponse(String accessToken, String refreshToken, Long expiresIn, UserInfoResponse userInfo) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
this.tokenType = "Bearer";
|
||||
this.expiresIn = expiresIn;
|
||||
this.userInfo = userInfo;
|
||||
}
|
||||
}
|
||||
49
src/main/java/com/lxy/hsend/dto/auth/RegisterRequest.java
Normal file
49
src/main/java/com/lxy/hsend/dto/auth/RegisterRequest.java
Normal file
@ -0,0 +1,49 @@
|
||||
package com.lxy.hsend.dto.auth;
|
||||
|
||||
import com.lxy.hsend.annotation.ValidEmail;
|
||||
import com.lxy.hsend.annotation.ValidPassword;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 用户注册请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class RegisterRequest {
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@ValidEmail(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@ValidPassword(message = "密码格式不正确,密码长度应为6-20位,包含字母和数字")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 确认密码
|
||||
*/
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@NotBlank(message = "昵称不能为空")
|
||||
@Size(min = 1, max = 50, message = "昵称长度应在1-50个字符之间")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 头像URL(可选)
|
||||
*/
|
||||
private String avatarUrl;
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.lxy.hsend.dto.favorite;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 添加收藏请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AddFavoriteRequest {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@NotBlank(message = "消息ID不能为空")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@NotBlank(message = "会话ID不能为空")
|
||||
private String sessionId;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.lxy.hsend.dto.favorite;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Max;
|
||||
|
||||
/**
|
||||
* 收藏列表查询请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FavoriteListRequest {
|
||||
|
||||
/**
|
||||
* 搜索关键词(会话标题、消息内容)
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 页码(从1开始)
|
||||
*/
|
||||
@Min(value = 1, message = "页码必须大于0")
|
||||
private Integer page = 1;
|
||||
|
||||
/**
|
||||
* 每页数量
|
||||
*/
|
||||
@Min(value = 1, message = "每页数量必须大于0")
|
||||
@Max(value = 100, message = "每页数量不能超过100")
|
||||
private Integer pageSize = 20;
|
||||
|
||||
/**
|
||||
* 消息类型过滤(user/assistant/all)
|
||||
*/
|
||||
private String messageRole = "all";
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.lxy.hsend.dto.favorite;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 收藏响应DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class FavoriteResponse {
|
||||
|
||||
/**
|
||||
* 收藏ID
|
||||
*/
|
||||
private String favoriteId;
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String sessionTitle;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String messageContent;
|
||||
|
||||
/**
|
||||
* 消息角色(user/assistant)
|
||||
*/
|
||||
private String messageRole;
|
||||
|
||||
/**
|
||||
* 消息创建时间
|
||||
*/
|
||||
private LocalDateTime messageCreatedAt;
|
||||
|
||||
/**
|
||||
* 收藏时间
|
||||
*/
|
||||
private LocalDateTime favoritedAt;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.lxy.hsend.dto.favorite;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 取消收藏请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RemoveFavoriteRequest {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@NotBlank(message = "消息ID不能为空")
|
||||
private String messageId;
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.lxy.hsend.dto.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Max;
|
||||
|
||||
/**
|
||||
* 消息列表查询请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MessageListRequest {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@NotBlank(message = "会话ID不能为空")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 页码(从1开始)
|
||||
*/
|
||||
@Min(value = 1, message = "页码必须大于0")
|
||||
private Integer page = 1;
|
||||
|
||||
/**
|
||||
* 每页数量
|
||||
*/
|
||||
@Min(value = 1, message = "每页数量必须大于0")
|
||||
@Max(value = 100, message = "每页数量不能超过100")
|
||||
private Integer pageSize = 20;
|
||||
|
||||
/**
|
||||
* 消息类型过滤(user/assistant/all)
|
||||
*/
|
||||
private String role = "all";
|
||||
}
|
||||
63
src/main/java/com/lxy/hsend/dto/message/MessageResponse.java
Normal file
63
src/main/java/com/lxy/hsend/dto/message/MessageResponse.java
Normal file
@ -0,0 +1,63 @@
|
||||
package com.lxy.hsend.dto.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 消息响应DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class MessageResponse {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 消息角色(user/assistant)
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 消息状态(pending/completed/failed)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 是否深度思考
|
||||
*/
|
||||
private Boolean deepThinking;
|
||||
|
||||
/**
|
||||
* 是否联网搜索
|
||||
*/
|
||||
private Boolean webSearch;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.lxy.hsend.dto.message;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 发送消息请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SendMessageRequest {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@NotBlank(message = "会话ID不能为空")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@NotBlank(message = "消息内容不能为空")
|
||||
@Size(min = 1, max = 4000, message = "消息内容长度必须在1-4000字符之间")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 是否深度思考
|
||||
*/
|
||||
private Boolean deepThinking = false;
|
||||
|
||||
/**
|
||||
* 是否联网搜索
|
||||
*/
|
||||
private Boolean webSearch = false;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 创建会话请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class CreateSessionRequest {
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
@NotBlank(message = "会话标题不能为空")
|
||||
@Size(min = 1, max = 100, message = "会话标题长度应在1-100个字符之间")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 初始消息内容(可选)
|
||||
*/
|
||||
@Size(max = 10000, message = "消息内容长度不能超过10000个字符")
|
||||
private String initialMessage;
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 会话详情响应DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SessionDetailResponse {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 消息总数
|
||||
*/
|
||||
private Integer messageCount;
|
||||
|
||||
/**
|
||||
* 最后消息时间
|
||||
*/
|
||||
private Long lastMessageTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Long createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Long updatedAt;
|
||||
|
||||
/**
|
||||
* 检查是否有消息
|
||||
*/
|
||||
public boolean hasMessages() {
|
||||
return messageCount != null && messageCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查会话是否为空(无消息)
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return !hasMessages();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 会话列表响应DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SessionListResponse {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 消息总数
|
||||
*/
|
||||
private Integer messageCount;
|
||||
|
||||
/**
|
||||
* 最后一条消息的内容摘要
|
||||
*/
|
||||
private String lastMessageContent;
|
||||
|
||||
/**
|
||||
* 最后消息时间
|
||||
*/
|
||||
private Long lastMessageTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Long createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Long updatedAt;
|
||||
|
||||
/**
|
||||
* 获取最后消息内容的摘要(限制长度)
|
||||
*/
|
||||
public String getLastMessageSummary() {
|
||||
if (lastMessageContent == null || lastMessageContent.trim().isEmpty()) {
|
||||
return "暂无消息";
|
||||
}
|
||||
|
||||
String content = lastMessageContent.trim();
|
||||
if (content.length() <= 50) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return content.substring(0, 47) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有消息
|
||||
*/
|
||||
public boolean hasMessages() {
|
||||
return messageCount != null && messageCount > 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Max;
|
||||
|
||||
/**
|
||||
* 会话搜索请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class SessionSearchRequest {
|
||||
|
||||
/**
|
||||
* 搜索关键词(标题和消息内容)
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 页码(从1开始)
|
||||
*/
|
||||
@Min(value = 1, message = "页码必须大于0")
|
||||
private Integer page = 1;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
@Min(value = 1, message = "每页大小必须大于0")
|
||||
@Max(value = 100, message = "每页大小不能超过100")
|
||||
private Integer pageSize = 10;
|
||||
|
||||
/**
|
||||
* 排序字段(支持:created_at, updated_at, last_message_time)
|
||||
*/
|
||||
private String sortBy = "last_message_time";
|
||||
|
||||
/**
|
||||
* 排序方向(asc: 升序, desc: 降序)
|
||||
*/
|
||||
private String sortOrder = "desc";
|
||||
|
||||
/**
|
||||
* 检查是否有搜索关键词
|
||||
*/
|
||||
public boolean hasKeyword() {
|
||||
return keyword != null && !keyword.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取清理后的关键词
|
||||
*/
|
||||
public String getCleanKeyword() {
|
||||
return hasKeyword() ? keyword.trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查排序方向是否为升序
|
||||
*/
|
||||
public boolean isAscOrder() {
|
||||
return "asc".equalsIgnoreCase(sortOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查排序方向是否为降序
|
||||
*/
|
||||
public boolean isDescOrder() {
|
||||
return "desc".equalsIgnoreCase(sortOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的排序字段
|
||||
*/
|
||||
public String getValidSortBy() {
|
||||
if (sortBy == null) {
|
||||
return "last_message_time";
|
||||
}
|
||||
|
||||
switch (sortBy.toLowerCase()) {
|
||||
case "created_at":
|
||||
case "updated_at":
|
||||
case "last_message_time":
|
||||
return sortBy.toLowerCase();
|
||||
default:
|
||||
return "last_message_time";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.lxy.hsend.dto.session;
|
||||
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 更新会话请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class UpdateSessionRequest {
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
@Size(min = 1, max = 100, message = "会话标题长度应在1-100个字符之间")
|
||||
private String title;
|
||||
}
|
||||
26
src/main/java/com/lxy/hsend/dto/user/UpdateUserRequest.java
Normal file
26
src/main/java/com/lxy/hsend/dto/user/UpdateUserRequest.java
Normal file
@ -0,0 +1,26 @@
|
||||
package com.lxy.hsend.dto.user;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 更新用户信息请求DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
public class UpdateUserRequest {
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@Size(min = 1, max = 50, message = "昵称长度应在1-50个字符之间")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
@Size(max = 500, message = "头像URL长度不能超过500个字符")
|
||||
private String avatarUrl;
|
||||
}
|
||||
93
src/main/java/com/lxy/hsend/dto/user/UserInfoResponse.java
Normal file
93
src/main/java/com/lxy/hsend/dto/user/UserInfoResponse.java
Normal file
@ -0,0 +1,93 @@
|
||||
package com.lxy.hsend.dto.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 用户信息响应DTO
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserInfoResponse {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
private String avatarUrl;
|
||||
|
||||
/**
|
||||
* 用户状态(1-正常,2-锁定,3-删除)
|
||||
*/
|
||||
private Byte status;
|
||||
|
||||
/**
|
||||
* 最后登录时间
|
||||
*/
|
||||
private Long lastLoginTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Long createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Long updatedAt;
|
||||
|
||||
/**
|
||||
* 获取状态描述
|
||||
*/
|
||||
public String getStatusText() {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "正常";
|
||||
case 2:
|
||||
return "锁定";
|
||||
case 3:
|
||||
return "删除";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户状态是否正常
|
||||
*/
|
||||
public boolean isNormal() {
|
||||
return status != null && status == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否被锁定
|
||||
*/
|
||||
public boolean isLocked() {
|
||||
return status != null && status == 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否被删除
|
||||
*/
|
||||
public boolean isDeleted() {
|
||||
return status != null && status == 3;
|
||||
}
|
||||
}
|
||||
181
src/main/java/com/lxy/hsend/entity/Favorite.java
Normal file
181
src/main/java/com/lxy/hsend/entity/Favorite.java
Normal file
@ -0,0 +1,181 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 收藏实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "favorites",
|
||||
indexes = {
|
||||
@Index(name = "idx_user_id", columnList = "userId"),
|
||||
@Index(name = "idx_message_id", columnList = "messageId"),
|
||||
@Index(name = "idx_session_id", columnList = "sessionId"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_user_message", columnNames = {"user_id", "message_id"})
|
||||
}
|
||||
)
|
||||
public class Favorite {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "favorite_id", nullable = false, unique = true, length = 36)
|
||||
private String favoriteId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "message_id", nullable = false, length = 36)
|
||||
private String messageId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "session_id", nullable = false, length = 36)
|
||||
private String sessionId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 关联用户实体
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
// 构造函数
|
||||
public Favorite() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.favoriteId = UUID.randomUUID().toString();
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
public Favorite(Long userId, String messageId, String sessionId) {
|
||||
this();
|
||||
this.userId = userId;
|
||||
this.messageId = messageId;
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFavoriteId() {
|
||||
return favoriteId;
|
||||
}
|
||||
|
||||
public void setFavoriteId(String favoriteId) {
|
||||
this.favoriteId = favoriteId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Favorite{" +
|
||||
"id=" + id +
|
||||
", favoriteId='" + favoriteId + '\'' +
|
||||
", userId=" + userId +
|
||||
", messageId='" + messageId + '\'' +
|
||||
", sessionId='" + sessionId + '\'' +
|
||||
", createdAt=" + createdAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
286
src/main/java/com/lxy/hsend/entity/File.java
Normal file
286
src/main/java/com/lxy/hsend/entity/File.java
Normal file
@ -0,0 +1,286 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 文件实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "files", indexes = {
|
||||
@Index(name = "idx_user_id", columnList = "userId"),
|
||||
@Index(name = "idx_session_id", columnList = "sessionId"),
|
||||
@Index(name = "idx_file_type", columnList = "fileType"),
|
||||
@Index(name = "idx_upload_time", columnList = "uploadTime"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
})
|
||||
public class File {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "file_id", nullable = false, unique = true, length = 36)
|
||||
private String fileId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Size(max = 36)
|
||||
@Column(name = "session_id", length = 36)
|
||||
private String sessionId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
@Column(name = "filename", nullable = false)
|
||||
private String filename;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
@Column(name = "stored_filename", nullable = false)
|
||||
private String storedFilename;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 500)
|
||||
@Column(name = "file_path", nullable = false, length = 500)
|
||||
private String filePath;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "file_size", nullable = false)
|
||||
private Long fileSize;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 100)
|
||||
@Column(name = "file_type", nullable = false, length = 100)
|
||||
private String fileType;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "mime_type", length = 100)
|
||||
private String mimeType;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "upload_time", nullable = false)
|
||||
private Long uploadTime;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 关联用户实体
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
// 构造函数
|
||||
public File() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.fileId = UUID.randomUUID().toString();
|
||||
this.uploadTime = now;
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
public File(Long userId, String filename, String storedFilename, String filePath,
|
||||
Long fileSize, String fileType, String mimeType) {
|
||||
this();
|
||||
this.userId = userId;
|
||||
this.filename = filename;
|
||||
this.storedFilename = storedFilename;
|
||||
this.filePath = filePath;
|
||||
this.fileSize = fileSize;
|
||||
this.fileType = fileType;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public void setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public String getStoredFilename() {
|
||||
return storedFilename;
|
||||
}
|
||||
|
||||
public void setStoredFilename(String storedFilename) {
|
||||
this.storedFilename = storedFilename;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public Long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public void setFileSize(Long fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public String getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
public void setFileType(String fileType) {
|
||||
this.fileType = fileType;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public void setMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
public Long getUploadTime() {
|
||||
return uploadTime;
|
||||
}
|
||||
|
||||
public void setUploadTime(Long uploadTime) {
|
||||
this.uploadTime = uploadTime;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
public String getFileSizeFormatted() {
|
||||
if (fileSize == null) return "0 B";
|
||||
|
||||
long size = fileSize;
|
||||
String[] units = {"B", "KB", "MB", "GB", "TB"};
|
||||
int unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return size + " " + units[unitIndex];
|
||||
}
|
||||
|
||||
public boolean isImageFile() {
|
||||
if (mimeType == null) return false;
|
||||
return mimeType.startsWith("image/");
|
||||
}
|
||||
|
||||
public boolean isDocumentFile() {
|
||||
if (fileType == null) return false;
|
||||
String lowerType = fileType.toLowerCase();
|
||||
return lowerType.equals("pdf") || lowerType.equals("doc") ||
|
||||
lowerType.equals("docx") || lowerType.equals("txt") ||
|
||||
lowerType.equals("md");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "File{" +
|
||||
"id=" + id +
|
||||
", fileId='" + fileId + '\'' +
|
||||
", userId=" + userId +
|
||||
", filename='" + filename + '\'' +
|
||||
", fileSize=" + fileSize +
|
||||
", fileType='" + fileType + '\'' +
|
||||
", uploadTime=" + uploadTime +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
266
src/main/java/com/lxy/hsend/entity/Message.java
Normal file
266
src/main/java/com/lxy/hsend/entity/Message.java
Normal file
@ -0,0 +1,266 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 消息实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "messages", indexes = {
|
||||
@Index(name = "idx_session_id", columnList = "sessionId"),
|
||||
@Index(name = "idx_user_id", columnList = "userId"),
|
||||
@Index(name = "idx_role", columnList = "role"),
|
||||
@Index(name = "idx_timestamp", columnList = "timestamp"),
|
||||
@Index(name = "idx_is_favorited", columnList = "isFavorited"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
})
|
||||
public class Message {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "message_id", nullable = false, unique = true, length = 36)
|
||||
private String messageId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "session_id", nullable = false, length = 36)
|
||||
private String sessionId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 20)
|
||||
@Column(name = "role", nullable = false, length = 20)
|
||||
private String role; // user/assistant
|
||||
|
||||
@NotBlank
|
||||
@Lob
|
||||
@Column(name = "content", nullable = false, columnDefinition = "LONGTEXT")
|
||||
private String content;
|
||||
|
||||
@Size(max = 50)
|
||||
@Column(name = "model_used", length = 50)
|
||||
private String modelUsed;
|
||||
|
||||
@Column(name = "deep_thinking")
|
||||
private Boolean deepThinking = false;
|
||||
|
||||
@Column(name = "web_search")
|
||||
private Boolean webSearch = false;
|
||||
|
||||
@Column(name = "is_favorited")
|
||||
private Boolean isFavorited = false;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "timestamp", nullable = false)
|
||||
private Long timestamp;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 关联用户实体
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
// 构造函数
|
||||
public Message() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.messageId = UUID.randomUUID().toString();
|
||||
this.timestamp = now;
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
public Message(String sessionId, Long userId, String role, String content) {
|
||||
this();
|
||||
this.sessionId = sessionId;
|
||||
this.userId = userId;
|
||||
this.role = role;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getModelUsed() {
|
||||
return modelUsed;
|
||||
}
|
||||
|
||||
public void setModelUsed(String modelUsed) {
|
||||
this.modelUsed = modelUsed;
|
||||
}
|
||||
|
||||
public Boolean getDeepThinking() {
|
||||
return deepThinking;
|
||||
}
|
||||
|
||||
public void setDeepThinking(Boolean deepThinking) {
|
||||
this.deepThinking = deepThinking;
|
||||
}
|
||||
|
||||
public Boolean getWebSearch() {
|
||||
return webSearch;
|
||||
}
|
||||
|
||||
public void setWebSearch(Boolean webSearch) {
|
||||
this.webSearch = webSearch;
|
||||
}
|
||||
|
||||
public Boolean getIsFavorited() {
|
||||
return isFavorited;
|
||||
}
|
||||
|
||||
public void setIsFavorited(Boolean isFavorited) {
|
||||
this.isFavorited = isFavorited;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
public boolean isUserMessage() {
|
||||
return "user".equals(role);
|
||||
}
|
||||
|
||||
public boolean isAssistantMessage() {
|
||||
return "assistant".equals(role);
|
||||
}
|
||||
|
||||
public void toggleFavorite() {
|
||||
this.isFavorited = !Boolean.TRUE.equals(this.isFavorited);
|
||||
}
|
||||
|
||||
// 消息角色常量
|
||||
public static final String ROLE_USER = "user";
|
||||
public static final String ROLE_ASSISTANT = "assistant";
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Message{" +
|
||||
"id=" + id +
|
||||
", messageId='" + messageId + '\'' +
|
||||
", sessionId='" + sessionId + '\'' +
|
||||
", role='" + role + '\'' +
|
||||
", isFavorited=" + isFavorited +
|
||||
", timestamp=" + timestamp +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
192
src/main/java/com/lxy/hsend/entity/Session.java
Normal file
192
src/main/java/com/lxy/hsend/entity/Session.java
Normal file
@ -0,0 +1,192 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 会话实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sessions", indexes = {
|
||||
@Index(name = "idx_user_id", columnList = "userId"),
|
||||
@Index(name = "idx_last_message_time", columnList = "lastMessageTime"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
})
|
||||
public class Session {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 36)
|
||||
@Column(name = "session_id", nullable = false, unique = true, length = 36)
|
||||
private String sessionId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 200)
|
||||
@Column(name = "title", nullable = false, length = 200)
|
||||
private String title;
|
||||
|
||||
@Column(name = "message_count")
|
||||
private Integer messageCount = 0;
|
||||
|
||||
@Column(name = "last_message_time")
|
||||
private Long lastMessageTime;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 关联用户实体
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
// 构造函数
|
||||
public Session() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.sessionId = UUID.randomUUID().toString();
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
public Session(Long userId, String title) {
|
||||
this();
|
||||
this.userId = userId;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public Integer getMessageCount() {
|
||||
return messageCount;
|
||||
}
|
||||
|
||||
public void setMessageCount(Integer messageCount) {
|
||||
this.messageCount = messageCount;
|
||||
}
|
||||
|
||||
public Long getLastMessageTime() {
|
||||
return lastMessageTime;
|
||||
}
|
||||
|
||||
public void setLastMessageTime(Long lastMessageTime) {
|
||||
this.lastMessageTime = lastMessageTime;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
public void incrementMessageCount() {
|
||||
this.messageCount = (this.messageCount == null ? 0 : this.messageCount) + 1;
|
||||
this.lastMessageTime = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
public void decrementMessageCount() {
|
||||
this.messageCount = Math.max(0, (this.messageCount == null ? 0 : this.messageCount) - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Session{" +
|
||||
"id=" + id +
|
||||
", sessionId='" + sessionId + '\'' +
|
||||
", userId=" + userId +
|
||||
", title='" + title + '\'' +
|
||||
", messageCount=" + messageCount +
|
||||
", createdAt=" + createdAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
185
src/main/java/com/lxy/hsend/entity/User.java
Normal file
185
src/main/java/com/lxy/hsend/entity/User.java
Normal file
@ -0,0 +1,185 @@
|
||||
package com.lxy.hsend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* 用户实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "users", indexes = {
|
||||
@Index(name = "idx_status", columnList = "status"),
|
||||
@Index(name = "idx_created_at", columnList = "createdAt")
|
||||
})
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Email
|
||||
@Size(max = 100)
|
||||
@Column(name = "email", nullable = false, unique = true, length = 100)
|
||||
private String email;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
@Column(name = "password_hash", nullable = false)
|
||||
private String passwordHash;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 50)
|
||||
@Column(name = "nickname", nullable = false, length = 50)
|
||||
private String nickname;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "avatar_url", length = 500)
|
||||
private String avatarUrl;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "status", nullable = false, columnDefinition = "TINYINT")
|
||||
private Byte status = 1; // 1-正常,2-锁定,3-删除
|
||||
|
||||
@Column(name = "last_login_time")
|
||||
private Long lastLoginTime;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Long createdAt;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Long updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Long deletedAt;
|
||||
|
||||
// 构造函数
|
||||
public User() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
}
|
||||
|
||||
// JPA生命周期回调
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public void setPasswordHash(String passwordHash) {
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public void setNickname(String nickname) {
|
||||
this.nickname = nickname;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public Byte getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Byte status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getLastLoginTime() {
|
||||
return lastLoginTime;
|
||||
}
|
||||
|
||||
public void setLastLoginTime(Long lastLoginTime) {
|
||||
this.lastLoginTime = lastLoginTime;
|
||||
}
|
||||
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
public boolean isNormal() {
|
||||
return status == 1;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return status == 2;
|
||||
}
|
||||
|
||||
public void markAsDeleted() {
|
||||
this.deletedAt = Instant.now().getEpochSecond();
|
||||
this.status = 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", email='" + email + '\'' +
|
||||
", nickname='" + nickname + '\'' +
|
||||
", status=" + status +
|
||||
", createdAt=" + createdAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
76
src/main/java/com/lxy/hsend/exception/BusinessException.java
Normal file
76
src/main/java/com/lxy/hsend/exception/BusinessException.java
Normal file
@ -0,0 +1,76 @@
|
||||
package com.lxy.hsend.exception;
|
||||
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
public BusinessException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public BusinessException(ErrorCode errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public BusinessException(ErrorCode errorCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
// 便捷构造方法
|
||||
public static BusinessException of(ErrorCode errorCode) {
|
||||
return new BusinessException(errorCode);
|
||||
}
|
||||
|
||||
public static BusinessException of(ErrorCode errorCode, String message) {
|
||||
return new BusinessException(errorCode, message);
|
||||
}
|
||||
|
||||
public static BusinessException of(ErrorCode errorCode, String message, Throwable cause) {
|
||||
return new BusinessException(errorCode, message, cause);
|
||||
}
|
||||
|
||||
// 常用业务异常
|
||||
public static BusinessException userNotFound() {
|
||||
return new BusinessException(ErrorCode.USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static BusinessException userAlreadyExists() {
|
||||
return new BusinessException(ErrorCode.USER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
public static BusinessException passwordError() {
|
||||
return new BusinessException(ErrorCode.PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
public static BusinessException sessionNotFound() {
|
||||
return new BusinessException(ErrorCode.SESSION_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static BusinessException messageNotFound() {
|
||||
return new BusinessException(ErrorCode.MESSAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static BusinessException fileNotFound() {
|
||||
return new BusinessException(ErrorCode.FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static BusinessException fileUploadError(String message) {
|
||||
return new BusinessException(ErrorCode.FILE_UPLOAD_ERROR, message);
|
||||
}
|
||||
|
||||
public static BusinessException aiServiceError(String message) {
|
||||
return new BusinessException(ErrorCode.AI_SERVICE_ERROR, message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,199 @@
|
||||
package com.lxy.hsend.exception;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 业务异常处理
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleBusinessException(BusinessException e, HttpServletRequest request) {
|
||||
log.warn("业务异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.error(e.getErrorCode().getCode(), e.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数验证异常处理
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数验证异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定异常处理
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleBindException(BindException e, HttpServletRequest request) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数绑定异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 约束违反异常处理
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
|
||||
String message = e.getConstraintViolations().stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("约束违反异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺少请求参数异常处理
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleMissingParamException(MissingServletRequestParameterException e, HttpServletRequest request) {
|
||||
String message = String.format("缺少必需的请求参数: %s", e.getParameterName());
|
||||
log.warn("缺少请求参数异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数类型不匹配异常处理
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
|
||||
String message = String.format("参数类型不匹配: %s", e.getName());
|
||||
log.warn("参数类型不匹配异常: {} - {}", request.getRequestURI(), message);
|
||||
return ResponseEntity.ok(ApiResponse.validationError(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP消息不可读异常处理
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
|
||||
log.warn("HTTP消息不可读异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.validationError("请求体格式错误"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证异常处理
|
||||
*/
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleAuthenticationException(AuthenticationException e, HttpServletRequest request) {
|
||||
log.warn("认证异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(ApiResponse.authError("认证失败,请重新登录"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 凭据错误异常处理
|
||||
*/
|
||||
@ExceptionHandler(BadCredentialsException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleBadCredentialsException(BadCredentialsException e, HttpServletRequest request) {
|
||||
log.warn("凭据错误异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(ApiResponse.authError("用户名或密码错误"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限不足异常处理
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
|
||||
log.warn("权限不足异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.forbiddenError("权限不足,无法访问该资源"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求方法不支持异常处理
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
|
||||
log.warn("请求方法不支持异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
.body(ApiResponse.error(ErrorCode.METHOD_NOT_ALLOWED.getCode(), "请求方法不支持"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源不存在异常处理
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
|
||||
log.warn("资源不存在异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(ApiResponse.notFoundError("请求的资源不存在"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传大小超限异常处理
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e, HttpServletRequest request) {
|
||||
log.warn("文件上传大小超限异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.FILE_UPLOAD_ERROR.getCode(), "文件大小超过限制"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据完整性违反异常处理
|
||||
*/
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleDataIntegrityViolationException(DataIntegrityViolationException e, HttpServletRequest request) {
|
||||
log.error("数据完整性违反异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.DATA_INTEGRITY_ERROR.getCode(), "数据操作违反完整性约束"));
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL异常处理
|
||||
*/
|
||||
@ExceptionHandler(SQLException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleSQLException(SQLException e, HttpServletRequest request) {
|
||||
log.error("SQL异常: {} - {}", request.getRequestURI(), e.getMessage());
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.DATABASE_ERROR.getCode(), "数据库操作失败"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用异常处理
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleGenericException(Exception e, HttpServletRequest request) {
|
||||
log.error("未知异常: {} - {}", request.getRequestURI(), e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.systemError("系统内部错误,请稍后重试"));
|
||||
}
|
||||
}
|
||||
231
src/main/java/com/lxy/hsend/repository/FavoriteRepository.java
Normal file
231
src/main/java/com/lxy/hsend/repository/FavoriteRepository.java
Normal file
@ -0,0 +1,231 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.Favorite;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 收藏数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface FavoriteRepository extends JpaRepository<Favorite, Long> {
|
||||
|
||||
/**
|
||||
* 根据收藏ID查找收藏
|
||||
*/
|
||||
Optional<Favorite> findByFavoriteId(String favoriteId);
|
||||
|
||||
/**
|
||||
* 根据收藏ID查找未删除的收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.favoriteId = :favoriteId AND f.deletedAt IS NULL")
|
||||
Optional<Favorite> findByFavoriteIdAndNotDeleted(@Param("favoriteId") String favoriteId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和消息ID查找收藏
|
||||
*/
|
||||
Optional<Favorite> findByUserIdAndMessageId(Long userId, String messageId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和消息ID查找未删除的收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
Optional<Favorite> findByUserIdAndMessageIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找所有收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
List<Favorite> findByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
Page<Favorite> findByUserIdAndNotDeleted(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
List<Favorite> findBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和会话ID查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
List<Favorite> findByUserIdAndSessionIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和会话ID分页查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
Page<Favorite> findByUserIdAndSessionIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("sessionId") String sessionId,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据时间范围查找收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.createdAt BETWEEN :startTime AND :endTime AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
List<Favorite> findByUserIdAndCreatedAtBetween(@Param("userId") Long userId,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 检查用户是否已收藏某条消息
|
||||
*/
|
||||
@Query("SELECT COUNT(f) > 0 FROM Favorite f WHERE f.userId = :userId AND f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
boolean existsByUserIdAndMessageIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 统计用户收藏总数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM Favorite f WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
long countByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计会话收藏总数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM Favorite f WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL")
|
||||
long countBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据消息ID查找所有收藏记录
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
List<Favorite> findByMessageIdAndNotDeleted(@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 软删除收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.favoriteId = :favoriteId")
|
||||
void softDeleteByFavoriteId(@Param("favoriteId") String favoriteId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 根据用户ID和消息ID软删除收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.userId = :userId AND f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
void softDeleteByUserIdAndMessageId(@Param("userId") Long userId,
|
||||
@Param("messageId") String messageId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除会话的所有收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除用户的所有收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllByUserId(@Param("userId") Long userId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除指定消息的所有收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Favorite f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.messageId = :messageId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllByMessageId(@Param("messageId") String messageId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 检查收藏是否属于指定用户
|
||||
*/
|
||||
@Query("SELECT COUNT(f) > 0 FROM Favorite f WHERE f.favoriteId = :favoriteId AND f.userId = :userId AND f.deletedAt IS NULL")
|
||||
boolean existsByFavoriteIdAndUserIdAndNotDeleted(@Param("favoriteId") String favoriteId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找用户最近的收藏
|
||||
*/
|
||||
@Query("SELECT f FROM Favorite f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.createdAt DESC")
|
||||
Page<Favorite> findRecentFavoritesByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计今日收藏数量
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM Favorite f WHERE f.createdAt >= :todayStart AND f.deletedAt IS NULL")
|
||||
long countTodayFavorites(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 统计系统总收藏数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM Favorite f WHERE f.deletedAt IS NULL")
|
||||
long countAllNotDeleted();
|
||||
|
||||
/**
|
||||
* 查找收藏数最多的消息
|
||||
*/
|
||||
@Query("SELECT f.messageId, COUNT(f) as favoriteCount FROM Favorite f WHERE f.deletedAt IS NULL GROUP BY f.messageId ORDER BY favoriteCount DESC")
|
||||
Page<Object[]> findMostFavoritedMessages(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 删除会话的所有收藏记录
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM Favorite f WHERE f.sessionId = :sessionId")
|
||||
void deleteBySessionId(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据关键词搜索用户收藏(搜索消息内容和会话标题)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT f FROM Favorite f
|
||||
JOIN Message m ON f.messageId = m.messageId
|
||||
JOIN Session s ON f.sessionId = s.sessionId
|
||||
WHERE f.userId = :userId
|
||||
AND f.deletedAt IS NULL
|
||||
AND m.deletedAt IS NULL
|
||||
AND s.deletedAt IS NULL
|
||||
AND (m.content LIKE %:keyword% OR s.title LIKE %:keyword%)
|
||||
ORDER BY f.createdAt DESC
|
||||
""")
|
||||
Page<Favorite> findByUserIdAndKeywordAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("keyword") String keyword,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据关键词和消息角色搜索用户收藏
|
||||
*/
|
||||
@Query("""
|
||||
SELECT f FROM Favorite f
|
||||
JOIN Message m ON f.messageId = m.messageId
|
||||
JOIN Session s ON f.sessionId = s.sessionId
|
||||
WHERE f.userId = :userId
|
||||
AND f.deletedAt IS NULL
|
||||
AND m.deletedAt IS NULL
|
||||
AND s.deletedAt IS NULL
|
||||
AND m.role = :role
|
||||
AND (m.content LIKE %:keyword% OR s.title LIKE %:keyword%)
|
||||
ORDER BY f.createdAt DESC
|
||||
""")
|
||||
Page<Favorite> findByUserIdAndKeywordAndRoleAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("role") String role,
|
||||
Pageable pageable);
|
||||
}
|
||||
218
src/main/java/com/lxy/hsend/repository/FileRepository.java
Normal file
218
src/main/java/com/lxy/hsend/repository/FileRepository.java
Normal file
@ -0,0 +1,218 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.File;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 文件数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface FileRepository extends JpaRepository<File, Long> {
|
||||
|
||||
/**
|
||||
* 根据文件ID查找文件
|
||||
*/
|
||||
Optional<File> findByFileId(String fileId);
|
||||
|
||||
/**
|
||||
* 根据文件ID查找未删除的文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.fileId = :fileId AND f.deletedAt IS NULL")
|
||||
Optional<File> findByFileIdAndNotDeleted(@Param("fileId") String fileId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找所有文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
Page<File> findByUserIdAndNotDeleted(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和会话ID查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.sessionId = :sessionId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndSessionIdAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据文件类型查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.fileType = :fileType AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndFileTypeAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("fileType") String fileType);
|
||||
|
||||
/**
|
||||
* 根据文件名搜索文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.filename LIKE %:filename% AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndFilenameLikeAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("filename") String filename);
|
||||
|
||||
/**
|
||||
* 根据MIME类型查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.mimeType = :mimeType AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndMimeTypeAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("mimeType") String mimeType);
|
||||
|
||||
/**
|
||||
* 根据文件大小范围查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.fileSize BETWEEN :minSize AND :maxSize AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndFileSizeBetween(@Param("userId") Long userId,
|
||||
@Param("minSize") Long minSize,
|
||||
@Param("maxSize") Long maxSize);
|
||||
|
||||
/**
|
||||
* 根据上传时间范围查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.uploadTime BETWEEN :startTime AND :endTime AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findByUserIdAndUploadTimeBetween(@Param("userId") Long userId,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
@Query("SELECT COUNT(f) > 0 FROM File f WHERE f.storedFilename = :storedFilename AND f.deletedAt IS NULL")
|
||||
boolean existsByStoredFilenameAndNotDeleted(@Param("storedFilename") String storedFilename);
|
||||
|
||||
/**
|
||||
* 统计用户文件总数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
long countByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计用户文件总大小
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(f.fileSize), 0) FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
long sumFileSizeByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计会话文件数量
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM File f WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL")
|
||||
long countBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据文件类型统计数量
|
||||
*/
|
||||
@Query("SELECT f.fileType, COUNT(f) FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL GROUP BY f.fileType")
|
||||
List<Object[]> countByFileTypeAndUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找大文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.fileSize > :threshold AND f.deletedAt IS NULL ORDER BY f.fileSize DESC")
|
||||
List<File> findLargeFilesByUserId(@Param("userId") Long userId,
|
||||
@Param("threshold") Long threshold);
|
||||
|
||||
/**
|
||||
* 查找图片文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.mimeType LIKE 'image/%' AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findImageFilesByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找文档文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.fileType IN ('pdf', 'doc', 'docx', 'txt', 'md') AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
List<File> findDocumentFilesByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 软删除文件
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE File f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.fileId = :fileId")
|
||||
void softDeleteByFileId(@Param("fileId") String fileId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除会话的所有文件
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE File f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.sessionId = :sessionId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除用户的所有文件
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE File f SET f.deletedAt = :deletedAt, f.updatedAt = :updateTime WHERE f.userId = :userId AND f.deletedAt IS NULL")
|
||||
void softDeleteAllByUserId(@Param("userId") Long userId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 检查文件是否属于指定用户
|
||||
*/
|
||||
@Query("SELECT COUNT(f) > 0 FROM File f WHERE f.fileId = :fileId AND f.userId = :userId AND f.deletedAt IS NULL")
|
||||
boolean existsByFileIdAndUserIdAndNotDeleted(@Param("fileId") String fileId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找用户最近上传的文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.userId = :userId AND f.deletedAt IS NULL ORDER BY f.uploadTime DESC")
|
||||
Page<File> findRecentFilesByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找旧文件(用于清理)
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.uploadTime < :threshold AND f.deletedAt IS NULL")
|
||||
List<File> findOldFiles(@Param("threshold") Long threshold);
|
||||
|
||||
/**
|
||||
* 统计今日上传文件数量
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM File f WHERE f.uploadTime >= :todayStart AND f.deletedAt IS NULL")
|
||||
long countTodayUploads(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 统计今日上传文件总大小
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(f.fileSize), 0) FROM File f WHERE f.uploadTime >= :todayStart AND f.deletedAt IS NULL")
|
||||
long sumTodayUploadSize(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 统计系统总文件数
|
||||
*/
|
||||
@Query("SELECT COUNT(f) FROM File f WHERE f.deletedAt IS NULL")
|
||||
long countAllNotDeleted();
|
||||
|
||||
/**
|
||||
* 统计系统文件总大小
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(f.fileSize), 0) FROM File f WHERE f.deletedAt IS NULL")
|
||||
long sumAllFileSize();
|
||||
|
||||
/**
|
||||
* 根据文件路径查找文件
|
||||
*/
|
||||
@Query("SELECT f FROM File f WHERE f.filePath = :filePath AND f.deletedAt IS NULL")
|
||||
Optional<File> findByFilePathAndNotDeleted(@Param("filePath") String filePath);
|
||||
}
|
||||
199
src/main/java/com/lxy/hsend/repository/MessageRepository.java
Normal file
199
src/main/java/com/lxy/hsend/repository/MessageRepository.java
Normal file
@ -0,0 +1,199 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 消息数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface MessageRepository extends JpaRepository<Message, Long> {
|
||||
|
||||
/**
|
||||
* 根据消息ID查找消息
|
||||
*/
|
||||
Optional<Message> findByMessageId(String messageId);
|
||||
|
||||
/**
|
||||
* 根据消息ID查找未删除的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.messageId = :messageId AND m.deletedAt IS NULL")
|
||||
Optional<Message> findByMessageIdAndNotDeleted(@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找所有消息
|
||||
*/
|
||||
List<Message> findBySessionId(String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找未删除的消息,按时间排序
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findBySessionIdAndNotDeletedOrderByTimestamp(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID分页查找未删除的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
Page<Message> findBySessionIdAndNotDeleted(@Param("sessionId") String sessionId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.userId = :userId AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findByUserIdAndNotDeleted(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据角色查找消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.role = :role AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findBySessionIdAndRoleAndNotDeleted(@Param("sessionId") String sessionId,
|
||||
@Param("role") String role);
|
||||
|
||||
/**
|
||||
* 查找收藏的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.isFavorited = true AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findFavoritedBySessionId(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找所有收藏的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.userId = :userId AND m.isFavorited = true AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findFavoritedByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据内容关键词搜索消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.content LIKE %:keyword% AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findBySessionIdAndContentContaining(@Param("sessionId") String sessionId,
|
||||
@Param("keyword") String keyword);
|
||||
|
||||
/**
|
||||
* 根据AI模型查找消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.modelUsed = :model AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findByModelUsed(@Param("model") String model, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计会话消息数量
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL")
|
||||
long countBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 统计用户消息数量
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM Message m WHERE m.userId = :userId AND m.deletedAt IS NULL")
|
||||
long countByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计用户收藏消息数量
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM Message m WHERE m.userId = :userId AND m.isFavorited = true AND m.deletedAt IS NULL")
|
||||
long countFavoritedByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 获取会话的最后一条消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findLastMessageBySessionId(@Param("sessionId") String sessionId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 获取会话的第一条消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
Page<Message> findFirstMessageBySessionId(@Param("sessionId") String sessionId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找指定时间范围内的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.sessionId = :sessionId AND m.timestamp BETWEEN :startTime AND :endTime AND m.deletedAt IS NULL ORDER BY m.timestamp ASC")
|
||||
List<Message> findBySessionIdAndTimestampBetween(@Param("sessionId") String sessionId,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 更新消息收藏状态
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.isFavorited = :isFavorited, m.updatedAt = :updateTime WHERE m.messageId = :messageId")
|
||||
void updateFavoriteStatus(@Param("messageId") String messageId,
|
||||
@Param("isFavorited") Boolean isFavorited,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 软删除消息
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.deletedAt = :deletedAt, m.updatedAt = :updateTime WHERE m.messageId = :messageId")
|
||||
void softDeleteByMessageId(@Param("messageId") String messageId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 软删除会话的所有消息
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.deletedAt = :deletedAt, m.updatedAt = :updateTime WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL")
|
||||
void softDeleteAllBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量取消收藏
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.isFavorited = false, m.updatedAt = :updateTime WHERE m.messageId IN :messageIds")
|
||||
void unfavoriteMessages(@Param("messageIds") List<String> messageIds,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 检查消息是否属于指定用户
|
||||
*/
|
||||
@Query("SELECT COUNT(m) > 0 FROM Message m WHERE m.messageId = :messageId AND m.userId = :userId AND m.deletedAt IS NULL")
|
||||
boolean existsByMessageIdAndUserIdAndNotDeleted(@Param("messageId") String messageId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找使用深度思考的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.userId = :userId AND m.deepThinking = true AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findDeepThinkingMessages(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找使用联网搜索的消息
|
||||
*/
|
||||
@Query("SELECT m FROM Message m WHERE m.userId = :userId AND m.webSearch = true AND m.deletedAt IS NULL ORDER BY m.timestamp DESC")
|
||||
Page<Message> findWebSearchMessages(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计各模型使用次数
|
||||
*/
|
||||
@Query("SELECT m.modelUsed, COUNT(m) FROM Message m WHERE m.modelUsed IS NOT NULL AND m.deletedAt IS NULL GROUP BY m.modelUsed")
|
||||
List<Object[]> countByModelUsage();
|
||||
|
||||
/**
|
||||
* 统计今日消息数量
|
||||
*/
|
||||
@Query("SELECT COUNT(m) FROM Message m WHERE m.timestamp >= :todayStart AND m.deletedAt IS NULL")
|
||||
long countTodayMessages(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 软删除会话的所有消息
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Message m SET m.deletedAt = :deletedAt, m.updatedAt = :updateTime WHERE m.sessionId = :sessionId AND m.deletedAt IS NULL")
|
||||
void softDeleteBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
}
|
||||
162
src/main/java/com/lxy/hsend/repository/SessionRepository.java
Normal file
162
src/main/java/com/lxy/hsend/repository/SessionRepository.java
Normal file
@ -0,0 +1,162 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 会话数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface SessionRepository extends JpaRepository<Session, Long> {
|
||||
|
||||
/**
|
||||
* 根据会话ID查找会话
|
||||
*/
|
||||
Optional<Session> findBySessionId(String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID查找未删除的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.sessionId = :sessionId AND s.deletedAt IS NULL")
|
||||
Optional<Session> findBySessionIdAndNotDeleted(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找所有会话
|
||||
*/
|
||||
List<Session> findByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找未删除的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.deletedAt IS NULL ORDER BY s.lastMessageTime DESC NULLS LAST, s.createdAt DESC")
|
||||
List<Session> findByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查找未删除的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.deletedAt IS NULL")
|
||||
Page<Session> findByUserIdAndNotDeleted(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据标题关键词搜索会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.title LIKE %:keyword% AND s.deletedAt IS NULL ORDER BY s.lastMessageTime DESC NULLS LAST")
|
||||
Page<Session> findByUserIdAndTitleContainingAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("keyword") String keyword,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据关键词搜索会话(搜索标题和消息内容)
|
||||
*/
|
||||
@Query("SELECT DISTINCT s FROM Session s LEFT JOIN Message m ON s.sessionId = m.sessionId " +
|
||||
"WHERE s.userId = :userId AND s.deletedAt IS NULL " +
|
||||
"AND (s.title LIKE %:keyword% OR m.content LIKE %:keyword%) " +
|
||||
"ORDER BY s.lastMessageTime DESC NULLS LAST, s.createdAt DESC")
|
||||
Page<Session> findByUserIdAndKeywordAndNotDeleted(@Param("userId") Long userId,
|
||||
@Param("keyword") String keyword,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据会话ID和用户ID查找未删除的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.sessionId = :sessionId AND s.userId = :userId AND s.deletedAt IS NULL")
|
||||
Optional<Session> findBySessionIdAndUserIdAndNotDeleted(@Param("sessionId") String sessionId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID和时间范围查找会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.createdAt BETWEEN :startTime AND :endTime AND s.deletedAt IS NULL")
|
||||
List<Session> findByUserIdAndCreatedAtBetween(@Param("userId") Long userId,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 统计用户的会话总数
|
||||
*/
|
||||
@Query("SELECT COUNT(s) FROM Session s WHERE s.userId = :userId AND s.deletedAt IS NULL")
|
||||
long countByUserIdAndNotDeleted(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找用户最近的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.deletedAt IS NULL ORDER BY s.lastMessageTime DESC NULLS LAST, s.createdAt DESC")
|
||||
Page<Session> findRecentSessionsByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找有消息的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.messageCount > 0 AND s.deletedAt IS NULL ORDER BY s.lastMessageTime DESC")
|
||||
List<Session> findSessionsWithMessages(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找空会话(没有消息的会话)
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.userId = :userId AND (s.messageCount = 0 OR s.messageCount IS NULL) AND s.deletedAt IS NULL")
|
||||
List<Session> findEmptySessions(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 更新会话的消息统计
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Session s SET s.messageCount = :messageCount, s.lastMessageTime = :lastMessageTime, s.updatedAt = :updateTime WHERE s.sessionId = :sessionId")
|
||||
void updateMessageStatistics(@Param("sessionId") String sessionId,
|
||||
@Param("messageCount") Integer messageCount,
|
||||
@Param("lastMessageTime") Long lastMessageTime,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Session s SET s.title = :title, s.updatedAt = :updateTime WHERE s.sessionId = :sessionId")
|
||||
void updateTitle(@Param("sessionId") String sessionId,
|
||||
@Param("title") String title,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 软删除会话
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Session s SET s.deletedAt = :deletedAt, s.updatedAt = :updateTime WHERE s.sessionId = :sessionId")
|
||||
void softDeleteBySessionId(@Param("sessionId") String sessionId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量软删除用户的所有会话
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Session s SET s.deletedAt = :deletedAt, s.updatedAt = :updateTime WHERE s.userId = :userId AND s.deletedAt IS NULL")
|
||||
void softDeleteAllByUserId(@Param("userId") Long userId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 检查会话是否属于指定用户
|
||||
*/
|
||||
@Query("SELECT COUNT(s) > 0 FROM Session s WHERE s.sessionId = :sessionId AND s.userId = :userId AND s.deletedAt IS NULL")
|
||||
boolean existsBySessionIdAndUserIdAndNotDeleted(@Param("sessionId") String sessionId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查找长时间未活动的会话
|
||||
*/
|
||||
@Query("SELECT s FROM Session s WHERE s.lastMessageTime < :threshold AND s.deletedAt IS NULL")
|
||||
List<Session> findInactiveSessions(@Param("threshold") Long threshold);
|
||||
|
||||
/**
|
||||
* 统计系统总会话数
|
||||
*/
|
||||
@Query("SELECT COUNT(s) FROM Session s WHERE s.deletedAt IS NULL")
|
||||
long countAllNotDeleted();
|
||||
}
|
||||
132
src/main/java/com/lxy/hsend/repository/UserRepository.java
Normal file
132
src/main/java/com/lxy/hsend/repository/UserRepository.java
Normal file
@ -0,0 +1,132 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.User;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 用户数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
/**
|
||||
* 根据邮箱查找用户
|
||||
*/
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
/**
|
||||
* 根据邮箱查找未删除的用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.email = :email AND u.deletedAt IS NULL")
|
||||
Optional<User> findByEmailAndNotDeleted(@Param("email") String email);
|
||||
|
||||
/**
|
||||
* 根据邮箱查找未删除的用户(Spring Data方法命名)
|
||||
*/
|
||||
Optional<User> findByEmailAndDeletedAtIsNull(String email);
|
||||
|
||||
/**
|
||||
* 根据ID查找未删除的用户
|
||||
*/
|
||||
Optional<User> findByIdAndDeletedAtIsNull(Long id);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否存在(未删除)
|
||||
*/
|
||||
boolean existsByEmailAndDeletedAtIsNull(String email);
|
||||
|
||||
/**
|
||||
* 根据昵称查找用户
|
||||
*/
|
||||
List<User> findByNicknameContaining(String nickname);
|
||||
|
||||
/**
|
||||
* 根据状态查找用户
|
||||
*/
|
||||
List<User> findByStatus(Byte status);
|
||||
|
||||
/**
|
||||
* 查找未删除的用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.deletedAt IS NULL")
|
||||
List<User> findAllNotDeleted();
|
||||
|
||||
/**
|
||||
* 分页查找未删除的用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.deletedAt IS NULL")
|
||||
Page<User> findAllNotDeleted(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*/
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在(排除指定用户)
|
||||
*/
|
||||
@Query("SELECT COUNT(u) > 0 FROM User u WHERE u.email = :email AND u.id != :userId AND u.deletedAt IS NULL")
|
||||
boolean existsByEmailAndNotUserId(@Param("email") String email, @Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据状态和创建时间范围查找用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.status = :status AND u.createdAt BETWEEN :startTime AND :endTime AND u.deletedAt IS NULL")
|
||||
List<User> findByStatusAndCreatedAtBetween(@Param("status") Byte status,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
/**
|
||||
* 统计正常状态的用户数量
|
||||
*/
|
||||
@Query("SELECT COUNT(u) FROM User u WHERE u.status = 1 AND u.deletedAt IS NULL")
|
||||
long countActiveUsers();
|
||||
|
||||
/**
|
||||
* 统计今日注册用户数量
|
||||
*/
|
||||
@Query("SELECT COUNT(u) FROM User u WHERE u.createdAt >= :todayStart AND u.deletedAt IS NULL")
|
||||
long countTodayRegistrations(@Param("todayStart") Long todayStart);
|
||||
|
||||
/**
|
||||
* 更新用户最后登录时间
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE User u SET u.lastLoginTime = :loginTime, u.updatedAt = :updateTime WHERE u.id = :userId")
|
||||
void updateLastLoginTime(@Param("userId") Long userId,
|
||||
@Param("loginTime") Long loginTime,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 软删除用户
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE User u SET u.deletedAt = :deletedAt, u.status = 3, u.updatedAt = :updateTime WHERE u.id = :userId")
|
||||
void softDeleteUser(@Param("userId") Long userId,
|
||||
@Param("deletedAt") Long deletedAt,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 批量更新用户状态
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE User u SET u.status = :status, u.updatedAt = :updateTime WHERE u.id IN :userIds")
|
||||
void updateStatusByIds(@Param("userIds") List<Long> userIds,
|
||||
@Param("status") Byte status,
|
||||
@Param("updateTime") Long updateTime);
|
||||
|
||||
/**
|
||||
* 查找长期未登录的用户
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.lastLoginTime < :threshold AND u.deletedAt IS NULL")
|
||||
List<User> findInactiveUsers(@Param("threshold") Long threshold);
|
||||
}
|
||||
243
src/main/java/com/lxy/hsend/service/FavoriteService.java
Normal file
243
src/main/java/com/lxy/hsend/service/FavoriteService.java
Normal file
@ -0,0 +1,243 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.dto.favorite.AddFavoriteRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteListRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteResponse;
|
||||
import com.lxy.hsend.dto.favorite.RemoveFavoriteRequest;
|
||||
import com.lxy.hsend.entity.Favorite;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.exception.BusinessException;
|
||||
import com.lxy.hsend.repository.FavoriteRepository;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 收藏服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FavoriteService {
|
||||
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final SessionRepository sessionRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* 用户收藏数量上限
|
||||
*/
|
||||
private static final long MAX_FAVORITES_PER_USER = 1000;
|
||||
|
||||
/**
|
||||
* 添加收藏
|
||||
*/
|
||||
@Transactional
|
||||
public ApiResponse<FavoriteResponse> addFavorite(Long userId, AddFavoriteRequest request) {
|
||||
// 1. 验证用户存在
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 验证消息存在且属于用户
|
||||
Message message = messageRepository.findByMessageIdAndNotDeleted(request.getMessageId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "消息不存在"));
|
||||
|
||||
if (!message.getUserId().equals(userId)) {
|
||||
throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "无权限收藏此消息");
|
||||
}
|
||||
|
||||
// 3. 验证会话存在且属于用户
|
||||
Session session = sessionRepository.findBySessionIdAndUserIdAndNotDeleted(request.getSessionId(), userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "会话不存在"));
|
||||
|
||||
// 4. 检查是否已经收藏
|
||||
if (favoriteRepository.existsByUserIdAndMessageIdAndNotDeleted(userId, request.getMessageId())) {
|
||||
throw new BusinessException(ErrorCode.BUSINESS_ERROR, "该消息已经收藏过了");
|
||||
}
|
||||
|
||||
// 5. 检查用户收藏数量是否超过限制
|
||||
long userFavoriteCount = favoriteRepository.countByUserIdAndNotDeleted(userId);
|
||||
if (userFavoriteCount >= MAX_FAVORITES_PER_USER) {
|
||||
throw new BusinessException(ErrorCode.BUSINESS_ERROR, "收藏数量已达上限(" + MAX_FAVORITES_PER_USER + "条)");
|
||||
}
|
||||
|
||||
// 6. 创建收藏记录
|
||||
Favorite favorite = createFavorite(userId, request.getMessageId(), request.getSessionId());
|
||||
Favorite savedFavorite = favoriteRepository.save(favorite);
|
||||
|
||||
log.info("用户添加收藏成功: userId={}, messageId={}, favoriteId={}",
|
||||
userId, request.getMessageId(), savedFavorite.getFavoriteId());
|
||||
|
||||
// 7. 转换为响应DTO
|
||||
FavoriteResponse response = convertToFavoriteResponse(savedFavorite, message, session);
|
||||
return ApiResponse.success(response, "收藏成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消收藏
|
||||
*/
|
||||
@Transactional
|
||||
public ApiResponse<Void> removeFavorite(Long userId, RemoveFavoriteRequest request) {
|
||||
// 1. 验证用户存在
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 查找收藏记录
|
||||
Favorite favorite = favoriteRepository.findByUserIdAndMessageIdAndNotDeleted(userId, request.getMessageId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "收藏记录不存在"));
|
||||
|
||||
// 3. 软删除收藏记录
|
||||
Long currentTime = TimeUtil.getCurrentTimestamp();
|
||||
favoriteRepository.softDeleteByUserIdAndMessageId(userId, request.getMessageId(), currentTime, currentTime);
|
||||
|
||||
log.info("用户取消收藏成功: userId={}, messageId={}, favoriteId={}",
|
||||
userId, request.getMessageId(), favorite.getFavoriteId());
|
||||
|
||||
return ApiResponse.success(null, "取消收藏成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收藏列表
|
||||
*/
|
||||
public ApiResponse<PageUtil.PageResponse<FavoriteResponse>> getFavoriteList(Long userId, FavoriteListRequest request) {
|
||||
// 1. 验证用户存在
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 构建分页参数
|
||||
Pageable pageable = PageRequest.of(request.getPage() - 1, request.getPageSize());
|
||||
|
||||
// 3. 查询收藏列表
|
||||
Page<Favorite> favoritePage;
|
||||
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
// 有关键词搜索
|
||||
if ("all".equals(request.getMessageRole())) {
|
||||
favoritePage = favoriteRepository.findByUserIdAndKeywordAndNotDeleted(userId, request.getKeyword(), pageable);
|
||||
} else {
|
||||
favoritePage = favoriteRepository.findByUserIdAndKeywordAndRoleAndNotDeleted(
|
||||
userId, request.getKeyword(), request.getMessageRole(), pageable);
|
||||
}
|
||||
} else {
|
||||
// 无关键词,查询所有收藏
|
||||
favoritePage = favoriteRepository.findByUserIdAndNotDeleted(userId, pageable);
|
||||
}
|
||||
|
||||
// 4. 转换为响应DTO列表
|
||||
List<FavoriteResponse> favoriteResponses = favoritePage.getContent().stream()
|
||||
.map(this::convertToFavoriteResponseWithLookup)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 5. 构建分页响应
|
||||
PageUtil.PageResponse<FavoriteResponse> pageResponse = new PageUtil.PageResponse<>(
|
||||
favoriteResponses,
|
||||
request.getPage(),
|
||||
request.getPageSize(),
|
||||
favoritePage.getTotalElements(),
|
||||
favoritePage.getTotalPages(),
|
||||
favoritePage.isFirst(),
|
||||
favoritePage.isLast(),
|
||||
favoritePage.hasNext(),
|
||||
favoritePage.hasPrevious()
|
||||
);
|
||||
|
||||
return ApiResponse.success(pageResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否被收藏
|
||||
*/
|
||||
public ApiResponse<Boolean> isFavorited(Long userId, String messageId) {
|
||||
boolean isFavorited = favoriteRepository.existsByUserIdAndMessageIdAndNotDeleted(userId, messageId);
|
||||
return ApiResponse.success(isFavorited);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户收藏数量
|
||||
*/
|
||||
public ApiResponse<Long> getUserFavoriteCount(Long userId) {
|
||||
// 1. 验证用户存在
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 统计收藏数量
|
||||
long count = favoriteRepository.countByUserIdAndNotDeleted(userId);
|
||||
return ApiResponse.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建收藏对象
|
||||
*/
|
||||
private Favorite createFavorite(Long userId, String messageId, String sessionId) {
|
||||
Favorite favorite = new Favorite();
|
||||
favorite.setFavoriteId(UUID.randomUUID().toString());
|
||||
favorite.setUserId(userId);
|
||||
favorite.setMessageId(messageId);
|
||||
favorite.setSessionId(sessionId);
|
||||
|
||||
Long currentTime = TimeUtil.getCurrentTimestamp();
|
||||
favorite.setCreatedAt(currentTime);
|
||||
favorite.setUpdatedAt(currentTime);
|
||||
|
||||
return favorite;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为收藏响应DTO(使用已有的消息和会话对象)
|
||||
*/
|
||||
private FavoriteResponse convertToFavoriteResponse(Favorite favorite, Message message, Session session) {
|
||||
return FavoriteResponse.builder()
|
||||
.favoriteId(favorite.getFavoriteId())
|
||||
.messageId(favorite.getMessageId())
|
||||
.sessionId(favorite.getSessionId())
|
||||
.sessionTitle(session.getTitle())
|
||||
.messageContent(message.getContent())
|
||||
.messageRole(message.getRole())
|
||||
.messageCreatedAt(TimeUtil.timestampToLocalDateTime(message.getCreatedAt()))
|
||||
.favoritedAt(TimeUtil.timestampToLocalDateTime(favorite.getCreatedAt()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为收藏响应DTO(需要查询消息和会话信息)
|
||||
*/
|
||||
private FavoriteResponse convertToFavoriteResponseWithLookup(Favorite favorite) {
|
||||
// 查询消息信息
|
||||
Message message = messageRepository.findByMessageIdAndNotDeleted(favorite.getMessageId())
|
||||
.orElse(null);
|
||||
|
||||
// 查询会话信息
|
||||
Session session = sessionRepository.findBySessionIdAndNotDeleted(favorite.getSessionId())
|
||||
.orElse(null);
|
||||
|
||||
return FavoriteResponse.builder()
|
||||
.favoriteId(favorite.getFavoriteId())
|
||||
.messageId(favorite.getMessageId())
|
||||
.sessionId(favorite.getSessionId())
|
||||
.sessionTitle(session != null ? session.getTitle() : "未知会话")
|
||||
.messageContent(message != null ? message.getContent() : "消息已删除")
|
||||
.messageRole(message != null ? message.getRole() : "unknown")
|
||||
.messageCreatedAt(message != null ? TimeUtil.timestampToLocalDateTime(message.getCreatedAt()) : null)
|
||||
.favoritedAt(TimeUtil.timestampToLocalDateTime(favorite.getCreatedAt()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
244
src/main/java/com/lxy/hsend/service/MessageService.java
Normal file
244
src/main/java/com/lxy/hsend/service/MessageService.java
Normal file
@ -0,0 +1,244 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.dto.message.MessageListRequest;
|
||||
import com.lxy.hsend.dto.message.MessageResponse;
|
||||
import com.lxy.hsend.dto.message.SendMessageRequest;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.exception.BusinessException;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 消息服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MessageService {
|
||||
|
||||
private final MessageRepository messageRepository;
|
||||
private final SessionRepository sessionRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
@Transactional
|
||||
public ApiResponse<MessageResponse> sendMessage(Long userId, SendMessageRequest request) {
|
||||
// 1. 验证用户
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 验证会话
|
||||
Session session = sessionRepository.findBySessionIdAndUserIdAndNotDeleted(request.getSessionId(), userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "会话不存在"));
|
||||
|
||||
// 3. 内容过滤和验证
|
||||
String filteredContent = filterMessageContent(request.getContent());
|
||||
if (filteredContent.trim().isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "消息内容不能为空");
|
||||
}
|
||||
|
||||
// 4. 创建用户消息
|
||||
Message userMessage = createMessage(
|
||||
userId,
|
||||
request.getSessionId(),
|
||||
"user",
|
||||
filteredContent,
|
||||
request.getDeepThinking(),
|
||||
request.getWebSearch()
|
||||
);
|
||||
|
||||
// 5. 保存用户消息
|
||||
Message savedUserMessage = messageRepository.save(userMessage);
|
||||
log.info("用户消息已保存: userId={}, sessionId={}, messageId={}",
|
||||
userId, request.getSessionId(), savedUserMessage.getMessageId());
|
||||
|
||||
// 6. 更新会话统计
|
||||
updateSessionStatistics(session, savedUserMessage);
|
||||
|
||||
// 7. 预留AI响应生成(暂时返回占位响应)
|
||||
// TODO: 集成AI服务生成响应
|
||||
Message aiMessage = createAIPlaceholderMessage(userId, request.getSessionId(), savedUserMessage);
|
||||
Message savedAiMessage = messageRepository.save(aiMessage);
|
||||
|
||||
// 8. 再次更新会话统计
|
||||
updateSessionStatistics(session, savedAiMessage);
|
||||
|
||||
// 9. 返回AI消息响应
|
||||
MessageResponse response = convertToMessageResponse(savedAiMessage);
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息列表
|
||||
*/
|
||||
public ApiResponse<PageUtil.PageResponse<MessageResponse>> getMessages(Long userId, MessageListRequest request) {
|
||||
// 1. 验证用户
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
|
||||
// 2. 验证会话权限
|
||||
Session session = sessionRepository.findBySessionIdAndUserIdAndNotDeleted(request.getSessionId(), userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "会话不存在"));
|
||||
|
||||
// 3. 构建分页参数
|
||||
Pageable pageable = PageRequest.of(request.getPage() - 1, request.getPageSize());
|
||||
|
||||
// 4. 查询消息
|
||||
Page<Message> messagePage;
|
||||
if ("all".equals(request.getRole())) {
|
||||
messagePage = messageRepository.findBySessionIdAndNotDeleted(request.getSessionId(), pageable);
|
||||
} else {
|
||||
// 如果指定了角色过滤,需要添加对应的repository方法
|
||||
messagePage = messageRepository.findBySessionIdAndNotDeleted(request.getSessionId(), pageable);
|
||||
}
|
||||
|
||||
// 5. 转换为响应DTO
|
||||
List<MessageResponse> messageResponses = messagePage.getContent().stream()
|
||||
.filter(message -> "all".equals(request.getRole()) || request.getRole().equals(message.getRole()))
|
||||
.map(this::convertToMessageResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 6. 构建分页响应
|
||||
PageUtil.PageResponse<MessageResponse> pageResponse = new PageUtil.PageResponse<>(
|
||||
messageResponses,
|
||||
request.getPage(),
|
||||
request.getPageSize(),
|
||||
messagePage.getTotalElements(),
|
||||
messagePage.getTotalPages(),
|
||||
messagePage.isFirst(),
|
||||
messagePage.isLast(),
|
||||
messagePage.hasNext(),
|
||||
messagePage.hasPrevious()
|
||||
);
|
||||
|
||||
return ApiResponse.success(pageResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据消息ID获取消息详情
|
||||
*/
|
||||
public ApiResponse<MessageResponse> getMessageById(Long userId, String messageId) {
|
||||
// 1. 验证消息存在且属于用户
|
||||
if (!messageRepository.existsByMessageIdAndUserIdAndNotDeleted(messageId, userId)) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "消息不存在");
|
||||
}
|
||||
|
||||
// 2. 获取消息
|
||||
Message message = messageRepository.findByMessageIdAndNotDeleted(messageId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "消息不存在"));
|
||||
|
||||
// 3. 转换为响应DTO
|
||||
MessageResponse response = convertToMessageResponse(message);
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息对象
|
||||
*/
|
||||
private Message createMessage(Long userId, String sessionId, String role, String content,
|
||||
Boolean deepThinking, Boolean webSearch) {
|
||||
Message message = new Message();
|
||||
message.setMessageId(UUID.randomUUID().toString());
|
||||
message.setSessionId(sessionId);
|
||||
message.setUserId(userId);
|
||||
message.setRole(role);
|
||||
message.setContent(content);
|
||||
// 消息状态通过其他字段表示,无需单独status字段
|
||||
message.setDeepThinking(deepThinking != null ? deepThinking : false);
|
||||
message.setWebSearch(webSearch != null ? webSearch : false);
|
||||
message.setIsFavorited(false);
|
||||
|
||||
Long currentTime = TimeUtil.getCurrentTimestamp();
|
||||
message.setTimestamp(currentTime);
|
||||
message.setCreatedAt(currentTime);
|
||||
message.setUpdatedAt(currentTime);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建AI占位响应消息
|
||||
*/
|
||||
private Message createAIPlaceholderMessage(Long userId, String sessionId, Message userMessage) {
|
||||
String aiContent = generateAIPlaceholderResponse(userMessage.getContent());
|
||||
|
||||
return createMessage(
|
||||
userId,
|
||||
sessionId,
|
||||
"assistant",
|
||||
aiContent,
|
||||
userMessage.getDeepThinking(),
|
||||
userMessage.getWebSearch()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI占位响应(后续将被实际AI服务替换)
|
||||
*/
|
||||
private String generateAIPlaceholderResponse(String userContent) {
|
||||
return "感谢您的提问。这是一个临时响应,实际的AI服务将在后续版本中集成。您的问题是:" + userContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息内容过滤
|
||||
*/
|
||||
private String filterMessageContent(String content) {
|
||||
if (content == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 基础过滤:去除首尾空格,限制长度
|
||||
String filtered = content.trim();
|
||||
|
||||
// TODO: 添加更多过滤规则(敏感词、恶意内容等)
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话统计
|
||||
*/
|
||||
private void updateSessionStatistics(Session session, Message message) {
|
||||
session.setMessageCount(session.getMessageCount() + 1);
|
||||
session.setLastMessageTime(message.getTimestamp());
|
||||
session.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
sessionRepository.save(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为消息响应DTO
|
||||
*/
|
||||
private MessageResponse convertToMessageResponse(Message message) {
|
||||
return MessageResponse.builder()
|
||||
.messageId(message.getMessageId())
|
||||
.sessionId(message.getSessionId())
|
||||
.role(message.getRole())
|
||||
.content(message.getContent())
|
||||
.status("completed")
|
||||
.deepThinking(message.getDeepThinking())
|
||||
.webSearch(message.getWebSearch())
|
||||
.createdAt(TimeUtil.timestampToLocalDateTime(message.getCreatedAt()))
|
||||
.updatedAt(TimeUtil.timestampToLocalDateTime(message.getUpdatedAt()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
307
src/main/java/com/lxy/hsend/service/SessionService.java
Normal file
307
src/main/java/com/lxy/hsend/service/SessionService.java
Normal file
@ -0,0 +1,307 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.dto.session.*;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.exception.BusinessException;
|
||||
import com.lxy.hsend.repository.FavoriteRepository;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 会话服务类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
public class SessionService {
|
||||
|
||||
private final SessionRepository sessionRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
|
||||
@Autowired
|
||||
public SessionService(SessionRepository sessionRepository,
|
||||
MessageRepository messageRepository,
|
||||
FavoriteRepository favoriteRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.messageRepository = messageRepository;
|
||||
this.favoriteRepository = favoriteRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*/
|
||||
public SessionDetailResponse createSession(Long userId, CreateSessionRequest request) {
|
||||
log.info("创建会话: userId={}, title={}", userId, request.getTitle());
|
||||
|
||||
// 创建会话实体
|
||||
Session session = new Session();
|
||||
session.setSessionId(UUID.randomUUID().toString());
|
||||
session.setUserId(userId);
|
||||
session.setTitle(request.getTitle());
|
||||
session.setMessageCount(0);
|
||||
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
session.setCreatedAt(now);
|
||||
session.setUpdatedAt(now);
|
||||
|
||||
// 保存会话
|
||||
Session savedSession = sessionRepository.save(session);
|
||||
|
||||
// 如果有初始消息,创建第一条消息
|
||||
if (StringUtil.isNotEmpty(request.getInitialMessage())) {
|
||||
Message initialMessage = new Message();
|
||||
initialMessage.setSessionId(savedSession.getSessionId());
|
||||
initialMessage.setUserId(userId);
|
||||
initialMessage.setContent(request.getInitialMessage());
|
||||
initialMessage.setRole("user");
|
||||
initialMessage.setCreatedAt(now);
|
||||
initialMessage.setUpdatedAt(now);
|
||||
|
||||
messageRepository.save(initialMessage);
|
||||
|
||||
// 更新会话统计
|
||||
updateSessionStatistics(savedSession.getSessionId());
|
||||
}
|
||||
|
||||
log.info("会话创建成功: sessionId={}", savedSession.getSessionId());
|
||||
return convertToSessionDetailResponse(savedSession);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表(支持搜索和分页)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<SessionListResponse> getSessionList(Long userId, SessionSearchRequest request) {
|
||||
log.debug("获取会话列表: userId={}, keyword={}, page={}, pageSize={}",
|
||||
userId, request.getKeyword(), request.getPage(), request.getPageSize());
|
||||
|
||||
// 创建分页对象
|
||||
Sort sort = createSort(request.getValidSortBy(), request.isDescOrder());
|
||||
Pageable pageable = PageRequest.of(request.getPage() - 1, request.getPageSize(), sort);
|
||||
|
||||
Page<Session> sessionPage;
|
||||
|
||||
// 根据是否有关键词选择不同的查询方法
|
||||
if (request.hasKeyword()) {
|
||||
sessionPage = sessionRepository.findByUserIdAndKeywordAndNotDeleted(
|
||||
userId, request.getCleanKeyword(), pageable);
|
||||
} else {
|
||||
sessionPage = sessionRepository.findByUserIdAndNotDeleted(userId, pageable);
|
||||
}
|
||||
|
||||
// 转换为响应DTO
|
||||
return sessionPage.map(this::convertToSessionListResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话详情
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public SessionDetailResponse getSessionDetail(Long userId, String sessionId) {
|
||||
log.debug("获取会话详情: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
Session session = findSessionByIdAndUserId(sessionId, userId);
|
||||
return convertToSessionDetailResponse(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
*/
|
||||
public SessionDetailResponse updateSession(Long userId, String sessionId, UpdateSessionRequest request) {
|
||||
log.info("更新会话: userId={}, sessionId={}, newTitle={}", userId, sessionId, request.getTitle());
|
||||
|
||||
// 验证会话存在和权限
|
||||
Session session = findSessionByIdAndUserId(sessionId, userId);
|
||||
|
||||
if (StringUtil.isNotEmpty(request.getTitle()) && !StringUtil.equals(session.getTitle(), request.getTitle())) {
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
sessionRepository.updateTitle(sessionId, request.getTitle(), now);
|
||||
|
||||
// 更新本地对象
|
||||
session.setTitle(request.getTitle());
|
||||
session.setUpdatedAt(now);
|
||||
|
||||
log.info("会话标题更新成功: sessionId={}", sessionId);
|
||||
}
|
||||
|
||||
return convertToSessionDetailResponse(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话(软删除,级联删除消息和收藏)
|
||||
*/
|
||||
public void deleteSession(Long userId, String sessionId) {
|
||||
log.info("删除会话: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
// 验证会话存在和权限
|
||||
findSessionByIdAndUserId(sessionId, userId);
|
||||
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
|
||||
// 软删除会话
|
||||
sessionRepository.softDeleteBySessionId(sessionId, now, now);
|
||||
|
||||
// 软删除关联的消息
|
||||
messageRepository.softDeleteBySessionId(sessionId, now, now);
|
||||
|
||||
// 删除收藏记录
|
||||
favoriteRepository.deleteBySessionId(sessionId);
|
||||
|
||||
log.info("会话删除成功: sessionId={}", sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空会话(只删除消息,保留会话)
|
||||
*/
|
||||
public void clearSession(Long userId, String sessionId) {
|
||||
log.info("清空会话: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
// 验证会话存在和权限
|
||||
findSessionByIdAndUserId(sessionId, userId);
|
||||
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
|
||||
// 软删除关联的消息
|
||||
messageRepository.softDeleteBySessionId(sessionId, now, now);
|
||||
|
||||
// 删除收藏记录
|
||||
favoriteRepository.deleteBySessionId(sessionId);
|
||||
|
||||
// 重置会话统计
|
||||
sessionRepository.updateMessageStatistics(sessionId, 0, null, now);
|
||||
|
||||
log.info("会话清空成功: sessionId={}", sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话统计信息
|
||||
*/
|
||||
public void updateSessionStatistics(String sessionId) {
|
||||
log.debug("更新会话统计: sessionId={}", sessionId);
|
||||
|
||||
// 获取消息统计
|
||||
long messageCount = messageRepository.countBySessionIdAndNotDeleted(sessionId);
|
||||
|
||||
// 获取最后一条消息的时间
|
||||
Long lastMessageTime = null;
|
||||
if (messageCount > 0) {
|
||||
Page<Message> lastMessagePage = messageRepository.findBySessionIdAndNotDeleted(
|
||||
sessionId, PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createdAt")));
|
||||
|
||||
if (!lastMessagePage.isEmpty()) {
|
||||
lastMessageTime = lastMessagePage.getContent().get(0).getCreatedAt();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
sessionRepository.updateMessageStatistics(sessionId, (int) messageCount, lastMessageTime, now);
|
||||
|
||||
log.debug("会话统计更新完成: sessionId={}, messageCount={}, lastMessageTime={}",
|
||||
sessionId, messageCount, lastMessageTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证会话权限
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public boolean hasSessionPermission(Long userId, String sessionId) {
|
||||
return sessionRepository.existsBySessionIdAndUserIdAndNotDeleted(sessionId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找用户的会话并验证权限
|
||||
*/
|
||||
private Session findSessionByIdAndUserId(String sessionId, Long userId) {
|
||||
return sessionRepository.findBySessionIdAndUserIdAndNotDeleted(sessionId, userId)
|
||||
.orElseThrow(() -> BusinessException.of(ErrorCode.RESOURCE_NOT_FOUND, "会话不存在或无访问权限"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建排序对象
|
||||
*/
|
||||
private Sort createSort(String sortBy, boolean isDesc) {
|
||||
Sort.Direction direction = isDesc ? Sort.Direction.DESC : Sort.Direction.ASC;
|
||||
|
||||
switch (sortBy) {
|
||||
case "created_at":
|
||||
return Sort.by(direction, "createdAt");
|
||||
case "updated_at":
|
||||
return Sort.by(direction, "updatedAt");
|
||||
case "last_message_time":
|
||||
default:
|
||||
// 对于 last_message_time,需要处理 NULL 值
|
||||
if (isDesc) {
|
||||
return Sort.by(Sort.Direction.DESC, "lastMessageTime")
|
||||
.and(Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
} else {
|
||||
return Sort.by(Sort.Direction.ASC, "lastMessageTime")
|
||||
.and(Sort.by(Sort.Direction.ASC, "createdAt"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为会话列表响应DTO
|
||||
*/
|
||||
private SessionListResponse convertToSessionListResponse(Session session) {
|
||||
SessionListResponse response = new SessionListResponse();
|
||||
response.setSessionId(session.getSessionId());
|
||||
response.setTitle(session.getTitle());
|
||||
response.setMessageCount(session.getMessageCount() != null ? session.getMessageCount() : 0);
|
||||
response.setLastMessageTime(session.getLastMessageTime());
|
||||
response.setCreatedAt(session.getCreatedAt());
|
||||
response.setUpdatedAt(session.getUpdatedAt());
|
||||
|
||||
// 获取最后一条消息的内容摘要
|
||||
if (session.getMessageCount() != null && session.getMessageCount() > 0) {
|
||||
try {
|
||||
Page<Message> lastMessagePage = messageRepository.findBySessionIdAndNotDeleted(
|
||||
session.getSessionId(), PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createdAt")));
|
||||
|
||||
if (!lastMessagePage.isEmpty()) {
|
||||
Message lastMessage = lastMessagePage.getContent().get(0);
|
||||
response.setLastMessageContent(lastMessage.getContent());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取最后消息内容失败: sessionId={}, error={}", session.getSessionId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为会话详情响应DTO
|
||||
*/
|
||||
private SessionDetailResponse convertToSessionDetailResponse(Session session) {
|
||||
SessionDetailResponse response = new SessionDetailResponse();
|
||||
response.setSessionId(session.getSessionId());
|
||||
response.setUserId(session.getUserId());
|
||||
response.setTitle(session.getTitle());
|
||||
response.setMessageCount(session.getMessageCount() != null ? session.getMessageCount() : 0);
|
||||
response.setLastMessageTime(session.getLastMessageTime());
|
||||
response.setCreatedAt(session.getCreatedAt());
|
||||
response.setUpdatedAt(session.getUpdatedAt());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
287
src/main/java/com/lxy/hsend/service/UserService.java
Normal file
287
src/main/java/com/lxy/hsend/service/UserService.java
Normal file
@ -0,0 +1,287 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import com.lxy.hsend.common.ErrorCode;
|
||||
import com.lxy.hsend.dto.auth.LoginRequest;
|
||||
import com.lxy.hsend.dto.auth.LoginResponse;
|
||||
import com.lxy.hsend.dto.auth.RegisterRequest;
|
||||
import com.lxy.hsend.dto.user.UpdateUserRequest;
|
||||
import com.lxy.hsend.dto.user.UserInfoResponse;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.exception.BusinessException;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.CryptoUtil;
|
||||
import com.lxy.hsend.util.JwtUtil;
|
||||
import com.lxy.hsend.util.StringUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 用户服务类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
public UserService(UserRepository userRepository, JwtUtil jwtUtil, RedisTemplate<String, Object> redisTemplate) {
|
||||
this.userRepository = userRepository;
|
||||
this.jwtUtil = jwtUtil;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
public UserInfoResponse register(RegisterRequest request) {
|
||||
log.info("用户注册请求: {}", request.getEmail());
|
||||
|
||||
// 验证确认密码
|
||||
if (!StringUtil.equals(request.getPassword(), request.getConfirmPassword())) {
|
||||
throw BusinessException.of(ErrorCode.VALIDATION_ERROR, "两次输入的密码不一致");
|
||||
}
|
||||
|
||||
// 转换邮箱为小写
|
||||
String email = request.getEmail().toLowerCase();
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (userRepository.existsByEmailAndDeletedAtIsNull(email)) {
|
||||
throw BusinessException.of(ErrorCode.USER_ALREADY_EXISTS, "该邮箱已被注册");
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
User user = new User();
|
||||
user.setEmail(email);
|
||||
user.setPasswordHash(CryptoUtil.encryptPassword(request.getPassword()));
|
||||
user.setNickname(request.getNickname());
|
||||
user.setAvatarUrl(StringUtil.isEmpty(request.getAvatarUrl()) ? getDefaultAvatarUrl() : request.getAvatarUrl());
|
||||
user.setStatus(Constants.USER_STATUS_NORMAL);
|
||||
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
user.setCreatedAt(now);
|
||||
user.setUpdatedAt(now);
|
||||
|
||||
// 保存用户
|
||||
User savedUser = userRepository.save(user);
|
||||
log.info("用户注册成功: userId={}, email={}", savedUser.getId(), savedUser.getEmail());
|
||||
|
||||
return convertToUserInfoResponse(savedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
public LoginResponse login(LoginRequest request) {
|
||||
String email = request.getEmail().toLowerCase();
|
||||
log.info("用户登录请求: {}", email);
|
||||
|
||||
// 检查登录失败次数
|
||||
checkLoginAttempts(email);
|
||||
|
||||
// 查找用户
|
||||
User user = userRepository.findByEmailAndDeletedAtIsNull(email)
|
||||
.orElseThrow(() -> BusinessException.of(ErrorCode.USER_NOT_FOUND, "用户不存在"));
|
||||
|
||||
// 检查用户状态
|
||||
checkUserStatus(user);
|
||||
|
||||
// 验证密码
|
||||
if (!CryptoUtil.verifyPassword(request.getPassword(), user.getPasswordHash())) {
|
||||
// 记录登录失败
|
||||
recordLoginFailure(email);
|
||||
throw BusinessException.of(ErrorCode.PASSWORD_ERROR, "密码错误");
|
||||
}
|
||||
|
||||
// 清除登录失败记录
|
||||
clearLoginFailures(email);
|
||||
|
||||
// 更新最后登录时间
|
||||
long now = TimeUtil.getCurrentTimestamp();
|
||||
user.setLastLoginTime(now);
|
||||
user.setUpdatedAt(now);
|
||||
userRepository.save(user);
|
||||
|
||||
// 生成Token
|
||||
String accessToken = jwtUtil.generateToken(user.getId(), user.getEmail());
|
||||
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getEmail());
|
||||
|
||||
// 缓存用户信息到Redis
|
||||
cacheUserInfo(user);
|
||||
|
||||
log.info("用户登录成功: userId={}, email={}", user.getId(), user.getEmail());
|
||||
|
||||
UserInfoResponse userInfo = convertToUserInfoResponse(user);
|
||||
return new LoginResponse(accessToken, refreshToken, Constants.JWT_EXPIRATION_TIME / 1000, userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
public void logout(Long userId, String token) {
|
||||
log.info("用户登出: userId={}", userId);
|
||||
|
||||
// 将token加入黑名单
|
||||
jwtUtil.blacklistToken(token);
|
||||
|
||||
// 清除Redis中的用户信息缓存
|
||||
String cacheKey = Constants.REDIS_USER_INFO_PREFIX + userId;
|
||||
redisTemplate.delete(cacheKey);
|
||||
|
||||
log.info("用户登出成功: userId={}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户信息
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public UserInfoResponse getUserById(Long userId) {
|
||||
// 先尝试从Redis获取
|
||||
String cacheKey = Constants.REDIS_USER_INFO_PREFIX + userId;
|
||||
UserInfoResponse cachedUser = (UserInfoResponse) redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cachedUser != null) {
|
||||
return cachedUser;
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> BusinessException.of(ErrorCode.USER_NOT_FOUND, "用户不存在"));
|
||||
|
||||
UserInfoResponse userInfo = convertToUserInfoResponse(user);
|
||||
|
||||
// 缓存到Redis
|
||||
redisTemplate.opsForValue().set(cacheKey, userInfo, Constants.REDIS_USER_INFO_EXPIRE_TIME, TimeUnit.SECONDS);
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
public UserInfoResponse updateUser(Long userId, UpdateUserRequest request) {
|
||||
log.info("更新用户信息: userId={}", userId);
|
||||
|
||||
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
|
||||
.orElseThrow(() -> BusinessException.of(ErrorCode.USER_NOT_FOUND, "用户不存在"));
|
||||
|
||||
// 更新字段
|
||||
boolean updated = false;
|
||||
if (StringUtil.isNotEmpty(request.getNickname()) && !StringUtil.equals(user.getNickname(), request.getNickname())) {
|
||||
user.setNickname(request.getNickname());
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (StringUtil.isNotEmpty(request.getAvatarUrl()) && !StringUtil.equals(user.getAvatarUrl(), request.getAvatarUrl())) {
|
||||
user.setAvatarUrl(request.getAvatarUrl());
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
user.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
userRepository.save(user);
|
||||
|
||||
// 更新缓存
|
||||
cacheUserInfo(user);
|
||||
|
||||
log.info("用户信息更新成功: userId={}", userId);
|
||||
}
|
||||
|
||||
return convertToUserInfoResponse(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public boolean existsByEmail(String email) {
|
||||
return userRepository.existsByEmailAndDeletedAtIsNull(email.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户状态
|
||||
*/
|
||||
private void checkUserStatus(User user) {
|
||||
if (user.getStatus() == Constants.USER_STATUS_LOCKED) {
|
||||
throw BusinessException.of(ErrorCode.FORBIDDEN_ERROR, "账户已被锁定,请联系管理员");
|
||||
}
|
||||
if (user.getStatus() == Constants.USER_STATUS_DELETED) {
|
||||
throw BusinessException.of(ErrorCode.USER_NOT_FOUND, "用户不存在");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录失败次数
|
||||
*/
|
||||
private void checkLoginAttempts(String email) {
|
||||
String key = Constants.REDIS_RATE_LIMIT_PREFIX + "login_fail:" + email;
|
||||
Integer attempts = (Integer) redisTemplate.opsForValue().get(key);
|
||||
if (attempts != null && attempts >= 5) {
|
||||
throw BusinessException.of(ErrorCode.RATE_LIMIT_ERROR, "登录失败次数过多,请稍后再试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录失败
|
||||
*/
|
||||
private void recordLoginFailure(String email) {
|
||||
String key = Constants.REDIS_RATE_LIMIT_PREFIX + "login_fail:" + email;
|
||||
Integer attempts = (Integer) redisTemplate.opsForValue().get(key);
|
||||
attempts = (attempts == null) ? 1 : attempts + 1;
|
||||
redisTemplate.opsForValue().set(key, attempts, 15, TimeUnit.MINUTES);
|
||||
|
||||
log.warn("登录失败: email={}, 失败次数={}", email, attempts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除登录失败记录
|
||||
*/
|
||||
private void clearLoginFailures(String email) {
|
||||
String key = Constants.REDIS_RATE_LIMIT_PREFIX + "login_fail:" + email;
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存用户信息到Redis
|
||||
*/
|
||||
private void cacheUserInfo(User user) {
|
||||
String cacheKey = Constants.REDIS_USER_INFO_PREFIX + user.getId();
|
||||
UserInfoResponse userInfo = convertToUserInfoResponse(user);
|
||||
redisTemplate.opsForValue().set(cacheKey, userInfo, Constants.REDIS_USER_INFO_EXPIRE_TIME, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认头像URL
|
||||
*/
|
||||
private String getDefaultAvatarUrl() {
|
||||
// 可以配置默认头像列表,随机选择一个
|
||||
return "https://via.placeholder.com/150x150?text=User";
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换User实体为UserInfoResponse
|
||||
*/
|
||||
private UserInfoResponse convertToUserInfoResponse(User user) {
|
||||
UserInfoResponse response = new UserInfoResponse();
|
||||
response.setUserId(user.getId());
|
||||
response.setEmail(user.getEmail());
|
||||
response.setNickname(user.getNickname());
|
||||
response.setAvatarUrl(user.getAvatarUrl());
|
||||
response.setStatus(user.getStatus());
|
||||
response.setLastLoginTime(user.getLastLoginTime());
|
||||
response.setCreatedAt(user.getCreatedAt());
|
||||
response.setUpdatedAt(user.getUpdatedAt());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
246
src/main/java/com/lxy/hsend/util/CryptoUtil.java
Normal file
246
src/main/java/com/lxy/hsend/util/CryptoUtil.java
Normal file
@ -0,0 +1,246 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 加密工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
public class CryptoUtil {
|
||||
|
||||
private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
private static final String AES_ALGORITHM = "AES";
|
||||
private static final String AES_TRANSFORMATION = "AES/ECB/PKCS5Padding";
|
||||
|
||||
/**
|
||||
* 使用BCrypt加密密码
|
||||
*/
|
||||
public static String encryptPassword(String plainPassword) {
|
||||
return passwordEncoder.encode(plainPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*/
|
||||
public static boolean verifyPassword(String plainPassword, String encryptedPassword) {
|
||||
return passwordEncoder.matches(plainPassword, encryptedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5加密
|
||||
*/
|
||||
public static String md5(String input) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] messageDigest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(messageDigest);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("MD5加密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256加密
|
||||
*/
|
||||
public static String sha256(String input) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("SHA-256加密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-512加密
|
||||
*/
|
||||
public static String sha512(String input) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("SHA-512加密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AES密钥
|
||||
*/
|
||||
public static String generateAESKey() {
|
||||
try {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(AES_ALGORITHM);
|
||||
keyGenerator.init(256);
|
||||
SecretKey secretKey = keyGenerator.generateKey();
|
||||
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("生成AES密钥失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密
|
||||
*/
|
||||
public static String aesEncrypt(String plainText, String key) {
|
||||
try {
|
||||
SecretKeySpec secretKey = new SecretKeySpec(Base64.getDecoder().decode(key), AES_ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
} catch (Exception e) {
|
||||
log.error("AES加密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解密
|
||||
*/
|
||||
public static String aesDecrypt(String encryptedText, String key) {
|
||||
try {
|
||||
SecretKeySpec secretKey = new SecretKeySpec(Base64.getDecoder().decode(key), AES_ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.error("AES解密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64编码
|
||||
*/
|
||||
public static String base64Encode(String input) {
|
||||
return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64解码
|
||||
*/
|
||||
public static String base64Decode(String encodedInput) {
|
||||
try {
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(encodedInput);
|
||||
return new String(decodedBytes, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.error("Base64解码失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机盐值
|
||||
*/
|
||||
public static String generateSalt() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] salt = new byte[16];
|
||||
random.nextBytes(salt);
|
||||
return bytesToHex(salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带盐值的哈希
|
||||
*/
|
||||
public static String hashWithSalt(String input, String salt) {
|
||||
return sha256(input + salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证带盐值的哈希
|
||||
*/
|
||||
public static boolean verifyHashWithSalt(String input, String salt, String hash) {
|
||||
String computedHash = hashWithSalt(input, salt);
|
||||
return StringUtil.equals(computedHash, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 字节数组转十六进制字符串
|
||||
*/
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
result.append(String.format("%02x", b));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 十六进制字符串转字节数组
|
||||
*/
|
||||
public static byte[] hexToBytes(String hex) {
|
||||
int len = hex.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
|
||||
+ Character.digit(hex.charAt(i + 1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成安全的随机数
|
||||
*/
|
||||
public static String generateSecureRandomString(int length) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append(chars.charAt(random.nextInt(chars.length())));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的异或加密/解密
|
||||
*/
|
||||
public static String xorEncrypt(String input, String key) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
result.append((char) (input.charAt(i) ^ key.charAt(i % key.length())));
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(result.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 异或解密
|
||||
*/
|
||||
public static String xorDecrypt(String encryptedInput, String key) {
|
||||
try {
|
||||
String decoded = new String(Base64.getDecoder().decode(encryptedInput), StandardCharsets.UTF_8);
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < decoded.length(); i++) {
|
||||
result.append((char) (decoded.charAt(i) ^ key.charAt(i % key.length())));
|
||||
}
|
||||
return result.toString();
|
||||
} catch (Exception e) {
|
||||
log.error("异或解密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private CryptoUtil() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
237
src/main/java/com/lxy/hsend/util/JwtUtil.java
Normal file
237
src/main/java/com/lxy/hsend/util/JwtUtil.java
Normal file
@ -0,0 +1,237 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* JWT工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
public JwtUtil(RedisTemplate<String, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.secretKey = Keys.hmacShaKeyFor(Constants.JWT_SECRET_KEY.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成JWT Token
|
||||
*/
|
||||
public String generateToken(Long userId, String email) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("email", email);
|
||||
claims.put("type", "access");
|
||||
|
||||
return createToken(claims, userId.toString(), Constants.JWT_EXPIRATION_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成刷新Token
|
||||
*/
|
||||
public String generateRefreshToken(Long userId, String email) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("email", email);
|
||||
claims.put("type", "refresh");
|
||||
|
||||
return createToken(claims, userId.toString(), Constants.JWT_REFRESH_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Token
|
||||
*/
|
||||
private String createToken(Map<String, Object> claims, String subject, long expiration) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(secretKey, SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
// 检查是否在黑名单中
|
||||
if (isTokenBlacklisted(token)) {
|
||||
log.warn("Token已被加入黑名单: {}", token);
|
||||
return false;
|
||||
}
|
||||
|
||||
Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token);
|
||||
return true;
|
||||
} catch (SecurityException e) {
|
||||
log.error("JWT签名无效: {}", e.getMessage());
|
||||
} catch (MalformedJwtException e) {
|
||||
log.error("JWT格式错误: {}", e.getMessage());
|
||||
} catch (ExpiredJwtException e) {
|
||||
log.warn("JWT已过期: {}", e.getMessage());
|
||||
} catch (UnsupportedJwtException e) {
|
||||
log.error("不支持的JWT: {}", e.getMessage());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("JWT为空: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取用户ID
|
||||
*/
|
||||
public Long getUserIdFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.get("userId", Long.class) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取邮箱
|
||||
*/
|
||||
public String getEmailFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.get("email", String.class) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取Token类型
|
||||
*/
|
||||
public String getTokenTypeFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.get("type", String.class) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token过期时间
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.getExpiration() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Token是否过期
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration != null && expiration.before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取Claims
|
||||
*/
|
||||
private Claims getClaimsFromToken(String token) {
|
||||
try {
|
||||
return Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
} catch (Exception e) {
|
||||
log.error("解析Token失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Token加入黑名单
|
||||
*/
|
||||
public void blacklistToken(String token) {
|
||||
try {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
if (expiration != null) {
|
||||
String key = Constants.REDIS_JWT_BLACKLIST_PREFIX + token;
|
||||
long ttl = expiration.getTime() - System.currentTimeMillis();
|
||||
if (ttl > 0) {
|
||||
redisTemplate.opsForValue().set(key, "blacklisted", ttl, TimeUnit.MILLISECONDS);
|
||||
log.info("Token已加入黑名单: {}", token);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("将Token加入黑名单失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Token是否在黑名单中
|
||||
*/
|
||||
public boolean isTokenBlacklisted(String token) {
|
||||
try {
|
||||
String key = Constants.REDIS_JWT_BLACKLIST_PREFIX + token;
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||||
} catch (Exception e) {
|
||||
log.error("检查Token黑名单状态失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
public String refreshToken(String refreshToken) {
|
||||
if (!validateToken(refreshToken)) {
|
||||
throw new IllegalArgumentException("刷新Token无效");
|
||||
}
|
||||
|
||||
String tokenType = getTokenTypeFromToken(refreshToken);
|
||||
if (!"refresh".equals(tokenType)) {
|
||||
throw new IllegalArgumentException("Token类型错误");
|
||||
}
|
||||
|
||||
Long userId = getUserIdFromToken(refreshToken);
|
||||
String email = getEmailFromToken(refreshToken);
|
||||
|
||||
if (userId == null || email == null) {
|
||||
throw new IllegalArgumentException("Token信息不完整");
|
||||
}
|
||||
|
||||
return generateToken(userId, email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取Token
|
||||
*/
|
||||
public String extractTokenFromHeader(String authHeader) {
|
||||
if (authHeader != null && authHeader.startsWith(Constants.JWT_TOKEN_PREFIX)) {
|
||||
return authHeader.substring(Constants.JWT_TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token剩余有效时间(秒)
|
||||
*/
|
||||
public long getTokenRemainingTime(String token) {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
if (expiration != null) {
|
||||
long remaining = expiration.getTime() - System.currentTimeMillis();
|
||||
return Math.max(0, remaining / 1000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
243
src/main/java/com/lxy/hsend/util/PageUtil.java
Normal file
243
src/main/java/com/lxy/hsend/util/PageUtil.java
Normal file
@ -0,0 +1,243 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class PageUtil {
|
||||
|
||||
/**
|
||||
* 创建分页对象
|
||||
*/
|
||||
public static Pageable createPageable(Integer page, Integer size) {
|
||||
return createPageable(page, size, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带排序的分页对象
|
||||
*/
|
||||
public static Pageable createPageable(Integer page, Integer size, Sort sort) {
|
||||
page = validatePage(page);
|
||||
size = validateSize(size);
|
||||
|
||||
if (sort != null) {
|
||||
return PageRequest.of(page - 1, size, sort);
|
||||
} else {
|
||||
return PageRequest.of(page - 1, size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建降序排序的分页对象
|
||||
*/
|
||||
public static Pageable createPageableDesc(Integer page, Integer size, String... properties) {
|
||||
Sort sort = Sort.by(Sort.Direction.DESC, properties);
|
||||
return createPageable(page, size, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建升序排序的分页对象
|
||||
*/
|
||||
public static Pageable createPageableAsc(Integer page, Integer size, String... properties) {
|
||||
Sort sort = Sort.by(Sort.Direction.ASC, properties);
|
||||
return createPageable(page, size, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证页码
|
||||
*/
|
||||
private static Integer validatePage(Integer page) {
|
||||
if (page == null || page < 1) {
|
||||
return Constants.DEFAULT_PAGE_NUM;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证页面大小
|
||||
*/
|
||||
private static Integer validateSize(Integer size) {
|
||||
if (size == null || size < 1) {
|
||||
return Constants.DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
if (size > Constants.MAX_PAGE_SIZE) {
|
||||
return Constants.MAX_PAGE_SIZE;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Spring Data的Page对象转换为自定义的分页响应对象
|
||||
*/
|
||||
public static <T> PageResponse<T> toPageResponse(Page<T> page) {
|
||||
return new PageResponse<T>(
|
||||
page.getContent(),
|
||||
page.getNumber() + 1, // Spring Data的页码从0开始,转换为从1开始
|
||||
page.getSize(),
|
||||
page.getTotalElements(),
|
||||
page.getTotalPages(),
|
||||
page.isFirst(),
|
||||
page.isLast(),
|
||||
page.hasNext(),
|
||||
page.hasPrevious()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空的分页响应
|
||||
*/
|
||||
public static <T> PageResponse<T> emptyPageResponse(Integer page, Integer size) {
|
||||
page = validatePage(page);
|
||||
size = validateSize(size);
|
||||
return new PageResponse<T>(
|
||||
List.of(),
|
||||
page,
|
||||
size,
|
||||
0L,
|
||||
0,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PageResponse<T> {
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
private List<T> content;
|
||||
|
||||
/**
|
||||
* 当前页码(从1开始)
|
||||
*/
|
||||
private Integer page;
|
||||
|
||||
/**
|
||||
* 页面大小
|
||||
*/
|
||||
private Integer size;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
private Long total;
|
||||
|
||||
/**
|
||||
* 总页数
|
||||
*/
|
||||
private Integer totalPages;
|
||||
|
||||
/**
|
||||
* 是否是第一页
|
||||
*/
|
||||
private Boolean first;
|
||||
|
||||
/**
|
||||
* 是否是最后一页
|
||||
*/
|
||||
private Boolean last;
|
||||
|
||||
/**
|
||||
* 是否有下一页
|
||||
*/
|
||||
private Boolean hasNext;
|
||||
|
||||
/**
|
||||
* 是否有上一页
|
||||
*/
|
||||
private Boolean hasPrevious;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询参数
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PageQuery {
|
||||
/**
|
||||
* 页码(从1开始)
|
||||
*/
|
||||
private Integer page = Constants.DEFAULT_PAGE_NUM;
|
||||
|
||||
/**
|
||||
* 页面大小
|
||||
*/
|
||||
private Integer size = Constants.DEFAULT_PAGE_SIZE;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private String sortBy;
|
||||
|
||||
/**
|
||||
* 排序方向(asc/desc)
|
||||
*/
|
||||
private String sortDir = "desc";
|
||||
|
||||
/**
|
||||
* 获取Pageable对象
|
||||
*/
|
||||
public Pageable toPageable() {
|
||||
if (StringUtil.isNotEmpty(sortBy)) {
|
||||
Sort.Direction direction = "asc".equalsIgnoreCase(sortDir) ?
|
||||
Sort.Direction.ASC : Sort.Direction.DESC;
|
||||
Sort sort = Sort.by(direction, sortBy);
|
||||
return PageUtil.createPageable(page, size, sort);
|
||||
} else {
|
||||
return PageUtil.createPageable(page, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算偏移量
|
||||
*/
|
||||
public static long calculateOffset(Integer page, Integer size) {
|
||||
page = validatePage(page);
|
||||
size = validateSize(size);
|
||||
return (long) (page - 1) * size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算总页数
|
||||
*/
|
||||
public static int calculateTotalPages(long totalElements, int size) {
|
||||
return (int) Math.ceil((double) totalElements / size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查页码是否有效
|
||||
*/
|
||||
public static boolean isValidPage(Integer page, long totalElements, Integer size) {
|
||||
if (page == null || page < 1) {
|
||||
return false;
|
||||
}
|
||||
size = validateSize(size);
|
||||
int totalPages = calculateTotalPages(totalElements, size);
|
||||
return page <= totalPages;
|
||||
}
|
||||
|
||||
private PageUtil() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
276
src/main/java/com/lxy/hsend/util/StringUtil.java
Normal file
276
src/main/java/com/lxy/hsend/util/StringUtil.java
Normal file
@ -0,0 +1,276 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 字符串工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class StringUtil {
|
||||
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile(Constants.EMAIL_REGEX);
|
||||
private static final Pattern PASSWORD_PATTERN = Pattern.compile(Constants.PASSWORD_REGEX);
|
||||
|
||||
/**
|
||||
* 检查字符串是否为空或null
|
||||
*/
|
||||
public static boolean isEmpty(String str) {
|
||||
return str == null || str.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字符串是否不为空
|
||||
*/
|
||||
public static boolean isNotEmpty(String str) {
|
||||
return !isEmpty(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的字符串比较(避免空指针异常)
|
||||
*/
|
||||
public static boolean equals(String str1, String str2) {
|
||||
if (str1 == null && str2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (str1 == null || str2 == null) {
|
||||
return false;
|
||||
}
|
||||
return str1.equals(str2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的字符串比较(忽略大小写)
|
||||
*/
|
||||
public static boolean equalsIgnoreCase(String str1, String str2) {
|
||||
if (str1 == null && str2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (str1 == null || str2 == null) {
|
||||
return false;
|
||||
}
|
||||
return str1.equalsIgnoreCase(str2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成UUID字符串(去掉连字符)
|
||||
*/
|
||||
public static String generateUuid() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成标准UUID字符串(带连字符)
|
||||
*/
|
||||
public static String generateStandardUuid() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱格式
|
||||
*/
|
||||
public static boolean isValidEmail(String email) {
|
||||
return isNotEmpty(email) && EMAIL_PATTERN.matcher(email).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码格式
|
||||
*/
|
||||
public static boolean isValidPassword(String password) {
|
||||
return isNotEmpty(password) && PASSWORD_PATTERN.matcher(password).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户名长度
|
||||
*/
|
||||
public static boolean isValidNickname(String nickname) {
|
||||
return isNotEmpty(nickname) &&
|
||||
nickname.length() >= Constants.NICKNAME_MIN_LENGTH &&
|
||||
nickname.length() <= Constants.NICKNAME_MAX_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 掩码邮箱地址(隐藏部分字符)
|
||||
*/
|
||||
public static String maskEmail(String email) {
|
||||
if (isEmpty(email) || !email.contains("@")) {
|
||||
return email;
|
||||
}
|
||||
|
||||
String[] parts = email.split("@");
|
||||
String username = parts[0];
|
||||
String domain = parts[1];
|
||||
|
||||
if (username.length() <= 2) {
|
||||
return email;
|
||||
}
|
||||
|
||||
String maskedUsername = username.charAt(0) +
|
||||
"*".repeat(username.length() - 2) +
|
||||
username.charAt(username.length() - 1);
|
||||
|
||||
return maskedUsername + "@" + domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 掩码手机号码
|
||||
*/
|
||||
public static String maskPhoneNumber(String phoneNumber) {
|
||||
if (isEmpty(phoneNumber) || phoneNumber.length() < 7) {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
return phoneNumber.substring(0, 3) +
|
||||
"*".repeat(phoneNumber.length() - 6) +
|
||||
phoneNumber.substring(phoneNumber.length() - 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断字符串到指定长度
|
||||
*/
|
||||
public static String truncate(String str, int maxLength) {
|
||||
if (isEmpty(str) || str.length() <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
return str.substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除字符串中的HTML标签
|
||||
*/
|
||||
public static String removeHtmlTags(String html) {
|
||||
if (isEmpty(html)) {
|
||||
return html;
|
||||
}
|
||||
return html.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义HTML特殊字符
|
||||
*/
|
||||
public static String escapeHtml(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return str;
|
||||
}
|
||||
return str.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串转换为驼峰命名
|
||||
*/
|
||||
public static String toCamelCase(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean capitalizeNext = false;
|
||||
|
||||
for (char c : str.toCharArray()) {
|
||||
if (c == '_' || c == '-' || c == ' ') {
|
||||
capitalizeNext = true;
|
||||
} else if (capitalizeNext) {
|
||||
result.append(Character.toUpperCase(c));
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
result.append(Character.toLowerCase(c));
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将驼峰命名转换为下划线命名
|
||||
*/
|
||||
public static String toSnakeCase(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
char c = str.charAt(i);
|
||||
if (Character.isUpperCase(c)) {
|
||||
if (i > 0) {
|
||||
result.append('_');
|
||||
}
|
||||
result.append(Character.toLowerCase(c));
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
public static String generateRandomString(int length) {
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
result.append(chars.charAt((int) (Math.random() * chars.length())));
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机数字字符串
|
||||
*/
|
||||
public static String generateRandomNumbers(int length) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
result.append((int) (Math.random() * 10));
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计字符串中的字数(中文按1个字计算,英文按0.5个字计算)
|
||||
*/
|
||||
public static double countWords(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double count = 0;
|
||||
for (char c : str.toCharArray()) {
|
||||
if (c >= 0x4e00 && c <= 0x9fa5) {
|
||||
// 中文字符
|
||||
count += 1;
|
||||
} else if (Character.isLetter(c)) {
|
||||
// 英文字符
|
||||
count += 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串的字节长度(UTF-8编码)
|
||||
*/
|
||||
public static int getByteLength(String str) {
|
||||
if (isEmpty(str)) {
|
||||
return 0;
|
||||
}
|
||||
return str.getBytes().length;
|
||||
}
|
||||
|
||||
private StringUtil() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
220
src/main/java/com/lxy/hsend/util/TimeUtil.java
Normal file
220
src/main/java/com/lxy/hsend/util/TimeUtil.java
Normal file
@ -0,0 +1,220 @@
|
||||
package com.lxy.hsend.util;
|
||||
|
||||
import com.lxy.hsend.common.Constants;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 时间工具类
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
public class TimeUtil {
|
||||
|
||||
private static final ZoneId ZONE_ID = ZoneId.of(Constants.TIMEZONE);
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(Constants.DATE_FORMAT);
|
||||
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(Constants.DATETIME_FORMAT);
|
||||
|
||||
/**
|
||||
* 获取当前时间戳(毫秒)
|
||||
*/
|
||||
public static long getCurrentTimestamp() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳(秒)
|
||||
*/
|
||||
public static long getCurrentTimestampSeconds() {
|
||||
return System.currentTimeMillis() / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期时间
|
||||
*/
|
||||
public static LocalDateTime getCurrentDateTime() {
|
||||
return LocalDateTime.now(ZONE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期
|
||||
*/
|
||||
public static LocalDate getCurrentDate() {
|
||||
return LocalDate.now(ZONE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间戳转LocalDateTime
|
||||
*/
|
||||
public static LocalDateTime timestampToLocalDateTime(long timestamp) {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZONE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime转时间戳
|
||||
*/
|
||||
public static long localDateTimeToTimestamp(LocalDateTime dateTime) {
|
||||
return dateTime.atZone(ZONE_ID).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间戳转Date
|
||||
*/
|
||||
public static Date timestampToDate(long timestamp) {
|
||||
return new Date(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Date转时间戳
|
||||
*/
|
||||
public static long dateToTimestamp(Date date) {
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
public static String formatDateTime(LocalDateTime dateTime) {
|
||||
return dateTime.format(DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
public static String formatDate(LocalDate date) {
|
||||
return date.format(DATE_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间戳为日期时间字符串
|
||||
*/
|
||||
public static String formatTimestamp(long timestamp) {
|
||||
return formatDateTime(timestampToLocalDateTime(timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期时间字符串
|
||||
*/
|
||||
public static LocalDateTime parseDateTime(String dateTimeStr) {
|
||||
return LocalDateTime.parse(dateTimeStr, DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期字符串
|
||||
*/
|
||||
public static LocalDate parseDate(String dateStr) {
|
||||
return LocalDate.parse(dateStr, DATE_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳之间的天数差
|
||||
*/
|
||||
public static long daysBetween(long timestamp1, long timestamp2) {
|
||||
LocalDate date1 = timestampToLocalDateTime(timestamp1).toLocalDate();
|
||||
LocalDate date2 = timestampToLocalDateTime(timestamp2).toLocalDate();
|
||||
return Duration.between(date1.atStartOfDay(), date2.atStartOfDay()).toDays();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳之间的小时差
|
||||
*/
|
||||
public static long hoursBetween(long timestamp1, long timestamp2) {
|
||||
return (timestamp2 - timestamp1) / (1000 * 60 * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳之间的分钟差
|
||||
*/
|
||||
public static long minutesBetween(long timestamp1, long timestamp2) {
|
||||
return (timestamp2 - timestamp1) / (1000 * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳之间的秒数差
|
||||
*/
|
||||
public static long secondsBetween(long timestamp1, long timestamp2) {
|
||||
return (timestamp2 - timestamp1) / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今天开始时间戳
|
||||
*/
|
||||
public static long getTodayStartTimestamp() {
|
||||
LocalDateTime startOfDay = getCurrentDate().atStartOfDay();
|
||||
return localDateTimeToTimestamp(startOfDay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今天结束时间戳
|
||||
*/
|
||||
public static long getTodayEndTimestamp() {
|
||||
LocalDateTime endOfDay = getCurrentDate().atTime(23, 59, 59, 999_000_000);
|
||||
return localDateTimeToTimestamp(endOfDay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定天数前的时间戳
|
||||
*/
|
||||
public static long getDaysAgoTimestamp(int days) {
|
||||
LocalDateTime daysAgo = getCurrentDateTime().minusDays(days);
|
||||
return localDateTimeToTimestamp(daysAgo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定小时前的时间戳
|
||||
*/
|
||||
public static long getHoursAgoTimestamp(int hours) {
|
||||
LocalDateTime hoursAgo = getCurrentDateTime().minusHours(hours);
|
||||
return localDateTimeToTimestamp(hoursAgo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定分钟前的时间戳
|
||||
*/
|
||||
public static long getMinutesAgoTimestamp(int minutes) {
|
||||
LocalDateTime minutesAgo = getCurrentDateTime().minusMinutes(minutes);
|
||||
return localDateTimeToTimestamp(minutesAgo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查时间戳是否是今天
|
||||
*/
|
||||
public static boolean isToday(long timestamp) {
|
||||
LocalDate date = timestampToLocalDateTime(timestamp).toLocalDate();
|
||||
return date.equals(getCurrentDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查时间戳是否是昨天
|
||||
*/
|
||||
public static boolean isYesterday(long timestamp) {
|
||||
LocalDate date = timestampToLocalDateTime(timestamp).toLocalDate();
|
||||
return date.equals(getCurrentDate().minusDays(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取友好的时间显示(如:刚刚、5分钟前、1小时前等)
|
||||
*/
|
||||
public static String getFriendlyTime(long timestamp) {
|
||||
long now = getCurrentTimestamp();
|
||||
long diff = now - timestamp;
|
||||
|
||||
if (diff < 60 * 1000) {
|
||||
return "刚刚";
|
||||
} else if (diff < 60 * 60 * 1000) {
|
||||
return (diff / (60 * 1000)) + "分钟前";
|
||||
} else if (diff < 24 * 60 * 60 * 1000) {
|
||||
return (diff / (60 * 60 * 1000)) + "小时前";
|
||||
} else if (diff < 30L * 24 * 60 * 60 * 1000) {
|
||||
return (diff / (24 * 60 * 60 * 1000)) + "天前";
|
||||
} else {
|
||||
return formatTimestamp(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private TimeUtil() {
|
||||
// 工具类不允许实例化
|
||||
}
|
||||
}
|
||||
61
src/main/resources/application-dev.yml
Normal file
61
src/main/resources/application-dev.yml
Normal file
@ -0,0 +1,61 @@
|
||||
# 开发环境配置
|
||||
spring:
|
||||
# 数据库配置
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/hsai?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: true
|
||||
|
||||
# Flyway配置
|
||||
flyway:
|
||||
enabled: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.lxy.hsend: debug
|
||||
org.springframework.security: debug
|
||||
org.hibernate.SQL: debug
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: trace
|
||||
|
||||
# JWT配置(开发环境使用简单密钥)
|
||||
jwt:
|
||||
secret: hsendDevSecretKey2024
|
||||
expiration: 86400 # 1天
|
||||
|
||||
# AI服务配置(开发环境)
|
||||
ai:
|
||||
service:
|
||||
timeout: 30000
|
||||
retry-count: 3
|
||||
base-url: https://api-dev.example.com
|
||||
api-key: dev-api-key
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
upload:
|
||||
path: ./uploads/dev
|
||||
|
||||
# 跨域配置(开发环境允许所有来源)
|
||||
cors:
|
||||
allowed-origins: "*"
|
||||
62
src/main/resources/application-prod.yml
Normal file
62
src/main/resources/application-prod.yml
Normal file
@ -0,0 +1,62 @@
|
||||
# 生产环境配置
|
||||
spring:
|
||||
# 数据库配置(生产环境从环境变量读取)
|
||||
datasource:
|
||||
url: ${DB_URL}
|
||||
username: ${DB_USERNAME}
|
||||
password: ${DB_PASSWORD}
|
||||
hikari:
|
||||
maximum-pool-size: 50
|
||||
minimum-idle: 10
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 300000
|
||||
max-lifetime: 900000
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: false
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD}
|
||||
database: 0
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: ${SERVER_PORT:8080}
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.lxy.hsend: info
|
||||
org.springframework.security: warn
|
||||
org.hibernate.SQL: warn
|
||||
file:
|
||||
name: /var/log/hs-end/hs-end.log
|
||||
|
||||
# JWT配置(生产环境必须使用环境变量)
|
||||
jwt:
|
||||
secret: ${JWT_SECRET}
|
||||
expiration: ${JWT_EXPIRATION:604800}
|
||||
|
||||
# AI服务配置
|
||||
ai:
|
||||
service:
|
||||
timeout: ${AI_TIMEOUT:30000}
|
||||
retry-count: ${AI_RETRY:3}
|
||||
base-url: ${AI_SERVICE_URL}
|
||||
api-key: ${AI_API_KEY}
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
upload:
|
||||
path: ${FILE_UPLOAD_PATH:/data/uploads}
|
||||
|
||||
# 跨域配置(生产环境严格控制)
|
||||
cors:
|
||||
allowed-origins: ${CORS_ORIGINS}
|
||||
62
src/main/resources/application-test.yml
Normal file
62
src/main/resources/application-test.yml
Normal file
@ -0,0 +1,62 @@
|
||||
# 测试环境配置
|
||||
spring:
|
||||
# 数据库配置 - 使用H2内存数据库进行测试
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
username: sa
|
||||
password:
|
||||
driver-class-name: org.h2.Driver
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.H2Dialect
|
||||
format_sql: true
|
||||
|
||||
# H2控制台配置
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
|
||||
# Flyway配置 - 测试环境禁用
|
||||
flyway:
|
||||
enabled: false
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 1
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.lxy.hsend: info
|
||||
org.springframework.security: info
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: hsendTestSecretKey2024
|
||||
expiration: 3600 # 1小时
|
||||
|
||||
# AI服务配置(测试环境使用Mock)
|
||||
ai:
|
||||
service:
|
||||
timeout: 10000
|
||||
retry-count: 1
|
||||
base-url: http://localhost:8082/mock
|
||||
api-key: test-api-key
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
upload:
|
||||
path: ./uploads/test
|
||||
134
src/main/resources/application.yml
Normal file
134
src/main/resources/application.yml
Normal file
@ -0,0 +1,134 @@
|
||||
spring:
|
||||
application:
|
||||
name: hs-end
|
||||
|
||||
profiles:
|
||||
active: dev
|
||||
|
||||
# 数据库配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: ${DB_URL:jdbc:mysql://localhost:3306/hsai?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true}
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:root}
|
||||
|
||||
# 连接池配置
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
format_sql: true
|
||||
|
||||
# Flyway数据库迁移配置
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
validate-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
database: 0
|
||||
timeout: 5000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-wait: -1ms
|
||||
max-idle: 10
|
||||
min-idle: 0
|
||||
|
||||
# 文件上传配置
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 10MB
|
||||
max-request-size: 10MB
|
||||
|
||||
# Jackson配置
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: Asia/Shanghai
|
||||
default-property-inclusion: non_null
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
encoding:
|
||||
charset: UTF-8
|
||||
enabled: true
|
||||
force: true
|
||||
|
||||
# Actuator健康检查配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.lxy.hsend: info
|
||||
org.springframework.security: debug
|
||||
org.hibernate.SQL: debug
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: trace
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: logs/hs-end.log
|
||||
max-size: 10MB
|
||||
max-history: 30
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:hsendSecretKeyForJWTTokenGenerationAndValidation2024}
|
||||
expiration: 604800 # 7天(秒)
|
||||
|
||||
# AI服务配置
|
||||
ai:
|
||||
service:
|
||||
timeout: 30000
|
||||
retry-count: 3
|
||||
base-url: ${AI_SERVICE_URL:https://api.example.com}
|
||||
api-key: ${AI_API_KEY:your-api-key}
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
upload:
|
||||
path: ${FILE_UPLOAD_PATH:./uploads}
|
||||
allowed-types: .txt,.pdf,.md,.docx,.doc,.xlsx,.xls,.png,.jpg,.jpeg
|
||||
max-size: 10485760 # 10MB
|
||||
|
||||
# Swagger配置
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
|
||||
# 跨域配置
|
||||
cors:
|
||||
allowed-origins: ${CORS_ORIGINS:http://localhost:3000,http://localhost:8080}
|
||||
allowed-methods: GET,POST,PUT,DELETE,OPTIONS
|
||||
allowed-headers: "*"
|
||||
allow-credentials: true
|
||||
166
src/main/resources/db/migration/V1__Create_initial_tables.sql
Normal file
166
src/main/resources/db/migration/V1__Create_initial_tables.sql
Normal file
@ -0,0 +1,166 @@
|
||||
-- 创建数据库初始表结构
|
||||
-- 版本: V1
|
||||
-- 描述: 创建用户、会话、消息、收藏、文件等核心业务表
|
||||
|
||||
-- 设置字符集
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- 用户表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `users` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
`email` VARCHAR(100) NOT NULL COMMENT '用户邮箱',
|
||||
`password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希',
|
||||
`nickname` VARCHAR(50) NOT NULL COMMENT '用户昵称',
|
||||
`avatar_url` VARCHAR(500) DEFAULT NULL COMMENT '头像URL',
|
||||
`status` TINYINT DEFAULT 1 COMMENT '用户状态:1-正常,2-锁定,3-删除',
|
||||
`last_login_time` BIGINT DEFAULT NULL COMMENT '最后登录时间戳',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_email` (`email`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 会话表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `sessions` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '会话自增ID',
|
||||
`session_id` VARCHAR(36) NOT NULL COMMENT '会话UUID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`title` VARCHAR(200) NOT NULL COMMENT '会话标题',
|
||||
`message_count` INT DEFAULT 0 COMMENT '消息数量',
|
||||
`last_message_time` BIGINT DEFAULT NULL COMMENT '最后消息时间戳',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_session_id` (`session_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_last_message_time` (`last_message_time`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `fk_sessions_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天会话表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 消息表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `messages` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '消息自增ID',
|
||||
`message_id` VARCHAR(36) NOT NULL COMMENT '消息UUID',
|
||||
`session_id` VARCHAR(36) NOT NULL COMMENT '会话ID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`role` VARCHAR(20) NOT NULL COMMENT '角色:user/assistant',
|
||||
`content` LONGTEXT NOT NULL COMMENT '消息内容',
|
||||
`model_used` VARCHAR(50) DEFAULT NULL COMMENT '使用的AI模型',
|
||||
`deep_thinking` BOOLEAN DEFAULT FALSE COMMENT '是否启用深度思考',
|
||||
`web_search` BOOLEAN DEFAULT FALSE COMMENT '是否启用联网搜索',
|
||||
`is_favorited` BOOLEAN DEFAULT FALSE COMMENT '是否已收藏',
|
||||
`timestamp` BIGINT NOT NULL COMMENT '消息时间戳',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_message_id` (`message_id`),
|
||||
KEY `idx_session_id` (`session_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_role` (`role`),
|
||||
KEY `idx_timestamp` (`timestamp`),
|
||||
KEY `idx_is_favorited` (`is_favorited`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `fk_messages_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 收藏表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `favorites` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '收藏自增ID',
|
||||
`favorite_id` VARCHAR(36) NOT NULL COMMENT '收藏UUID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`message_id` VARCHAR(36) NOT NULL COMMENT '消息ID',
|
||||
`session_id` VARCHAR(36) NOT NULL COMMENT '会话ID',
|
||||
`created_at` BIGINT NOT NULL COMMENT '收藏时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_favorite_id` (`favorite_id`),
|
||||
UNIQUE KEY `uk_user_message` (`user_id`, `message_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_message_id` (`message_id`),
|
||||
KEY `idx_session_id` (`session_id`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `fk_favorites_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收藏表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 文件表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `files` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '文件自增ID',
|
||||
`file_id` VARCHAR(36) NOT NULL COMMENT '文件UUID',
|
||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||
`session_id` VARCHAR(36) DEFAULT NULL COMMENT '关联会话ID',
|
||||
`filename` VARCHAR(255) NOT NULL COMMENT '原始文件名',
|
||||
`stored_filename` VARCHAR(255) NOT NULL COMMENT '存储文件名',
|
||||
`file_path` VARCHAR(500) NOT NULL COMMENT '文件存储路径',
|
||||
`file_size` BIGINT NOT NULL COMMENT '文件大小(字节)',
|
||||
`file_type` VARCHAR(100) NOT NULL COMMENT '文件类型',
|
||||
`mime_type` VARCHAR(100) DEFAULT NULL COMMENT 'MIME类型',
|
||||
`upload_time` BIGINT NOT NULL COMMENT '上传时间戳',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
`deleted_at` BIGINT DEFAULT NULL COMMENT '删除时间戳(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_file_id` (`file_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_session_id` (`session_id`),
|
||||
KEY `idx_file_type` (`file_type`),
|
||||
KEY `idx_upload_time` (`upload_time`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
CONSTRAINT `fk_files_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 操作日志表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `operation_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '操作用户ID',
|
||||
`operation_type` VARCHAR(50) NOT NULL COMMENT '操作类型',
|
||||
`operation_desc` VARCHAR(500) DEFAULT NULL COMMENT '操作描述',
|
||||
`ip_address` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址',
|
||||
`user_agent` VARCHAR(500) DEFAULT NULL COMMENT '用户代理',
|
||||
`request_params` TEXT DEFAULT NULL COMMENT '请求参数',
|
||||
`response_result` TEXT DEFAULT NULL COMMENT '响应结果',
|
||||
`execution_time` INT DEFAULT NULL COMMENT '执行时长(毫秒)',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_operation_type` (`operation_type`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 系统配置表
|
||||
-- ----------------------------
|
||||
CREATE TABLE `system_configs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '配置ID',
|
||||
`config_key` VARCHAR(100) NOT NULL COMMENT '配置键',
|
||||
`config_value` TEXT DEFAULT NULL COMMENT '配置值',
|
||||
`config_desc` VARCHAR(500) DEFAULT NULL COMMENT '配置描述',
|
||||
`config_type` VARCHAR(20) DEFAULT 'STRING' COMMENT '配置类型:STRING/NUMBER/BOOLEAN/JSON',
|
||||
`is_encrypted` BOOLEAN DEFAULT FALSE COMMENT '是否加密存储',
|
||||
`created_at` BIGINT NOT NULL COMMENT '创建时间戳',
|
||||
`updated_at` BIGINT NOT NULL COMMENT '更新时间戳',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_config_key` (`config_key`),
|
||||
KEY `idx_config_type` (`config_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
13
src/test/java/com/lxy/hsend/HsEndApplicationTests.java
Normal file
13
src/test/java/com/lxy/hsend/HsEndApplicationTests.java
Normal file
@ -0,0 +1,13 @@
|
||||
package com.lxy.hsend;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class HsEndApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
105
src/test/java/com/lxy/hsend/controller/AuthControllerTest.java
Normal file
105
src/test/java/com/lxy/hsend/controller/AuthControllerTest.java
Normal file
@ -0,0 +1,105 @@
|
||||
package com.lxy.hsend.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.lxy.hsend.dto.auth.LoginRequest;
|
||||
import com.lxy.hsend.dto.auth.RegisterRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* 认证控制器集成测试
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
public class AuthControllerTest {
|
||||
|
||||
@Autowired
|
||||
private WebApplicationContext webApplicationContext;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
public void testUserRegistrationFlow() throws Exception {
|
||||
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
|
||||
|
||||
// 1. 测试用户注册
|
||||
RegisterRequest registerRequest = new RegisterRequest();
|
||||
registerRequest.setEmail("test@example.com");
|
||||
registerRequest.setPassword("Test123456");
|
||||
registerRequest.setConfirmPassword("Test123456");
|
||||
registerRequest.setNickname("测试用户");
|
||||
|
||||
String registerJson = objectMapper.writeValueAsString(registerRequest);
|
||||
|
||||
mockMvc.perform(post("/api/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(registerJson))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.body.email").value("test@example.com"))
|
||||
.andExpect(jsonPath("$.body.nickname").value("测试用户"))
|
||||
.andExpect(jsonPath("$.message").value("注册成功"));
|
||||
|
||||
System.out.println("✅ 用户注册测试通过");
|
||||
|
||||
// 2. 测试用户登录
|
||||
LoginRequest loginRequest = new LoginRequest();
|
||||
loginRequest.setEmail("test@example.com");
|
||||
loginRequest.setPassword("Test123456");
|
||||
|
||||
String loginJson = objectMapper.writeValueAsString(loginRequest);
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(loginJson))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.body.accessToken").exists())
|
||||
.andExpect(jsonPath("$.body.refreshToken").exists())
|
||||
.andExpect(jsonPath("$.body.userInfo.email").value("test@example.com"))
|
||||
.andExpect(jsonPath("$.message").value("登录成功"));
|
||||
|
||||
System.out.println("✅ 用户登录测试通过");
|
||||
|
||||
// 3. 测试邮箱重复注册
|
||||
mockMvc.perform(post("/api/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(registerJson))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
.andExpect(jsonPath("$.error").value(2003));
|
||||
|
||||
System.out.println("✅ 邮箱重复注册验证测试通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmailCheck() throws Exception {
|
||||
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
|
||||
|
||||
// 测试邮箱是否已存在检查
|
||||
mockMvc.perform(get("/api/auth/check-email")
|
||||
.param("email", "nonexistent@example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.body").value(false))
|
||||
.andExpect(jsonPath("$.message").value("邮箱可用"));
|
||||
|
||||
System.out.println("✅ 邮箱检查测试通过");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,203 @@
|
||||
package com.lxy.hsend.repository;
|
||||
|
||||
import com.lxy.hsend.entity.User;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.ResultSet;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 数据库连接和表创建测试
|
||||
*/
|
||||
@DataJpaTest
|
||||
@ActiveProfiles("test")
|
||||
public class DatabaseConnectionTest {
|
||||
|
||||
@Autowired
|
||||
private TestEntityManager entityManager;
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
public void testDatabaseConnection() throws Exception {
|
||||
// 测试数据库连接
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
assertThat(connection).isNotNull();
|
||||
assertThat(connection.isValid(1)).isTrue();
|
||||
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
System.out.println("Database Product Name: " + metaData.getDatabaseProductName());
|
||||
System.out.println("Database Product Version: " + metaData.getDatabaseProductVersion());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTablesExist() throws Exception {
|
||||
// 测试核心表是否存在
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
|
||||
// 检查用户表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "users", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ users table exists");
|
||||
}
|
||||
|
||||
// 检查会话表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "sessions", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ sessions table exists");
|
||||
}
|
||||
|
||||
// 检查消息表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "messages", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ messages table exists");
|
||||
}
|
||||
|
||||
// 检查收藏表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "favorites", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ favorites table exists");
|
||||
}
|
||||
|
||||
// 检查文件表
|
||||
try (ResultSet rs = metaData.getTables(null, null, "files", null)) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
System.out.println("✓ files table exists");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserRepositoryBasicOperations() {
|
||||
// 测试基本的CRUD操作
|
||||
|
||||
// 创建测试用户
|
||||
User user = new User();
|
||||
user.setEmail("test@example.com");
|
||||
user.setPasswordHash("hashedPassword");
|
||||
user.setNickname("TestUser");
|
||||
user.setStatus((byte) 1);
|
||||
|
||||
// 保存用户
|
||||
User savedUser = userRepository.save(user);
|
||||
assertThat(savedUser.getId()).isNotNull();
|
||||
assertThat(savedUser.getEmail()).isEqualTo("test@example.com");
|
||||
System.out.println("✓ User saved successfully: " + savedUser);
|
||||
|
||||
// 通过邮箱查找用户
|
||||
var foundUser = userRepository.findByEmail("test@example.com");
|
||||
assertThat(foundUser).isPresent();
|
||||
assertThat(foundUser.get().getNickname()).isEqualTo("TestUser");
|
||||
System.out.println("✓ User found by email: " + foundUser.get());
|
||||
|
||||
// 检查邮箱是否存在
|
||||
boolean exists = userRepository.existsByEmail("test@example.com");
|
||||
assertThat(exists).isTrue();
|
||||
System.out.println("✓ Email exists check passed");
|
||||
|
||||
// 统计用户数量
|
||||
long count = userRepository.count();
|
||||
assertThat(count).isGreaterThan(0);
|
||||
System.out.println("✓ User count: " + count);
|
||||
|
||||
// 删除用户
|
||||
userRepository.delete(savedUser);
|
||||
|
||||
// 验证删除
|
||||
var deletedUser = userRepository.findById(savedUser.getId());
|
||||
assertThat(deletedUser).isEmpty();
|
||||
System.out.println("✓ User deleted successfully");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserValidation() {
|
||||
// 测试用户实体验证
|
||||
User user = new User();
|
||||
user.setEmail("invalid-email"); // 无效邮箱格式
|
||||
user.setPasswordHash("password");
|
||||
user.setNickname("Test");
|
||||
|
||||
try {
|
||||
entityManager.persistAndFlush(user);
|
||||
// 如果没有抛出异常,说明验证可能有问题
|
||||
} catch (Exception e) {
|
||||
// 预期会抛出验证异常
|
||||
System.out.println("✓ Email validation working: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimestampFields() {
|
||||
// 测试时间戳字段
|
||||
User user = new User();
|
||||
user.setEmail("timestamp@test.com");
|
||||
user.setPasswordHash("password");
|
||||
user.setNickname("TimestampTest");
|
||||
|
||||
long beforeSave = Instant.now().getEpochSecond();
|
||||
User savedUser = userRepository.save(user);
|
||||
long afterSave = Instant.now().getEpochSecond();
|
||||
|
||||
// 验证创建时间和更新时间
|
||||
assertThat(savedUser.getCreatedAt()).isNotNull();
|
||||
assertThat(savedUser.getUpdatedAt()).isNotNull();
|
||||
assertThat(savedUser.getCreatedAt()).isBetween(beforeSave - 1, afterSave + 1);
|
||||
assertThat(savedUser.getUpdatedAt()).isBetween(beforeSave - 1, afterSave + 1);
|
||||
|
||||
System.out.println("✓ Timestamp fields working correctly");
|
||||
System.out.println(" Created at: " + savedUser.getCreatedAt());
|
||||
System.out.println(" Updated at: " + savedUser.getUpdatedAt());
|
||||
|
||||
// 清理
|
||||
userRepository.delete(savedUser);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSoftDelete() {
|
||||
// 测试软删除功能
|
||||
User user = new User();
|
||||
user.setEmail("softdelete@test.com");
|
||||
user.setPasswordHash("password");
|
||||
user.setNickname("SoftDeleteTest");
|
||||
|
||||
User savedUser = userRepository.save(user);
|
||||
Long userId = savedUser.getId();
|
||||
|
||||
// 执行软删除
|
||||
savedUser.markAsDeleted();
|
||||
userRepository.save(savedUser);
|
||||
|
||||
// 验证软删除
|
||||
User deletedUser = userRepository.findById(userId).orElse(null);
|
||||
assertThat(deletedUser).isNotNull();
|
||||
assertThat(deletedUser.isDeleted()).isTrue();
|
||||
assertThat(deletedUser.getDeletedAt()).isNotNull();
|
||||
|
||||
System.out.println("✓ Soft delete working correctly");
|
||||
System.out.println(" Deleted at: " + deletedUser.getDeletedAt());
|
||||
|
||||
// 测试查找未删除的用户
|
||||
var activeUser = userRepository.findByEmailAndNotDeleted("softdelete@test.com");
|
||||
assertThat(activeUser).isEmpty();
|
||||
|
||||
System.out.println("✓ Soft delete query filter working");
|
||||
|
||||
// 清理
|
||||
userRepository.delete(deletedUser);
|
||||
}
|
||||
}
|
||||
250
src/test/java/com/lxy/hsend/service/FavoriteServiceTest.java
Normal file
250
src/test/java/com/lxy/hsend/service/FavoriteServiceTest.java
Normal file
@ -0,0 +1,250 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.favorite.AddFavoriteRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteListRequest;
|
||||
import com.lxy.hsend.dto.favorite.FavoriteResponse;
|
||||
import com.lxy.hsend.dto.favorite.RemoveFavoriteRequest;
|
||||
import com.lxy.hsend.entity.Favorite;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.repository.FavoriteRepository;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 收藏服务测试
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
class FavoriteServiceTest {
|
||||
|
||||
@Autowired
|
||||
private FavoriteService favoriteService;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private SessionRepository sessionRepository;
|
||||
|
||||
@Autowired
|
||||
private MessageRepository messageRepository;
|
||||
|
||||
@Autowired
|
||||
private FavoriteRepository favoriteRepository;
|
||||
|
||||
private User testUser;
|
||||
private Session testSession;
|
||||
private Message testMessage;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 创建测试用户
|
||||
testUser = new User();
|
||||
testUser.setEmail("test@example.com");
|
||||
testUser.setNickname("testuser");
|
||||
testUser.setPasswordHash("hashedpassword");
|
||||
testUser.setStatus((byte) 1);
|
||||
testUser = userRepository.save(testUser);
|
||||
|
||||
// 创建测试会话
|
||||
testSession = new Session();
|
||||
testSession.setSessionId(UUID.randomUUID().toString());
|
||||
testSession.setUserId(testUser.getId());
|
||||
testSession.setTitle("测试会话");
|
||||
testSession.setMessageCount(0);
|
||||
testSession.setLastMessageTime(TimeUtil.getCurrentTimestamp());
|
||||
testSession = sessionRepository.save(testSession);
|
||||
|
||||
// 创建测试消息
|
||||
testMessage = new Message();
|
||||
testMessage.setMessageId(UUID.randomUUID().toString());
|
||||
testMessage.setSessionId(testSession.getSessionId());
|
||||
testMessage.setUserId(testUser.getId());
|
||||
testMessage.setRole("user");
|
||||
testMessage.setContent("这是一条测试消息内容");
|
||||
testMessage.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
testMessage.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
testMessage.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
testMessage = messageRepository.save(testMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddFavorite() {
|
||||
// 准备请求
|
||||
AddFavoriteRequest request = new AddFavoriteRequest();
|
||||
request.setMessageId(testMessage.getMessageId());
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
|
||||
// 添加收藏
|
||||
ApiResponse<FavoriteResponse> response = favoriteService.addFavorite(testUser.getId(), request);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
FavoriteResponse favoriteResponse = response.getBody();
|
||||
assertEquals(testMessage.getMessageId(), favoriteResponse.getMessageId());
|
||||
assertEquals(testSession.getSessionId(), favoriteResponse.getSessionId());
|
||||
assertEquals(testSession.getTitle(), favoriteResponse.getSessionTitle());
|
||||
assertEquals(testMessage.getContent(), favoriteResponse.getMessageContent());
|
||||
assertEquals(testMessage.getRole(), favoriteResponse.getMessageRole());
|
||||
assertNotNull(favoriteResponse.getFavoriteId());
|
||||
assertNotNull(favoriteResponse.getFavoritedAt());
|
||||
|
||||
// 验证数据库中的收藏记录
|
||||
boolean exists = favoriteRepository.existsByUserIdAndMessageIdAndNotDeleted(
|
||||
testUser.getId(), testMessage.getMessageId());
|
||||
assertTrue(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddFavoriteDuplicate() {
|
||||
// 先添加一次收藏
|
||||
AddFavoriteRequest request = new AddFavoriteRequest();
|
||||
request.setMessageId(testMessage.getMessageId());
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
|
||||
favoriteService.addFavorite(testUser.getId(), request);
|
||||
|
||||
// 再次尝试添加收藏,应该失败
|
||||
try {
|
||||
favoriteService.addFavorite(testUser.getId(), request);
|
||||
fail("应该抛出重复收藏的异常");
|
||||
} catch (Exception e) {
|
||||
assertTrue(e.getMessage().contains("已经收藏过了"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveFavorite() {
|
||||
// 先添加收藏
|
||||
AddFavoriteRequest addRequest = new AddFavoriteRequest();
|
||||
addRequest.setMessageId(testMessage.getMessageId());
|
||||
addRequest.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), addRequest);
|
||||
|
||||
// 准备取消收藏请求
|
||||
RemoveFavoriteRequest removeRequest = new RemoveFavoriteRequest();
|
||||
removeRequest.setMessageId(testMessage.getMessageId());
|
||||
|
||||
// 取消收藏
|
||||
ApiResponse<Void> response = favoriteService.removeFavorite(testUser.getId(), removeRequest);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
|
||||
// 验证收藏记录已被删除
|
||||
boolean exists = favoriteRepository.existsByUserIdAndMessageIdAndNotDeleted(
|
||||
testUser.getId(), testMessage.getMessageId());
|
||||
assertFalse(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFavoriteList() {
|
||||
// 先添加几个收藏
|
||||
AddFavoriteRequest request1 = new AddFavoriteRequest();
|
||||
request1.setMessageId(testMessage.getMessageId());
|
||||
request1.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), request1);
|
||||
|
||||
// 创建第二条消息和收藏
|
||||
Message message2 = new Message();
|
||||
message2.setMessageId(UUID.randomUUID().toString());
|
||||
message2.setSessionId(testSession.getSessionId());
|
||||
message2.setUserId(testUser.getId());
|
||||
message2.setRole("assistant");
|
||||
message2.setContent("这是AI助手的回答");
|
||||
message2.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
message2.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
message2.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
messageRepository.save(message2);
|
||||
|
||||
AddFavoriteRequest request2 = new AddFavoriteRequest();
|
||||
request2.setMessageId(message2.getMessageId());
|
||||
request2.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), request2);
|
||||
|
||||
// 准备查询请求
|
||||
FavoriteListRequest listRequest = new FavoriteListRequest();
|
||||
listRequest.setPage(1);
|
||||
listRequest.setPageSize(10);
|
||||
listRequest.setMessageRole("all");
|
||||
|
||||
// 获取收藏列表
|
||||
ApiResponse<PageUtil.PageResponse<FavoriteResponse>> response =
|
||||
favoriteService.getFavoriteList(testUser.getId(), listRequest);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
PageUtil.PageResponse<FavoriteResponse> pageResponse = response.getBody();
|
||||
assertNotNull(pageResponse.getContent());
|
||||
assertEquals(2, pageResponse.getContent().size());
|
||||
assertEquals(2L, pageResponse.getTotal());
|
||||
assertEquals(1, pageResponse.getPage());
|
||||
assertEquals(10, pageResponse.getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFavorited() {
|
||||
// 检查未收藏的消息
|
||||
ApiResponse<Boolean> response1 = favoriteService.isFavorited(testUser.getId(), testMessage.getMessageId());
|
||||
assertNotNull(response1);
|
||||
assertTrue(response1.getSuccess());
|
||||
assertFalse(response1.getBody());
|
||||
|
||||
// 添加收藏
|
||||
AddFavoriteRequest request = new AddFavoriteRequest();
|
||||
request.setMessageId(testMessage.getMessageId());
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), request);
|
||||
|
||||
// 检查已收藏的消息
|
||||
ApiResponse<Boolean> response2 = favoriteService.isFavorited(testUser.getId(), testMessage.getMessageId());
|
||||
assertNotNull(response2);
|
||||
assertTrue(response2.getSuccess());
|
||||
assertTrue(response2.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoriteCount() {
|
||||
// 初始收藏数量应该为0
|
||||
ApiResponse<Long> response1 = favoriteService.getUserFavoriteCount(testUser.getId());
|
||||
assertNotNull(response1);
|
||||
assertTrue(response1.getSuccess());
|
||||
assertEquals(0L, response1.getBody());
|
||||
|
||||
// 添加一个收藏
|
||||
AddFavoriteRequest request = new AddFavoriteRequest();
|
||||
request.setMessageId(testMessage.getMessageId());
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
favoriteService.addFavorite(testUser.getId(), request);
|
||||
|
||||
// 收藏数量应该为1
|
||||
ApiResponse<Long> response2 = favoriteService.getUserFavoriteCount(testUser.getId());
|
||||
assertNotNull(response2);
|
||||
assertTrue(response2.getSuccess());
|
||||
assertEquals(1L, response2.getBody());
|
||||
}
|
||||
}
|
||||
176
src/test/java/com/lxy/hsend/service/MessageServiceTest.java
Normal file
176
src/test/java/com/lxy/hsend/service/MessageServiceTest.java
Normal file
@ -0,0 +1,176 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.common.ApiResponse;
|
||||
import com.lxy.hsend.dto.message.MessageListRequest;
|
||||
import com.lxy.hsend.dto.message.MessageResponse;
|
||||
import com.lxy.hsend.dto.message.SendMessageRequest;
|
||||
import com.lxy.hsend.entity.Message;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.repository.MessageRepository;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import com.lxy.hsend.util.PageUtil;
|
||||
import com.lxy.hsend.util.TimeUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 消息服务测试
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
class MessageServiceTest {
|
||||
|
||||
@Autowired
|
||||
private MessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private SessionRepository sessionRepository;
|
||||
|
||||
@Autowired
|
||||
private MessageRepository messageRepository;
|
||||
|
||||
private User testUser;
|
||||
private Session testSession;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 创建测试用户
|
||||
testUser = new User();
|
||||
testUser.setEmail("test@example.com");
|
||||
testUser.setNickname("testuser");
|
||||
testUser.setPasswordHash("hashedpassword");
|
||||
testUser.setStatus((byte) 1);
|
||||
testUser = userRepository.save(testUser);
|
||||
|
||||
// 创建测试会话
|
||||
testSession = new Session();
|
||||
testSession.setSessionId(UUID.randomUUID().toString());
|
||||
testSession.setUserId(testUser.getId());
|
||||
testSession.setTitle("测试会话");
|
||||
testSession.setMessageCount(0);
|
||||
testSession.setLastMessageTime(TimeUtil.getCurrentTimestamp());
|
||||
testSession = sessionRepository.save(testSession);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendMessage() {
|
||||
// 准备请求
|
||||
SendMessageRequest request = new SendMessageRequest();
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
request.setContent("你好,这是一个测试消息");
|
||||
request.setDeepThinking(false);
|
||||
request.setWebSearch(false);
|
||||
|
||||
// 发送消息
|
||||
ApiResponse<MessageResponse> response = messageService.sendMessage(testUser.getId(), request);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
MessageResponse messageResponse = response.getBody();
|
||||
assertEquals(testSession.getSessionId(), messageResponse.getSessionId());
|
||||
assertEquals("assistant", messageResponse.getRole());
|
||||
assertNotNull(messageResponse.getContent());
|
||||
assertNotNull(messageResponse.getMessageId());
|
||||
assertNotNull(messageResponse.getCreatedAt());
|
||||
|
||||
// 验证数据库中的消息
|
||||
long messageCount = messageRepository.countBySessionIdAndNotDeleted(testSession.getSessionId());
|
||||
assertEquals(2, messageCount); // 用户消息 + AI响应
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessages() {
|
||||
// 先创建一些测试消息
|
||||
Message userMessage = new Message();
|
||||
userMessage.setMessageId(UUID.randomUUID().toString());
|
||||
userMessage.setSessionId(testSession.getSessionId());
|
||||
userMessage.setUserId(testUser.getId());
|
||||
userMessage.setRole("user");
|
||||
userMessage.setContent("用户消息");
|
||||
userMessage.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
userMessage.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
userMessage.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
messageRepository.save(userMessage);
|
||||
|
||||
Message aiMessage = new Message();
|
||||
aiMessage.setMessageId(UUID.randomUUID().toString());
|
||||
aiMessage.setSessionId(testSession.getSessionId());
|
||||
aiMessage.setUserId(testUser.getId());
|
||||
aiMessage.setRole("assistant");
|
||||
aiMessage.setContent("AI响应");
|
||||
aiMessage.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
aiMessage.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
aiMessage.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
messageRepository.save(aiMessage);
|
||||
|
||||
// 准备请求
|
||||
MessageListRequest request = new MessageListRequest();
|
||||
request.setSessionId(testSession.getSessionId());
|
||||
request.setPage(1);
|
||||
request.setPageSize(10);
|
||||
request.setRole("all");
|
||||
|
||||
// 获取消息列表
|
||||
ApiResponse<PageUtil.PageResponse<MessageResponse>> response =
|
||||
messageService.getMessages(testUser.getId(), request);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
PageUtil.PageResponse<MessageResponse> pageResponse = response.getBody();
|
||||
assertNotNull(pageResponse.getContent());
|
||||
assertEquals(2, pageResponse.getContent().size());
|
||||
assertEquals(2L, pageResponse.getTotal());
|
||||
assertEquals(1, pageResponse.getPage());
|
||||
assertEquals(10, pageResponse.getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMessageById() {
|
||||
// 创建测试消息
|
||||
Message testMessage = new Message();
|
||||
testMessage.setMessageId(UUID.randomUUID().toString());
|
||||
testMessage.setSessionId(testSession.getSessionId());
|
||||
testMessage.setUserId(testUser.getId());
|
||||
testMessage.setRole("user");
|
||||
testMessage.setContent("测试消息内容");
|
||||
testMessage.setTimestamp(TimeUtil.getCurrentTimestamp());
|
||||
testMessage.setCreatedAt(TimeUtil.getCurrentTimestamp());
|
||||
testMessage.setUpdatedAt(TimeUtil.getCurrentTimestamp());
|
||||
testMessage = messageRepository.save(testMessage);
|
||||
|
||||
// 获取消息详情
|
||||
ApiResponse<MessageResponse> response =
|
||||
messageService.getMessageById(testUser.getId(), testMessage.getMessageId());
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getSuccess());
|
||||
assertNotNull(response.getBody());
|
||||
|
||||
MessageResponse messageResponse = response.getBody();
|
||||
assertEquals(testMessage.getMessageId(), messageResponse.getMessageId());
|
||||
assertEquals(testMessage.getSessionId(), messageResponse.getSessionId());
|
||||
assertEquals(testMessage.getRole(), messageResponse.getRole());
|
||||
assertEquals(testMessage.getContent(), messageResponse.getContent());
|
||||
}
|
||||
}
|
||||
168
src/test/java/com/lxy/hsend/service/SessionServiceTest.java
Normal file
168
src/test/java/com/lxy/hsend/service/SessionServiceTest.java
Normal file
@ -0,0 +1,168 @@
|
||||
package com.lxy.hsend.service;
|
||||
|
||||
import com.lxy.hsend.dto.session.CreateSessionRequest;
|
||||
import com.lxy.hsend.dto.session.SessionDetailResponse;
|
||||
import com.lxy.hsend.dto.session.SessionSearchRequest;
|
||||
import com.lxy.hsend.dto.session.UpdateSessionRequest;
|
||||
import com.lxy.hsend.entity.Session;
|
||||
import com.lxy.hsend.entity.User;
|
||||
import com.lxy.hsend.repository.SessionRepository;
|
||||
import com.lxy.hsend.repository.UserRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 会话服务测试
|
||||
*
|
||||
* @author lxy
|
||||
*/
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
public class SessionServiceTest {
|
||||
|
||||
@Autowired
|
||||
private SessionService sessionService;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private SessionRepository sessionRepository;
|
||||
|
||||
@Test
|
||||
public void testSessionManagement() {
|
||||
log.info("开始测试会话管理功能");
|
||||
|
||||
// 1. 创建测试用户
|
||||
User testUser = createTestUser();
|
||||
Long userId = testUser.getId();
|
||||
|
||||
// 2. 测试创建会话
|
||||
CreateSessionRequest createRequest = new CreateSessionRequest();
|
||||
createRequest.setTitle("测试会话标题");
|
||||
createRequest.setInitialMessage("这是一条初始消息");
|
||||
|
||||
SessionDetailResponse createdSession = sessionService.createSession(userId, createRequest);
|
||||
|
||||
assertNotNull(createdSession);
|
||||
assertNotNull(createdSession.getSessionId());
|
||||
assertEquals("测试会话标题", createdSession.getTitle());
|
||||
assertEquals(userId, createdSession.getUserId());
|
||||
log.info("✅ 会话创建成功: sessionId={}", createdSession.getSessionId());
|
||||
|
||||
// 3. 测试获取会话详情
|
||||
SessionDetailResponse sessionDetail = sessionService.getSessionDetail(userId, createdSession.getSessionId());
|
||||
|
||||
assertNotNull(sessionDetail);
|
||||
assertEquals(createdSession.getSessionId(), sessionDetail.getSessionId());
|
||||
assertEquals("测试会话标题", sessionDetail.getTitle());
|
||||
log.info("✅ 获取会话详情成功");
|
||||
|
||||
// 4. 测试更新会话标题
|
||||
UpdateSessionRequest updateRequest = new UpdateSessionRequest();
|
||||
updateRequest.setTitle("更新后的标题");
|
||||
|
||||
SessionDetailResponse updatedSession = sessionService.updateSession(userId, createdSession.getSessionId(), updateRequest);
|
||||
|
||||
assertNotNull(updatedSession);
|
||||
assertEquals("更新后的标题", updatedSession.getTitle());
|
||||
log.info("✅ 会话标题更新成功");
|
||||
|
||||
// 5. 测试获取会话列表
|
||||
SessionSearchRequest searchRequest = new SessionSearchRequest();
|
||||
searchRequest.setPage(1);
|
||||
searchRequest.setPageSize(10);
|
||||
|
||||
Page<com.lxy.hsend.dto.session.SessionListResponse> sessionList = sessionService.getSessionList(userId, searchRequest);
|
||||
|
||||
assertNotNull(sessionList);
|
||||
assertTrue(sessionList.getTotalElements() >= 1);
|
||||
log.info("✅ 获取会话列表成功,总数: {}", sessionList.getTotalElements());
|
||||
|
||||
// 6. 测试会话权限验证
|
||||
boolean hasPermission = sessionService.hasSessionPermission(userId, createdSession.getSessionId());
|
||||
assertTrue(hasPermission);
|
||||
log.info("✅ 会话权限验证成功");
|
||||
|
||||
// 7. 测试清空会话
|
||||
sessionService.clearSession(userId, createdSession.getSessionId());
|
||||
log.info("✅ 会话清空成功");
|
||||
|
||||
// 8. 测试删除会话
|
||||
sessionService.deleteSession(userId, createdSession.getSessionId());
|
||||
log.info("✅ 会话删除成功");
|
||||
|
||||
// 验证会话已被软删除
|
||||
boolean hasPermissionAfterDelete = sessionService.hasSessionPermission(userId, createdSession.getSessionId());
|
||||
assertFalse(hasPermissionAfterDelete);
|
||||
log.info("✅ 会话软删除验证成功");
|
||||
|
||||
System.out.println("✅ 所有会话管理功能测试通过!");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionSearch() {
|
||||
log.info("开始测试会话搜索功能");
|
||||
|
||||
// 创建测试用户
|
||||
User testUser = createTestUser();
|
||||
Long userId = testUser.getId();
|
||||
|
||||
// 创建多个测试会话
|
||||
CreateSessionRequest request1 = new CreateSessionRequest();
|
||||
request1.setTitle("Java开发学习");
|
||||
sessionService.createSession(userId, request1);
|
||||
|
||||
CreateSessionRequest request2 = new CreateSessionRequest();
|
||||
request2.setTitle("Python数据分析");
|
||||
sessionService.createSession(userId, request2);
|
||||
|
||||
CreateSessionRequest request3 = new CreateSessionRequest();
|
||||
request3.setTitle("前端React开发");
|
||||
sessionService.createSession(userId, request3);
|
||||
|
||||
// 测试关键词搜索
|
||||
SessionSearchRequest searchRequest = new SessionSearchRequest();
|
||||
searchRequest.setKeyword("开发");
|
||||
searchRequest.setPage(1);
|
||||
searchRequest.setPageSize(10);
|
||||
|
||||
Page<com.lxy.hsend.dto.session.SessionListResponse> searchResult = sessionService.getSessionList(userId, searchRequest);
|
||||
|
||||
assertNotNull(searchResult);
|
||||
assertTrue(searchResult.getTotalElements() >= 2); // 至少找到2个包含"开发"的会话
|
||||
log.info("✅ 会话关键词搜索成功,找到 {} 个结果", searchResult.getTotalElements());
|
||||
|
||||
// 测试排序
|
||||
searchRequest.setKeyword(null);
|
||||
searchRequest.setSortBy("created_at");
|
||||
searchRequest.setSortOrder("desc");
|
||||
|
||||
Page<com.lxy.hsend.dto.session.SessionListResponse> sortedResult = sessionService.getSessionList(userId, searchRequest);
|
||||
assertNotNull(sortedResult);
|
||||
log.info("✅ 会话排序功能测试成功");
|
||||
|
||||
System.out.println("✅ 会话搜索功能测试通过!");
|
||||
}
|
||||
|
||||
private User createTestUser() {
|
||||
User user = new User();
|
||||
user.setEmail("session-test@example.com");
|
||||
user.setPasswordHash("test-password-hash");
|
||||
user.setNickname("会话测试用户");
|
||||
user.setStatus((byte) 1);
|
||||
user.setCreatedAt(System.currentTimeMillis());
|
||||
user.setUpdatedAt(System.currentTimeMillis());
|
||||
|
||||
return userRepository.save(user);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user