chore: 【ops】工单业务日志注解
This commit is contained in:
@@ -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.*;
|
||||
|
||||
/**
|
||||
* 业务日志注解
|
||||
* <p>
|
||||
* 用于标记需要记录业务日志的方法,通过 AOP 自动记录
|
||||
* <p>
|
||||
* 使用示例:
|
||||
* <pre>
|
||||
* {@code
|
||||
* @BusinessLog(type = LogType.DISPATCH, scope = LogScope.ORDER,
|
||||
* description = "自动派单", includeParams = true, includeResult = true)
|
||||
* public DispatchResult dispatch(OrderDispatchContext context) {
|
||||
* // ...
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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)
|
||||
* <p>
|
||||
* 示例:
|
||||
* <ul>
|
||||
* <li>"#context.orderId" - 提取 orderId</li>
|
||||
* <li>"#request.orderId + '-' + #request.targetStatus" - 组合多个参数</li>
|
||||
* </ul>
|
||||
*/
|
||||
String[] params() default {};
|
||||
|
||||
/**
|
||||
* 结果提取表达式(SpEL)
|
||||
* <p>
|
||||
* 示例:
|
||||
* <ul>
|
||||
* <li>"#result.success" - 提取成功标志</li>
|
||||
* <li>"#result.assigneeId" - 提取执行人ID</li>
|
||||
* </ul>
|
||||
*/
|
||||
String result() default "";
|
||||
|
||||
/**
|
||||
* 是否异步记录
|
||||
*/
|
||||
boolean async() default true;
|
||||
|
||||
/**
|
||||
* 自定义标签
|
||||
*/
|
||||
String[] tags() default {};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 业务日志切面
|
||||
* <p>
|
||||
* 职责:
|
||||
* 1. 拦截 @BusinessLog 注解的方法
|
||||
* 2. 提取方法参数和结果
|
||||
* 3. 发布业务日志
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* - 使用环绕通知
|
||||
* - 支持 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从表达式中提取键名
|
||||
* <p>
|
||||
* 示例:
|
||||
* - "#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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 业务日志上下文
|
||||
* <p>
|
||||
* 存储业务日志的上下文信息
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 操作人类<E4BABA><E7B1BB><EFBFBD>(如admin、cleaner、system)
|
||||
*/
|
||||
private String operatorType;
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private Boolean success;
|
||||
|
||||
/**
|
||||
* 错误消息
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 日志时间
|
||||
*/
|
||||
@Builder.Default
|
||||
private LocalDateTime logTime = LocalDateTime.now();
|
||||
|
||||
/**
|
||||
* 扩展信息
|
||||
*/
|
||||
@Builder.Default
|
||||
private Map<String, Object> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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", "队列"),
|
||||
|
||||
/**
|
||||
* 保洁员<E6B481><E59198>志
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.publisher;
|
||||
|
||||
import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext;
|
||||
|
||||
/**
|
||||
* 业务日志发布器接口
|
||||
* <p>
|
||||
* 用于发布业务日志,支持异步发布
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 默认业务日志发布器
|
||||
* <p>
|
||||
* 职责:
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user