chore: 【ops】工单业务日志注解

This commit is contained in:
lzh
2026-01-09 17:38:29 +08:00
parent 5974c767d5
commit ea3c7829e9
7 changed files with 709 additions and 0 deletions

View File

@@ -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 {};
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}