From 5b010d32fb4bc05d8dc128da283f255cf5021b3d Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Wed, 1 Apr 2026 16:54:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(sys=5Foper=5Flog):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=97=A5=E5=BF=97=E5=A4=87=E6=B3=A8=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dromara/common/log/annotation/Log.java | 7 + .../dromara/common/log/aspect/LogAspect.java | 142 ++++++++++++++++++ .../common/log/event/OperLogEvent.java | 5 + ...emPlcBufferBatteryLifecycleController.java | 10 +- .../src/main/resources/vm/java/mapper.java.vm | 1 + 5 files changed, 160 insertions(+), 5 deletions(-) diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/annotation/Log.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/annotation/Log.java index 2dced97..3c51cd8 100644 --- a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/annotation/Log.java +++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/annotation/Log.java @@ -45,4 +45,11 @@ public @interface Log { */ String[] excludeParamNames() default {}; + /** + * 操作备注(支持SpEL表达式引用方法参数) + * 为空时自动拼接: "模块标题 - 操作类型" + * 示例: "新增能源类型【#bo.energyName}】" + */ + String remark() default ""; + } diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java index 2c22811..7b3ebf6 100644 --- a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java +++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java @@ -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 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 占位符并替换为实际值 + *

+ * 模板格式示例:{@code "新增了能源类型【#bo.energyName}】"} + * 其中 {@code #bo.energyName} 是 SpEL 占位符,} 是模板闭合符,两者在替换时一并移除。 + *

+ * 处理流程: + * 1. 正则扫描模板,逐个提取形如 {@code #paramName.fieldName} 的占位符 + * 2. 将占位符去掉 # 前缀后作为 SpEL 表达式(如 {@code bo.energyName})放入解析器求值 + * 3. SpEL 通过 {@link MethodBasedEvaluationContext} 按方法参数名定位到实际对象,再取其属性值 + * 4. 将求值结果替换回原模板中的占位符位置,得到最终的可读备注文本 + *

+ * 降级策略:若某个占位符解析失败(如参数名不匹配、属性不存在), + * 不影响其他占位符的解析,仅保留该占位符的原始文本。 + * + * @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中 * diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/OperLogEvent.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/OperLogEvent.java index 0386192..2ed88f2 100644 --- a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/OperLogEvent.java +++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/event/OperLogEvent.java @@ -112,4 +112,9 @@ public class OperLogEvent implements Serializable { * 消耗时间 */ private Long costTime; + + /** + * 操作备注(中文描述,方便普通用户查看) + */ + private String operRemark; } diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/info/controller/LuggageSystemPlcBufferBatteryLifecycleController.java b/ruoyi-ems/src/main/java/org/dromara/ems/info/controller/LuggageSystemPlcBufferBatteryLifecycleController.java index fa40984..e0bdb62 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/info/controller/LuggageSystemPlcBufferBatteryLifecycleController.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/info/controller/LuggageSystemPlcBufferBatteryLifecycleController.java @@ -48,7 +48,7 @@ public class LuggageSystemPlcBufferBatteryLifecycleController extends EmsBaseCon * 导出行李系统PLC缓冲电池生命周期列表 */ @SaCheckPermission("ems/info:plcBufferBatteryLifecycle:export") - @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.EXPORT) + @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.EXPORT, remark = "导出了行李系统PLC缓冲电池生命周期数据") @PostMapping("/export") public void export(HttpServletResponse response, LuggageSystemPlcBufferBatteryLifecycle luggageSystemPlcBufferBatteryLifecycle) { @@ -70,7 +70,7 @@ public class LuggageSystemPlcBufferBatteryLifecycleController extends EmsBaseCon * 新增行李系统PLC缓冲电池生命周期 */ @SaCheckPermission("ems/info:plcBufferBatteryLifecycle:add") - @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.INSERT) + @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.INSERT, remark = "新增了行李系统PLC缓冲电池生命周期【#luggageSystemPlcBufferBatteryLifecycle.installationCabinetName}】") @PostMapping public R add(@RequestBody LuggageSystemPlcBufferBatteryLifecycle luggageSystemPlcBufferBatteryLifecycle) { @@ -81,7 +81,7 @@ public class LuggageSystemPlcBufferBatteryLifecycleController extends EmsBaseCon * 修改行李系统PLC缓冲电池生命周期 */ @SaCheckPermission("ems/info:plcBufferBatteryLifecycle:edit") - @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.UPDATE) + @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.UPDATE, remark = "修改了行李系统PLC缓冲电池生命周期【#luggageSystemPlcBufferBatteryLifecycle.installationCabinetName}】") @PutMapping public R edit(@RequestBody LuggageSystemPlcBufferBatteryLifecycle luggageSystemPlcBufferBatteryLifecycle) { @@ -92,7 +92,7 @@ public class LuggageSystemPlcBufferBatteryLifecycleController extends EmsBaseCon * 删除行李系统PLC缓冲电池生命周期 */ @SaCheckPermission("ems/info:plcBufferBatteryLifecycle:remove") - @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.DELETE) + @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.DELETE, remark = "删除了行李系统PLC缓冲电池生命周期,ID:【#objids}】") @DeleteMapping("/{objids}") public R remove(@PathVariable Long[] objids) { @@ -103,7 +103,7 @@ public class LuggageSystemPlcBufferBatteryLifecycleController extends EmsBaseCon * 导入行李系统PLC缓冲电池生命周期数据 */ @SaCheckPermission("ems/info:plcBufferBatteryLifecycle:add") - @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.IMPORT) + @Log(title = "行李系统PLC缓冲电池生命周期", businessType = BusinessType.IMPORT, remark = "导入了行李系统PLC缓冲电池生命周期数据") @PostMapping("/importData") public R importData(MultipartFile file, @RequestParam(value = "updateSupport", defaultValue = "false") boolean updateSupport) throws Exception { diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm index 66d7829..58180d7 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm @@ -2,6 +2,7 @@ package ${packageName}.mapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Param; import ${packageName}.domain.${ClassName}; import ${packageName}.domain.vo.${ClassName}Vo;