This commit is contained in:
yuhaiming
2026-09-16 10:11:41 +08:00
commit 926426fe4f
901 changed files with 103701 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-log</artifactId>
<description>
ruoyi-common-log 日志记录
</description>
<dependencies>
<!-- satoken -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
<!-- 序列化模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<!-- api模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,48 @@
package org.dromara.common.log.annotation;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.log.enums.OperatorType;
import java.lang.annotation.*;
/**
* 自定义操作日志记录注解
*
* @author ruoyi
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块
*/
String title() default "";
/**
* 功能
*/
BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
boolean isSaveResponseData() default true;
/**
* 排除指定的请求参数
*/
String[] excludeParamNames() default {};
}

View File

@@ -0,0 +1,275 @@
package org.dromara.common.log.aspect;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessStatus;
import org.dromara.common.log.event.OperLogEvent;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.model.LoginUser;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Array;
import java.util.*;
/**
* 操作日志记录处理
*
* @author Lion Li
*/
@Slf4j
@Aspect
@AutoConfiguration
public class LogAspect {
/**
* URL 最大记录长度
*/
private static final int MAX_URL_LENGTH = 255;
/**
* 客户端标识最大记录长度
*/
private static final int MAX_CLIENT_KEY_LENGTH = 32;
/**
* 日志内容最大记录长度
*/
private static final int MAX_CONTENT_LENGTH = 3800;
/**
* 执行目标方法并记录操作日志。
*
* @param joinPoint 切点
* @param controllerLog 日志注解
* @return 目标方法返回值
* @throws Throwable 目标方法异常
*/
@Around(value = "@annotation(controllerLog)")
public Object doAround(ProceedingJoinPoint joinPoint, Log controllerLog) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
Object jsonResult = joinPoint.proceed();
handleLog(joinPoint, controllerLog, null, jsonResult, stopWatch);
return jsonResult;
} catch (Exception e) {
handleLog(joinPoint, controllerLog, e, null, stopWatch);
throw e;
}
}
/**
* 组装并发布操作日志事件。
*
* @param joinPoint 切点
* @param controllerLog 日志注解
* @param e 异常信息
* @param jsonResult 返回结果
* @param stopWatch 耗时统计
*/
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult, StopWatch stopWatch) {
try {
// *========数据库日志=========*//
OperLogEvent operLog = new OperLogEvent();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
HttpServletRequest request = ServletUtils.getRequest();
// 请求的地址
String ip = ServletUtils.getClientIP();
operLog.setOperIp(ip);
operLog.setOperUrl(limit(request.getRequestURI(), MAX_URL_LENGTH));
operLog.setClientKey(limit(request.getHeader(LoginHelper.CLIENT_KEY), MAX_CLIENT_KEY_LENGTH));
LoginUser loginUser = LoginHelper.getLoginUser();
if (ObjectUtil.isNotNull(loginUser)) {
operLog.setOperName(loginUser.getUsername());
operLog.setUserId(loginUser.getUserId());
operLog.setDeptId(loginUser.getDeptId());
operLog.setDeptName(loginUser.getDeptName());
operLog.setDeviceType(loginUser.getDeviceType());
operLog.setBrowser(loginUser.getBrowser());
operLog.setOs(loginUser.getOs());
if (StringUtils.isBlank(operLog.getClientKey())) {
operLog.setClientKey(loginUser.getClientKey());
}
}
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(limit(e.getMessage(), MAX_CONTENT_LENGTH));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(request.getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 设置消耗时间
stopWatch.stop();
operLog.setCostTime(stopWatch.getDuration().toMillis());
// 发布事件保存数据库
SpringUtils.context().publishEvent(operLog);
} catch (Exception exp) {
// 记录本地异常日志
log.error("记录操作日志异常", exp);
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param joinPoint 切点
* @param log 日志
* @param operLog 操作日志
* @param jsonResult 返回结果
* @throws Exception 异常
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperLogEvent operLog, Object jsonResult) throws Exception {
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,参数和值
if (log.isSaveRequestData()) {
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog, log.excludeParamNames());
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
operLog.setJsonResult(limit(JsonUtils.toJsonString(jsonResult), MAX_CONTENT_LENGTH));
}
}
/**
* 获取请求的参数,放到log中
*
* @param joinPoint 切点
* @param operLog 操作日志
* @param excludeParamNames 排除参数名
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, OperLogEvent operLog, String[] excludeParamNames) throws Exception {
Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
String requestMethod = operLog.getRequestMethod();
if (MapUtil.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) {
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(limit(params, MAX_CONTENT_LENGTH));
} else {
MapUtil.removeAny(paramsMap, SystemConstants.EXCLUDE_PROPERTIES);
MapUtil.removeAny(paramsMap, excludeParamNames);
operLog.setOperParam(limit(JsonUtils.toJsonString(paramsMap), MAX_CONTENT_LENGTH));
}
}
/**
* 将方法参数序列化为日志字符串。
*
* @param paramsArray 参数数组
* @param excludeParamNames 排除字段名
* @return 参数字符串
*/
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
String[] exclude = ArrayUtil.addAll(excludeParamNames, SystemConstants.EXCLUDE_PROPERTIES);
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
params.add(serializeArg(o, exclude));
}
}
return params.toString();
}
/**
* 序列化单个方法参数,并移除排除字段。
*
* @param arg 参数对象
* @param exclude 排除字段名
* @return 参数日志字符串
*/
private String serializeArg(Object arg, String[] exclude) {
return JsonUtils.toJsonStringExcludeFields(arg, exclude);
}
/**
* 限制日志字段长度。
*
* @param value 原始字符串
* @param maxLength 最大长度
* @return 截断后的字符串
*/
private String limit(String value, int maxLength) {
return StringUtils.substring(value, 0, maxLength);
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
if (MultipartFile.class.isAssignableFrom(clazz.getComponentType())) {
return true;
}
int length = Array.getLength(o);
for (int i = 0; i < length; i++) {
if (isFilterValue(Array.get(o, i))) {
return true;
}
}
return false;
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection<?> collection = (Collection<?>) o;
for (Object value : collection) {
if (isFilterValue(value)) {
return true;
}
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map<?, ?> map = (Map<?, ?>) o;
for (Object value : map.values()) {
if (isFilterValue(value)) {
return true;
}
}
}
return isFilterValue(o);
}
/**
* 判断是否为日志参数过滤类型。
*
* @param value 参数值
* @return true 需要过滤 false 不需要过滤
*/
private boolean isFilterValue(Object value) {
return value instanceof MultipartFile || value instanceof HttpServletRequest || value instanceof HttpServletResponse
|| value instanceof BindingResult;
}
}

View File

@@ -0,0 +1,18 @@
package org.dromara.common.log.enums;
/**
* 操作状态
*
* @author ruoyi
*/
public enum BusinessStatus {
/**
* 成功
*/
SUCCESS,
/**
* 失败
*/
FAIL,
}

View File

@@ -0,0 +1,58 @@
package org.dromara.common.log.enums;
/**
* 业务操作类型
*
* @author ruoyi
*/
public enum BusinessType {
/**
* 其它
*/
OTHER,
/**
* 新增
*/
INSERT,
/**
* 修改
*/
UPDATE,
/**
* 删除
*/
DELETE,
/**
* 授权
*/
GRANT,
/**
* 导出
*/
EXPORT,
/**
* 导入
*/
IMPORT,
/**
* 强退
*/
FORCE,
/**
* 生成代码
*/
GENCODE,
/**
* 清空数据
*/
CLEAN,
}

View File

@@ -0,0 +1,23 @@
package org.dromara.common.log.enums;
/**
* 操作人类别
*
* @author ruoyi
*/
public enum OperatorType {
/**
* 其它
*/
OTHER,
/**
* 后台用户
*/
MANAGE,
/**
* 手机端用户
*/
MOBILE
}

View File

@@ -0,0 +1,55 @@
package org.dromara.common.log.event;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 登录事件
*
* @author Lion Li
*/
@Data
public class LoginInfoEvent implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户账号
*/
private String username;
/**
* 登录状态 0成功 1失败
*/
private String status;
/**
* 提示消息
*/
private String message;
/**
* 客户端IP
*/
private String ip;
/**
* 用户代理
*/
private String userAgent;
/**
* 客户端标识
*/
private String clientId;
/**
* 其他参数
*/
private Object[] args;
}

View File

@@ -0,0 +1,140 @@
package org.dromara.common.log.event;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 操作日志事件
*
* @author Lion Li
*/
@Data
public class OperLogEvent implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 日志主键
*/
private Long operId;
/**
* 操作模块
*/
private String title;
/**
* 业务类型(0其它 1新增 2修改 3删除)
*/
private Integer businessType;
/**
* 业务类型数组
*/
private Integer[] businessTypes;
/**
* 请求方法
*/
private String method;
/**
* 请求方式
*/
private String requestMethod;
/**
* 操作类别(0其它 1后台用户 2手机端用户)
*/
private Integer operatorType;
/**
* 操作人员
*/
private String operName;
/**
* 操作用户ID
*/
private Long userId;
/**
* 操作部门ID
*/
private Long deptId;
/**
* 部门名称
*/
private String deptName;
/**
* 客户端
*/
private String clientKey;
/**
* 设备类型
*/
private String deviceType;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 请求url
*/
private String operUrl;
/**
* 操作地址
*/
private String operIp;
/**
* 操作地点
*/
private String operLocation;
/**
* 请求参数
*/
private String operParam;
/**
* 返回参数
*/
private String jsonResult;
/**
* 操作状态(0正常 1异常)
*/
private Integer status;
/**
* 错误消息
*/
private String errorMsg;
/**
* 操作时间
*/
private LocalDateTime operTime;
/**
* 消耗时间
*/
private Long costTime;
}