初始化
This commit is contained in:
77
mokee-gateway-common/pom.xml
Normal file
77
mokee-gateway-common/pom.xml
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.mokee</groupId>
|
||||
<artifactId>mokee-gateway</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>mokee-gateway-common</artifactId>
|
||||
<name>mokee-gateway-common</name>
|
||||
<description>公共模块:Entity、DTO、工具类、异常</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.mokee.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 统一返回类
|
||||
* <p>
|
||||
* 封装所有 Controller 返回结果,提供链式静态工厂方法
|
||||
*
|
||||
* @param <T> 数据泛型
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Result<T> {
|
||||
|
||||
/** 状态码 */
|
||||
private int code;
|
||||
|
||||
/** 返回消息 */
|
||||
private String msg;
|
||||
|
||||
/** 返回数据 */
|
||||
private T data;
|
||||
|
||||
/** 时间戳 */
|
||||
private long timestamp;
|
||||
|
||||
// ==================== 成功响应 ====================
|
||||
|
||||
/** 操作成功(无数据) */
|
||||
public static <T> Result<T> ok() {
|
||||
return new Result<>(200, "操作成功", null, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/** 操作成功(带数据) */
|
||||
public static <T> Result<T> ok(T data) {
|
||||
return new Result<>(200, "操作成功", data, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/** 操作成功(自定义消息 + 数据) */
|
||||
public static <T> Result<T> ok(String msg, T data) {
|
||||
return new Result<>(200, msg, data, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// ==================== 失败响应 ====================
|
||||
|
||||
/** 操作失败(指定错误码和消息) */
|
||||
public static <T> Result<T> fail(int code, String msg) {
|
||||
return new Result<>(code, msg, null, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/** 操作失败(仅消息,默认500) */
|
||||
public static <T> Result<T> fail(String msg) {
|
||||
return new Result<>(500, msg, null, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.mokee.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 操作日志注解
|
||||
* <p>
|
||||
* 标记在 Controller 方法上,由 OperationLogAspect 拦截并自动记录操作日志
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface OperationLog {
|
||||
|
||||
/** 操作模块名称,如"用户管理"、"角色管理" */
|
||||
String module() default "";
|
||||
|
||||
/** 操作类型,如 CREATE / UPDATE / DELETE / QUERY */
|
||||
String action() default "";
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.mokee.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* 异步任务配置
|
||||
* <p>
|
||||
* 提供专用线程池用于日志异步写入,避免数据库日志写入阻塞主业务线程
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
/**
|
||||
* 异步日志写入专用线程池
|
||||
* <p>
|
||||
* 核心线程 4,最大 16,队列容量 200。队列满时回调主线程执行(CallerRunsPolicy),
|
||||
* 确保日志不丢失(牺牲一点性能换取数据完整性)
|
||||
*/
|
||||
@Bean("logTaskExecutor")
|
||||
public Executor logTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(4);
|
||||
executor.setMaxPoolSize(16);
|
||||
executor.setQueueCapacity(200);
|
||||
executor.setKeepAliveSeconds(60);
|
||||
executor.setThreadNamePrefix("async-log-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
executor.setAwaitTerminationSeconds(30);
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.mokee.common.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Jackson 序列化配置
|
||||
* <p>
|
||||
* 统一日期格式和时区,避免前后端时区不一致导致的时间偏差
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
/**
|
||||
* 配置 ObjectMapper
|
||||
* <ul>
|
||||
* <li>时区设为 Asia/Shanghai(UTC+8)</li>
|
||||
* <li>日期格式化 yyyy-MM-dd HH:mm:ss</li>
|
||||
* <li>注册 JavaTimeModule 并禁用时间戳写入</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 设置时区
|
||||
objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
|
||||
|
||||
// 设置日期格式
|
||||
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
|
||||
|
||||
// 注册 JavaTimeModule,禁用 WRITE_DATES_AS_TIMESTAMPS
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.mokee.common.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置
|
||||
* <p>
|
||||
* 注册分页插件和自动填充处理器
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 拦截器,注册 MySQL 分页插件
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
|
||||
interceptor.addInnerInterceptor(paginationInterceptor);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动填充处理器
|
||||
* <p>
|
||||
* 插入时自动填充 createTime / updateTime,更新时自动填充 updateTime
|
||||
*/
|
||||
@Bean
|
||||
public MetaObjectHandler metaObjectHandler() {
|
||||
return new MyMetaObjectHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动填充处理器内部类
|
||||
*/
|
||||
public static class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/** 创建时间字段名 */
|
||||
private static final String CREATE_TIME = "createTime";
|
||||
|
||||
/** 更新时间字段名 */
|
||||
private static final String UPDATE_TIME = "updateTime";
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
this.strictInsertFill(metaObject, CREATE_TIME, LocalDateTime.class, now);
|
||||
this.strictInsertFill(metaObject, UPDATE_TIME, LocalDateTime.class, now);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, UPDATE_TIME, LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.mokee.common.dto.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 登录请求 DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginRequest {
|
||||
|
||||
/** 用户名 */
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
/** 密码 */
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/** 业务系统编码(可选,用于指定登录哪个系统) */
|
||||
private String systemCode;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.mokee.common.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 登录响应 DTO
|
||||
* <p>
|
||||
* 登录成功后返回 Token、用户信息、可切换系统列表等
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
|
||||
/** JWT Token */
|
||||
private String token;
|
||||
|
||||
/** 用户信息 */
|
||||
private UserInfoVO userInfo;
|
||||
|
||||
/** 可登录的业务系统列表 */
|
||||
private List<SystemVO> systemList;
|
||||
|
||||
/** 是否需要选择系统(多系统时需前端展示系统选择页) */
|
||||
private Boolean needSelectSystem;
|
||||
|
||||
/** 当前登录的系统 */
|
||||
private SystemVO currentSystem;
|
||||
|
||||
/** 是否为平台管理员(拥有 super_admin 角色),可进入后台管理 */
|
||||
private Boolean isAdmin;
|
||||
|
||||
/** 按钮权限标识列表(如 system:user:add),前端 v-permission 指令使用 */
|
||||
private List<String> permissions;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.mokee.common.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 菜单元数据 VO
|
||||
* <p>
|
||||
* 路由的 meta 信息,用于前端渲染面包屑、图标等
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MenuMetaVO {
|
||||
|
||||
/** 菜单标题 */
|
||||
private String title;
|
||||
|
||||
/** 菜单图标 */
|
||||
private String icon;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.mokee.common.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单路由响应对象
|
||||
* <p>
|
||||
* 包含系统的菜单路由树和所有权限标识列表,供业务系统前端一次性获取
|
||||
* 完整的前端路由和权限控制所需数据。
|
||||
* <p>
|
||||
* 使用场景:
|
||||
* <ul>
|
||||
* <li>业务系统前端初始化时调用,获取菜单结构用于渲染侧边栏</li>
|
||||
* <li>同时获取该系统所有权限标识,用于前端按钮级权限控制(v-permission)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 返回示例:
|
||||
* <pre>
|
||||
* {
|
||||
* "code": 200,
|
||||
* "data": {
|
||||
* "menu": [{ "name": "用户管理", "path": "/user", ... }],
|
||||
* "permission": ["system:user:list", "system:user:add", "system:user:edit", "system:user:delete"]
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MenuRouterResponse {
|
||||
|
||||
/** 菜单路由树(前端侧边栏渲染所需的结构) */
|
||||
private List<MenuRouterVO> menu;
|
||||
|
||||
/** 该系统的所有权限标识列表(用于前端 v-permission 指令控制按钮显隐) */
|
||||
private List<String> permission;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.mokee.common.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单路由 VO(视图对象)
|
||||
* <p>
|
||||
* 前端动态路由的树形结构节点,用于构建侧边栏菜单和路由表。
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* <ul>
|
||||
* <li>这是 SysMenu 实体到前端路由的映射对象,去掉了数据库相关的字段(systemId、createdAt等),
|
||||
* 只保留前端路由渲染所需的字段</li>
|
||||
* <li>通过 parentId 的递归关系构建树形结构:每个节点的 children 是其子路由列表</li>
|
||||
* <li>与 SysMenu 的关键区别:增加了 meta(MenuMetaVO)字段用于 Vue Router 的 meta 配置,
|
||||
* 同时将 menuPath 映射为 path、menuName 映射为 name</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 字段详细说明:
|
||||
* <ul>
|
||||
* <li><b>menuType</b>:控制菜单渲染方式
|
||||
* <ul>
|
||||
* <li>1 = 目录(无路由路径,只作为分组折叠项)</li>
|
||||
* <li>2 = 菜单(有路由路径,可点击跳转)</li>
|
||||
* <li>3 = 按钮(权限控制点,通常不在菜单中显示)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>sortOrder</b>:控制同级菜单的显示顺序(数字越小越靠前)。
|
||||
* 在 MenuServiceImpl.buildMenuTree() 构建树时保持数据库的排序,
|
||||
* 前端按此顺序渲染侧边栏菜单项</li>
|
||||
* <li><b>status</b>:控制菜单是否在前端显示
|
||||
* <ul>
|
||||
* <li>0 = 禁用/隐藏:该菜单不在侧边栏显示,路由也不注册</li>
|
||||
* <li>1 = 启用/显示:正常显示和路由注册</li>
|
||||
* </ul>
|
||||
* 当前 Service 层查询时不按 status 过滤,由前端根据此字段决定是否渲染</li>
|
||||
* <li><b>children</b>:子路由列表。叶子节点(无子菜单)时此字段为 null(非空列表),
|
||||
* 前端据此判断是否显示展开箭头和子菜单区域</li>
|
||||
* <li><b>permission</b>:权限标识字符串(如 "system:user:list"),用于前端按钮级权限控制</li>
|
||||
* <li><b>component</b>:Vue 组件路径(如 "system/user/index"),前端动态 import 此路径的组件</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MenuRouterVO {
|
||||
|
||||
/** 菜单ID(对应 SysMenu.id) */
|
||||
private String id;
|
||||
|
||||
/** 菜单显示名称 */
|
||||
private String menuName;
|
||||
|
||||
/**
|
||||
* 菜单类型:1=目录(无路由路径), 2=菜单(有路由路径), 3=按钮(权限控制点)
|
||||
*/
|
||||
private Integer menuType;
|
||||
|
||||
/** 菜单图标(如 "el-icon-user") */
|
||||
private String icon;
|
||||
|
||||
/** 权限标识字符串(如 "system:user:list"),用于前端按钮级权限控制 */
|
||||
private String permission;
|
||||
|
||||
/** 前端路由路径(如 "/system/user"),对应 SysMenu.menuPath */
|
||||
private String path;
|
||||
|
||||
/** 路由名称(兼容旧字段,与 menuName 相同) */
|
||||
private String name;
|
||||
|
||||
/** Vue 组件路径(如 "system/user/index"),前端动态 import */
|
||||
private String component;
|
||||
|
||||
/**
|
||||
* 排序号(数字越小越靠前)
|
||||
* <p>
|
||||
* 控制同级菜单在侧边栏中的显示顺序。
|
||||
* 数据库查询时已按 sortOrder 升序排列,构建树时保持该顺序。
|
||||
*/
|
||||
private Integer sortOrder;
|
||||
|
||||
/**
|
||||
* 菜单状态:0=禁用/隐藏, 1=启用/显示
|
||||
* <p>
|
||||
* 前端根据此字段决定是否渲染该菜单路由:
|
||||
* <ul>
|
||||
* <li>0 = 不注册路由,不在侧边栏显示</li>
|
||||
* <li>1 = 正常注册路由并显示</li>
|
||||
* </ul>
|
||||
* 当前 Service 层不按 status 过滤,所有菜单都返回,由前端自行过滤。
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 菜单元数据(Vue Router meta 配置)
|
||||
* <p>
|
||||
* 包含菜单名称(title)和图标(icon),用于前端路由的 meta 属性:
|
||||
* <pre>
|
||||
* {
|
||||
* path: '/system/user',
|
||||
* meta: { title: '用户管理', icon: 'el-icon-user' }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
private MenuMetaVO meta;
|
||||
|
||||
/**
|
||||
* 子路由列表
|
||||
* <p>
|
||||
* 叶子节点(无子菜单)时此字段为 <b>null</b>(非空列表),
|
||||
* 前端据此判断是否显示展开箭头和渲染子菜单区域。
|
||||
*/
|
||||
private List<MenuRouterVO> children;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.mokee.common.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 角色信息
|
||||
* <p>
|
||||
* TokenInfo 内部使用的角色摘要,存储于 Redis 会话中
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RoleInfo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 角色ID */
|
||||
private String roleId;
|
||||
|
||||
/** 角色名称 */
|
||||
private String roleName;
|
||||
|
||||
/** 角色编码 */
|
||||
private String roleCode;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.mokee.common.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 业务系统 VO
|
||||
* <p>
|
||||
* 用户可登录的业务系统信息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SystemVO {
|
||||
|
||||
/** 系统ID */
|
||||
private String systemId;
|
||||
|
||||
/** 系统编码 */
|
||||
private String systemCode;
|
||||
|
||||
/** 系统名称 */
|
||||
private String systemName;
|
||||
|
||||
/** 前端入口地址 */
|
||||
private String frontUrl;
|
||||
|
||||
/** 系统图标 */
|
||||
private String icon;
|
||||
|
||||
/** 系统描述 */
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.mokee.common.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Redis 会话对象
|
||||
* <p>
|
||||
* 存储在 Redis 中的用户登录会话信息,包含用户基础信息、当前系统及角色权限
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TokenInfo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 用户ID */
|
||||
private String userId;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** 真实姓名 */
|
||||
private String realName;
|
||||
|
||||
/** 邮箱 */
|
||||
private String email;
|
||||
|
||||
/** 手机号 */
|
||||
private String phone;
|
||||
|
||||
/** 头像地址 */
|
||||
private String avatar;
|
||||
|
||||
/** 岗位 */
|
||||
private String post;
|
||||
|
||||
/** 当前系统ID */
|
||||
private String currentSystemId;
|
||||
|
||||
/** 当前系统编码 */
|
||||
private String currentSystemCode;
|
||||
|
||||
/** 当前系统名称 */
|
||||
private String currentSystemName;
|
||||
|
||||
/** 角色列表 */
|
||||
private List<RoleInfo> roles;
|
||||
|
||||
/** 权限标识列表 */
|
||||
private List<String> permissions;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.mokee.common.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户信息 VO
|
||||
* <p>
|
||||
* 用户基础信息,嵌入 LoginResponse 使用
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserInfoVO {
|
||||
|
||||
/** 用户ID */
|
||||
private String userId;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** 真实姓名 */
|
||||
private String realName;
|
||||
|
||||
/** 邮箱 */
|
||||
private String email;
|
||||
|
||||
/** 手机号 */
|
||||
private String phone;
|
||||
|
||||
/** 头像地址 */
|
||||
private String avatar;
|
||||
|
||||
/** 岗位 */
|
||||
private String post;
|
||||
|
||||
/** 创建时间 */
|
||||
private String createdAt;
|
||||
|
||||
/** 最后登录时间 */
|
||||
private String lastLoginTime;
|
||||
|
||||
/** 按钮权限列表 */
|
||||
private List<String> permissions;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统API实体
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_api")
|
||||
@Schema(description = "系统API")
|
||||
public class SysApi {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField(value = "system_id")
|
||||
@Schema(description = "系统ID")
|
||||
private String systemId;
|
||||
|
||||
@TableField(value = "api_path")
|
||||
@Schema(description = "API路径")
|
||||
private String apiPath;
|
||||
|
||||
@TableField(value = "api_method")
|
||||
@Schema(description = "请求方法(GET/POST/PUT/DELETE)")
|
||||
private String apiMethod;
|
||||
|
||||
@TableField(value = "api_name")
|
||||
@Schema(description = "API名称")
|
||||
private String apiName;
|
||||
|
||||
@TableField(value = "api_desc")
|
||||
@Schema(description = "API描述")
|
||||
private String apiDesc;
|
||||
|
||||
@TableField(value = "status")
|
||||
@Schema(description = "状态(0停用 1启用)")
|
||||
private Integer status;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统配置实体
|
||||
* <p>
|
||||
* 键值对形式的系统级配置,如文件存储路径等。
|
||||
* 通过 ConfigService 内存缓存读取,避免每次请求查库。
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_config")
|
||||
@Schema(description = "系统配置")
|
||||
public class SysConfig {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField("config_key")
|
||||
@Schema(description = "配置键")
|
||||
private String configKey;
|
||||
|
||||
@TableField("config_value")
|
||||
@Schema(description = "配置值")
|
||||
private String configValue;
|
||||
|
||||
@TableField("description")
|
||||
@Schema(description = "配置说明")
|
||||
private String description;
|
||||
|
||||
@TableField("updated_at")
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统字典实体
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_dict")
|
||||
@Schema(description = "系统字典")
|
||||
public class SysDict {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField(value = "dict_type")
|
||||
@Schema(description = "字典类型")
|
||||
private String dictType;
|
||||
|
||||
@TableField(value = "dict_label")
|
||||
@Schema(description = "字典标签")
|
||||
private String dictLabel;
|
||||
|
||||
@TableField(value = "dict_value")
|
||||
@Schema(description = "字典值")
|
||||
private String dictValue;
|
||||
|
||||
@TableField(value = "sort_order")
|
||||
@Schema(description = "排序号")
|
||||
private Integer sortOrder;
|
||||
|
||||
@TableField(value = "status")
|
||||
@Schema(description = "状态(0停用 1启用)")
|
||||
private Integer status;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 邮件发送记录 */
|
||||
@Data @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
@TableName("sys_email_log")
|
||||
public class SysEmailLog {
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
@TableField("to_address")
|
||||
private String toAddress;
|
||||
|
||||
@TableField("subject")
|
||||
private String subject;
|
||||
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
@TableField("content_type")
|
||||
private String contentType;
|
||||
|
||||
@TableField("status")
|
||||
private Integer status;
|
||||
|
||||
@TableField("error_msg")
|
||||
private String errorMsg;
|
||||
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统文件实体
|
||||
* <p>
|
||||
* 文件二进制数据存储于本地磁盘,数据库仅保存元信息。
|
||||
*/
|
||||
@Data @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
@TableName("sys_file")
|
||||
@Schema(description = "系统文件")
|
||||
public class SysFile {
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "文件ID")
|
||||
private String id;
|
||||
|
||||
@TableField("system_id")
|
||||
@Schema(description = "所属系统ID")
|
||||
private String systemId;
|
||||
|
||||
@TableField("file_name")
|
||||
@Schema(description = "原始文件名")
|
||||
private String fileName;
|
||||
|
||||
@TableField("storage_file_name")
|
||||
@Schema(description = "磁盘存储的实际文件名(如 uuid.png)")
|
||||
private String storageFileName;
|
||||
|
||||
@TableField("file_type")
|
||||
@Schema(description = "文件类型(icon/avatar/doc/other)")
|
||||
private String fileType;
|
||||
|
||||
@TableField("file_size")
|
||||
@Schema(description = "文件大小(字节)")
|
||||
private Long fileSize;
|
||||
|
||||
@TableField("mime_type")
|
||||
@Schema(description = "MIME类型")
|
||||
private String mimeType;
|
||||
|
||||
@TableField("md5")
|
||||
@Schema(description = "文件MD5摘要,用于去重校验")
|
||||
private String md5;
|
||||
|
||||
@TableField("upload_user_id")
|
||||
@Schema(description = "上传用户ID")
|
||||
private String uploadUserId;
|
||||
|
||||
@TableField("upload_user_name")
|
||||
@Schema(description = "上传者显示名(用户姓名或系统名称)")
|
||||
private String uploadUserName;
|
||||
|
||||
@TableField("file_data")
|
||||
@Schema(description = "文件二进制数据(LONGBLOB) — 存量兼容,新文件为null")
|
||||
private byte[] fileData;
|
||||
|
||||
@TableField("created_at")
|
||||
@Schema(description = "上传时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 登录日志实体
|
||||
* <p>
|
||||
* 对应数据库表 {@code sys_login_log},记录每次用户登录的详细信息。
|
||||
* <p>
|
||||
* 数据写入方:网关的登录过滤器在用户登录成功/失败时插入记录。
|
||||
* 数据读取方:LogController → LogServiceImpl 分页查询,用于后台审计和统计分析。
|
||||
* <p>
|
||||
* 字段说明:
|
||||
* <ul>
|
||||
* <li><b>status</b>:1=登录成功, 0=登录失败。DashboardController 据此统计今日登录人数和成功率</li>
|
||||
* <li><b>failReason</b>:登录失败时记录具体原因(如 "密码错误"、"账号被锁定" 等)</li>
|
||||
* <li><b>systemId</b>:记录用户在哪个业务系统登录的</li>
|
||||
* <li><b>systemName</b>:<b>非数据库字段</b>(@TableField(exist=false)),查询时由 Service 层
|
||||
* 通过 systemId 联表查询 sys_system 表后填充,用于前端直接展示系统名称</li>
|
||||
* <li><b>browser / os</b>:通过解析 User-Agent 请求头获得,用于安全审计(识别异常设备登录)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 统计分析用途:
|
||||
* <ul>
|
||||
* <li>DashboardController 统计今日登录人数(去重 user_id)和登录总次数</li>
|
||||
* <li>DashboardController 统计最近 30 分钟在线用户数</li>
|
||||
* <li>DashboardController 统计过去 30 天每日登录趋势(折线图数据)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_login_log")
|
||||
@Schema(description = "登录日志")
|
||||
public class SysLoginLog {
|
||||
|
||||
/** 主键ID,使用 UUID 自动生成 */
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
/** 用户ID,关联 sys_user 表 */
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description = "用户ID")
|
||||
private String userId;
|
||||
|
||||
/** 登录用户名 */
|
||||
@TableField(value = "username")
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
/** 用户真实姓名 */
|
||||
@TableField(value = "real_name")
|
||||
@Schema(description = "用户姓名")
|
||||
private String realName;
|
||||
|
||||
/** 登录所在系统ID,关联 sys_system 表 */
|
||||
@TableField(value = "system_id")
|
||||
@Schema(description = "系统ID")
|
||||
private String systemId;
|
||||
|
||||
/** 客户端IP地址 */
|
||||
@TableField(value = "login_ip")
|
||||
@Schema(description = "登录IP")
|
||||
private String loginIp;
|
||||
|
||||
/** 原始 User-Agent 请求头 */
|
||||
@TableField(value = "user_agent")
|
||||
@Schema(description = "用户代理")
|
||||
private String userAgent;
|
||||
|
||||
/** 浏览器类型(解析 User-Agent 获得,如 Chrome、Firefox) */
|
||||
@TableField(value = "browser")
|
||||
@Schema(description = "浏览器")
|
||||
private String browser;
|
||||
|
||||
/** 操作系统类型(解析 User-Agent 获得,如 Windows 10、macOS) */
|
||||
@TableField(value = "os")
|
||||
@Schema(description = "操作系统")
|
||||
private String os;
|
||||
|
||||
/** 登录状态:0=失败, 1=成功 */
|
||||
@TableField(value = "status")
|
||||
@Schema(description = "登录状态(0失败 1成功)")
|
||||
private Integer status;
|
||||
|
||||
/** 登录失败时的具体原因(成功时为 null) */
|
||||
@TableField(value = "fail_reason")
|
||||
@Schema(description = "失败原因")
|
||||
private String failReason;
|
||||
|
||||
/** 记录创建时间(即登录时间) */
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 系统名称(查询时联表填充,非数据库字段)
|
||||
* <p>
|
||||
* {@code @TableField(exist = false)} 表示此字段不对应数据库列。
|
||||
* 在 LogServiceImpl.loginLogPage() 中,通过 systemId 查询 sys_system 表
|
||||
* 获取 system_name 后填充此字段,供前端直接展示系统名称而非系统ID。
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "系统名称")
|
||||
private String systemName;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统菜单实体
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_menu")
|
||||
@Schema(description = "系统菜单")
|
||||
public class SysMenu {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField(value = "system_id")
|
||||
@Schema(description = "系统ID")
|
||||
private String systemId;
|
||||
|
||||
@TableField(value = "parent_id")
|
||||
@Schema(description = "父菜单ID")
|
||||
private String parentId;
|
||||
|
||||
@TableField(value = "menu_name")
|
||||
@Schema(description = "菜单名称")
|
||||
private String menuName;
|
||||
|
||||
@TableField(value = "menu_path")
|
||||
@Schema(description = "菜单路径")
|
||||
private String menuPath;
|
||||
|
||||
@TableField(value = "component")
|
||||
@Schema(description = "前端组件路径")
|
||||
private String component;
|
||||
|
||||
@TableField(value = "icon")
|
||||
@Schema(description = "图标")
|
||||
private String icon;
|
||||
|
||||
@TableField(value = "menu_type")
|
||||
@Schema(description = "菜单类型(0目录 1菜单 2按钮)")
|
||||
private Integer menuType;
|
||||
|
||||
@TableField(value = "permission")
|
||||
@Schema(description = "权限标识")
|
||||
private String permission;
|
||||
|
||||
@TableField(value = "sort_order")
|
||||
@Schema(description = "排序号")
|
||||
private Integer sortOrder;
|
||||
|
||||
@TableField(value = "status")
|
||||
@Schema(description = "状态(0停用 1启用)")
|
||||
private Integer status;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 开放API Token获取/消费日志 */
|
||||
@Data @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
@TableName("sys_open_token_log")
|
||||
public class SysOpenTokenLog {
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
@TableField("system_id")
|
||||
private String systemId;
|
||||
|
||||
@TableField("system_code")
|
||||
private String systemCode;
|
||||
|
||||
@TableField("token_id")
|
||||
private String tokenId;
|
||||
|
||||
@TableField("token_preview")
|
||||
private String tokenPreview;
|
||||
|
||||
@TableField("acquired_at")
|
||||
private LocalDateTime acquiredAt;
|
||||
|
||||
@TableField("consumed")
|
||||
private Integer consumed;
|
||||
|
||||
@TableField("consumed_at")
|
||||
private LocalDateTime consumedAt;
|
||||
|
||||
@TableField("consume_api")
|
||||
private String consumeApi;
|
||||
|
||||
@TableField("consume_ip")
|
||||
private String consumeIp;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 操作日志实体
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_operation_log")
|
||||
@Schema(description = "操作日志")
|
||||
public class SysOperationLog {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description = "用户ID")
|
||||
private String userId;
|
||||
|
||||
@TableField(value = "username")
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@TableField(value = "system_id")
|
||||
@Schema(description = "系统ID")
|
||||
private String systemId;
|
||||
|
||||
@TableField(value = "module")
|
||||
@Schema(description = "操作模块")
|
||||
private String module;
|
||||
|
||||
@TableField(value = "action")
|
||||
@Schema(description = "操作动作")
|
||||
private String action;
|
||||
|
||||
@TableField(value = "request_url")
|
||||
@Schema(description = "请求地址")
|
||||
private String requestUrl;
|
||||
|
||||
@TableField(value = "request_method")
|
||||
@Schema(description = "请求方法")
|
||||
private String requestMethod;
|
||||
|
||||
@TableField(value = "request_body")
|
||||
@Schema(description = "请求体")
|
||||
private String requestBody;
|
||||
|
||||
@TableField(value = "response_status")
|
||||
@Schema(description = "响应状态码")
|
||||
private Integer responseStatus;
|
||||
|
||||
@TableField(value = "error_msg")
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMsg;
|
||||
|
||||
@TableField(value = "response_body")
|
||||
@Schema(description = "响应体(截断至2000字符)")
|
||||
private String responseBody;
|
||||
|
||||
@TableField(value = "cost_time")
|
||||
@Schema(description = "耗时(毫秒)")
|
||||
private Integer costTime;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统角色实体
|
||||
* <p>
|
||||
* 对应数据库表 {@code sys_role},定义每个业务系统中的角色。
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* <ul>
|
||||
* <li>角色隶属于具体的业务系统(通过 systemId 关联),不同系统的角色相互隔离</li>
|
||||
* <li>role_code 是角色的唯一编码标识,用于权限判断。如 "super_admin"、"admin"、"user"</li>
|
||||
* <li>权限判断逻辑(SystemController.listAll())通过检查 role_code == "super_admin" 来决定是否返回全部系统</li>
|
||||
* <li>角色通过 sys_role_menu 中间表关联菜单,实现 RBAC 权限模型</li>
|
||||
* <li>角色通过 sys_user_role 中间表关联用户</li>
|
||||
* <li>使用 MyBatis-Plus 逻辑删除(@TableLogic),删除时 is_deleted 标记为 1</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 关键字段:
|
||||
* <ul>
|
||||
* <li><b>systemName</b>:<b>非数据库字段</b>(@TableField(exist=false)),查询时由 RoleServiceImpl
|
||||
* 通过 systemId 联表查询 sys_system 表后填充,用于前端直接展示所属系统名称</li>
|
||||
* <li><b>is_deleted</b>:逻辑删除标记,使用 @TableLogic 注解实现自动过滤</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_role")
|
||||
@Schema(description = "系统角色")
|
||||
public class SysRole {
|
||||
|
||||
/** 主键ID,使用 UUID 自动生成 */
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
/** 所属系统ID,关联 sys_system 表 */
|
||||
@TableField(value = "system_id")
|
||||
@Schema(description = "系统ID")
|
||||
private String systemId;
|
||||
|
||||
/** 角色名称(显示用,如 "超级管理员") */
|
||||
@TableField(value = "role_name")
|
||||
@Schema(description = "角色名称")
|
||||
private String roleName;
|
||||
|
||||
/** 角色编码(权限判断用,如 "super_admin"、"admin"、"user") */
|
||||
@TableField(value = "role_code")
|
||||
@Schema(description = "角色编码")
|
||||
private String roleCode;
|
||||
|
||||
/** 角色描述 */
|
||||
@TableField(value = "description")
|
||||
@Schema(description = "描述")
|
||||
private String description;
|
||||
|
||||
/** 状态:0=停用, 1=启用 */
|
||||
@TableField(value = "status")
|
||||
@Schema(description = "状态(0停用 1启用)")
|
||||
private Integer status;
|
||||
|
||||
/** 创建人ID */
|
||||
@TableField(value = "created_by")
|
||||
@Schema(description = "创建人ID")
|
||||
private String createdBy;
|
||||
|
||||
/** 更新人ID */
|
||||
@TableField(value = "updated_by")
|
||||
@Schema(description = "更新人ID")
|
||||
private String updatedBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 逻辑删除标记
|
||||
* <p>
|
||||
* 使用 MyBatis-Plus @TableLogic 注解:
|
||||
* <ul>
|
||||
* <li>value="0":正常记录</li>
|
||||
* <li>delval="1":已删除</li>
|
||||
* <li>自动过滤:select 时自动添加 WHERE is_deleted=0</li>
|
||||
* <li>逻辑删除:deleteById 时执行 UPDATE is_deleted=1 而非物理 DELETE</li>
|
||||
* </ul>
|
||||
*/
|
||||
@TableField(value = "is_deleted")
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
@Schema(description = "逻辑删除(0未删除 1已删除)")
|
||||
private Integer isDeleted;
|
||||
|
||||
/**
|
||||
* 所属系统名称(查询时联表填充,非数据库字段)
|
||||
* <p>
|
||||
* {@code @TableField(exist = false)} 表示此字段不对应数据库列。
|
||||
* 在 RoleServiceImpl.page() 中,通过 systemId 查询 sys_system 表
|
||||
* 获取 system_name 后填充此字段,供前端直接展示系统名称而非系统ID。
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "所属系统名称")
|
||||
private String systemName;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 角色菜单关联实体
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_role_menu")
|
||||
@Schema(description = "角色菜单关联")
|
||||
public class SysRoleMenu {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField(value = "role_id")
|
||||
@Schema(description = "角色ID")
|
||||
private String roleId;
|
||||
|
||||
@TableField(value = "menu_id")
|
||||
@Schema(description = "菜单ID")
|
||||
private String menuId;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统信息实体
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_system")
|
||||
@Schema(description = "系统信息")
|
||||
public class SysSystem {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField(value = "system_code")
|
||||
@Schema(description = "系统编码")
|
||||
private String systemCode;
|
||||
|
||||
@TableField(value = "system_name")
|
||||
@Schema(description = "系统名称")
|
||||
private String systemName;
|
||||
|
||||
@TableField(value = "front_url")
|
||||
@Schema(description = "前端地址")
|
||||
private String frontUrl;
|
||||
|
||||
@TableField(value = "backend_url")
|
||||
@Schema(description = "后端地址")
|
||||
private String backendUrl;
|
||||
|
||||
@TableField(value = "gateway_url")
|
||||
@Schema(description = "网关地址(已废弃,前端使用全局配置)")
|
||||
private String gatewayUrl;
|
||||
|
||||
@TableField(value = "icon")
|
||||
@Schema(description = "图标")
|
||||
private String icon;
|
||||
|
||||
@TableField(value = "sort_order")
|
||||
@Schema(description = "排序号")
|
||||
private Integer sortOrder;
|
||||
|
||||
@TableField(value = "status")
|
||||
@Schema(description = "状态(0停用 1启用)")
|
||||
private Integer status;
|
||||
|
||||
@TableField(value = "description")
|
||||
@Schema(description = "描述")
|
||||
private String description;
|
||||
|
||||
@TableField(value = "doc_token")
|
||||
@Schema(description = "文档访问令牌(UUID)")
|
||||
private String docToken;
|
||||
|
||||
@TableField(value = "created_by")
|
||||
@Schema(description = "创建人ID")
|
||||
private String createdBy;
|
||||
|
||||
@TableField(value = "updated_by")
|
||||
@Schema(description = "更新人ID")
|
||||
private String updatedBy;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@TableField(value = "is_deleted")
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
@Schema(description = "逻辑删除(0未删除 1已删除)")
|
||||
private Integer isDeleted;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统用户实体
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_user")
|
||||
@Schema(description = "系统用户")
|
||||
public class SysUser {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField(value = "username")
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@TableField(value = "password")
|
||||
@Schema(description = "密码")
|
||||
private String password;
|
||||
|
||||
@TableField(value = "real_name")
|
||||
@Schema(description = "真实姓名")
|
||||
private String realName;
|
||||
|
||||
@TableField(value = "email")
|
||||
@Schema(description = "邮箱")
|
||||
private String email;
|
||||
|
||||
@TableField(value = "phone")
|
||||
@Schema(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
@TableField(value = "avatar")
|
||||
@Schema(description = "头像")
|
||||
private String avatar;
|
||||
|
||||
@TableField(value = "post")
|
||||
@Schema(description = "岗位")
|
||||
private String post;
|
||||
|
||||
@TableField(value = "status")
|
||||
@Schema(description = "状态(0停用 1启用)")
|
||||
private Integer status;
|
||||
|
||||
@TableField(value = "lock_until")
|
||||
@Schema(description = "锁定截止时间")
|
||||
private LocalDateTime lockUntil;
|
||||
|
||||
@TableField(value = "login_fail_count")
|
||||
@Schema(description = "登录失败次数")
|
||||
private Integer loginFailCount;
|
||||
|
||||
@TableField(value = "last_login_ip")
|
||||
@Schema(description = "最后登录IP")
|
||||
private String lastLoginIp;
|
||||
|
||||
@TableField(value = "last_login_time")
|
||||
@Schema(description = "最后登录时间")
|
||||
private LocalDateTime lastLoginTime;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField(value = "updated_at")
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@TableField(value = "is_deleted")
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
@Schema(description = "逻辑删除(0未删除 1已删除)")
|
||||
private Integer isDeleted;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户角色关联实体
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_user_role")
|
||||
@Schema(description = "用户角色关联")
|
||||
public class SysUserRole {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description = "用户ID")
|
||||
private String userId;
|
||||
|
||||
@TableField(value = "role_id")
|
||||
@Schema(description = "角色ID")
|
||||
private String roleId;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.mokee.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户系统关联实体
|
||||
*
|
||||
* @author mokee
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("sys_user_system")
|
||||
@Schema(description = "用户系统关联")
|
||||
public class SysUserSystem {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@TableField(value = "user_id")
|
||||
@Schema(description = "用户ID")
|
||||
private String userId;
|
||||
|
||||
@TableField(value = "system_id")
|
||||
@Schema(description = "系统ID")
|
||||
private String systemId;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.mokee.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 操作类型枚举
|
||||
* <p>
|
||||
* 用于操作日志记录,标识用户执行的业务动作
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ActionEnum {
|
||||
|
||||
/** 新增 */
|
||||
CREATE("CREATE", "新增"),
|
||||
|
||||
/** 编辑 */
|
||||
UPDATE("UPDATE", "编辑"),
|
||||
|
||||
/** 删除 */
|
||||
DELETE("DELETE", "删除"),
|
||||
|
||||
/** 查询 */
|
||||
QUERY("QUERY", "查询"),
|
||||
|
||||
/** 导入 */
|
||||
IMPORT("IMPORT", "导入"),
|
||||
|
||||
/** 导出 */
|
||||
EXPORT("EXPORT", "导出"),
|
||||
|
||||
/** 登录 */
|
||||
LOGIN("LOGIN", "登录"),
|
||||
|
||||
/** 登出 */
|
||||
LOGOUT("LOGOUT", "登出");
|
||||
|
||||
private final String code;
|
||||
|
||||
private final String desc;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.mokee.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 菜单类型枚举
|
||||
* <p>
|
||||
* 区分前端路由树的节点类型
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum MenuTypeEnum {
|
||||
|
||||
/** 目录 */
|
||||
CATALOG(1, "目录"),
|
||||
|
||||
/** 菜单 */
|
||||
MENU(2, "菜单"),
|
||||
|
||||
/** 按钮 */
|
||||
BUTTON(3, "按钮");
|
||||
|
||||
private final int code;
|
||||
|
||||
private final String desc;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.mokee.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通用状态枚举
|
||||
* <p>
|
||||
* 用于用户、系统、角色等实体的状态标记
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum StatusEnum {
|
||||
|
||||
/** 禁用 */
|
||||
DISABLED(0, "禁用"),
|
||||
|
||||
/** 启用 */
|
||||
ENABLED(1, "启用"),
|
||||
|
||||
/** 锁定(仅用户) */
|
||||
LOCKED(2, "锁定");
|
||||
|
||||
private final int code;
|
||||
|
||||
private final String desc;
|
||||
|
||||
/**
|
||||
* 根据状态码获取对应枚举值
|
||||
*
|
||||
* @param code 状态码
|
||||
* @return 对应的枚举值,未匹配时返回 null
|
||||
*/
|
||||
public static StatusEnum getByCode(int code) {
|
||||
for (StatusEnum status : values()) {
|
||||
if (status.getCode() == code) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.mokee.common.exception;
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
* <p>
|
||||
* 统一业务异常类,携带错误码。由 GlobalExceptionHandler 统一拦截处理
|
||||
*/
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 错误码 */
|
||||
private int code;
|
||||
|
||||
public BusinessException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = 500;
|
||||
}
|
||||
|
||||
public BusinessException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.mokee.common.exception;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 错误码枚举
|
||||
* <p>
|
||||
* 定义业务异常的统一错误码及对应描述,覆盖认证、授权、参数校验、服务端异常等场景
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ErrorCode {
|
||||
|
||||
/** Token 已过期,请重新登录 */
|
||||
TOKEN_EXPIRED(401, "Token已过期,请重新登录"),
|
||||
|
||||
/** Token 无效 */
|
||||
TOKEN_INVALID(401, "Token无效"),
|
||||
|
||||
/** 未授权访问 */
|
||||
UNAUTHORIZED(401, "未授权访问"),
|
||||
|
||||
/** 接口无访问权限 */
|
||||
FORBIDDEN(403, "接口无访问权限"),
|
||||
|
||||
/** 资源不存在 */
|
||||
NOT_FOUND(404, "资源不存在"),
|
||||
|
||||
/** 操作太频繁,请稍后再试 */
|
||||
RATE_LIMITED(429, "操作太频繁,请稍后再试"),
|
||||
|
||||
/** 账号已被锁定,请稍后重试 */
|
||||
ACCOUNT_LOCKED(423, "账号已被锁定,请稍后重试"),
|
||||
|
||||
/** 账号或密码错误 */
|
||||
USERNAME_PASSWORD_ERROR(400, "账号或密码错误"),
|
||||
|
||||
/** 账号已被禁用 */
|
||||
USER_DISABLED(400, "账号已被禁用"),
|
||||
|
||||
/** 业务系统已被禁用 */
|
||||
SYSTEM_DISABLED(400, "业务系统已被禁用"),
|
||||
|
||||
/** 参数错误 */
|
||||
PARAM_ERROR(400, "参数错误"),
|
||||
|
||||
/** 服务器内部错误 */
|
||||
INTERNAL_ERROR(500, "服务器内部错误");
|
||||
|
||||
/** 错误码 */
|
||||
private final int code;
|
||||
|
||||
/** 错误描述 */
|
||||
private final String message;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.mokee.common.exception;
|
||||
|
||||
import com.mokee.common.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
* <p>
|
||||
* 统一拦截 Controller 层抛出的异常,封装为 Result 返回
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Result<Void> handleBusinessException(BusinessException e) {
|
||||
log.warn("业务异常 - code: {}, message: {}", e.getCode(), e.getMessage());
|
||||
return Result.fail(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常(@Valid 校验失败)
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
String errorMsg = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数校验失败 - {}", errorMsg);
|
||||
return Result.fail(400, errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理未预期的异常(兜底)
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Result<Void> handleException(Exception e) {
|
||||
log.error("服务器内部错误", e);
|
||||
return Result.fail("服务器内部错误");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.mokee.common.service;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 邮件服务 — 自动添加环境前缀 [QA]/[PROD]
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class EmailService {
|
||||
|
||||
@Autowired(required = false)
|
||||
private JavaMailSender mailSender;
|
||||
|
||||
@Value("${spring.mail.username:}")
|
||||
private String from;
|
||||
|
||||
@Value("${app.env-prefix:[QA]}")
|
||||
private String envPrefix;
|
||||
|
||||
private JavaMailSender requireMailSender() {
|
||||
if (mailSender == null) {
|
||||
throw new IllegalStateException("邮件服务未配置(缺少 spring.mail 配置)");
|
||||
}
|
||||
return mailSender;
|
||||
}
|
||||
|
||||
/** 发送纯文本邮件 */
|
||||
public void sendText(String to, String subject, String text) {
|
||||
SimpleMailMessage msg = new SimpleMailMessage();
|
||||
msg.setFrom(from);
|
||||
msg.setTo(to);
|
||||
msg.setSubject(envPrefix + " " + subject);
|
||||
msg.setText(text);
|
||||
requireMailSender().send(msg);
|
||||
log.info("邮件已发送(纯文本) -> to: {}, subject: {}", to, subject);
|
||||
}
|
||||
|
||||
/** 发送 HTML 邮件 */
|
||||
public void sendHtml(String to, String subject, String html) {
|
||||
try {
|
||||
MimeMessage mimeMsg = requireMailSender().createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, true, "UTF-8");
|
||||
helper.setFrom(from);
|
||||
helper.setTo(to);
|
||||
helper.setSubject(envPrefix + " " + subject);
|
||||
helper.setText(html, true);
|
||||
requireMailSender().send(mimeMsg);
|
||||
log.info("邮件已发送(HTML) -> from: {}, to: {}, subject: {}", from, to, subject);
|
||||
} catch (MessagingException e) {
|
||||
log.error("邮件发送失败 -> to: {}, subject: {}, error: {}", to, subject, e.getMessage());
|
||||
throw new RuntimeException("邮件发送失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送带附件的 HTML 邮件 */
|
||||
public void sendHtmlWithAttachment(String to, String subject, String html,
|
||||
String attachmentName, byte[] attachmentData, String mimeType) {
|
||||
try {
|
||||
MimeMessage mimeMsg = requireMailSender().createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, true, "UTF-8");
|
||||
helper.setFrom(from);
|
||||
helper.setTo(to);
|
||||
helper.setSubject(envPrefix + " " + subject);
|
||||
helper.setText(html, true);
|
||||
helper.addAttachment(attachmentName, () -> new java.io.ByteArrayInputStream(attachmentData), mimeType);
|
||||
requireMailSender().send(mimeMsg);
|
||||
log.info("邮件已发送(带附件) -> to: {}, subject: {}, attachment: {}", to, subject, attachmentName);
|
||||
} catch (MessagingException e) {
|
||||
log.error("邮件发送失败 -> to: {}, subject: {}, error: {}", to, subject, e.getMessage());
|
||||
throw new RuntimeException("邮件发送失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.mokee.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* IP 工具类
|
||||
* <p>
|
||||
* 从 HttpServletRequest 中提取客户端真实 IP,依次尝试常用代理头
|
||||
*/
|
||||
@Slf4j
|
||||
public class IpUtil {
|
||||
|
||||
private static final String UNKNOWN = "unknown";
|
||||
|
||||
/**
|
||||
* 获取客户端真实 IP
|
||||
* <p>
|
||||
* 依次从 X-Forwarded-For, X-Real-IP, Proxy-Client-IP, WL-Proxy-Client-IP 头中获取,
|
||||
* 多层代理时 X-Forwarded-For 取第一个 IP。均获取不到时回退到 getRemoteAddr()
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @return 客户端 IP 地址
|
||||
*/
|
||||
public static String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip)) {
|
||||
// 多次反向代理后会有多个 IP,取第一个
|
||||
int index = ip.indexOf(',');
|
||||
if (index != -1) {
|
||||
ip = ip.substring(0, index).trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
if (ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip)) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
if (ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip)) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
if (ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip)) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.mokee.common.utils;
|
||||
|
||||
import com.mokee.common.exception.BusinessException;
|
||||
import com.mokee.common.exception.ErrorCode;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* JWT 工具类
|
||||
* <p>
|
||||
* 负责 JWT Token 的生成、解析、校验,配置可从 application.yml 读取也可使用默认值
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
/** JWT 签名密钥(默认值) */
|
||||
@Value("${jwt.secret:mokee-gateway-secret-key-2024}")
|
||||
private String secret;
|
||||
|
||||
/** JWT 过期时间,单位秒(默认2小时) */
|
||||
@Value("${jwt.expiration:7200}")
|
||||
private long expiration;
|
||||
|
||||
/** 缓存的签名密钥,避免每次请求都重新生成 */
|
||||
private volatile SecretKey cachedSigningKey;
|
||||
private volatile String cachedSecret;
|
||||
|
||||
/**
|
||||
* 生成签名密钥(带缓存,避免重复计算 HMAC-SHA 密钥派生)
|
||||
* <p>
|
||||
* Keys.hmacShaKeyFor() 涉及哈希计算,每次请求都调用会浪费 CPU。
|
||||
* 只要 secret 配置不变,缓存结果即可。
|
||||
*/
|
||||
private SecretKey getSigningKey() {
|
||||
if (cachedSigningKey != null && secret.equals(cachedSecret)) {
|
||||
return cachedSigningKey;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (cachedSigningKey != null && secret.equals(cachedSecret)) {
|
||||
return cachedSigningKey;
|
||||
}
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
cachedSigningKey = Keys.hmacShaKeyFor(keyBytes);
|
||||
cachedSecret = secret;
|
||||
return cachedSigningKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 JWT Token
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @return JWT Token 字符串
|
||||
*/
|
||||
public String generateToken(String userId, String username) {
|
||||
String tokenId = UUID.randomUUID().toString();
|
||||
Date now = new Date();
|
||||
Date expirationDate = new Date(now.getTime() + expiration * 1000);
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("userId", userId)
|
||||
.claim("username", username)
|
||||
.claim("tokenId", tokenId)
|
||||
.issuedAt(now)
|
||||
.expiration(expirationDate)
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JWT Token,返回 Claims
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 解析后的 Claims
|
||||
*/
|
||||
public Claims parseToken(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Claims 中获取 tokenId
|
||||
*/
|
||||
public String getTokenId(Claims claims) {
|
||||
return claims.get("tokenId", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Claims 中获取用户ID
|
||||
*/
|
||||
public String getUserId(Claims claims) {
|
||||
return claims.get("userId", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Claims 中获取用户名
|
||||
*/
|
||||
public String getUsername(Claims claims) {
|
||||
return claims.get("username", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Token 是否已过期
|
||||
*/
|
||||
public boolean isTokenExpired(Claims claims) {
|
||||
return claims.getExpiration().before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成对外开放 API Token(用于外部系统调用,type=open_api 区分用户 token)
|
||||
*
|
||||
* @param systemId 系统ID
|
||||
* @param systemCode 系统编码
|
||||
* @param tokenId 预生成的 token 唯一ID
|
||||
* @return JWT Token 字符串
|
||||
*/
|
||||
public String generateOpenToken(String systemId, String systemCode, String tokenId) {
|
||||
return generateOpenToken(systemId, systemCode, tokenId, expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成开放 API Token(自定义过期时间)
|
||||
*
|
||||
* @param systemId 系统ID
|
||||
* @param systemCode 系统编码
|
||||
* @param tokenId Token唯一ID
|
||||
* @param expireSeconds 过期时间(秒),如 43200 = 12小时
|
||||
*/
|
||||
public String generateOpenToken(String systemId, String systemCode, String tokenId, long expireSeconds) {
|
||||
Date now = new Date();
|
||||
Date expirationDate = new Date(now.getTime() + expireSeconds * 1000);
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(systemId)
|
||||
.claim("systemId", systemId)
|
||||
.claim("systemCode", systemCode)
|
||||
.claim("tokenId", tokenId)
|
||||
.claim("type", "open_api")
|
||||
.issuedAt(now)
|
||||
.expiration(expirationDate)
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 JWT Token
|
||||
* <p>
|
||||
* 解析 Token 并校验是否过期,过期或格式错误均抛出对应异常
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 解析后的 Claims
|
||||
* @throws BusinessException Token 过期或无效时抛出
|
||||
*/
|
||||
public Claims validateToken(String token) {
|
||||
try {
|
||||
Claims claims = parseToken(token);
|
||||
if (isTokenExpired(claims)) {
|
||||
throw new BusinessException(ErrorCode.TOKEN_EXPIRED);
|
||||
}
|
||||
return claims;
|
||||
} catch (ExpiredJwtException e) {
|
||||
log.warn("Token 已过期: {}", e.getMessage());
|
||||
throw new BusinessException(ErrorCode.TOKEN_EXPIRED);
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
log.warn("Token 无效: {}", e.getMessage());
|
||||
throw new BusinessException(ErrorCode.TOKEN_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.mokee.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis 工具类
|
||||
* <p>
|
||||
* 封装常用的 StringRedisTemplate 操作,统一异常处理
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RedisUtil {
|
||||
|
||||
/** Redis Key 统一前缀(所有 key 共用,改一处全改) */
|
||||
public static final String KEY_PREFIX = "GATEWAY:";
|
||||
|
||||
/**
|
||||
* 生成带统一前缀的 Redis Key
|
||||
* <p>
|
||||
* 所有 Redis 操作都应通过此方法生成 key,确保前缀统一。
|
||||
* <pre>
|
||||
* RedisUtil.key("auth:token:" + tokenId) // → "GATEWAY:auth:token:xxx"
|
||||
* </pre>
|
||||
*/
|
||||
public static String key(String suffix) {
|
||||
return KEY_PREFIX + suffix;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 设置缓存(带过期时间)
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param timeout 过期时间
|
||||
* @param unit 时间单位
|
||||
*/
|
||||
public void set(String key, String value, long timeout, TimeUnit unit) {
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis set 异常 - key: {}, value: {}", key, value, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值,不存在返回 null
|
||||
*/
|
||||
public String get(String key) {
|
||||
try {
|
||||
return stringRedisTemplate.opsForValue().get(key);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis get 异常 - key: {}", key, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public void delete(String key) {
|
||||
try {
|
||||
stringRedisTemplate.delete(key);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis delete 异常 - key: {}", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param timeout 过期时间
|
||||
* @param unit 时间单位
|
||||
*/
|
||||
public void expire(String key, long timeout, TimeUnit unit) {
|
||||
try {
|
||||
stringRedisTemplate.expire(key, timeout, unit);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis expire 异常 - key: {}", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期时间
|
||||
*
|
||||
* @param key 键
|
||||
* @return 剩余秒数,-1 永不过期,-2 不存在
|
||||
*/
|
||||
public Long getExpire(String key) {
|
||||
try {
|
||||
return stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis getExpire 异常 - key: {}", key, e);
|
||||
return -2L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自增并返回新值
|
||||
*
|
||||
* @param key 键
|
||||
* @return 自增后的值
|
||||
*/
|
||||
public Long incr(String key) {
|
||||
try {
|
||||
return stringRedisTemplate.opsForValue().increment(key);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis incr 异常 - key: {}", key, e);
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 key 是否存在
|
||||
*
|
||||
* @param key 键
|
||||
* @return true 存在,false 不存在
|
||||
*/
|
||||
public boolean exists(String key) {
|
||||
try {
|
||||
Boolean result = stringRedisTemplate.hasKey(key);
|
||||
return result != null && result;
|
||||
} catch (Exception e) {
|
||||
log.error("Redis exists 异常 - key: {}", key, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lua 脚本:一次往返完成 GET + TTL + 条件续期
|
||||
* <p>
|
||||
* 返回 List: [value, ttl_seconds, renewed_flag]
|
||||
* renewed_flag: 0=未续期, 1=已续期
|
||||
* <p>
|
||||
* 用于 Token 验证场景,将原本 3 次 Redis 往返(GET + TTL + EXPIRE)合并为 1 次。
|
||||
*/
|
||||
private static final String GET_WITH_RENEW_SCRIPT =
|
||||
"local val = redis.call('GET', KEYS[1])\n" +
|
||||
"if val == false then return {val, -2, 0} end\n" +
|
||||
"local ttl = redis.call('TTL', KEYS[1])\n" +
|
||||
"local renewed = 0\n" +
|
||||
"if ttl > 0 and ttl < tonumber(ARGV[1]) then\n" +
|
||||
" redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))\n" +
|
||||
" renewed = 1\n" +
|
||||
"end\n" +
|
||||
"return {val, ttl, renewed}";
|
||||
|
||||
/** Lua 脚本缓存,避免每次请求都重新创建 DefaultRedisScript */
|
||||
private volatile DefaultRedisScript<List> getWithRenewScript;
|
||||
|
||||
/**
|
||||
* GET + TTL + 条件续期(一次 Redis 往返)
|
||||
*
|
||||
* @param key Redis key
|
||||
* @param renewThreshold 续期阈值(秒),TTL 小于此值时续期
|
||||
* @param newTtl 续期时长(秒)
|
||||
* @return [value, ttl, renewed] — value 可能为 null(key 不存在),
|
||||
* ttl 为 -2 表示不存在,renewed 1=已续期 0=未续期
|
||||
*/
|
||||
public List<Object> getWithRenew(String key, long renewThreshold, long newTtl) {
|
||||
try {
|
||||
if (getWithRenewScript == null) {
|
||||
synchronized (this) {
|
||||
if (getWithRenewScript == null) {
|
||||
DefaultRedisScript<List> script = new DefaultRedisScript<>();
|
||||
script.setScriptText(GET_WITH_RENEW_SCRIPT);
|
||||
script.setResultType(List.class);
|
||||
getWithRenewScript = script;
|
||||
}
|
||||
}
|
||||
}
|
||||
return stringRedisTemplate.execute(
|
||||
getWithRenewScript,
|
||||
Arrays.asList(key),
|
||||
String.valueOf(renewThreshold),
|
||||
String.valueOf(newTtl)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis getWithRenew 异常 - key: {}", key, e);
|
||||
return Arrays.asList(null, -2L, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.mokee.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* User-Agent 工具类
|
||||
* <p>
|
||||
* 从 User-Agent 字符串中解析浏览器和操作系统信息
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserAgentUtil {
|
||||
|
||||
/**
|
||||
* 解析浏览器名称
|
||||
*
|
||||
* @param userAgent User-Agent 字符串(不区分大小写匹配)
|
||||
* @return 浏览器名称:Chrome / Firefox / Safari / Edge / IE / Unknown
|
||||
*/
|
||||
public static String parseBrowser(String userAgent) {
|
||||
if (userAgent == null || userAgent.isEmpty()) {
|
||||
return "Unknown";
|
||||
}
|
||||
String ua = userAgent.toLowerCase();
|
||||
|
||||
if (ua.contains("edg") || ua.contains("edge")) {
|
||||
return "Edge";
|
||||
}
|
||||
if (ua.contains("chrome") && !ua.contains("edg")) {
|
||||
return "Chrome";
|
||||
}
|
||||
if (ua.contains("firefox") || ua.contains("fxios")) {
|
||||
return "Firefox";
|
||||
}
|
||||
if (ua.contains("safari") && !ua.contains("chrome") && !ua.contains("edg")) {
|
||||
return "Safari";
|
||||
}
|
||||
if (ua.contains("msie") || ua.contains("trident")) {
|
||||
return "IE";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析操作系统名称
|
||||
*
|
||||
* @param userAgent User-Agent 字符串(不区分大小写匹配)
|
||||
* @return 操作系统名称:Windows / Mac / Linux / Android / iOS / Unknown
|
||||
*/
|
||||
public static String parseOs(String userAgent) {
|
||||
if (userAgent == null || userAgent.isEmpty()) {
|
||||
return "Unknown";
|
||||
}
|
||||
String ua = userAgent.toLowerCase();
|
||||
|
||||
if (ua.contains("windows")) {
|
||||
return "Windows";
|
||||
}
|
||||
if (ua.contains("mac") || ua.contains("macintosh")) {
|
||||
return "Mac";
|
||||
}
|
||||
if (ua.contains("linux") && !ua.contains("android")) {
|
||||
return "Linux";
|
||||
}
|
||||
if (ua.contains("android")) {
|
||||
return "Android";
|
||||
}
|
||||
if (ua.contains("iphone") || ua.contains("ipad") || ua.contains("ipod")) {
|
||||
return "iOS";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user