企业级系统操作审计方案设计

作者:忆笙智云官方 | 发布时间:2026-05-22 09:00 | 更新时间:2026-07-22 09:00

企业级系统操作审计方案设计

引言

操作审计是企业级系统的合规性要求和安全保障手段。完整的审计体系能够记录"谁在什么时间对什么数据做了什么操作",为问题追溯、安全审计和合规检查提供数据支撑。在金融、医疗、政务等受监管行业,操作审计更是法律强制要求。

本文将介绍操作日志 @MyLog 注解设计、登录/登出日志、异常日志自动采集、日志统计分析及数据清理策略的完整方案。

核心内容

审计日志体系全景

graph TD
    A[审计日志体系] --> B[操作日志]
    A --> C[登录日志]
    A --> D[异常日志]

    B --> B1[增删改操作]
    B --> B2[导出操作]
    B --> B3[审批操作]
    B --> B4[敏感数据访问]

    C --> C1[登录成功]
    C --> C2[登录失败]
    C --> C3[登出记录]
    C --> C4[登录IP/设备]

    D --> D1[系统异常]
    D --> D2[业务异常]
    D --> D3[接口异常]
    D --> D4[定时任务异常]

一、操作日志 @MyLog 注解设计

1. 注解定义

/**
 * 操作日志注解
 * 标注在Controller方法上,自动记录操作日志
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {

    /** 操作模块 */
    String module() default "";

    /** 操作类型 */
    OperateType type() default OperateType.OTHER;

    /** 操作描述 */
    String description() default "";

    /** 是否保存请求参数 */
    boolean saveParams() default true;

    /** 是否保存响应结果 */
    boolean saveResult() default false;

    /** 排除的参数字段(敏感信息脱敏) */
    String[] excludeParams() default {"password", "oldPassword", "token"};
}

/** 操作类型枚举 */
public enum OperateType {
    CREATE("新增"),
    UPDATE("修改"),
    DELETE("删除"),
    EXPORT("导出"),
    IMPORT("导入"),
    QUERY("查询"),
    APPROVE("审批"),
    OTHER("其他");

    private final String label;

    OperateType(String label) {
        this.label = label;
    }

    public String getLabel() {
        return label;
    }
}

2. 日志实体

/**
 * 操作日志实体
 */
@Data
@TableName("sys_oper_log")
public class OperLog {

    /** 日志主键 */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /** 操作模块 */
    private String module;

    /** 操作类型 */
    private String operateType;

    /** 操作描述 */
    private String description;

    /** 请求方法 */
    private String requestMethod;

    /** 请求URL */
    private String requestUrl;

    /** 请求参数 */
    private String requestParams;

    /** 响应结果 */
    private String responseResult;

    /** 操作人ID */
    private Long operatorId;

    /** 操作人姓名 */
    private String operatorName;

    /** 操作人IP */
    private String operatorIp;

    /** 操作人地址 */
    private String operatorLocation;

    /** 执行耗时(毫秒) */
    private Long duration;

    /** 操作状态:0-成功 1-失败 */
    private Integer status;

    /** 错误信息 */
    private String errorMsg;

    /** 操作时间 */
    private LocalDateTime operateTime;
}

3. AOP 切面实现

sequenceDiagram
    participant 请求
    participant Controller
    participant MyLogAspect
    participant 日志服务

    请求->>Controller: 调用方法
    Controller->>MyLogAspect: AOP拦截
    MyLogAspect->>MyLogAspect: 记录开始时间
    MyLogAspect->>Controller: 执行业务方法
    Controller-->>MyLogAspect: 返回结果/异常
    MyLogAspect->>MyLogAspect: 计算耗时
    MyLogAspect->>MyLogAspect: 构建日志对象
    MyLogAspect->>日志服务: 异步保存日志
    日志服务-->>MyLogAspect: 保存完成
    MyLogAspect-->>请求: 返回响应
/**
 * 操作日志切面
 */
@Aspect
@Component
@Slf4j
public class MyLogAspect {

    @Autowired
    private OperLogService operLogService;

    /**
     * 环绕通知:记录操作日志
     */
    @Around("@annotation(myLog)")
    public Object around(ProceedingJoinPoint point, MyLog myLog) throws Throwable {
        long startTime = System.currentTimeMillis();
        OperLog operLog = new OperLog();

        try {
            // 填充基本信息
            fillBaseInfo(operLog, point, myLog);

            // 执行业务方法
            Object result = point.proceed();

            // 记录成功日志
            operLog.setStatus(0);
            if (myLog.saveResult() && result != null) {
                operLog.setResponseResult(truncate(toJson(result), 2000));
            }

            return result;
        } catch (Throwable e) {
            // 记录失败日志
            operLog.setStatus(1);
            operLog.setErrorMsg(truncate(e.getMessage(), 2000));
            throw e;
        } finally {
            // 计算耗时
            operLog.setDuration(System.currentTimeMillis() - startTime);
            operLog.setOperateTime(LocalDateTime.now());

            // 异步保存日志
            operLogService.saveAsync(operLog);
        }
    }

    /** 填充基本信息 */
    private void fillBaseInfo(OperLog operLog, ProceedingJoinPoint point, MyLog myLog) {
        // 注解信息
        operLog.setModule(myLog.module());
        operLog.setOperateType(myLog.type().name());
        operLog.setDescription(myLog.description());

        // 请求信息
        ServletRequestAttributes attributes =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            operLog.setRequestMethod(request.getMethod());
            operLog.setRequestUrl(request.getRequestURI());
            operLog.setOperatorIp(getIpAddress(request));
        }

        // 操作人信息
        if (StpUtil.isLogin()) {
            operLog.setOperatorId(StpUtil.getLoginIdAsLong());
            operLog.setOperatorName(getCurrentUsername());
        }

        // 请求参数
        if (myLog.saveParams()) {
            String params = buildParams(point, myLog.excludeParams());
            operLog.setRequestParams(truncate(params, 2000));
        }
    }

    /** 构建请求参数(脱敏处理) */
    private String buildParams(ProceedingJoinPoint point, String[] excludeParams) {
        Object[] args = point.getArgs();
        if (args == null || args.length == 0) {
            return "";
        }

        // 将参数转为Map并脱敏
        Map<String, Object> paramsMap = new LinkedHashMap<>();
        String[] paramNames = getParameterNames(point);

        for (int i = 0; i < args.length; i++) {
            String name = (paramNames != null && i < paramNames.length)
                ? paramNames[i] : "arg" + i;
            paramsMap.put(name, args[i]);
        }

        // 敏感字段脱敏
        for (String exclude : excludeParams) {
            if (paramsMap.containsKey(exclude)) {
                paramsMap.put(exclude, "******");
            }
        }

        return toJson(paramsMap);
    }
}

4. 使用示例

@RestController
@RequestMapping("/api/system/user")
@Tag(name = "用户管理")
public class UserController {

    @MyLog(module = "用户管理", type = OperateType.CREATE, description = "创建用户")
    @PostMapping
    public Result<Void> create(@RequestBody @Valid UserCreateDTO dto) {
        userService.createUser(dto);
        return Result.success();
    }

    @MyLog(module = "用户管理", type = OperateType.UPDATE, description = "更新用户")
    @PutMapping
    public Result<Void> update(@RequestBody @Valid UserUpdateDTO dto) {
        userService.updateUser(dto);
        return Result.success();
    }

    @MyLog(module = "用户管理", type = OperateType.DELETE, description = "删除用户")
    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        userService.deleteUser(id);
        return Result.success();
    }

    @MyLog(module = "用户管理", type = OperateType.EXPORT, description = "导出用户",
           saveResult = false, saveParams = false)
    @GetMapping("/export")
    public void export(HttpServletResponse response) {
        userService.exportUsers(response);
    }
}

二、登录/登出日志

/**
 * 登录日志实体
 */
@Data
@TableName("sys_login_log")
public class LoginLog {

    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /** 用户名 */
    private String username;

    /** 登录IP */
    private String loginIp;

    /** 登录地点 */
    private String loginLocation;

    /** 浏览器 */
    private String browser;

    /** 操作系统 */
    private String os;

    /** 登录状态:0-成功 1-失败 */
    private Integer status;

    /** 提示消息 */
    private String message;

    /** 登录时间 */
    private LocalDateTime loginTime;
}
/**
 * 登录日志记录服务
 */
@Service
public class LoginLogService {

    @Autowired
    private AsyncLogService asyncLogService;

    /**
     * 记录登录日志
     */
    public void recordLoginLog(String username, boolean success, String message) {
        HttpServletRequest request = getCurrentRequest();

        LoginLog loginLog = new LoginLog();
        loginLog.setUsername(username);
        loginLog.setLoginIp(getIpAddress(request));
        loginLog.setBrowser(getBrowser(request));
        loginLog.setOs(getOs(request));
        loginLog.setStatus(success ? 0 : 1);
        loginLog.setMessage(message);
        loginLog.setLoginTime(LocalDateTime.now());

        // 异步保存
        asyncLogService.saveLoginLog(loginLog);
    }
}

三、异常日志自动采集

graph TD
    A[异常发生] --> B{异常类型}
    B -->|Controller层| C[@RestControllerAdvice捕获]
    B -->|Service层| D[向上传播至Controller]
    B -->|定时任务| E[TaskExceptionHandler捕获]
    B -->|未捕获| F[全局异常处理器]

    C --> G[构建异常日志]
    D --> G
    E --> G
    F --> G

    G --> H[异步保存异常日志]
/**
 * 全局异常处理器——自动采集异常日志
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @Autowired
    private AsyncLogService asyncLogService;

    /**
     * 业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
        log.warn("业务异常: {}", e.getMessage());
        saveErrorLog(request, e, "BUSINESS");
        return Result.fail(e.getCode(), e.getMessage());
    }

    /**
     * 参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException e,
                                                   HttpServletRequest request) {
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
            .collect(Collectors.joining("; "));
        saveErrorLog(request, e, "VALIDATION");
        return Result.fail(400, message);
    }

    /**
     * 系统异常
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e, HttpServletRequest request) {
        log.error("系统异常", e);
        saveErrorLog(request, e, "SYSTEM");
        return Result.fail(500, "系统异常,请联系管理员");
    }

    /** 保存异常日志 */
    private void saveErrorLog(HttpServletRequest request, Exception e, String errorType) {
        ErrorLog errorLog = new ErrorLog();
        errorLog.setErrorType(errorType);
        errorLog.setRequestUrl(request.getRequestURI());
        errorLog.setRequestMethod(request.getMethod());
        errorLog.setErrorMessage(truncate(e.getMessage(), 2000));
        errorLog.setErrorStack(truncate(getStackTrace(e), 4000));
        errorLog.setOperatorIp(getIpAddress(request));

        if (StpUtil.isLogin()) {
            errorLog.setOperatorId(StpUtil.getLoginIdAsLong());
        }

        asyncLogService.saveErrorLog(errorLog);
    }
}

四、日志统计分析

graph TD
    A[日志统计分析] --> B[操作排行]
    A --> C[异常排行]
    A --> D[用户活跃度]
    A --> E[趋势分析]

    B --> B1[操作类型分布]
    B --> B2[高频操作Top10]
    B --> B3[模块操作统计]

    C --> C1[异常类型分布]
    C --> C2[高频异常Top10]
    C --> C3[异常趋势]

    D --> D1[日活/周活/月活]
    D --> D2[用户操作频次]
    D --> D3[在线时长统计]

    E --> E1[每日操作量趋势]
    E --> E2[登录趋势]
    E --> E3[异常趋势]
/**
 * 日志统计服务
 */
@Service
public class LogStatisticsService {

    /**
     * 操作类型分布统计
     */
    public List<StatisticsVO> operateTypeDistribution(LocalDate startDate, LocalDate endDate) {
        return operLogMapper.selectList(new LambdaQueryWrapper<OperLog>()
                .between(OperLog::getOperateTime, startDate.atStartOfDay(),
                         endDate.atTime(23, 59, 59))
                .select(OperLog::getOperateType))
            .stream()
            .collect(Collectors.groupingBy(OperLog::getOperateType, Collectors.counting()))
            .entrySet().stream()
            .map(e -> new StatisticsVO(e.getKey(), e.getValue()))
            .sorted(Comparator.comparing(StatisticsVO::getCount).reversed())
            .toList();
    }

    /**
     * 高频操作Top10
     */
    public List<StatisticsVO> topOperations(LocalDate startDate, LocalDate endDate, int limit) {
        return operLogMapper.selectTopOperations(startDate, endDate, limit);
    }

    /**
     * 用户活跃度统计
     */
    public UserActivityVO userActivity(LocalDate startDate, LocalDate endDate) {
        long dau = loginLogMapper.countDistinctUser(startDate, endDate); // 日活
        long totalOperations = operLogMapper.countByDateRange(startDate, endDate);
        double avgOperations = totalOperations / (double) dau;

        return new UserActivityVO(dau, totalOperations, avgOperations);
    }

    /**
     * 每日操作量趋势
     */
    public List<TrendVO> operateTrend(LocalDate startDate, LocalDate endDate) {
        return operLogMapper.selectDailyTrend(startDate, endDate);
    }
}

五、日志数据清理策略

graph TD
    A[日志清理策略] --> B[按时间清理]
    A --> C[按数量清理]
    A --> D[归档后清理]

    B --> B1[操作日志保留90天]
    B --> B2[登录日志保留180天]
    B --> B3[异常日志保留365天]

    C --> C1[单表最大100万条]
    C --> C2[超出后删除最旧数据]

    D --> D1[归档到历史表]
    D --> D2[导出为文件]
    D --> D3[压缩存储]

    style B1 fill:#c8e6c9
    style B2 fill:#c8e6c9
    style B3 fill:#c8e6c9
/**
 * 日志清理定时任务
 */
@Component
@Slf4j
public class LogCleanupTask {

    @Autowired
    private OperLogMapper operLogMapper;
    @Autowired
    private LoginLogMapper loginLogMapper;
    @Autowired
    private ErrorLogMapper errorLogMapper;

    /**
     * 每天凌晨2点执行日志清理
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void cleanupLogs() {
        log.info("开始执行日志清理任务...");

        // 操作日志保留90天
        int operDeleted = operLogMapper.deleteBeforeDate(
            LocalDate.now().minusDays(90));
        log.info("清理操作日志: {}条", operDeleted);

        // 登录日志保留180天
        int loginDeleted = loginLogMapper.deleteBeforeDate(
            LocalDate.now().minusDays(180));
        log.info("清理登录日志: {}条", loginDeleted);

        // 异常日志保留365天
        int errorDeleted = errorLogMapper.deleteBeforeDate(
            LocalDate.now().minusDays(365));
        log.info("清理异常日志: {}条", errorDeleted);

        log.info("日志清理任务完成");
    }
}

六、异步日志保存

/**
 * 异步日志保存服务
 * 使用独立线程池,避免影响业务性能
 */
@Service
@Slf4j
public class AsyncLogService {

    private final ExecutorService logExecutor = Executors.newFixedThreadPool(
        2, r -> {
            Thread t = new Thread(r, "log-writer");
            t.setDaemon(true);
            return t;
        });

    @Autowired
    private OperLogMapper operLogMapper;
    @Autowired
    private LoginLogMapper loginLogMapper;
    @Autowired
    private ErrorLogMapper errorLogMapper;

    /** 异步保存操作日志 */
    public void saveOperLog(OperLog operLog) {
        logExecutor.execute(() -> {
            try {
                operLogMapper.insert(operLog);
            } catch (Exception e) {
                log.error("保存操作日志失败", e);
            }
        });
    }

    /** 异步保存登录日志 */
    public void saveLoginLog(LoginLog loginLog) {
        logExecutor.execute(() -> {
            try {
                loginLogMapper.insert(loginLog);
            } catch (Exception e) {
                log.error("保存登录日志失败", e);
            }
        });
    }

    /** 异步保存异常日志 */
    public void saveErrorLog(ErrorLog errorLog) {
        logExecutor.execute(() -> {
            try {
                errorLogMapper.insert(errorLog);
            } catch (Exception e) {
                log.error("保存异常日志失败", e);
            }
        });
    }
}

结论与建议

日志保留策略

日志类型 保留时间 单表上限 清理方式
操作日志 90天 100万条 按时间+按数量
登录日志 180天 50万条 按时间
异常日志 365天 50万条 按时间

最佳实践建议

  1. 异步保存:日志保存必须异步执行,避免影响业务接口的响应时间。

  2. 敏感信息脱敏:请求参数中的密码、Token等敏感信息必须脱敏处理后再存储。

  3. 内容截断:对请求参数、响应结果、异常堆栈设置最大长度限制,防止超长数据撑爆数据库。

  4. 按需记录:查询操作默认不记录日志,只记录增删改等变更操作。敏感查询(如查看密码)除外。

  5. 索引优化:为日志表的 operateTimeoperatorIdoperateType 字段建立索引,加速查询。

  6. 冷热分离:近期日志存储在MySQL,历史日志归档到Elasticsearch或对象存储,降低数据库压力。

相关资源