refactor(ops): 重构业务日志注解和切面

- 更新 BusinessLog 注解以支持新的事件日志模型
- 更新 BusinessLogAspect 切面使用 EventLogRecorder

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-01-21 18:43:39 +08:00
parent a82b59ac46
commit a49994cc57
2 changed files with 295 additions and 118 deletions

View File

@@ -1,5 +1,7 @@
package com.viewsh.module.ops.infrastructure.log.annotation;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
@@ -13,11 +15,20 @@ import java.lang.annotation.*;
* 使用示例:
* <pre>
* {@code
* // 基础用法(使用 LogType/LogScope
* @BusinessLog(type = LogType.DISPATCH, scope = LogScope.ORDER,
* description = "自动派单", includeParams = true, includeResult = true)
* description = "自动派单", includeParams = true)
* public DispatchResult dispatch(OrderDispatchContext context) {
* // ...
* }
*
* // 新用法(直接指定 EventDomain 和 Module
* @BusinessLog(module = "clean", domain = EventDomain.DEVICE,
* eventType = "TTS_SENT", description = "语音播报",
* deviceId = "#deviceId", personId = "#context.cleanerId")
* public void broadcast(String text, Long deviceId) {
* // ...
* }
* }
* </pre>
*
@@ -28,15 +39,19 @@ import java.lang.annotation.*;
@Documented
public @interface BusinessLog {
/**
* 日志类型
*/
LogType type();
// ==================== 旧版字段(保持兼容) ====================
/**
* 日志作用域
* 日志类型(旧版,保持兼容)
* <p>
* 当指定了 domain 和 eventType 时,此字段可忽略
*/
LogScope scope();
LogType type() default LogType.SYSTEM;
/**
* 日志作用域(旧版,保持兼容)
*/
LogScope scope() default LogScope.SYSTEM;
/**
* 操作描述
@@ -84,4 +99,70 @@ public @interface BusinessLog {
* 自定义标签
*/
String[] tags() default {};
// ==================== 新版字段(支持 EventLogRecorder ====================
/**
* 模块标识(新版)
* <p>
* 如clean, repair, patrol
* <p>
* 当指定此字段时,将使用 EventLogRecorder 记录日志
*/
String module() default "";
/**
* 事件域(新版)
* <p>
* 当指定此字段时,将使用 EventLogRecorder 记录日志
*/
EventDomain domain() default EventDomain.SYSTEM;
/**
* 事件类型(新版)
* <p>
* 当指定此字段时,将使用 EventLogRecorder 记录日志
*/
String eventType() default "";
/**
* 日志级别(新版)
*/
EventLevel level() default EventLevel.INFO;
/**
* 设备ID提取表达式SpEL
* <p>
* 示例:"#deviceId", "#context.deviceId", "#request.deviceId"
*/
String deviceId() default "";
/**
* 人员ID提取表达式SpEL
* <p>
* 示例:"#cleanerId", "#context.userId", "#request.assigneeId"
*/
String personId() default "";
/**
* 业务实体ID提取表达式SpEL
* <p>
* 示例:"#orderId", "#context.orderId"
*/
String targetId() default "";
/**
* 业务实体类型
* <p>
* 如order, area, task
*/
String targetType() default "";
/**
* 简要摘要模板(用于列表展示)
* <p>
* 示例:"播报: {text}", "到岗: {areaName}"
*/
String summary() default "";
}

View File

@@ -1,10 +1,12 @@
package com.viewsh.module.ops.infrastructure.log.aspect;
import com.viewsh.module.ops.infrastructure.log.annotation.BusinessLog;
import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
import com.viewsh.module.ops.infrastructure.log.publisher.BusinessLogPublisher;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
@@ -19,8 +21,6 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* 业务日志切面
@@ -28,12 +28,12 @@ import java.util.List;
* 职责:
* 1. 拦截 @BusinessLog 注解的方法
* 2. 提取方法参数和结果
* 3. 发布业务日志
* 3. 统一通过 EventLogRecorder 记录日志
* <p>
* 设计说明:
* - 使用环绕通知
* - 支持 SpEL 表达式提取参数和结果
* - 支持异步日志发布
* - 所有日志异步记录,不阻塞主业务流程
*
* @author lzh
*/
@@ -43,69 +43,67 @@ import java.util.List;
public class BusinessLogAspect {
@Resource
private BusinessLogPublisher businessLogPublisher;
private EventLogRecorder eventLogRecorder;
private final ExpressionParser parser = new SpelExpressionParser();
@Around("@annotation(businessLog)")
public Object around(ProceedingJoinPoint joinPoint, BusinessLog businessLog) throws Throwable {
long startTime = System.currentTimeMillis();
// 构建日志上下文
BusinessLogContext context = buildContext(joinPoint, businessLog);
Object result = null;
Throwable throwable = null;
try {
// 执行目标方法
result = joinPoint.proceed();
// 提取结果信息
if (businessLog.includeResult() || !businessLog.result().isEmpty()) {
extractResultInfo(context, result, businessLog);
}
// 发布成功日志
if (businessLog.async()) {
businessLogPublisher.publishSuccess(context);
} else {
businessLogPublisher.publishSuccess(context);
}
// 发布日志
publishLog(joinPoint, businessLog, result, null);
return result;
} catch (Exception e) {
throwable = e;
// 发布失败日志
if (businessLog.async()) {
businessLogPublisher.publishError(context, e);
} else {
businessLogPublisher.publishError(context, e);
}
publishLog(joinPoint, businessLog, null, e);
throw e;
} finally {
// 记录执行时长
long duration = System.currentTimeMillis() - startTime;
context.putExtra("duration", duration);
// 记录参数信息
if (businessLog.includeParams()) {
extractParamsInfo(context, joinPoint, businessLog);
}
}
}
/**
* 构建日志上下文
* 发布日志
*/
private BusinessLogContext buildContext(ProceedingJoinPoint joinPoint, BusinessLog businessLog) {
LogType type = businessLog.type();
LogScope scope = businessLog.scope();
private void publishLog(ProceedingJoinPoint joinPoint, BusinessLog businessLog, Object result, Throwable throwable) {
try {
EventLogRecord record = buildEventLogRecord(joinPoint, businessLog);
// 设置日志级别
if (throwable != null) {
record.setLevel(EventLevel.ERROR);
} else {
record.setLevel(businessLog.level());
}
// 提取关联ID和扩展信息
EvaluationContext evalContext = createEvaluationContext(joinPoint, result);
extractIds(record, businessLog, evalContext);
extractExtraInfo(record, businessLog, evalContext, throwable);
// 处理摘要模板
if (!businessLog.summary().isEmpty()) {
String summary = processSummaryTemplate(businessLog.summary(), evalContext);
record.setSummary(summary);
}
// 异步记录record 方法已默认异步)
eventLogRecorder.record(record);
} catch (Exception e) {
log.warn("[BusinessLogAspect] 记录日志失败", e);
}
}
/**
* 构建 EventLogRecord
*/
private EventLogRecord buildEventLogRecord(ProceedingJoinPoint joinPoint, BusinessLog businessLog) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String className = method.getDeclaringClass().getSimpleName();
@@ -116,98 +114,170 @@ public class BusinessLogAspect {
description = className + "." + methodName;
}
BusinessLogContext context = BusinessLogContext.builder()
.type(type)
.scope(scope)
.description(description)
// 判断使用新版还是旧版注解
String module = !businessLog.module().isEmpty() ? businessLog.module() : "ops";
EventDomain domain = businessLog.domain() != EventDomain.SYSTEM ? businessLog.domain() : mapLogTypeToEventDomain(businessLog.type());
String eventType = !businessLog.eventType().isEmpty() ? businessLog.eventType() :
(businessLog.type() != null ? businessLog.type().getCode() : "UNKNOWN");
EventLogRecord record = EventLogRecord.builder()
.module(module)
.domain(domain)
.eventType(eventType)
.message(description)
.build();
// 添加标签
for (String tag : businessLog.tags()) {
context.putExtra("tag:" + tag, true);
// 添加扩展信息
if (businessLog.tags().length > 0) {
for (String tag : businessLog.tags()) {
record.putPayload("tag:" + tag, true);
}
}
record.putPayload("className", className);
record.putPayload("methodName", methodName);
record.putPayload("scope", businessLog.scope().getCode());
// 添加方法信息
context.putExtra("className", className);
context.putExtra("methodName", methodName);
return context;
return record;
}
/**
* 提取参数信息
* 提取设备、人员、业务实体ID
*/
private void extractParamsInfo(BusinessLogContext context, ProceedingJoinPoint joinPoint,
BusinessLog businessLog) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();
if (parameterNames == null || parameterNames.length == 0) {
return;
private void extractIds(EventLogRecord record, BusinessLog businessLog, EvaluationContext evalContext) {
// 提取设备ID
if (!businessLog.deviceId().isEmpty()) {
extractId(record, businessLog.deviceId(), evalContext, "deviceId", true);
}
// 提取指定参数
// 提取人员ID
if (!businessLog.personId().isEmpty()) {
extractId(record, businessLog.personId(), evalContext, "personId", true);
}
// 提取业务实体ID
if (!businessLog.targetId().isEmpty()) {
extractId(record, businessLog.targetId(), evalContext, "targetId", true);
}
// 设置业务实体类型
if (!businessLog.targetType().isEmpty()) {
record.setTargetType(businessLog.targetType());
} else if (businessLog.scope() == LogScope.ORDER) {
record.setTargetType("order");
}
}
/**
* 提取扩展信息
*/
private void extractExtraInfo(EventLogRecord record, BusinessLog businessLog,
EvaluationContext evalContext, Throwable throwable) {
// 提取结果信息
if (businessLog.includeResult() && !businessLog.result().isEmpty()) {
try {
Expression expression = parser.parseExpression(businessLog.result());
Object value = expression.getValue(evalContext);
if (value != null) {
String key = extractKey(businessLog.result());
record.putPayload(key, value);
}
} catch (Exception e) {
log.debug("Failed to extract result: {}", businessLog.result());
}
}
// 提取自定义参数
if (businessLog.params().length > 0) {
EvaluationContext evalContext = createEvaluationContext(joinPoint);
for (String param : businessLog.params()) {
try {
Expression expression = parser.parseExpression(param);
Object value = expression.getValue(evalContext);
String key = extractKey(param);
context.putExtra(key, value);
if (value != null) {
String key = extractKey(param);
record.putPayload(key, value);
}
} catch (Exception e) {
log.warn("Failed to extract param: {}", param, e);
log.debug("Failed to extract param: {}", param);
}
}
} else {
// 提取所有参数
for (int i = 0; i < Math.min(parameterNames.length, args.length); i++) {
context.putExtra("param:" + parameterNames[i], args[i]);
}
}
// 添加错误信息
if (throwable != null) {
record.putPayload("errorMessage", throwable.getMessage());
record.putPayload("errorClass", throwable.getClass().getSimpleName());
}
}
/**
* 提取结果信息
* 提取ID值
*/
private void extractResultInfo(BusinessLogContext context, Object result, BusinessLog businessLog) {
if (!businessLog.result().isEmpty()) {
// 使用 SpEL 表达式提取
EvaluationContext evalContext = new StandardEvaluationContext(result);
try {
Expression expression = parser.parseExpression(businessLog.result());
Object value = expression.getValue(evalContext);
String key = extractKey(businessLog.result());
context.putExtra(key, value);
} catch (Exception e) {
log.warn("Failed to extract result: {}", businessLog.result(), e);
}
} else {
// 存储完整结果
context.putExtra("result", result);
}
// 尝试提取 success 字段
if (result != null) {
try {
EvaluationContext evalContext = new StandardEvaluationContext(result);
Expression expression = parser.parseExpression("success");
Object success = expression.getValue(evalContext);
if (success instanceof Boolean) {
context.setSuccess((Boolean) success);
private void extractId(EventLogRecord record, String expression, EvaluationContext evalContext,
String fieldName, boolean setField) {
try {
Expression expr = parser.parseExpression(expression);
Object value = expr.getValue(evalContext);
if (value instanceof Long) {
if (setField && "deviceId".equals(fieldName)) {
record.setDeviceId((Long) value);
} else if (setField && "personId".equals(fieldName)) {
record.setPersonId((Long) value);
} else if (setField && "targetId".equals(fieldName)) {
record.setTargetId((Long) value);
}
} catch (Exception ignored) {
// 没有 success 字段,默认为成功
context.setSuccess(true);
} else if (value instanceof Integer) {
long longValue = ((Integer) value).longValue();
if (setField && "deviceId".equals(fieldName)) {
record.setDeviceId(longValue);
} else if (setField && "personId".equals(fieldName)) {
record.setPersonId(longValue);
} else if (setField && "targetId".equals(fieldName)) {
record.setTargetId(longValue);
}
} else if (value != null) {
record.putPayload(fieldName, value);
}
} catch (Exception e) {
log.debug("Failed to extract {}: {}", fieldName, expression);
}
}
/**
* 处理摘要模板
* <p>
* 支持 {paramName} 占位符替换
*/
private String processSummaryTemplate(String template, EvaluationContext evalContext) {
try {
String result = template;
int start = template.indexOf('{');
while (start >= 0) {
int end = template.indexOf('}', start);
if (end < 0) break;
String paramName = template.substring(start + 1, end);
try {
Expression expression = parser.parseExpression("#" + paramName);
Object value = expression.getValue(evalContext);
if (value != null) {
result = result.replace("{" + paramName + "}", String.valueOf(value));
}
} catch (Exception e) {
// 保持原样
}
start = template.indexOf('{', end + 1);
}
return result;
} catch (Exception e) {
return template;
}
}
/**
* 创建 SpEL 评估上下文
*/
private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint) {
private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint, Object result) {
StandardEvaluationContext context = new StandardEvaluationContext();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
@@ -220,15 +290,41 @@ public class BusinessLogAspect {
}
}
// 添加 result 变量(用于结果提取)
if (result != null) {
context.setVariable("result", result);
}
return context;
}
/**
* 将 LogType 映射到 EventDomain
*/
private EventDomain mapLogTypeToEventDomain(LogType logType) {
if (logType == null) {
return EventDomain.SYSTEM;
}
switch (logType) {
case DISPATCH:
return EventDomain.DISPATCH;
case DEVICE:
return EventDomain.DEVICE;
case NOTIFICATION:
return EventDomain.DEVICE;
case CLEANER:
case SYSTEM:
case QUEUE:
case LIFECYCLE:
case TRANSITION:
default:
return EventDomain.SYSTEM;
}
}
/**
* 从表达式中提取键名
* <p>
* 示例:
* - "#context.orderId" -> "orderId"
* - "#result.assigneeId" -> "assigneeId"
*/
private String extractKey(String expression) {
if (expression.contains(".")) {