Files
label_backend/src/main/java/com/label/aspect/AuditAspect.java

77 lines
3.0 KiB
Java

package com.label.aspect;
import com.label.annotation.OperationLog;
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);
}
}