操作审计日志-使用指南.md 13 KB

操作审计日志 - 使用指南

📅 创建日期:2026-06-16
⚠️ 重要提示:异步线程中无法获取HTTP请求信息,必须在Controller层填充


🔧 核心问题与解决方案

问题

OperationAuditLogService使用@Async异步记录日志,在异步线程中无法通过RequestContextHolder获取HTTP请求信息(IP、User-Agent等)。

解决方案

提供两种使用方式:

  1. 推荐方式:使用logWithRequestInfo()方法,自动在同步线程中获取请求信息
  2. 手动方式:使用静态方法getClientIpFromRequest()getUserAgentFromRequest()手动获取

✅ 推荐用法(最简单)

Controller层示例

@RestController
@RequestMapping("/api/admin/user-level-config")
@RequiredArgsConstructor
public class UserLevelConfigController {
    
    private final OperationAuditLogService auditLogService;
    
    /**
     * 创建等级配置版本
     */
    @PostMapping("/version")
    public Result<Long> createVersion(@RequestBody CreateVersionRequest request,
                                      @AuthenticationPrincipal UserDetails userDetails) {
        Long versionId = userLevelConfigService.createVersion(request);
        
        // ✅ 推荐用法:一行代码搞定
        auditLogService.logWithRequestInfo(
            AuditLogBuilder.builder()
                .operatorId(userDetails.getUserId())
                .operatorName(userDetails.getNickname())
                .operatorRole("ADMIN")
                .operationType(OperationType.LEVEL_VERSION_CREATE.getCode())
                .module("等级配置")
                .description("创建等级配置版本: " + request.getVersion())
                .targetType("LEVEL_CONFIG")
                .targetId(versionId)
                .targetIdentifier(request.getVersion())
                .result("SUCCESS")
                .isSensitive(false)
                .remark("版本号: " + request.getVersion())
        );
        
        return Result.success(versionId);
    }
}

优点

  • ✅ 自动获取IP、User-Agent、Request URL、Request Method
  • ✅ 代码简洁,一行搞定
  • ✅ 不会遗漏请求信息

📝 手动用法(灵活控制)

场景1:需要自定义部分字段

@PostMapping("/bind")
public Result<Void> bindAccount(@RequestBody BindAccountRequest request,
                                @AuthenticationPrincipal UserDetails userDetails) {
    platformAccountService.bindAccount(userDetails.getUserId(), request);
    
    // 手动构建builder
    AuditLogBuilder builder = AuditLogBuilder.builder()
        .operatorId(userDetails.getUserId())
        .operatorName(userDetails.getNickname())
        .operatorRole("USER")
        .operationType(OperationType.ACCOUNT_BIND.getCode())
        .module("平台账号")
        .description("绑定" + request.getPlatformName() + "账号")
        .targetType("ACCOUNT")
        .targetIdentifier(request.getPlatformCode())
        .result("SUCCESS");
    
    // ✅ 手动填充请求信息
    builder.ipAddress(OperationAuditLogService.getClientIpFromRequest());
    builder.userAgent(OperationAuditLogService.getUserAgentFromRequest());
    
    // 异步记录
    auditLogService.logOperation(builder);
    
    return Result.success();
}

场景2:记录成功操作(快捷方法)

@PostMapping("/migrate-users")
public Result<Integer> batchMigrateUsers(@RequestBody MigrateUsersRequest request,
                                         @AuthenticationPrincipal UserDetails userDetails) {
    int count = userLevelConfigService.batchMigrateUsers(
        request.getTargetVersionId(), 
        request.getUserIds(),
        userDetails.getUserId(),
        request.getRemark()
    );
    
    // ✅ 使用快捷方法
    auditLogService.logSuccess(
        userDetails.getUserId(),
        userDetails.getNickname(),
        "ADMIN",
        OperationType.LEVEL_USER_BATCH_MIGRATE,
        "批量迁移用户",
        null,  // targetId
        null,  // beforeData
        Map.of("migratedCount", count),  // afterData
        request.getRemark()
    );
    // 注意:快捷方法不会自动填充请求信息,需要手动调用logWithRequestInfo或手动填充
    
    return Result.success(count);
}

场景3:记录失败操作

try {
    platformAccountService.verifyAccount(userId, platformCode);
} catch (Exception e) {
    // ✅ 记录失败操作
    auditLogService.logFailure(
        userId,
        userName,
        "USER",
        OperationType.ACCOUNT_VERIFY,
        "验证" + platformName + "账号",
        null,
        e.getMessage()
    );
    
    throw new BusinessException("账号验证失败: " + e.getMessage());
}

🎯 完整示例

示例1:敏感操作审计

/**
 * 批量迁移用户到新版本(敏感操作)
 */
@PostMapping("/migrate-users")
public Result<Integer> batchMigrateUsers(@RequestBody MigrateUsersRequest request,
                                         @AuthenticationPrincipal UserDetails userDetails) {
    Long operatorId = userDetails.getUserId();
    String operatorName = userDetails.getNickname();
    
    try {
        // 执行迁移
        int count = userLevelConfigService.batchMigrateUsers(
            request.getTargetVersionId(), 
            request.getUserIds(),
            operatorId,
            request.getRemark()
        );
        
        // ✅ 记录成功日志(敏感操作)
        auditLogService.logWithRequestInfo(
            AuditLogBuilder.builder()
                .operatorId(operatorId)
                .operatorName(operatorName)
                .operatorRole("ADMIN")
                .operationType(OperationType.LEVEL_USER_BATCH_MIGRATE.getCode())
                .module("等级配置")
                .description("批量迁移" + count + "个用户到新版本")
                .targetType("USER")
                .targetId(null)
                .targetIdentifier("批量迁移")
                .beforeData(Map.of("userCount", request.getUserIds().size()))
                .afterData(Map.of("migratedCount", count))
                .result("SUCCESS")
                .isSensitive(true)  // ⚠️ 标记为敏感操作
                .remark(request.getRemark())
        );
        
        return Result.success(count);
        
    } catch (Exception e) {
        // ✅ 记录失败日志
        auditLogService.logWithRequestInfo(
            AuditLogBuilder.builder()
                .operatorId(operatorId)
                .operatorName(operatorName)
                .operatorRole("ADMIN")
                .operationType(OperationType.LEVEL_USER_BATCH_MIGRATE.getCode())
                .module("等级配置")
                .description("批量迁移用户失败")
                .targetType("USER")
                .result("FAILED")
                .errorMessage(e.getMessage())
                .isSensitive(true)  // ⚠️ 敏感操作
        );
        
        throw new BusinessException("批量迁移失败: " + e.getMessage());
    }
}

示例2:数据变更审计

/**
 * 调整用户等级
 */
@PutMapping("/user/{userId}/level")
public Result<Void> adjustUserLevel(@PathVariable Long userId,
                                    @RequestBody AdjustLevelRequest request,
                                    @AuthenticationPrincipal UserDetails userDetails) {
    // 查询调整前的等级
    UserLevelVersionRelation oldRelation = versionRelationMapper.selectByUserId(userId);
    String oldLevelCode = oldRelation.getLevelCode();
    
    // 执行调整
    userLevelConfigService.adjustUserLevel(userId, request.getNewLevelCode());
    
    // ✅ 记录数据变更(包含前后快照)
    auditLogService.logWithRequestInfo(
        AuditLogBuilder.builder()
            .operatorId(userDetails.getUserId())
            .operatorName(userDetails.getNickname())
            .operatorRole("ADMIN")
            .operationType(OperationType.LEVEL_USER_ADJUST.getCode())
            .module("等级配置")
            .description("调整用户等级: " + oldLevelCode + " → " + request.getNewLevelCode())
            .targetType("USER")
            .targetId(userId)
            .targetIdentifier("用户ID: " + userId)
            .beforeData(Map.of(
                "levelCode", oldLevelCode,
                "configVersionId", oldRelation.getConfigVersionId()
            ))
            .afterData(Map.of(
                "levelCode", request.getNewLevelCode()
            ))
            .result("SUCCESS")
            .isSensitive(true)
            .remark(request.getReason())
    );
    
    return Result.success();
}

⚠️ 常见错误

❌ 错误1:在Service层直接调用logOperation

@Service
public class UserService {
    
    @Autowired
    private OperationAuditLogService auditLogService;
    
    public void updateUser(Long userId, UserInfo userInfo) {
        // 业务逻辑...
        
        // ❌ 错误:在Service层调用,无法获取HTTP请求信息
        auditLogService.logOperation(
            AuditLogBuilder.builder()
                .operatorId(userId)
                // ...
        );
    }
}

正确做法:在Controller层调用,或使用logWithRequestInfo()


❌ 错误2:忘记标记敏感操作

// ❌ 错误:删除用户是敏感操作,但没有标记
auditLogService.logWithRequestInfo(
    AuditLogBuilder.builder()
        .operationType(OperationType.USER_DELETE.getCode())
        .isSensitive(false)  // ❌ 应该是true
);

正确做法

// ✅ 正确:标记为敏感操作
auditLogService.logWithRequestInfo(
    AuditLogBuilder.builder()
        .operationType(OperationType.USER_DELETE.getCode())
        .isSensitive(true)  // ✅ 敏感操作
);

❌ 错误3:异步方法中尝试获取请求信息

@Async
public void asyncMethod() {
    // ❌ 错误:异步线程中RequestContextHolder为null
    String ip = OperationAuditLogService.getClientIpFromRequest();  // 返回"unknown"
}

正确做法:在调用异步方法前获取并传递

public void syncMethod() {
    // ✅ 在同步线程中获取
    String ip = OperationAuditLogService.getClientIpFromRequest();
    String userAgent = OperationAuditLogService.getUserAgentFromRequest();
    
    // 传递给异步方法
    asyncMethod(ip, userAgent);
}

@Async
public void asyncMethod(String ip, String userAgent) {
    // 使用传入的参数
}

📊 审计日志查询

1. 查询用户的操作日志

// 查询最近50条操作日志
List<OperationAuditLog> logs = auditLogService.getUserOperationLogs(userId, 50);

2. 查询目标对象的操作历史

// 查询某个等级配置版本的所有操作历史
List<OperationAuditLog> history = auditLogService.getTargetOperationHistory(
    "LEVEL_CONFIG", 
    versionId, 
    100
);

3. 分页查询(运营后台)

Page<OperationAuditLog> page = auditLogService.queryAuditLogs(
    "等级配置",           // module
    "LEVEL_VERSION_ACTIVATE",  // operationType
    null,                // operatorId
    startTime,           // startTime
    endTime,             // endTime
    true,                // isSensitive(只查敏感操作)
    1,                   // pageNum
    20                   // pageSize
);

4. 查询敏感操作日志

// 查询最近7天的敏感操作
LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7);
List<OperationAuditLog> sensitiveLogs = auditLogService.getSensitiveLogs(sevenDaysAgo);

🎯 最佳实践

1. 统一在Controller层记录日志

@RestController
public class MyController {
    
    @Autowired
    private OperationAuditLogService auditLogService;
    
    @PostMapping("/xxx")
    public Result<?> doSomething(...) {
        // 执行业务逻辑
        service.doBusiness();
        
        // ✅ 在Controller层记录日志
        auditLogService.logWithRequestInfo(...);
        
        return Result.success();
    }
}

2. 使用try-catch确保异常也被记录

try {
    service.doBusiness();
    
    // 记录成功
    auditLogService.logWithRequestInfo(...result("SUCCESS")...);
    
} catch (Exception e) {
    // 记录失败
    auditLogService.logWithRequestInfo(...result("FAILED").errorMessage(e.getMessage())...);
    
    throw e;
}

3. 敏感操作必须标记

以下操作应标记为敏感操作(isSensitive=true):

  • 删除用户/数据
  • 批量修改用户等级
  • 调整配额
  • 审核营业执照
  • 发放优惠券/配额
  • 修改系统配置

4. 重要操作记录数据快照

对于数据变更操作,记录beforeData和afterData:

.beforeData(Map.of("oldValue", oldValue))
.afterData(Map.of("newValue", newValue))

📞 技术支持

如有问题,请联系开发团队。


📌 总结

  • ✅ 推荐使用logWithRequestInfo()方法,自动获取请求信息
  • ✅ 在Controller层调用,不要在Service层或异步方法中调用
  • ✅ 敏感操作必须标记isSensitive=true
  • ✅ 重要操作记录数据快照(beforeData/afterData)
  • ❌ 避免在异步线程中尝试获取HTTP请求信息