feat: 项目初始化及当前全部内容提交
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>
|
||||
+57
@@ -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;
|
||||
}
|
||||
}
|
||||
+97
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+109
@@ -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");
|
||||
}
|
||||
}
|
||||
+53
@@ -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;
|
||||
}
|
||||
+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,130 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+234
@@ -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;
|
||||
}
|
||||
}
|
||||
+163
@@ -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);
|
||||
}, "批量生成数量为负数应该抛出异常");
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
artifactId=emotion-common
|
||||
groupId=com.emotionmuseum
|
||||
version=1.0.0
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
com/emotionmuseum/common/util/JwtUtil.class
|
||||
com/emotionmuseum/common/handler/EmotionMetaObjectHandler.class
|
||||
com/emotionmuseum/common/result/Result.class
|
||||
com/emotionmuseum/common/util/HttpUtil.class
|
||||
com/emotionmuseum/common/util/SnowflakeIdGenerator.class
|
||||
com/emotionmuseum/common/interceptor/UserContextInterceptor.class
|
||||
com/emotionmuseum/common/entity/BaseEntity.class
|
||||
com/emotionmuseum/common/config/SnowflakeConfig.class
|
||||
com/emotionmuseum/common/config/WebMvcConfig.class
|
||||
com/emotionmuseum/common/result/ResultCode.class
|
||||
com/emotionmuseum/common/handler/EmotionMetaObjectHandler$UserContextHolder.class
|
||||
com/emotionmuseum/common/dto/PageQuery.class
|
||||
com/emotionmuseum/common/util/UserContextUtil.class
|
||||
com/emotionmuseum/common/config/RestTemplateConfig.class
|
||||
com/emotionmuseum/common/config/MybatisPlusConfig.class
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/entity/BaseEntity.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/util/JwtUtil.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/util/HttpUtil.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/config/WebMvcConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/result/ResultCode.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/interceptor/UserContextInterceptor.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/config/SnowflakeConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/result/Result.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/dto/PageQuery.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/handler/EmotionMetaObjectHandler.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/util/UserContextUtil.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/config/RestTemplateConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/config/MybatisPlusConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/main/java/com/emotionmuseum/common/util/SnowflakeIdGenerator.java
|
||||
+1
@@ -0,0 +1 @@
|
||||
com/emotionmuseum/common/util/SnowflakeIdGeneratorTest.class
|
||||
+1
@@ -0,0 +1 @@
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-common/src/test/java/com/emotionmuseum/common/util/SnowflakeIdGeneratorTest.java
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user