新增多个模块和功能,包括用户评论、帖子、WebSocket通信优化及日志配置,更新文档和部署脚本,提升项目整体性能和可维护性。
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.emotionmuseum</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>emotion-common</artifactId>
|
||||
<name>emotion-common</name>
|
||||
<description>公共模块</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- API文档 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.emotionmuseum.common.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.emotionmuseum.common.handler.EmotionMetaObjectHandler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 拦截器配置
|
||||
* 添加分页插件
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
|
||||
// 分页插件
|
||||
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
|
||||
paginationInnerInterceptor.setDbType(DbType.MYSQL);
|
||||
paginationInnerInterceptor.setOverflow(false);
|
||||
paginationInnerInterceptor.setMaxLimit(1000L);
|
||||
interceptor.addInnerInterceptor(paginationInnerInterceptor);
|
||||
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局配置
|
||||
*/
|
||||
@Bean
|
||||
public GlobalConfig globalConfig() {
|
||||
GlobalConfig globalConfig = new GlobalConfig();
|
||||
|
||||
// 设置元数据处理器
|
||||
globalConfig.setMetaObjectHandler(new EmotionMetaObjectHandler());
|
||||
|
||||
// 设置逻辑删除配置
|
||||
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
|
||||
dbConfig.setLogicDeleteField("is_deleted"); // 逻辑删除字段名
|
||||
dbConfig.setLogicDeleteValue("1"); // 删除值
|
||||
dbConfig.setLogicNotDeleteValue("0"); // 未删除值
|
||||
globalConfig.setDbConfig(dbConfig);
|
||||
|
||||
return globalConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.emotionmuseum.common.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* RestTemplate配置类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
/**
|
||||
* 默认RestTemplate
|
||||
*/
|
||||
@Bean("restTemplate")
|
||||
public RestTemplate restTemplate(RestTemplateBuilder builder) {
|
||||
return builder
|
||||
.setConnectTimeout(Duration.ofSeconds(30))
|
||||
.setReadTimeout(Duration.ofSeconds(60))
|
||||
.interceptors(Collections.singletonList(loggingInterceptor()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 长连接RestTemplate(用于AI接口等耗时操作)
|
||||
*/
|
||||
@Bean("longRestTemplate")
|
||||
public RestTemplate longRestTemplate(RestTemplateBuilder builder) {
|
||||
return builder
|
||||
.setConnectTimeout(Duration.ofSeconds(60))
|
||||
.setReadTimeout(Duration.ofSeconds(300))
|
||||
.interceptors(Collections.singletonList(loggingInterceptor()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速RestTemplate(用于内部服务调用)
|
||||
*/
|
||||
@Bean("fastRestTemplate")
|
||||
public RestTemplate fastRestTemplate(RestTemplateBuilder builder) {
|
||||
return builder
|
||||
.setConnectTimeout(Duration.ofSeconds(5))
|
||||
.setReadTimeout(Duration.ofSeconds(10))
|
||||
.interceptors(Collections.singletonList(loggingInterceptor()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求日志拦截器
|
||||
*/
|
||||
private ClientHttpRequestInterceptor loggingInterceptor() {
|
||||
return (request, body, execution) -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 记录请求信息
|
||||
log.debug("HTTP Request: {} {}", request.getMethod(), request.getURI());
|
||||
if (body.length > 0 && body.length < 1000) {
|
||||
log.debug("Request Body: {}", new String(body));
|
||||
}
|
||||
|
||||
ClientHttpResponse response = null;
|
||||
try {
|
||||
response = execution.execute(request, body);
|
||||
|
||||
// 记录响应信息
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
log.debug("HTTP Response: {} {} - {}ms",
|
||||
response.getStatusCode().value(),
|
||||
response.getStatusText(),
|
||||
duration);
|
||||
|
||||
return response;
|
||||
} catch (IOException e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
log.error("HTTP Request failed: {} {} - {}ms, Error: {}",
|
||||
request.getMethod(),
|
||||
request.getURI(),
|
||||
duration,
|
||||
e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.emotionmuseum.common.config;
|
||||
|
||||
import com.emotionmuseum.common.util.SnowflakeIdGenerator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.util.Enumeration;
|
||||
|
||||
/**
|
||||
* 雪花算法配置类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class SnowflakeConfig {
|
||||
|
||||
/**
|
||||
* 机器ID配置,可通过配置文件指定
|
||||
*/
|
||||
@Value("${snowflake.machine-id:#{null}}")
|
||||
private Long configuredMachineId;
|
||||
|
||||
/**
|
||||
* 创建雪花算法ID生成器Bean
|
||||
*
|
||||
* @return SnowflakeIdGenerator实例
|
||||
*/
|
||||
@Bean
|
||||
public SnowflakeIdGenerator snowflakeIdGenerator() {
|
||||
long machineId = getMachineId();
|
||||
log.info("雪花算法配置完成,使用机器ID: {}", machineId);
|
||||
return new SnowflakeIdGenerator(machineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器ID
|
||||
* 优先级:配置文件 > 网络接口MAC地址 > 系统时间戳
|
||||
*
|
||||
* @return 机器ID
|
||||
*/
|
||||
private long getMachineId() {
|
||||
// 1. 优先使用配置文件中的机器ID
|
||||
if (configuredMachineId != null) {
|
||||
long machineId = configuredMachineId % 1024; // 确保在0-1023范围内
|
||||
log.info("使用配置文件中的机器ID: {} (原始值: {})", machineId, configuredMachineId);
|
||||
return machineId;
|
||||
}
|
||||
|
||||
// 2. 尝试使用网络接口MAC地址生成机器ID
|
||||
try {
|
||||
long machineId = getMachineIdFromMac();
|
||||
log.info("使用MAC地址生成的机器ID: {}", machineId);
|
||||
return machineId;
|
||||
} catch (Exception e) {
|
||||
log.warn("无法从MAC地址生成机器ID: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 3. 使用系统时间戳作为后备方案
|
||||
long machineId = System.currentTimeMillis() % 1024;
|
||||
log.info("使用系统时间戳生成的机器ID: {}", machineId);
|
||||
return machineId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MAC地址生成机器ID
|
||||
*
|
||||
* @return 机器ID
|
||||
* @throws Exception 获取MAC地址失败
|
||||
*/
|
||||
private long getMachineIdFromMac() throws Exception {
|
||||
// 获取本机所有网络接口
|
||||
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
|
||||
while (networkInterfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = networkInterfaces.nextElement();
|
||||
|
||||
// 跳过回环接口和虚拟接口
|
||||
if (networkInterface.isLoopback() || networkInterface.isVirtual() || !networkInterface.isUp()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] mac = networkInterface.getHardwareAddress();
|
||||
if (mac != null && mac.length >= 6) {
|
||||
// 使用MAC地址的后两个字节生成机器ID
|
||||
long machineId = ((long) (mac[mac.length - 2] & 0xFF) << 8)
|
||||
| (long) (mac[mac.length - 1] & 0xFF);
|
||||
return machineId % 1024; // 确保在0-1023范围内
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到合适的网络接口,使用本机IP地址
|
||||
InetAddress localHost = InetAddress.getLocalHost();
|
||||
byte[] address = localHost.getAddress();
|
||||
if (address.length >= 4) {
|
||||
// 使用IP地址的后两个字节生成机器ID
|
||||
long machineId = ((long) (address[address.length - 2] & 0xFF) << 8)
|
||||
| (long) (address[address.length - 1] & 0xFF);
|
||||
return machineId % 1024;
|
||||
}
|
||||
|
||||
throw new RuntimeException("无法获取网络接口信息生成机器ID");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.emotionmuseum.common.config;
|
||||
|
||||
import com.emotionmuseum.common.interceptor.UserContextInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 配置类
|
||||
* 注册拦截器和其他Web相关配置
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final UserContextInterceptor userContextInterceptor;
|
||||
|
||||
/**
|
||||
* 注册拦截器
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(userContextInterceptor)
|
||||
.addPathPatterns("/**") // 拦截所有请求
|
||||
.excludePathPatterns(
|
||||
// 排除静态资源
|
||||
"/static/**",
|
||||
"/css/**",
|
||||
"/js/**",
|
||||
"/images/**",
|
||||
"/favicon.ico",
|
||||
|
||||
// 排除Swagger相关
|
||||
"/swagger-ui/**",
|
||||
"/swagger-resources/**",
|
||||
"/v2/api-docs",
|
||||
"/v3/api-docs",
|
||||
"/doc.html",
|
||||
|
||||
// 排除健康检查
|
||||
"/actuator/**",
|
||||
"/health",
|
||||
|
||||
// 排除错误页面
|
||||
"/error"
|
||||
)
|
||||
.order(1); // 设置拦截器执行顺序
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.emotionmuseum.common.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 分页查询基类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分页查询参数")
|
||||
public class PageQuery {
|
||||
|
||||
/**
|
||||
* 页码
|
||||
*/
|
||||
@Schema(description = "页码", example = "1")
|
||||
@NotNull(message = "页码不能为空")
|
||||
@Min(value = 1, message = "页码最小为1")
|
||||
private Integer pageNum = 1;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
@Schema(description = "每页大小", example = "10")
|
||||
@NotNull(message = "每页大小不能为空")
|
||||
@Min(value = 1, message = "每页大小最小为1")
|
||||
@Max(value = 100, message = "每页大小最大为100")
|
||||
private Integer pageSize = 10;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
@Schema(description = "排序字段", example = "create_time")
|
||||
private String orderBy;
|
||||
|
||||
/**
|
||||
* 排序方向
|
||||
*/
|
||||
@Schema(description = "排序方向", example = "desc")
|
||||
private String orderDirection = "desc";
|
||||
|
||||
/**
|
||||
* 搜索关键词
|
||||
*/
|
||||
@Schema(description = "搜索关键词")
|
||||
private String keyword;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.emotionmuseum.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 基础实体类
|
||||
* 包含所有表的公共字段:create_by, create_time, update_by, update_time, is_deleted,
|
||||
* remarks
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
public abstract class BaseEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID - 使用雪花算法生成的字符串ID
|
||||
* 避免前端JavaScript精度丢失问题
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.INPUT)
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 创建人ID
|
||||
*/
|
||||
@TableField(value = "create_by", fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新人ID
|
||||
*/
|
||||
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 是否删除:0-未删除,1-已删除
|
||||
*/
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
@TableField(value = "is_deleted", fill = FieldFill.INSERT)
|
||||
private Integer isDeleted;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@TableField(value = "remarks")
|
||||
private String remarks;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.emotionmuseum.common.exception;
|
||||
|
||||
import com.emotionmuseum.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 认证异常
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Getter
|
||||
public class AuthException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private final Integer code;
|
||||
|
||||
/**
|
||||
* 错误消息
|
||||
*/
|
||||
private final String message;
|
||||
|
||||
public AuthException(String message) {
|
||||
super(message);
|
||||
this.code = ResultCode.UNAUTHORIZED.getCode();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public AuthException(Integer code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public AuthException(ResultCode resultCode) {
|
||||
super(resultCode.getMessage());
|
||||
this.code = resultCode.getCode();
|
||||
this.message = resultCode.getMessage();
|
||||
}
|
||||
|
||||
public AuthException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = ResultCode.UNAUTHORIZED.getCode();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public AuthException(Integer code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public AuthException(ResultCode resultCode, Throwable cause) {
|
||||
super(resultCode.getMessage(), cause);
|
||||
this.code = resultCode.getCode();
|
||||
this.message = resultCode.getMessage();
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.emotionmuseum.common.exception;
|
||||
|
||||
import com.emotionmuseum.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private final Integer code;
|
||||
|
||||
/**
|
||||
* 错误消息
|
||||
*/
|
||||
private final String message;
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = ResultCode.BUSINESS_ERROR.getCode();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public BusinessException(Integer code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public BusinessException(ResultCode resultCode) {
|
||||
super(resultCode.getMessage());
|
||||
this.code = resultCode.getCode();
|
||||
this.message = resultCode.getMessage();
|
||||
}
|
||||
|
||||
public BusinessException(ResultCode resultCode, String message) {
|
||||
super(message);
|
||||
this.code = resultCode.getCode();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public BusinessException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = ResultCode.BUSINESS_ERROR.getCode();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public BusinessException(Integer code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public BusinessException(ResultCode resultCode, Throwable cause) {
|
||||
super(resultCode.getMessage(), cause);
|
||||
this.code = resultCode.getCode();
|
||||
this.message = resultCode.getMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.emotionmuseum.common.exception;
|
||||
|
||||
import com.emotionmuseum.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 验证码异常
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Getter
|
||||
public class CaptchaException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private final Integer code;
|
||||
|
||||
/**
|
||||
* 错误消息
|
||||
*/
|
||||
private final String message;
|
||||
|
||||
public CaptchaException(String message) {
|
||||
super(message);
|
||||
this.code = ResultCode.CAPTCHA_ERROR.getCode();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public CaptchaException(Integer code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public CaptchaException(ResultCode resultCode) {
|
||||
super(resultCode.getMessage());
|
||||
this.code = resultCode.getCode();
|
||||
this.message = resultCode.getMessage();
|
||||
}
|
||||
|
||||
public CaptchaException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = ResultCode.CAPTCHA_ERROR.getCode();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public CaptchaException(Integer code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public CaptchaException(ResultCode resultCode, Throwable cause) {
|
||||
super(resultCode.getMessage(), cause);
|
||||
this.code = resultCode.getCode();
|
||||
this.message = resultCode.getMessage();
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package com.emotionmuseum.common.exception;
|
||||
|
||||
import com.emotionmuseum.common.result.Result;
|
||||
import com.emotionmuseum.common.result.ResultCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理认证异常
|
||||
*/
|
||||
@ExceptionHandler(AuthException.class)
|
||||
public Result<Void> handleAuthException(AuthException e, HttpServletRequest request) {
|
||||
log.warn("认证异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
|
||||
return Result.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理令牌异常
|
||||
*/
|
||||
@ExceptionHandler(TokenException.class)
|
||||
public Result<Void> handleTokenException(TokenException e, HttpServletRequest request) {
|
||||
log.warn("令牌异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
|
||||
return Result.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理验证码异常
|
||||
*/
|
||||
@ExceptionHandler(CaptchaException.class)
|
||||
public Result<Void> handleCaptchaException(CaptchaException e, HttpServletRequest request) {
|
||||
log.warn("验证码异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
|
||||
return Result.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
|
||||
log.warn("业务异常: {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
|
||||
return Result.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
|
||||
log.warn("参数校验失败: {} {}", request.getMethod(), request.getRequestURI(), e);
|
||||
|
||||
StringBuilder message = new StringBuilder("参数校验失败: ");
|
||||
for (FieldError error : e.getBindingResult().getFieldErrors()) {
|
||||
message.append(error.getField()).append(" ").append(error.getDefaultMessage()).append("; ");
|
||||
}
|
||||
|
||||
return Result.error(ResultCode.PARAM_VALIDATION_ERROR.getCode(), message.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Bean校验异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Void> handleBindException(BindException e, HttpServletRequest request) {
|
||||
log.warn("参数绑定失败: {} {}", request.getMethod(), request.getRequestURI(), e);
|
||||
|
||||
StringBuilder message = new StringBuilder("参数绑定失败: ");
|
||||
for (FieldError error : e.getBindingResult().getFieldErrors()) {
|
||||
message.append(error.getField()).append(" ").append(error.getDefaultMessage()).append("; ");
|
||||
}
|
||||
|
||||
return Result.error(ResultCode.PARAM_VALIDATION_ERROR.getCode(), message.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理约束校验异常
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Void> handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
|
||||
log.warn("约束校验失败: {} {}", request.getMethod(), request.getRequestURI(), e);
|
||||
|
||||
StringBuilder message = new StringBuilder("约束校验失败: ");
|
||||
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
|
||||
for (ConstraintViolation<?> violation : violations) {
|
||||
message.append(violation.getPropertyPath()).append(" ").append(violation.getMessage()).append("; ");
|
||||
}
|
||||
|
||||
return Result.error(ResultCode.PARAM_VALIDATION_ERROR.getCode(), message.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理非法参数异常
|
||||
*/
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Void> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
|
||||
log.warn("非法参数: {} {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return Result.error(ResultCode.BAD_REQUEST.getCode(), "参数错误: " + e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理空指针异常
|
||||
*/
|
||||
@ExceptionHandler(NullPointerException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Result<Void> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
|
||||
log.error("空指针异常: {} {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return Result.error(ResultCode.INTERNAL_SERVER_ERROR.getCode(), "系统内部错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理运行时异常
|
||||
*/
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Result<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
|
||||
log.error("运行时异常: {} {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return Result.error(ResultCode.INTERNAL_SERVER_ERROR.getCode(), "系统运行异常: " + e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理所有其他异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Result<Void> handleException(Exception e, HttpServletRequest request) {
|
||||
log.error("未知异常: {} {}", request.getMethod(), request.getRequestURI(), e);
|
||||
return Result.error(ResultCode.INTERNAL_SERVER_ERROR.getCode(), "系统异常,请联系管理员");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.emotionmuseum.common.exception;
|
||||
|
||||
import com.emotionmuseum.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Token异常
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Getter
|
||||
public class TokenException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private final Integer code;
|
||||
|
||||
/**
|
||||
* 错误消息
|
||||
*/
|
||||
private final String message;
|
||||
|
||||
public TokenException(String message) {
|
||||
super(message);
|
||||
this.code = ResultCode.TOKEN_INVALID.getCode();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public TokenException(Integer code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public TokenException(ResultCode resultCode) {
|
||||
super(resultCode.getMessage());
|
||||
this.code = resultCode.getCode();
|
||||
this.message = resultCode.getMessage();
|
||||
}
|
||||
|
||||
public TokenException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = ResultCode.TOKEN_INVALID.getCode();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public TokenException(Integer code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public TokenException(ResultCode resultCode, Throwable cause) {
|
||||
super(resultCode.getMessage(), cause);
|
||||
this.code = resultCode.getCode();
|
||||
this.message = resultCode.getMessage();
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package com.emotionmuseum.common.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.emotionmuseum.common.util.SnowflakeIdGenerator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 自动填充处理器
|
||||
* 自动填充公共字段:id, create_by, create_time, update_by, update_time
|
||||
* 支持雪花算法自动生成主键ID
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class EmotionMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 雪花算法ID生成器
|
||||
*/
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
/**
|
||||
* 插入时自动填充
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String currentUserId = getCurrentUserId();
|
||||
|
||||
// 填充主键ID(如果为空)
|
||||
fillPrimaryKey(metaObject);
|
||||
|
||||
// 填充创建时间
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
|
||||
|
||||
// 填充更新时间
|
||||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);
|
||||
|
||||
// 填充创建人ID
|
||||
if (currentUserId != null) {
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, currentUserId);
|
||||
}
|
||||
|
||||
// 填充更新人ID
|
||||
if (currentUserId != null) {
|
||||
this.strictInsertFill(metaObject, "updateBy", String.class, currentUserId);
|
||||
}
|
||||
|
||||
// 填充逻辑删除字段默认值
|
||||
this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0);
|
||||
|
||||
log.debug("插入时自动填充完成: createTime={}, updateTime={}, createBy={}, updateBy={}",
|
||||
now, now, currentUserId, currentUserId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 自动填充失败不应该影响业务逻辑
|
||||
log.warn("插入时自动填充失败,但不影响业务逻辑: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新时自动填充
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String currentUserId = getCurrentUserId();
|
||||
|
||||
// 填充更新时间
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, now);
|
||||
|
||||
// 填充更新人ID
|
||||
if (currentUserId != null) {
|
||||
this.strictUpdateFill(metaObject, "updateBy", String.class, currentUserId);
|
||||
}
|
||||
|
||||
log.debug("更新时自动填充完成: updateTime={}, updateBy={}", now, currentUserId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 自动填充失败不应该影响业务逻辑
|
||||
log.warn("更新时自动填充失败,但不影响业务逻辑: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充主键ID
|
||||
* 如果主键ID为空,则使用雪花算法生成
|
||||
*
|
||||
* @param metaObject 元对象
|
||||
*/
|
||||
private void fillPrimaryKey(MetaObject metaObject) {
|
||||
try {
|
||||
// 检查是否有id字段
|
||||
if (metaObject.hasSetter("id")) {
|
||||
Object idValue = metaObject.getValue("id");
|
||||
// 如果ID为空,则生成新的ID
|
||||
if (idValue == null || (idValue instanceof String && ((String) idValue).isEmpty())) {
|
||||
String newId = snowflakeIdGenerator.nextIdAsString();
|
||||
this.strictInsertFill(metaObject, "id", String.class, newId);
|
||||
log.debug("自动生成主键ID: {}", newId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("主键ID自动填充失败,但不影响业务逻辑: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
* 优先级:
|
||||
* 1. 从ThreadLocal获取(如果有用户上下文)
|
||||
* 2. 从Spring Security获取(如果有认证信息)
|
||||
* 3. 返回系统默认值
|
||||
*
|
||||
* @return 当前用户ID,如果获取失败返回null
|
||||
*/
|
||||
private String getCurrentUserId() {
|
||||
try {
|
||||
// 1. 尝试从ThreadLocal获取用户ID(如果有用户上下文工具类)
|
||||
String userIdFromContext = getUserIdFromContext();
|
||||
if (userIdFromContext != null) {
|
||||
return userIdFromContext;
|
||||
}
|
||||
|
||||
// 2. 尝试从Spring Security获取用户ID
|
||||
String userIdFromSecurity = getUserIdFromSecurity();
|
||||
if (userIdFromSecurity != null) {
|
||||
return userIdFromSecurity;
|
||||
}
|
||||
|
||||
// 3. 返回系统默认值(用于系统操作或未登录用户)
|
||||
return "system";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("获取当前用户ID失败: {}", e.getMessage());
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户上下文获取用户ID
|
||||
* 这里可以集成自定义的用户上下文工具类
|
||||
*
|
||||
* @return 用户ID或null
|
||||
*/
|
||||
private String getUserIdFromContext() {
|
||||
try {
|
||||
// TODO: 集成用户上下文工具类
|
||||
// 例如:return UserContextHolder.getCurrentUserId();
|
||||
|
||||
// 临时实现:从线程变量获取
|
||||
return UserContextHolder.getCurrentUserId();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("从用户上下文获取用户ID失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Spring Security获取用户ID
|
||||
*
|
||||
* @return 用户ID或null
|
||||
*/
|
||||
private String getUserIdFromSecurity() {
|
||||
try {
|
||||
// TODO: 集成Spring Security
|
||||
// Authentication authentication =
|
||||
// SecurityContextHolder.getContext().getAuthentication();
|
||||
// if (authentication != null && authentication.isAuthenticated()
|
||||
// && !"anonymousUser".equals(authentication.getPrincipal())) {
|
||||
// UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||
// return userDetails.getUsername(); // 或者从UserDetails中获取用户ID
|
||||
// }
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("从Spring Security获取用户ID失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户上下文持有者
|
||||
* 用于在当前线程中存储用户信息
|
||||
*/
|
||||
public static class UserContextHolder {
|
||||
|
||||
private static final ThreadLocal<String> USER_ID_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 设置当前用户ID
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public static void setCurrentUserId(String userId) {
|
||||
USER_ID_HOLDER.set(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*
|
||||
* @return 用户ID
|
||||
*/
|
||||
public static String getCurrentUserId() {
|
||||
return USER_ID_HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前用户ID
|
||||
*/
|
||||
public static void clear() {
|
||||
USER_ID_HOLDER.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package com.emotionmuseum.common.interceptor;
|
||||
|
||||
import com.emotionmuseum.common.handler.EmotionMetaObjectHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 用户上下文拦截器
|
||||
* 自动从请求头中提取用户信息并设置到ThreadLocal中
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserContextInterceptor implements HandlerInterceptor {
|
||||
|
||||
/**
|
||||
* 用户ID请求头名称
|
||||
*/
|
||||
private static final String USER_ID_HEADER = "X-User-Id";
|
||||
|
||||
/**
|
||||
* 用户名请求头名称
|
||||
*/
|
||||
private static final String USERNAME_HEADER = "X-Username";
|
||||
|
||||
/**
|
||||
* Authorization请求头名称
|
||||
*/
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
try {
|
||||
// 1. 优先从自定义请求头获取用户ID
|
||||
String userId = request.getHeader(USER_ID_HEADER);
|
||||
|
||||
// 2. 如果没有自定义请求头,尝试从其他方式获取
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
userId = extractUserIdFromRequest(request);
|
||||
}
|
||||
|
||||
// 3. 如果仍然没有用户ID,使用默认值
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
userId = generateGuestUserId(request);
|
||||
}
|
||||
|
||||
// 4. 设置到ThreadLocal中
|
||||
EmotionMetaObjectHandler.UserContextHolder.setCurrentUserId(userId);
|
||||
|
||||
log.debug("设置用户上下文: userId={}, requestUri={}", userId, request.getRequestURI());
|
||||
|
||||
} catch (Exception e) {
|
||||
// 设置用户上下文失败不应该影响请求处理
|
||||
log.warn("设置用户上下文失败,使用默认值: {}", e.getMessage());
|
||||
EmotionMetaObjectHandler.UserContextHolder.setCurrentUserId("system");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
|
||||
throws Exception {
|
||||
try {
|
||||
// 清除ThreadLocal,避免内存泄漏
|
||||
EmotionMetaObjectHandler.UserContextHolder.clear();
|
||||
log.debug("清除用户上下文: requestUri={}", request.getRequestURI());
|
||||
} catch (Exception e) {
|
||||
log.warn("清除用户上下文失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中提取用户ID
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 用户ID或null
|
||||
*/
|
||||
private String extractUserIdFromRequest(HttpServletRequest request) {
|
||||
try {
|
||||
// 1. 从Authorization头解析JWT token(如果有)
|
||||
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
|
||||
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7);
|
||||
String userIdFromToken = extractUserIdFromToken(token);
|
||||
if (StringUtils.hasText(userIdFromToken)) {
|
||||
return userIdFromToken;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从用户名请求头获取
|
||||
String username = request.getHeader(USERNAME_HEADER);
|
||||
if (StringUtils.hasText(username)) {
|
||||
return username;
|
||||
}
|
||||
|
||||
// 3. 从请求参数获取
|
||||
String userIdParam = request.getParameter("userId");
|
||||
if (StringUtils.hasText(userIdParam)) {
|
||||
return userIdParam;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("从请求中提取用户ID失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JWT token中提取用户ID
|
||||
*
|
||||
* @param token JWT token
|
||||
* @return 用户ID或null
|
||||
*/
|
||||
private String extractUserIdFromToken(String token) {
|
||||
try {
|
||||
// TODO: 实现JWT token解析逻辑
|
||||
// 这里可以集成JWT工具类来解析token
|
||||
// 例如:
|
||||
// Claims claims = JwtUtils.parseToken(token);
|
||||
// return claims.getSubject();
|
||||
|
||||
log.debug("JWT token解析功能待实现");
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("解析JWT token失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为访客用户生成临时用户ID
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 临时用户ID
|
||||
*/
|
||||
private String generateGuestUserId(HttpServletRequest request) {
|
||||
try {
|
||||
// 1. 尝试从Session获取
|
||||
String sessionId = request.getSession(false) != null ? request.getSession().getId() : null;
|
||||
if (StringUtils.hasText(sessionId)) {
|
||||
return "guest_session_" + sessionId;
|
||||
}
|
||||
|
||||
// 2. 基于IP和User-Agent生成
|
||||
String clientIp = getClientIpAddress(request);
|
||||
String userAgent = request.getHeader("User-Agent");
|
||||
|
||||
if (StringUtils.hasText(clientIp)) {
|
||||
String hash = String.valueOf((clientIp + userAgent).hashCode());
|
||||
return "guest_" + Math.abs(Integer.parseInt(hash));
|
||||
}
|
||||
|
||||
// 3. 使用时间戳作为最后的备选方案
|
||||
return "guest_" + System.currentTimeMillis();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("生成访客用户ID失败: {}", e.getMessage());
|
||||
return "guest_" + System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实IP地址
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @return 客户端IP地址
|
||||
*/
|
||||
private String getClientIpAddress(HttpServletRequest request) {
|
||||
try {
|
||||
String xForwardedFor = request.getHeader("X-Forwarded-For");
|
||||
if (StringUtils.hasText(xForwardedFor) && !"unknown".equalsIgnoreCase(xForwardedFor)) {
|
||||
return xForwardedFor.split(",")[0].trim();
|
||||
}
|
||||
|
||||
String xRealIp = request.getHeader("X-Real-IP");
|
||||
if (StringUtils.hasText(xRealIp) && !"unknown".equalsIgnoreCase(xRealIp)) {
|
||||
return xRealIp;
|
||||
}
|
||||
|
||||
String proxyClientIp = request.getHeader("Proxy-Client-IP");
|
||||
if (StringUtils.hasText(proxyClientIp) && !"unknown".equalsIgnoreCase(proxyClientIp)) {
|
||||
return proxyClientIp;
|
||||
}
|
||||
|
||||
String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP");
|
||||
if (StringUtils.hasText(wlProxyClientIp) && !"unknown".equalsIgnoreCase(wlProxyClientIp)) {
|
||||
return wlProxyClientIp;
|
||||
}
|
||||
|
||||
return request.getRemoteAddr();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("获取客户端IP地址失败: {}", e.getMessage());
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.emotionmuseum.common.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
/**
|
||||
* 基础分页请求类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "基础分页请求")
|
||||
public abstract class BasePageRequest extends BaseRequest {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 页码,从1开始
|
||||
*/
|
||||
@Schema(description = "页码,从1开始", example = "1", minimum = "1")
|
||||
@Min(value = 1, message = "页码必须大于0")
|
||||
private Integer pageNum = 1;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
@Schema(description = "每页大小", example = "20", minimum = "1", maximum = "100")
|
||||
@Min(value = 1, message = "每页大小必须大于0")
|
||||
@Max(value = 100, message = "每页大小不能超过100")
|
||||
private Integer pageSize = 20;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
@Schema(description = "排序字段", example = "create_time")
|
||||
private String sortField;
|
||||
|
||||
/**
|
||||
* 排序方向
|
||||
*/
|
||||
@Schema(description = "排序方向", example = "DESC", allowableValues = {"ASC", "DESC"})
|
||||
private String sortOrder = "DESC";
|
||||
|
||||
/**
|
||||
* 搜索关键词
|
||||
*/
|
||||
@Schema(description = "搜索关键词", example = "关键词")
|
||||
private String keyword;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.emotionmuseum.common.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 基础请求类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "基础请求")
|
||||
public abstract class BaseRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 请求ID,用于链路追踪
|
||||
*/
|
||||
@Schema(description = "请求ID", example = "req_123456789")
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 客户端IP地址
|
||||
*/
|
||||
@Schema(description = "客户端IP地址", example = "192.168.1.100")
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 用户代理信息
|
||||
*/
|
||||
@Schema(description = "用户代理信息", example = "Mozilla/5.0...")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 请求时间戳
|
||||
*/
|
||||
@Schema(description = "请求时间戳", example = "1721808000000")
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
@Schema(description = "设备类型", example = "WEB", allowableValues = {"WEB", "MOBILE", "APP"})
|
||||
private String deviceType;
|
||||
|
||||
/**
|
||||
* 应用版本
|
||||
*/
|
||||
@Schema(description = "应用版本", example = "1.0.0")
|
||||
private String appVersion;
|
||||
|
||||
public BaseRequest() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.emotionmuseum.common.response;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 基础分页响应类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "基础分页响应")
|
||||
public class BasePageResponse<T> extends BaseResponse {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 当前页码
|
||||
*/
|
||||
@Schema(description = "当前页码", example = "1")
|
||||
private Long current;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
@Schema(description = "每页大小", example = "20")
|
||||
private Long size;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
@Schema(description = "总记录数", example = "100")
|
||||
private Long total;
|
||||
|
||||
/**
|
||||
* 总页数
|
||||
*/
|
||||
@Schema(description = "总页数", example = "5")
|
||||
private Long pages;
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
@Schema(description = "数据列表")
|
||||
private List<T> records;
|
||||
|
||||
/**
|
||||
* 是否有下一页
|
||||
*/
|
||||
@Schema(description = "是否有下一页", example = "true")
|
||||
private Boolean hasNext;
|
||||
|
||||
/**
|
||||
* 是否有上一页
|
||||
*/
|
||||
@Schema(description = "是否有上一页", example = "false")
|
||||
private Boolean hasPrevious;
|
||||
|
||||
public BasePageResponse() {
|
||||
super();
|
||||
}
|
||||
|
||||
public BasePageResponse(IPage<T> page) {
|
||||
super();
|
||||
this.current = page.getCurrent();
|
||||
this.size = page.getSize();
|
||||
this.total = page.getTotal();
|
||||
this.pages = page.getPages();
|
||||
this.records = page.getRecords();
|
||||
this.hasNext = page.hasNext();
|
||||
this.hasPrevious = page.hasPrevious();
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态工厂方法
|
||||
*/
|
||||
public static <T> BasePageResponse<T> of(IPage<T> page) {
|
||||
return new BasePageResponse<>(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态工厂方法
|
||||
*/
|
||||
public static <T> BasePageResponse<T> of(List<T> records, Long current, Long size, Long total) {
|
||||
BasePageResponse<T> response = new BasePageResponse<>();
|
||||
response.setRecords(records);
|
||||
response.setCurrent(current);
|
||||
response.setSize(size);
|
||||
response.setTotal(total);
|
||||
response.setPages((total + size - 1) / size);
|
||||
response.setHasNext(current < response.getPages());
|
||||
response.setHasPrevious(current > 1);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.emotionmuseum.common.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 基础响应类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = "基础响应")
|
||||
public abstract class BaseResponse implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 响应时间戳
|
||||
*/
|
||||
@Schema(description = "响应时间戳", example = "1721808000000")
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 请求ID,用于链路追踪
|
||||
*/
|
||||
@Schema(description = "请求ID", example = "req_123456789")
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 服务器处理时间(毫秒)
|
||||
*/
|
||||
@Schema(description = "服务器处理时间(毫秒)", example = "150")
|
||||
private Long processingTime;
|
||||
|
||||
/**
|
||||
* 服务节点标识
|
||||
*/
|
||||
@Schema(description = "服务节点标识", example = "node-001")
|
||||
private String serverNode;
|
||||
|
||||
public BaseResponse() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求ID
|
||||
*/
|
||||
public BaseResponse requestId(String requestId) {
|
||||
this.requestId = requestId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置处理时间
|
||||
*/
|
||||
public BaseResponse processingTime(Long processingTime) {
|
||||
this.processingTime = processingTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务节点
|
||||
*/
|
||||
public BaseResponse serverNode(String serverNode) {
|
||||
this.serverNode = serverNode;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.emotionmuseum.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 统一响应结果
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class Result<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 响应码
|
||||
*/
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 请求ID
|
||||
*/
|
||||
private String requestId;
|
||||
|
||||
public Result() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public Result(Integer code, String message) {
|
||||
this();
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Result(Integer code, String message, T data) {
|
||||
this(code, message);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
*/
|
||||
public static <T> Result<T> success() {
|
||||
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(带数据)
|
||||
*/
|
||||
public static <T> Result<T> success(T data) {
|
||||
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(自定义消息)
|
||||
*/
|
||||
public static <T> Result<T> success(String message, T data) {
|
||||
return new Result<>(ResultCode.SUCCESS.getCode(), message, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应
|
||||
*/
|
||||
public static <T> Result<T> error() {
|
||||
return new Result<>(ResultCode.INTERNAL_SERVER_ERROR.getCode(), ResultCode.INTERNAL_SERVER_ERROR.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应(自定义消息)
|
||||
*/
|
||||
public static <T> Result<T> error(String message) {
|
||||
return new Result<>(ResultCode.INTERNAL_SERVER_ERROR.getCode(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应(自定义码和消息)
|
||||
*/
|
||||
public static <T> Result<T> error(Integer code, String message) {
|
||||
return new Result<>(code, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应(结果码枚举)
|
||||
*/
|
||||
public static <T> Result<T> error(ResultCode resultCode) {
|
||||
return new Result<>(resultCode.getCode(), resultCode.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应(结果码枚举 + 数据)
|
||||
*/
|
||||
public static <T> Result<T> error(ResultCode resultCode, T data) {
|
||||
return new Result<>(resultCode.getCode(), resultCode.getMessage(), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否成功
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return ResultCode.SUCCESS.getCode().equals(this.code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求ID
|
||||
*/
|
||||
public Result<T> requestId(String requestId) {
|
||||
this.requestId = requestId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 未授权响应
|
||||
*/
|
||||
public static <T> Result<T> unauthorized() {
|
||||
return new Result<>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 未授权响应(自定义消息)
|
||||
*/
|
||||
public static <T> Result<T> unauthorized(String message) {
|
||||
return new Result<>(ResultCode.UNAUTHORIZED.getCode(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止访问响应
|
||||
*/
|
||||
public static <T> Result<T> forbidden() {
|
||||
return new Result<>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止访问响应(自定义消息)
|
||||
*/
|
||||
public static <T> Result<T> forbidden(String message) {
|
||||
return new Result<>(ResultCode.FORBIDDEN.getCode(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数错误响应
|
||||
*/
|
||||
public static <T> Result<T> badRequest() {
|
||||
return new Result<>(ResultCode.BAD_REQUEST.getCode(), ResultCode.BAD_REQUEST.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数错误响应(自定义消息)
|
||||
*/
|
||||
public static <T> Result<T> badRequest(String message) {
|
||||
return new Result<>(ResultCode.BAD_REQUEST.getCode(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源不存在响应
|
||||
*/
|
||||
public static <T> Result<T> notFound() {
|
||||
return new Result<>(ResultCode.NOT_FOUND.getCode(), ResultCode.NOT_FOUND.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源不存在响应(自定义消息)
|
||||
*/
|
||||
public static <T> Result<T> notFound(String message) {
|
||||
return new Result<>(ResultCode.NOT_FOUND.getCode(), message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.emotionmuseum.common.result;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 响应状态码枚举
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ResultCode {
|
||||
|
||||
// ========== 通用状态码 ==========
|
||||
SUCCESS(200, "操作成功"),
|
||||
BAD_REQUEST(400, "请求参数错误"),
|
||||
UNAUTHORIZED(401, "未授权"),
|
||||
FORBIDDEN(403, "禁止访问"),
|
||||
NOT_FOUND(404, "资源不存在"),
|
||||
METHOD_NOT_ALLOWED(405, "请求方法不允许"),
|
||||
INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
|
||||
SERVICE_UNAVAILABLE(503, "服务不可用"),
|
||||
|
||||
// ========== 业务状态码 1000-1999 ==========
|
||||
BUSINESS_ERROR(1000, "业务处理失败"),
|
||||
PARAM_VALIDATION_ERROR(1001, "参数校验失败"),
|
||||
DATA_NOT_FOUND(1002, "数据不存在"),
|
||||
DATA_ALREADY_EXISTS(1003, "数据已存在"),
|
||||
OPERATION_NOT_ALLOWED(1004, "操作不被允许"),
|
||||
|
||||
// ========== 用户相关 2000-2099 ==========
|
||||
USER_NOT_FOUND(2000, "用户不存在"),
|
||||
USER_ALREADY_EXISTS(2001, "用户已存在"),
|
||||
USER_DISABLED(2002, "用户已被禁用"),
|
||||
USER_NOT_VERIFIED(2003, "用户未验证"),
|
||||
INVALID_CREDENTIALS(2004, "用户名或密码错误"),
|
||||
PASSWORD_TOO_WEAK(2005, "密码强度不够"),
|
||||
PHONE_ALREADY_EXISTS(2006, "手机号已存在"),
|
||||
EMAIL_ALREADY_EXISTS(2007, "邮箱已存在"),
|
||||
ACCOUNT_ALREADY_EXISTS(2008, "账号已存在"),
|
||||
|
||||
// ========== 认证相关 2100-2199 ==========
|
||||
TOKEN_INVALID(2100, "Token无效"),
|
||||
TOKEN_EXPIRED(2101, "Token已过期"),
|
||||
TOKEN_MISSING(2102, "Token缺失"),
|
||||
REFRESH_TOKEN_INVALID(2103, "刷新Token无效"),
|
||||
LOGIN_REQUIRED(2104, "请先登录"),
|
||||
PERMISSION_DENIED(2105, "权限不足"),
|
||||
CAPTCHA_ERROR(2106, "验证码错误"),
|
||||
CAPTCHA_EXPIRED(2107, "验证码已过期"),
|
||||
|
||||
// ========== AI对话相关 2200-2299 ==========
|
||||
AI_SERVICE_ERROR(2200, "AI服务异常"),
|
||||
CONVERSATION_NOT_FOUND(2201, "对话不存在"),
|
||||
MESSAGE_SEND_FAILED(2202, "消息发送失败"),
|
||||
EMOTION_ANALYSIS_FAILED(2203, "情绪分析失败"),
|
||||
AI_RESPONSE_TIMEOUT(2204, "AI响应超时"),
|
||||
|
||||
// ========== 情绪记录相关 2300-2399 ==========
|
||||
EMOTION_RECORD_NOT_FOUND(2300, "情绪记录不存在"),
|
||||
EMOTION_TYPE_INVALID(2301, "情绪类型无效"),
|
||||
EMOTION_INTENSITY_INVALID(2302, "情绪强度无效"),
|
||||
EMOTION_DATE_INVALID(2303, "情绪日期无效"),
|
||||
|
||||
// ========== 成长课题相关 2400-2499 ==========
|
||||
TOPIC_NOT_FOUND(2400, "课题不存在"),
|
||||
TOPIC_NOT_UNLOCKED(2401, "课题未解锁"),
|
||||
TOPIC_ALREADY_COMPLETED(2402, "课题已完成"),
|
||||
INTERACTION_NOT_FOUND(2403, "互动记录不存在"),
|
||||
TOPIC_CATEGORY_INVALID(2404, "课题分类无效"),
|
||||
|
||||
// ========== 地图探索相关 2500-2599 ==========
|
||||
LOCATION_NOT_FOUND(2500, "地点不存在"),
|
||||
LOCATION_ALREADY_EXISTS(2501, "地点已存在"),
|
||||
COORDINATE_INVALID(2502, "坐标无效"),
|
||||
POST_NOT_FOUND(2503, "帖子不存在"),
|
||||
COMMENT_NOT_FOUND(2504, "评论不存在"),
|
||||
|
||||
// ========== 成就奖励相关 2600-2699 ==========
|
||||
ACHIEVEMENT_NOT_FOUND(2600, "成就不存在"),
|
||||
ACHIEVEMENT_ALREADY_UNLOCKED(2601, "成就已解锁"),
|
||||
REWARD_NOT_FOUND(2602, "奖励不存在"),
|
||||
INSUFFICIENT_POINTS(2603, "积分不足"),
|
||||
REWARD_ALREADY_CLAIMED(2604, "奖励已领取"),
|
||||
|
||||
// ========== 统计分析相关 2700-2799 ==========
|
||||
STATS_CALCULATION_ERROR(2700, "统计计算错误"),
|
||||
REPORT_GENERATION_FAILED(2701, "报告生成失败"),
|
||||
DATA_EXPORT_FAILED(2702, "数据导出失败"),
|
||||
|
||||
// ========== 文件上传相关 2800-2899 ==========
|
||||
FILE_UPLOAD_FAILED(2800, "文件上传失败"),
|
||||
FILE_TYPE_NOT_SUPPORTED(2801, "文件类型不支持"),
|
||||
FILE_SIZE_EXCEEDED(2802, "文件大小超限"),
|
||||
FILE_NOT_FOUND(2803, "文件不存在"),
|
||||
|
||||
// ========== 第三方服务相关 2900-2999 ==========
|
||||
THIRD_PARTY_SERVICE_ERROR(2900, "第三方服务异常"),
|
||||
SMS_SEND_FAILED(2901, "短信发送失败"),
|
||||
EMAIL_SEND_FAILED(2902, "邮件发送失败"),
|
||||
MAP_SERVICE_ERROR(2903, "地图服务异常"),
|
||||
PAYMENT_SERVICE_ERROR(2904, "支付服务异常"),
|
||||
|
||||
// ========== 系统相关 9000-9999 ==========
|
||||
SYSTEM_MAINTENANCE(9000, "系统维护中"),
|
||||
RATE_LIMIT_EXCEEDED(9001, "请求频率超限"),
|
||||
DATABASE_ERROR(9002, "数据库异常"),
|
||||
CACHE_ERROR(9003, "缓存异常"),
|
||||
MQ_ERROR(9004, "消息队列异常"),
|
||||
CONFIG_ERROR(9005, "配置错误");
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
private final Integer code;
|
||||
|
||||
/**
|
||||
* 状态消息
|
||||
*/
|
||||
private final String message;
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.emotionmuseum.common.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* HTTP工具类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HttpUtil {
|
||||
|
||||
@Autowired
|
||||
@Qualifier("restTemplate")
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("longRestTemplate")
|
||||
private RestTemplate longRestTemplate;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("fastRestTemplate")
|
||||
private RestTemplate fastRestTemplate;
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
*/
|
||||
public <T> T get(String url, Class<T> responseType) {
|
||||
return get(url, null, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求(带请求头)
|
||||
*/
|
||||
public <T> T get(String url, HttpHeaders headers, Class<T> responseType) {
|
||||
try {
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.GET, entity, responseType);
|
||||
return response.getBody();
|
||||
} catch (RestClientException e) {
|
||||
log.error("GET请求失败: url={}, error={}", url, e.getMessage());
|
||||
throw new RuntimeException("HTTP GET请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
public <T> T post(String url, Object requestBody, Class<T> responseType) {
|
||||
return post(url, requestBody, null, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求(带请求头)
|
||||
*/
|
||||
public <T> T post(String url, Object requestBody, HttpHeaders headers, Class<T> responseType) {
|
||||
try {
|
||||
if (headers == null) {
|
||||
headers = new HttpHeaders();
|
||||
}
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
String jsonBody = requestBody instanceof String ?
|
||||
(String) requestBody : JSON.toJSONString(requestBody);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
|
||||
ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.POST, entity, responseType);
|
||||
return response.getBody();
|
||||
} catch (RestClientException e) {
|
||||
log.error("POST请求失败: url={}, body={}, error={}", url, requestBody, e.getMessage());
|
||||
throw new RuntimeException("HTTP POST请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
*/
|
||||
public <T> T put(String url, Object requestBody, Class<T> responseType) {
|
||||
return put(url, requestBody, null, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求(带请求头)
|
||||
*/
|
||||
public <T> T put(String url, Object requestBody, HttpHeaders headers, Class<T> responseType) {
|
||||
try {
|
||||
if (headers == null) {
|
||||
headers = new HttpHeaders();
|
||||
}
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
String jsonBody = requestBody instanceof String ?
|
||||
(String) requestBody : JSON.toJSONString(requestBody);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
|
||||
ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.PUT, entity, responseType);
|
||||
return response.getBody();
|
||||
} catch (RestClientException e) {
|
||||
log.error("PUT请求失败: url={}, body={}, error={}", url, requestBody, e.getMessage());
|
||||
throw new RuntimeException("HTTP PUT请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
*/
|
||||
public <T> T delete(String url, Class<T> responseType) {
|
||||
return delete(url, null, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求(带请求头)
|
||||
*/
|
||||
public <T> T delete(String url, HttpHeaders headers, Class<T> responseType) {
|
||||
try {
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.DELETE, entity, responseType);
|
||||
return response.getBody();
|
||||
} catch (RestClientException e) {
|
||||
log.error("DELETE请求失败: url={}, error={}", url, e.getMessage());
|
||||
throw new RuntimeException("HTTP DELETE请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 长连接POST请求(用于AI接口)
|
||||
*/
|
||||
public <T> T longPost(String url, Object requestBody, HttpHeaders headers, Class<T> responseType) {
|
||||
try {
|
||||
if (headers == null) {
|
||||
headers = new HttpHeaders();
|
||||
}
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
String jsonBody = requestBody instanceof String ?
|
||||
(String) requestBody : JSON.toJSONString(requestBody);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
|
||||
ResponseEntity<T> response = longRestTemplate.exchange(url, HttpMethod.POST, entity, responseType);
|
||||
return response.getBody();
|
||||
} catch (RestClientException e) {
|
||||
log.error("长连接POST请求失败: url={}, body={}, error={}", url, requestBody, e.getMessage());
|
||||
throw new RuntimeException("HTTP长连接POST请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速POST请求(用于内部服务调用)
|
||||
*/
|
||||
public <T> T fastPost(String url, Object requestBody, HttpHeaders headers, Class<T> responseType) {
|
||||
try {
|
||||
if (headers == null) {
|
||||
headers = new HttpHeaders();
|
||||
}
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
String jsonBody = requestBody instanceof String ?
|
||||
(String) requestBody : JSON.toJSONString(requestBody);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
|
||||
ResponseEntity<T> response = fastRestTemplate.exchange(url, HttpMethod.POST, entity, responseType);
|
||||
return response.getBody();
|
||||
} catch (RestClientException e) {
|
||||
log.error("快速POST请求失败: url={}, body={}, error={}", url, requestBody, e.getMessage());
|
||||
throw new RuntimeException("HTTP快速POST请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带Authorization的请求头
|
||||
*/
|
||||
public static HttpHeaders createAuthHeaders(String token) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
headers.set("Authorization", token.startsWith("Bearer ") ? token : "Bearer " + token);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带自定义请求头的HttpHeaders
|
||||
*/
|
||||
public static HttpHeaders createHeaders(Map<String, String> headerMap) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
if (headerMap != null && !headerMap.isEmpty()) {
|
||||
headerMap.forEach(headers::set);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package com.emotionmuseum.common.util;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JWT工具类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
/**
|
||||
* JWT密钥
|
||||
*/
|
||||
@Value("${jwt.secret:emotion-museum-secret-key-2025}")
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* JWT过期时间(秒)
|
||||
*/
|
||||
@Value("${jwt.expiration:86400}")
|
||||
private Long expiration;
|
||||
|
||||
/**
|
||||
* 刷新Token过期时间(秒)
|
||||
*/
|
||||
@Value("${jwt.refresh-expiration:604800}")
|
||||
private Long refreshExpiration;
|
||||
|
||||
/**
|
||||
* 获取密钥
|
||||
*/
|
||||
private SecretKey getSecretKey() {
|
||||
return Keys.hmacShaKeyFor(secret.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Token
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @return Token
|
||||
*/
|
||||
public String generateToken(String userId, String username) {
|
||||
return generateToken(userId, username, expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成刷新Token
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @return 刷新Token
|
||||
*/
|
||||
public String generateRefreshToken(String userId, String username) {
|
||||
return generateToken(userId, username, refreshExpiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Token
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @param expiration 过期时间(秒)
|
||||
* @return Token
|
||||
*/
|
||||
private String generateToken(String userId, String username, Long expiration) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration * 1000);
|
||||
|
||||
return Jwts.builder()
|
||||
.setSubject(userId)
|
||||
.claim("username", username)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(expiryDate)
|
||||
.signWith(getSecretKey(), SignatureAlgorithm.HS512)
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取用户ID
|
||||
*
|
||||
* @param token Token
|
||||
* @return 用户ID
|
||||
*/
|
||||
public String getUserIdFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.getSubject() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取用户名
|
||||
*
|
||||
* @param token Token
|
||||
* @return 用户名
|
||||
*/
|
||||
public String getUsernameFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.get("username", String.class) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取过期时间
|
||||
*
|
||||
* @param token Token
|
||||
* @return 过期时间
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
return claims != null ? claims.getExpiration() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取Claims
|
||||
*
|
||||
* @param token Token
|
||||
* @return Claims
|
||||
*/
|
||||
private Claims getClaimsFromToken(String token) {
|
||||
try {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(getSecretKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
} catch (Exception e) {
|
||||
log.warn("解析Token失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token是否有效
|
||||
*
|
||||
* @param token Token
|
||||
* @return 是否有效
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
if (StrUtil.isBlank(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
if (claims == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
Date expiration = claims.getExpiration();
|
||||
return expiration != null && expiration.after(new Date());
|
||||
} catch (Exception e) {
|
||||
log.warn("Token验证失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Token是否过期
|
||||
*
|
||||
* @param token Token
|
||||
* @return 是否过期
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration != null && expiration.before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*
|
||||
* @param token 原Token
|
||||
* @return 新Token
|
||||
*/
|
||||
public String refreshToken(String token) {
|
||||
try {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
if (claims == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String userId = claims.getSubject();
|
||||
String username = claims.get("username", String.class);
|
||||
|
||||
return generateToken(userId, username);
|
||||
} catch (Exception e) {
|
||||
log.warn("刷新Token失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取Token
|
||||
*
|
||||
* @param authHeader Authorization头
|
||||
* @return Token
|
||||
*/
|
||||
public String extractTokenFromHeader(String authHeader) {
|
||||
if (StrUtil.isNotBlank(authHeader) && authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package com.emotionmuseum.common.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 雪花算法ID生成器
|
||||
* 生成64位长整型ID,转换为字符串避免前端精度丢失问题
|
||||
*
|
||||
* 雪花算法结构:
|
||||
* 1位符号位(固定为0) + 41位时间戳 + 10位机器ID + 12位序列号
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SnowflakeIdGenerator {
|
||||
|
||||
/**
|
||||
* 起始时间戳 (2024-01-01 00:00:00)
|
||||
*/
|
||||
private static final long START_TIMESTAMP = 1704067200000L;
|
||||
|
||||
/**
|
||||
* 机器ID位数
|
||||
*/
|
||||
private static final long MACHINE_ID_BITS = 10L;
|
||||
|
||||
/**
|
||||
* 序列号位数
|
||||
*/
|
||||
private static final long SEQUENCE_BITS = 12L;
|
||||
|
||||
/**
|
||||
* 机器ID最大值
|
||||
*/
|
||||
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
|
||||
|
||||
/**
|
||||
* 序列号最大值
|
||||
*/
|
||||
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
|
||||
|
||||
/**
|
||||
* 机器ID左移位数
|
||||
*/
|
||||
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
|
||||
|
||||
/**
|
||||
* 时间戳左移位数
|
||||
*/
|
||||
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
|
||||
|
||||
/**
|
||||
* 机器ID
|
||||
*/
|
||||
private final long machineId;
|
||||
|
||||
/**
|
||||
* 序列号
|
||||
*/
|
||||
private long sequence = 0L;
|
||||
|
||||
/**
|
||||
* 上次生成ID的时间戳
|
||||
*/
|
||||
private long lastTimestamp = -1L;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param machineId 机器ID (0-1023)
|
||||
*/
|
||||
public SnowflakeIdGenerator(long machineId) {
|
||||
if (machineId > MAX_MACHINE_ID || machineId < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("机器ID必须在0到%d之间", MAX_MACHINE_ID));
|
||||
}
|
||||
this.machineId = machineId;
|
||||
log.info("雪花算法ID生成器初始化完成,机器ID: {}", machineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认构造函数,使用默认机器ID
|
||||
*/
|
||||
public SnowflakeIdGenerator() {
|
||||
// 使用当前时间戳的后10位作为默认机器ID
|
||||
this(System.currentTimeMillis() % (MAX_MACHINE_ID + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成下一个ID
|
||||
*
|
||||
* @return 生成的ID
|
||||
*/
|
||||
public synchronized long nextId() {
|
||||
long timestamp = getCurrentTimestamp();
|
||||
|
||||
// 如果当前时间小于上次ID生成的时间戳,说明系统时钟回退过,抛出异常
|
||||
if (timestamp < lastTimestamp) {
|
||||
throw new RuntimeException(
|
||||
String.format("系统时钟回退,拒绝生成ID。当前时间戳: %d, 上次时间戳: %d",
|
||||
timestamp, lastTimestamp));
|
||||
}
|
||||
|
||||
// 如果是同一时间戳,则在序列号上自增
|
||||
if (lastTimestamp == timestamp) {
|
||||
sequence = (sequence + 1) & MAX_SEQUENCE;
|
||||
// 如果序列号溢出,则等待下一个毫秒
|
||||
if (sequence == 0) {
|
||||
timestamp = getNextTimestamp(lastTimestamp);
|
||||
}
|
||||
} else {
|
||||
// 如果是新的时间戳,则序列号重置为0
|
||||
sequence = 0L;
|
||||
}
|
||||
|
||||
// 更新上次生成ID的时间戳
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
// 移位并通过或运算拼到一起组成64位的ID
|
||||
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
|
||||
| (machineId << MACHINE_ID_SHIFT)
|
||||
| sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成字符串格式的ID
|
||||
*
|
||||
* @return 字符串格式的ID
|
||||
*/
|
||||
public String nextIdAsString() {
|
||||
return String.valueOf(nextId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳
|
||||
*
|
||||
* @return 当前时间戳
|
||||
*/
|
||||
private long getCurrentTimestamp() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个时间戳
|
||||
*
|
||||
* @param lastTimestamp 上次时间戳
|
||||
* @return 下一个时间戳
|
||||
*/
|
||||
private long getNextTimestamp(long lastTimestamp) {
|
||||
long timestamp = getCurrentTimestamp();
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = getCurrentTimestamp();
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析ID获取时间戳
|
||||
*
|
||||
* @param id 雪花算法生成的ID
|
||||
* @return 时间戳
|
||||
*/
|
||||
public long parseTimestamp(long id) {
|
||||
return (id >> TIMESTAMP_SHIFT) + START_TIMESTAMP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析ID获取机器ID
|
||||
*
|
||||
* @param id 雪花算法生成的ID
|
||||
* @return 机器ID
|
||||
*/
|
||||
public long parseMachineId(long id) {
|
||||
return (id >> MACHINE_ID_SHIFT) & MAX_MACHINE_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析ID获取序列号
|
||||
*
|
||||
* @param id 雪花算法生成的ID
|
||||
* @return 序列号
|
||||
*/
|
||||
public long parseSequence(long id) {
|
||||
return id & MAX_SEQUENCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器ID
|
||||
*
|
||||
* @return 机器ID
|
||||
*/
|
||||
public long getMachineId() {
|
||||
return machineId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成ID
|
||||
*
|
||||
* @param count 生成数量
|
||||
* @return ID数组
|
||||
*/
|
||||
public long[] nextIds(int count) {
|
||||
if (count <= 0) {
|
||||
throw new IllegalArgumentException("生成数量必须大于0");
|
||||
}
|
||||
|
||||
long[] ids = new long[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
ids[i] = nextId();
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成字符串格式的ID
|
||||
*
|
||||
* @param count 生成数量
|
||||
* @return 字符串ID数组
|
||||
*/
|
||||
public String[] nextIdsAsString(int count) {
|
||||
if (count <= 0) {
|
||||
throw new IllegalArgumentException("生成数量必须大于0");
|
||||
}
|
||||
|
||||
String[] ids = new String[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
ids[i] = nextIdAsString();
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.emotionmuseum.common.util;
|
||||
|
||||
import com.emotionmuseum.common.handler.EmotionMetaObjectHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 用户上下文工具类
|
||||
* 提供手动设置和获取用户上下文的方法
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserContextUtil {
|
||||
|
||||
/**
|
||||
* 设置当前用户ID
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public static void setCurrentUserId(String userId) {
|
||||
if (StringUtils.hasText(userId)) {
|
||||
EmotionMetaObjectHandler.UserContextHolder.setCurrentUserId(userId);
|
||||
log.debug("手动设置用户上下文: userId={}", userId);
|
||||
} else {
|
||||
log.warn("尝试设置空的用户ID");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*
|
||||
* @return 当前用户ID,如果没有则返回null
|
||||
*/
|
||||
public static String getCurrentUserId() {
|
||||
return EmotionMetaObjectHandler.UserContextHolder.getCurrentUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前用户上下文
|
||||
*/
|
||||
public static void clearCurrentUser() {
|
||||
EmotionMetaObjectHandler.UserContextHolder.clear();
|
||||
log.debug("手动清除用户上下文");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有当前用户
|
||||
*
|
||||
* @return 如果有当前用户返回true,否则返回false
|
||||
*/
|
||||
public static boolean hasCurrentUser() {
|
||||
return StringUtils.hasText(getCurrentUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID,如果没有则返回默认值
|
||||
*
|
||||
* @param defaultUserId 默认用户ID
|
||||
* @return 当前用户ID或默认值
|
||||
*/
|
||||
public static String getCurrentUserIdOrDefault(String defaultUserId) {
|
||||
String currentUserId = getCurrentUserId();
|
||||
return StringUtils.hasText(currentUserId) ? currentUserId : defaultUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定用户上下文中执行操作
|
||||
* 执行完成后会恢复原来的用户上下文
|
||||
*
|
||||
* @param userId 临时用户ID
|
||||
* @param runnable 要执行的操作
|
||||
*/
|
||||
public static void runWithUser(String userId, Runnable runnable) {
|
||||
String originalUserId = getCurrentUserId();
|
||||
try {
|
||||
setCurrentUserId(userId);
|
||||
runnable.run();
|
||||
} finally {
|
||||
if (originalUserId != null) {
|
||||
setCurrentUserId(originalUserId);
|
||||
} else {
|
||||
clearCurrentUser();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定用户上下文中执行操作并返回结果
|
||||
* 执行完成后会恢复原来的用户上下文
|
||||
*
|
||||
* @param userId 临时用户ID
|
||||
* @param supplier 要执行的操作
|
||||
* @param <T> 返回值类型
|
||||
* @return 操作结果
|
||||
*/
|
||||
public static <T> T runWithUser(String userId, java.util.function.Supplier<T> supplier) {
|
||||
String originalUserId = getCurrentUserId();
|
||||
try {
|
||||
setCurrentUserId(userId);
|
||||
return supplier.get();
|
||||
} finally {
|
||||
if (originalUserId != null) {
|
||||
setCurrentUserId(originalUserId);
|
||||
} else {
|
||||
clearCurrentUser();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为访客用户生成临时ID
|
||||
*
|
||||
* @return 访客用户ID
|
||||
*/
|
||||
public static String generateGuestUserId() {
|
||||
return "guest_" + System.currentTimeMillis() + "_" +
|
||||
Integer.toHexString((int)(Math.random() * 0x10000));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户ID是否为访客用户
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 如果是访客用户返回true
|
||||
*/
|
||||
public static boolean isGuestUser(String userId) {
|
||||
return StringUtils.hasText(userId) && userId.startsWith("guest_");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户ID是否为系统用户
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 如果是系统用户返回true
|
||||
*/
|
||||
public static boolean isSystemUser(String userId) {
|
||||
return "system".equals(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户类型描述
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户类型描述
|
||||
*/
|
||||
public static String getUserTypeDescription(String userId) {
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
return "未知用户";
|
||||
}
|
||||
|
||||
if (isSystemUser(userId)) {
|
||||
return "系统用户";
|
||||
}
|
||||
|
||||
if (isGuestUser(userId)) {
|
||||
return "访客用户";
|
||||
}
|
||||
|
||||
return "注册用户";
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package com.emotionmuseum.common.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 雪花算法ID生成器测试类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
public class SnowflakeIdGeneratorTest {
|
||||
|
||||
private SnowflakeIdGenerator generator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
generator = new SnowflakeIdGenerator(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNextId() {
|
||||
long id = generator.nextId();
|
||||
assertTrue(id > 0, "生成的ID应该大于0");
|
||||
|
||||
// 测试连续生成的ID不相同
|
||||
long id2 = generator.nextId();
|
||||
assertNotEquals(id, id2, "连续生成的ID应该不相同");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNextIdAsString() {
|
||||
String id = generator.nextIdAsString();
|
||||
assertNotNull(id, "生成的字符串ID不应该为null");
|
||||
assertFalse(id.isEmpty(), "生成的字符串ID不应该为空");
|
||||
|
||||
// 验证是数字字符串
|
||||
assertDoesNotThrow(() -> Long.parseLong(id), "生成的字符串应该是有效的数字");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUniqueIds() {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
int count = 10000;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
long id = generator.nextId();
|
||||
assertTrue(ids.add(id), "生成的ID应该是唯一的: " + id);
|
||||
}
|
||||
|
||||
assertEquals(count, ids.size(), "应该生成指定数量的唯一ID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentGeneration() throws InterruptedException {
|
||||
int threadCount = 10;
|
||||
int idsPerThread = 1000;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||
CountDownLatch latch = new CountDownLatch(threadCount);
|
||||
Set<Long> allIds = new HashSet<>();
|
||||
AtomicInteger duplicateCount = new AtomicInteger(0);
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
Set<Long> threadIds = new HashSet<>();
|
||||
for (int j = 0; j < idsPerThread; j++) {
|
||||
long id = generator.nextId();
|
||||
threadIds.add(id);
|
||||
}
|
||||
|
||||
synchronized (allIds) {
|
||||
for (Long id : threadIds) {
|
||||
if (!allIds.add(id)) {
|
||||
duplicateCount.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
latch.await();
|
||||
executor.shutdown();
|
||||
|
||||
assertEquals(0, duplicateCount.get(), "并发生成的ID不应该有重复");
|
||||
assertEquals(threadCount * idsPerThread, allIds.size(), "应该生成正确数量的唯一ID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseTimestamp() {
|
||||
long id = generator.nextId();
|
||||
long timestamp = generator.parseTimestamp(id);
|
||||
|
||||
// 时间戳应该在合理范围内(当前时间前后1分钟)
|
||||
long currentTime = System.currentTimeMillis();
|
||||
assertTrue(Math.abs(timestamp - currentTime) < 60000,
|
||||
"解析的时间戳应该接近当前时间");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMachineId() {
|
||||
long id = generator.nextId();
|
||||
long machineId = generator.parseMachineId(id);
|
||||
|
||||
assertEquals(1L, machineId, "解析的机器ID应该等于设置的机器ID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseSequence() {
|
||||
long id = generator.nextId();
|
||||
long sequence = generator.parseSequence(id);
|
||||
|
||||
assertTrue(sequence >= 0 && sequence < 4096,
|
||||
"解析的序列号应该在0-4095范围内");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchGeneration() {
|
||||
int count = 100;
|
||||
long[] ids = generator.nextIds(count);
|
||||
|
||||
assertEquals(count, ids.length, "应该生成指定数量的ID");
|
||||
|
||||
// 验证所有ID都是唯一的
|
||||
Set<Long> uniqueIds = new HashSet<>();
|
||||
for (long id : ids) {
|
||||
assertTrue(uniqueIds.add(id), "批量生成的ID应该是唯一的");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchGenerationAsString() {
|
||||
int count = 100;
|
||||
String[] ids = generator.nextIdsAsString(count);
|
||||
|
||||
assertEquals(count, ids.length, "应该生成指定数量的字符串ID");
|
||||
|
||||
// 验证所有ID都是唯一的且为有效数字
|
||||
Set<String> uniqueIds = new HashSet<>();
|
||||
for (String id : ids) {
|
||||
assertNotNull(id, "字符串ID不应该为null");
|
||||
assertFalse(id.isEmpty(), "字符串ID不应该为空");
|
||||
assertDoesNotThrow(() -> Long.parseLong(id), "字符串ID应该是有效数字");
|
||||
assertTrue(uniqueIds.add(id), "批量生成的字符串ID应该是唯一的");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidMachineId() {
|
||||
// 测试无效的机器ID
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
new SnowflakeIdGenerator(-1L);
|
||||
}, "负数机器ID应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
new SnowflakeIdGenerator(1024L);
|
||||
}, "超出范围的机器ID应该抛出异常");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidBatchCount() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
generator.nextIds(0);
|
||||
}, "批量生成数量为0应该抛出异常");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
generator.nextIds(-1);
|
||||
}, "批量生成数量为负数应该抛出异常");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user