From 8fb730d281dc2a3f0b3a5ed6b9126683d322ed91 Mon Sep 17 00:00:00 2001 From: wh Date: Thu, 9 Apr 2026 13:28:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(common):=20=E6=B7=BB=E5=8A=A0=20@Operation?= =?UTF-8?q?Log=20=E6=B3=A8=E8=A7=A3=E5=92=8C=20AuditAspect=20(T016/T017)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/label/common/aop/AuditAspect.java | 75 +++++++++++++++++++ .../com/label/common/aop/OperationLog.java | 18 +++++ 2 files changed, 93 insertions(+) create mode 100644 src/main/java/com/label/common/aop/AuditAspect.java create mode 100644 src/main/java/com/label/common/aop/OperationLog.java diff --git a/src/main/java/com/label/common/aop/AuditAspect.java b/src/main/java/com/label/common/aop/AuditAspect.java new file mode 100644 index 0000000..a2f0b61 --- /dev/null +++ b/src/main/java/com/label/common/aop/AuditAspect.java @@ -0,0 +1,75 @@ +package com.label.common.aop; + +import com.label.common.context.CompanyContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +/** + * AOP aspect for audit logging. + * + * KEY DESIGN DECISIONS: + * 1. Uses JdbcTemplate directly (not MyBatis Mapper) to bypass TenantLineInnerInterceptor + * — operation logs need to capture company_id explicitly, not via thread-local injection + * 2. Written in finally block — audit log is written regardless of business method success/failure + * 3. Audit failures are logged as ERROR but NEVER rethrown — business transactions must not be + * affected by audit failures + * 4. Captures result of business method to log SUCCESS or FAILURE + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class AuditAspect { + + private final JdbcTemplate jdbcTemplate; + + @Around("@annotation(operationLog)") + public Object audit(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable { + Long companyId = CompanyContext.get(); + // operator_id can be obtained from SecurityContext or ThreadLocal in the future + // For now, use null as a safe default when not available + Long operatorId = null; + + String result = "SUCCESS"; + String errorMessage = null; + Object returnValue = null; + + try { + returnValue = joinPoint.proceed(); + } catch (Throwable e) { + result = "FAILURE"; + errorMessage = e.getMessage(); + throw e; // Always rethrow business exceptions + } finally { + // Write audit log in finally block — runs regardless of success or failure + // CRITICAL: Never throw from here — would swallow the original exception + try { + writeAuditLog(companyId, operatorId, operationLog.type(), + operationLog.targetType(), result, errorMessage); + } catch (Exception auditEx) { + // Audit failure must NOT affect business transaction + log.error("审计日志写入失败: type={}, error={}", + operationLog.type(), auditEx.getMessage(), auditEx); + } + } + + return returnValue; + } + + private void writeAuditLog(Long companyId, Long operatorId, String operationType, + String targetType, String result, String errorMessage) { + String sql = """ + INSERT INTO sys_operation_log + (company_id, operator_id, operation_type, target_type, result, error_message, operated_at) + VALUES (?, ?, ?, ?, ?, ?, NOW()) + """; + jdbcTemplate.update(sql, companyId, operatorId, operationType, + targetType.isEmpty() ? null : targetType, + result, errorMessage); + } +} diff --git a/src/main/java/com/label/common/aop/OperationLog.java b/src/main/java/com/label/common/aop/OperationLog.java new file mode 100644 index 0000000..8c96a4a --- /dev/null +++ b/src/main/java/com/label/common/aop/OperationLog.java @@ -0,0 +1,18 @@ +package com.label.common.aop; + +import java.lang.annotation.*; + +/** + * Marks a method for audit logging. + * The AuditAspect intercepts this annotation and writes to sys_operation_log. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface OperationLog { + /** Operation type, e.g., "EXTRACTION_APPROVE", "USER_LOGIN", "TASK_CLAIM" */ + String type(); + + /** Target entity type, e.g., "annotation_task", "sys_user" */ + String targetType() default ""; +}