2026-04-14 13:19:39 +08:00
|
|
|
package com.label.aspect;
|
2026-04-09 13:28:38 +08:00
|
|
|
|
2026-04-14 13:19:39 +08:00
|
|
|
import com.label.annotation.OperationLog;
|
2026-04-09 13:28:38 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|