feat(common): 添加 @OperationLog 注解和 AuditAspect (T016/T017)
This commit is contained in:
75
src/main/java/com/label/common/aop/AuditAspect.java
Normal file
75
src/main/java/com/label/common/aop/AuditAspect.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/java/com/label/common/aop/OperationLog.java
Normal file
18
src/main/java/com/label/common/aop/OperationLog.java
Normal file
@@ -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 "";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user