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