初始化

This commit is contained in:
zg
2026-07-15 16:13:27 +08:00
parent 6d6f559793
commit c4023c1623
276 changed files with 42622 additions and 0 deletions

View 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>

View File

@@ -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());
}
}

View File

@@ -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 "";
}

View File

@@ -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;
}
}

View File

@@ -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/ShanghaiUTC+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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 的关键区别:增加了 metaMenuMetaVO字段用于 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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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("服务器内部错误");
}
}

View File

@@ -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());
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 可能为 nullkey 不存在),
* 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);
}
}
}

View File

@@ -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";
}
}