From ea3c7829e910dcb5a825bc5c1e173e3b28ba6504 Mon Sep 17 00:00:00 2001 From: lzh Date: Fri, 9 Jan 2026 17:38:29 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E3=80=90ops=E3=80=91=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E4=B8=9A=E5=8A=A1=E6=97=A5=E5=BF=97=E6=B3=A8=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/annotation/BusinessLog.java | 87 +++++++ .../log/aspect/BusinessLogAspect.java | 240 ++++++++++++++++++ .../log/context/BusinessLogContext.java | 139 ++++++++++ .../log/enumeration/LogScope.java | 45 ++++ .../log/enumeration/LogType.java | 65 +++++ .../log/publisher/BusinessLogPublisher.java | 50 ++++ .../DefaultBusinessLogPublisher.java | 83 ++++++ 7 files changed, 709 insertions(+) create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/annotation/BusinessLog.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/aspect/BusinessLogAspect.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/context/BusinessLogContext.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/LogScope.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/LogType.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/publisher/BusinessLogPublisher.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/publisher/DefaultBusinessLogPublisher.java 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) + *

+ * 示例: + *

+ */ + String[] params() default {}; + + /** + * 结果提取表达式(SpEL) + *

+ * 示例: + *

+ */ + String result() default ""; + + /** + * 是否异步记录 + */ + boolean async() default true; + + /** + * 自定义标签 + */ + String[] tags() default {}; +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/aspect/BusinessLogAspect.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/aspect/BusinessLogAspect.java new file mode 100644 index 0000000..2d2381a --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/aspect/BusinessLogAspect.java @@ -0,0 +1,240 @@ +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.LogScope; +import com.viewsh.module.ops.infrastructure.log.enumeration.LogType; +import com.viewsh.module.ops.infrastructure.log.publisher.BusinessLogPublisher; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +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.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * 业务日志切面 + *

+ * 职责: + * 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 extra = new HashMap<>(); + + /** + * 添加扩展信息 + */ + public void putExtra(String key, Object value) { + if (this.extra == null) { + this.extra = new HashMap<>(); + } + this.extra.put(key, value); + } + + /** + * 获取扩展信息 + */ + public Object getExtra(String key) { + return extra != null ? extra.get(key) : null; + } + + /** + * 创建工单日志上下文 + */ + public static BusinessLogContext forOrder(LogType type, String description, Long orderId) { + return BusinessLogContext.builder() + .type(type) + .scope(LogScope.ORDER) + .description(description) + .targetId(orderId) + .targetType("order") + .build(); + } + + /** + * 创建队列日志上下文 + */ + public static BusinessLogContext forQueue(LogType type, String description, Long queueId) { + return BusinessLogContext.builder() + .type(type) + .scope(LogScope.QUEUE) + .description(description) + .targetId(queueId) + .targetType("queue") + .build(); + } + + /** + * 创建执行人日志上下文 + */ + public static BusinessLogContext forAssignee(LogType type, String description, Long assigneeId) { + return BusinessLogContext.builder() + .type(type) + .scope(LogScope.ASSIGNEE) + .description(description) + .targetId(assigneeId) + .targetType("assignee") + .build(); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/LogScope.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/LogScope.java new file mode 100644 index 0000000..0182872 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/LogScope.java @@ -0,0 +1,45 @@ +package com.viewsh.module.ops.infrastructure.log.enumeration; + +/** + * 日志作用域枚举 + * + * @author lzh + */ +public enum LogScope { + + /** + * 工单作用域 + */ + ORDER("order", "工单"), + + /** + * 队列作用域 + */ + QUEUE("queue", "队列"), + + /** + * 执行人作用域 + */ + ASSIGNEE("assignee", "执行人"), + + /** + * 系统作用域 + */ + SYSTEM("system", "系统"); + + private final String code; + private final String description; + + LogScope(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/LogType.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/LogType.java new file mode 100644 index 0000000..a7972ec --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/enumeration/LogType.java @@ -0,0 +1,65 @@ +package com.viewsh.module.ops.infrastructure.log.enumeration; + +/** + * 日志类型枚举 + * + * @author lzh + */ +public enum LogType { + + /** + * 派单日志 + */ + DISPATCH("dispatch", "派单"), + + /** + * 状态转换日志 + */ + TRANSITION("transition", "状态转换"), + + /** + * 生命周期日志 + */ + LIFECYCLE("lifecycle", "生命周期"), + + /** + * 队列日志 + */ + QUEUE("queue", "队列"), + + /** + * 保洁员��志 + */ + CLEANER("cleaner", "保洁员"), + + /** + * 设备日志 + */ + DEVICE("device", "设备"), + + /** + * 通知日志 + */ + NOTIFICATION("notification", "通知"), + + /** + * 系统日志 + */ + SYSTEM("system", "系统"); + + private final String code; + private final String description; + + LogType(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/publisher/BusinessLogPublisher.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/publisher/BusinessLogPublisher.java new file mode 100644 index 0000000..3cd9301 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/infrastructure/log/publisher/BusinessLogPublisher.java @@ -0,0 +1,50 @@ +package com.viewsh.module.ops.infrastructure.log.publisher; + +import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext; + +/** + * 业务日志发布器接口 + *

+ * 用于发布业务日志,支持异步发布 + * + * @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); + } +}