diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/annotation/BusinessLog.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/annotation/BusinessLog.java new file mode 100644 index 0000000..04512a4 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/annotation/BusinessLog.java @@ -0,0 +1,87 @@ +package com.viewsh.module.ops.infrastructure.log.annotation; + +import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope; +import com.viewsh.module.ops.infrastructure.log.enumeration.LogType; + +import java.lang.annotation.*; + +/** + * 业务日志注解 + *
+ * 用于标记需要记录业务日志的方法,通过 AOP 自动记录 + *
+ * 使用示例: + *
+ * {@code
+ * @BusinessLog(type = LogType.DISPATCH, scope = LogScope.ORDER,
+ * description = "自动派单", includeParams = true, includeResult = true)
+ * public DispatchResult dispatch(OrderDispatchContext context) {
+ * // ...
+ * }
+ * }
+ *
+ *
+ * @author lzh
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface BusinessLog {
+
+ /**
+ * 日志类型
+ */
+ LogType type();
+
+ /**
+ * 日志作用域
+ */
+ LogScope scope();
+
+ /**
+ * 操作描述
+ */
+ String description() default "";
+
+ /**
+ * 是否记录参数
+ */
+ boolean includeParams() default true;
+
+ /**
+ * 是否记录结果
+ */
+ boolean includeResult() default false;
+
+ /**
+ * 参数提取表达式(SpEL)
+ * + * 示例: + *
+ * 示例: + *
+ * 职责: + * 1. 拦截 @BusinessLog 注解的方法 + * 2. 提取方法参数和结果 + * 3. 发布业务日志 + *
+ * 设计说明: + * - 使用环绕通知 + * - 支持 SpEL 表达式提取参数和结果 + * - 支持异步日志发布 + * + * @author lzh + */ +@Slf4j +@Aspect +@Component +public class BusinessLogAspect { + + @Resource + private BusinessLogPublisher businessLogPublisher; + + 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); + } + + return result; + + } catch (Exception e) { + throwable = e; + + // 发布失败日志 + if (businessLog.async()) { + businessLogPublisher.publishError(context, e); + } else { + businessLogPublisher.publishError(context, 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(); + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + String className = method.getDeclaringClass().getSimpleName(); + String methodName = method.getName(); + + String description = businessLog.description(); + if (description.isEmpty()) { + description = className + "." + methodName; + } + + BusinessLogContext context = BusinessLogContext.builder() + .type(type) + .scope(scope) + .description(description) + .build(); + + // 添加标签 + for (String tag : businessLog.tags()) { + context.putExtra("tag:" + tag, true); + } + + // 添加方法信息 + context.putExtra("className", className); + context.putExtra("methodName", methodName); + + return context; + } + + /** + * 提取参数信息 + */ + 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; + } + + // 提取指定参数 + 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); + } catch (Exception e) { + log.warn("Failed to extract param: {}", param, e); + } + } + } else { + // 提取所有参数 + for (int i = 0; i < Math.min(parameterNames.length, args.length); i++) { + context.putExtra("param:" + parameterNames[i], args[i]); + } + } + } + + /** + * 提取结果信息 + */ + 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); + } + } catch (Exception ignored) { + // 没有 success 字段,默认为成功 + context.setSuccess(true); + } + } + } + + /** + * 创建 SpEL 评估上下文 + */ + private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint) { + StandardEvaluationContext context = new StandardEvaluationContext(); + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String[] parameterNames = signature.getParameterNames(); + Object[] args = joinPoint.getArgs(); + + if (parameterNames != null) { + for (int i = 0; i < Math.min(parameterNames.length, args.length); i++) { + context.setVariable(parameterNames[i], args[i]); + } + } + + return context; + } + + /** + * 从表达式中提取键名 + *
+ * 示例: + * - "#context.orderId" -> "orderId" + * - "#result.assigneeId" -> "assigneeId" + */ + private String extractKey(String expression) { + if (expression.contains(".")) { + int lastDotIndex = expression.lastIndexOf('.'); + return expression.substring(lastDotIndex + 1); + } + return expression; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/context/BusinessLogContext.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/context/BusinessLogContext.java new file mode 100644 index 0000000..cb4e1e8 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/context/BusinessLogContext.java @@ -0,0 +1,139 @@ +package com.viewsh.module.ops.infrastructure.log.context; + +import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope; +import com.viewsh.module.ops.infrastructure.log.enumeration.LogType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 业务日志上下文 + *
+ * 存储业务日志的上下文信息
+ *
+ * @author lzh
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BusinessLogContext {
+
+ /**
+ * 日志类型
+ */
+ private LogType type;
+
+ /**
+ * 日志作用域
+ */
+ private LogScope scope;
+
+ /**
+ * 操作描述
+ */
+ private String description;
+
+ /**
+ * 目标ID(如工单ID)
+ */
+ private Long targetId;
+
+ /**
+ * 目标类型(如order、queue)
+ */
+ private String targetType;
+
+ /**
+ * 操作人ID
+ */
+ private Long operatorId;
+
+ /**
+ * 操作人类���(如admin、cleaner、system)
+ */
+ private String operatorType;
+
+ /**
+ * 是否成功
+ */
+ private Boolean success;
+
+ /**
+ * 错误消息
+ */
+ private String errorMessage;
+
+ /**
+ * 日志时间
+ */
+ @Builder.Default
+ private LocalDateTime logTime = LocalDateTime.now();
+
+ /**
+ * 扩展信息
+ */
+ @Builder.Default
+ private Map
+ * 用于发布业务日志,支持异步发布
+ *
+ * @author lzh
+ */
+public interface BusinessLogPublisher {
+
+ /**
+ * 发布业务日志
+ *
+ * @param context 日志上下文
+ */
+ void publish(BusinessLogContext context);
+
+ /**
+ * 异步发布业务日志
+ *
+ * @param context 日志上下文
+ */
+ void publishAsync(BusinessLogContext context);
+
+ /**
+ * 发布成功日志
+ *
+ * @param context 日志上下文
+ */
+ void publishSuccess(BusinessLogContext context);
+
+ /**
+ * 发布失败日志
+ *
+ * @param context 日志上下文
+ * @param errorMessage 错误消息
+ */
+ void publishFailure(BusinessLogContext context, String errorMessage);
+
+ /**
+ * 发布异常日志
+ *
+ * @param context 日志上下文
+ * @param throwable 异常
+ */
+ void publishError(BusinessLogContext context, Throwable throwable);
+}
diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/publisher/DefaultBusinessLogPublisher.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/publisher/DefaultBusinessLogPublisher.java
new file mode 100644
index 0000000..5c96106
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/publisher/DefaultBusinessLogPublisher.java
@@ -0,0 +1,83 @@
+package com.viewsh.module.ops.infrastructure.log.publisher;
+
+import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+/**
+ * 默认业务日志发布器
+ *
+ * 职责:
+ * 1. 发布业务日志到日志系统
+ * 2. 支持异步发布
+ * 3. 支持结构化日志输出
+ *
+ * @author lzh
+ */
+@Slf4j
+@Component
+public class DefaultBusinessLogPublisher implements BusinessLogPublisher {
+
+ // TODO: 注入实际的日志存储服务(如 MongoDB、Elasticsearch 等)
+ // @Resource
+ // private BusinessLogRepository businessLogRepository;
+
+ @Override
+ public void publish(BusinessLogContext context) {
+ if (context == null) {
+ return;
+ }
+
+ // 输出结构化日志
+ log.info("[BusinessLog] type={}, scope={}, description={}, targetId={}, targetType={}, success={}",
+ context.getType(),
+ context.getScope(),
+ context.getDescription(),
+ context.getTargetId(),
+ context.getTargetType(),
+ context.getSuccess());
+
+ // TODO: 持久化到日志存储
+ // businessLogRepository.save(context);
+ }
+
+ @Override
+ @Async("ops-task-executor")
+ public void publishAsync(BusinessLogContext context) {
+ publish(context);
+ }
+
+ @Override
+ public void publishSuccess(BusinessLogContext context) {
+ if (context == null) {
+ return;
+ }
+ context.setSuccess(true);
+ publish(context);
+ }
+
+ @Override
+ public void publishFailure(BusinessLogContext context, String errorMessage) {
+ if (context == null) {
+ return;
+ }
+ context.setSuccess(false);
+ context.setErrorMessage(errorMessage);
+ publish(context);
+ }
+
+ @Override
+ public void publishError(BusinessLogContext context, Throwable throwable) {
+ if (context == null) {
+ return;
+ }
+ context.setSuccess(false);
+ context.setErrorMessage(throwable != null ? throwable.getMessage() : "Unknown error");
+ publish(context);
+
+ // 同时输出异常堆栈
+ log.error("[BusinessLog] Error occurred", throwable);
+ }
+}