|
|
|
|
@ -13,6 +13,7 @@ import org.aspectj.lang.annotation.AfterReturning;
|
|
|
|
|
import org.aspectj.lang.annotation.AfterThrowing;
|
|
|
|
|
import org.aspectj.lang.annotation.Aspect;
|
|
|
|
|
import org.aspectj.lang.annotation.Before;
|
|
|
|
|
import org.aspectj.lang.reflect.MethodSignature;
|
|
|
|
|
import org.dromara.common.core.constant.SystemConstants;
|
|
|
|
|
import org.dromara.common.core.domain.model.LoginUser;
|
|
|
|
|
import org.dromara.common.core.utils.ServletUtils;
|
|
|
|
|
@ -21,14 +22,26 @@ 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.enums.BusinessType;
|
|
|
|
|
import org.dromara.common.log.event.OperLogEvent;
|
|
|
|
|
import org.dromara.common.satoken.utils.LoginHelper;
|
|
|
|
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
|
|
|
|
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
|
|
|
|
import org.springframework.core.DefaultParameterNameDiscoverer;
|
|
|
|
|
import org.springframework.core.ParameterNameDiscoverer;
|
|
|
|
|
import org.springframework.expression.EvaluationContext;
|
|
|
|
|
import org.springframework.expression.Expression;
|
|
|
|
|
import org.springframework.expression.ExpressionParser;
|
|
|
|
|
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
|
|
|
|
import org.springframework.http.HttpMethod;
|
|
|
|
|
import org.springframework.validation.BindingResult;
|
|
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
|
|
|
|
import java.lang.reflect.Array;
|
|
|
|
|
import java.lang.reflect.Method;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.util.regex.Matcher;
|
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 操作日志记录处理
|
|
|
|
|
@ -45,6 +58,16 @@ public class LogAspect {
|
|
|
|
|
*/
|
|
|
|
|
private static final ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SpEL 表达式解析器
|
|
|
|
|
*/
|
|
|
|
|
private static final ExpressionParser SPEL_PARSER = new SpelExpressionParser();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 参数名发现器
|
|
|
|
|
*/
|
|
|
|
|
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理请求前执行
|
|
|
|
|
*/
|
|
|
|
|
@ -131,6 +154,8 @@ public class LogAspect {
|
|
|
|
|
operLog.setTitle(log.title());
|
|
|
|
|
// 设置操作人类别
|
|
|
|
|
operLog.setOperatorType(log.operatorType().ordinal());
|
|
|
|
|
// 设置操作备注
|
|
|
|
|
setOperRemark(joinPoint, log, operLog);
|
|
|
|
|
// 是否需要保存request,参数和值
|
|
|
|
|
if (log.isSaveRequestData()) {
|
|
|
|
|
// 获取参数的信息,传入到数据库中。
|
|
|
|
|
@ -142,6 +167,123 @@ public class LogAspect {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 匹配 remark 中的 SpEL 占位符:#param.field} 或 #param
|
|
|
|
|
* 其中 } 是 remark 模板中的闭合符(如 "新增了xx【#bo.name}】"),需要一并替换掉
|
|
|
|
|
*/
|
|
|
|
|
private static final Pattern REMARK_SPEL_PATTERN = Pattern.compile("#([\\w.]+)}?");
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置操作备注
|
|
|
|
|
* remark 支持模板语法:中文文本中嵌入 #param.field 占位符,运行时通过 SpEL 求值替换
|
|
|
|
|
* 如果 remark 为空,自动拼接 "模块标题 - 操作类型"
|
|
|
|
|
*/
|
|
|
|
|
private void setOperRemark(JoinPoint joinPoint, Log log, OperLogEvent operLog) {
|
|
|
|
|
String remark = log.remark();
|
|
|
|
|
if (StringUtils.isNotBlank(remark)) {
|
|
|
|
|
try {
|
|
|
|
|
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
|
|
|
|
Method method = signature.getMethod();
|
|
|
|
|
EvaluationContext context = new MethodBasedEvaluationContext(
|
|
|
|
|
joinPoint.getTarget(), method, joinPoint.getArgs(), PARAMETER_NAME_DISCOVERER
|
|
|
|
|
);
|
|
|
|
|
// 正则提取 #param.field 模式,逐个求值后替换回原文
|
|
|
|
|
String result = resolveSpelTemplate(remark, context);
|
|
|
|
|
operLog.setOperRemark(StringUtils.substring(result, 0, 500));
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
// 这里显式取类级 logger,避免被方法参数 log 注解对象遮蔽,导致整个模块无法编译。
|
|
|
|
|
org.slf4j.LoggerFactory.getLogger(LogAspect.class).warn("SpEL表达式解析失败,使用原始备注: {}", remark);
|
|
|
|
|
operLog.setOperRemark(StringUtils.substring(remark, 0, 500));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 自动兜底:模块标题 - 操作类型
|
|
|
|
|
String title = log.title();
|
|
|
|
|
String desc = log.businessType().getDescription();
|
|
|
|
|
operLog.setOperRemark(title + " - " + desc);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析 remark 模板中的 SpEL 占位符并替换为实际值
|
|
|
|
|
* <p>
|
|
|
|
|
* 模板格式示例:{@code "新增了能源类型【#bo.energyName}】"}
|
|
|
|
|
* 其中 {@code #bo.energyName} 是 SpEL 占位符,} 是模板闭合符,两者在替换时一并移除。
|
|
|
|
|
* <p>
|
|
|
|
|
* 处理流程:
|
|
|
|
|
* 1. 正则扫描模板,逐个提取形如 {@code #paramName.fieldName} 的占位符
|
|
|
|
|
* 2. 将占位符去掉 # 前缀后作为 SpEL 表达式(如 {@code bo.energyName})放入解析器求值
|
|
|
|
|
* 3. SpEL 通过 {@link MethodBasedEvaluationContext} 按方法参数名定位到实际对象,再取其属性值
|
|
|
|
|
* 4. 将求值结果替换回原模板中的占位符位置,得到最终的可读备注文本
|
|
|
|
|
* <p>
|
|
|
|
|
* 降级策略:若某个占位符解析失败(如参数名不匹配、属性不存在),
|
|
|
|
|
* 不影响其他占位符的解析,仅保留该占位符的原始文本。
|
|
|
|
|
*
|
|
|
|
|
* @param template 含 SpEL 占位符的备注模板
|
|
|
|
|
* @param context SpEL 求值上下文,包含当前方法的所有参数及其值
|
|
|
|
|
* @return 替换后的可读备注文本
|
|
|
|
|
*/
|
|
|
|
|
private String resolveSpelTemplate(String template, EvaluationContext context) {
|
|
|
|
|
// 用预编译正则匹配模板中所有 SpEL 占位符
|
|
|
|
|
// 正则 #([\w.]+)}? 能匹配:
|
|
|
|
|
// - #bo.energyName → 捕获 "bo.energyName",不匹配 }
|
|
|
|
|
// - #objIds} → 捕获 "objIds",同时消费掉闭合的 }
|
|
|
|
|
// 这样 "删除了xx【#objIds}】" 替换后变为 "删除了xx【1,2,3】",多余的 } 不会残留
|
|
|
|
|
Matcher matcher = REMARK_SPEL_PATTERN.matcher(template);
|
|
|
|
|
|
|
|
|
|
// StringBuffer 用于逐步拼接替换结果(Matcher.appendReplacement 要求 StringBuffer)
|
|
|
|
|
StringBuffer sb = new StringBuffer();
|
|
|
|
|
|
|
|
|
|
// 逐个处理模板中匹配到的占位符
|
|
|
|
|
while (matcher.find()) {
|
|
|
|
|
// 捕获组1:去掉 # 前缀后的纯表达式,如 "bo.energyName" 或 "objIds"
|
|
|
|
|
// 这正是 SpEL 可以直接解析的表达式(引用方法参数名.属性路径)
|
|
|
|
|
String spelExpr = matcher.group(1);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 用 SpEL 解析器求值
|
|
|
|
|
// 例如 spelExpr="bo.energyName",context 中 bo 对应方法参数,
|
|
|
|
|
// SpEL 会调用 bo.getEnergyName() 取值
|
|
|
|
|
Object value = SPEL_PARSER.parseExpression(spelExpr).getValue(context);
|
|
|
|
|
String strValue;
|
|
|
|
|
|
|
|
|
|
if (value == null) {
|
|
|
|
|
// 属性值为 null,替换为空串,避免日志中出现 "null" 字样
|
|
|
|
|
strValue = "";
|
|
|
|
|
} else if (value.getClass().isArray()) {
|
|
|
|
|
// 数组类型处理:DELETE 操作的参数通常是 Long[] ids,需要转为可读的逗号分隔形式
|
|
|
|
|
// 例如 Long[]{1, 2, 3} → "1,2,3"
|
|
|
|
|
// 使用 java.lang.reflect.Array 统一处理基本类型数组和对象数组
|
|
|
|
|
int len = Array.getLength(value);
|
|
|
|
|
StringBuilder arrStr = new StringBuilder();
|
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
|
|
|
if (i > 0) arrStr.append(",");
|
|
|
|
|
arrStr.append(Array.get(value, i));
|
|
|
|
|
}
|
|
|
|
|
strValue = arrStr.toString();
|
|
|
|
|
} else {
|
|
|
|
|
// 普通对象:直接 toString()
|
|
|
|
|
// 例如 String "电"、BigDecimal "12.50" 等
|
|
|
|
|
strValue = value.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将求值结果写入拼接缓冲区
|
|
|
|
|
// quoteReplacement 防止替换值中含 $ 或 \ 导致 Matcher.appendReplacement 异常
|
|
|
|
|
matcher.appendReplacement(sb, Matcher.quoteReplacement(strValue));
|
|
|
|
|
} catch (Exception ex) {
|
|
|
|
|
// 单个表达式解析失败时的降级处理:
|
|
|
|
|
// 不抛异常中断整个流程,仅保留原始占位符文本
|
|
|
|
|
// 这样即使某个字段名拼写错误,其他占位符仍能正常解析
|
|
|
|
|
matcher.appendReplacement(sb, Matcher.quoteReplacement(matcher.group(0)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将模板中最后一个占位符之后的剩余文本追加到缓冲区
|
|
|
|
|
matcher.appendTail(sb);
|
|
|
|
|
|
|
|
|
|
// 返回完整的替换结果
|
|
|
|
|
return sb.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取请求的参数,放到log中
|
|
|
|
|
*
|
|
|
|
|
|