操作日志记录完整方案:让每个操作都有迹可循!📝

95 阅读9分钟

标题: 操作日志还在手动记?AOP自动化来了!
副标题: 从简单记录到数据对比,全方位操作审计


🎬 开篇:一次操作纠纷的追溯

某电商平台客服系统:

客户:我的订单被人改了,原来是2000元,现在变成了3000元!
客服:查询订单记录...(没有操作日志)💀
客服:不知道谁改的,什么时候改的...

客户:我要投诉!
公司:无法追溯,只能赔偿 💸

损失:赔偿5000元 + 信誉受损

老板:为什么没有操作日志?!
开发:我忘了加... 😭

改造后(操作日志系统):
客户:订单被改了!
客服:查询日志...
日志:2025-01-15 10:30:25,用户张三(ID:123),将订单金额从2000元改为3000元

结果:
- 找到操作人 ✅
- 证据确凿 ✅
- 恢复原价 ✅
- 追究责任 ✅

教训:操作日志是系统的"黑匣子"!

🤔 什么是操作日志?

想象一下:

  • 淘宝: 谁修改了商品价格?
  • 银行: 谁给客户转了账?
  • ERP: 谁删除了库存记录?
  • CRM: 谁导出了客户数据?

操作日志 = 谁(Who) + 什么时候(When) + 在哪里(Where) + 做了什么(What)!


📚 知识地图

操作日志系统
├── 🎯 核心要素
│   ├── 操作人(Who)
│   ├── 操作时间(When)
│   ├── 操作模块(Where)
│   ├── 操作类型(What)
│   ├── 操作详情(How)
│   └── 操作结果(Result)
├── 🏗️ 实现方式
│   ├── AOP切面(推荐)⭐⭐⭐⭐⭐
│   ├── 自定义注解 ⭐⭐⭐⭐⭐
│   ├── 拦截器 ⭐⭐⭐⭐
│   └── 手动记录 ⭐⭐
├── ⚡ 高级功能
│   ├── 数据对比(前后值)
│   ├── 异步记录
│   ├── 敏感数据脱敏
│   ├── 批量操作记录
│   └── 操作回滚
└── 🛡️ 日志管理
    ├── 日志查询
    ├── 日志统计
    ├── 日志导出
    └── 日志归档

💾 数据库设计

-- 操作日志表
CREATE TABLE operation_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    trace_id VARCHAR(64) COMMENT '链路追踪ID',
    user_id BIGINT NOT NULL COMMENT '操作人ID',
    user_name VARCHAR(50) NOT NULL COMMENT '操作人姓名',
    module VARCHAR(50) NOT NULL COMMENT '操作模块:order/product/user',
    operation_type VARCHAR(20) NOT NULL COMMENT '操作类型:CREATE/UPDATE/DELETE/QUERY/EXPORT',
    business_type VARCHAR(50) COMMENT '业务类型',
    business_id BIGINT COMMENT '业务ID',
    method VARCHAR(200) COMMENT '方法名',
    request_uri VARCHAR(200) COMMENT '请求URI',
    request_method VARCHAR(10) COMMENT '请求方式:GET/POST',
    request_params TEXT COMMENT '请求参数',
    response_data TEXT COMMENT '响应数据',
    old_value TEXT COMMENT '修改前的值',
    new_value TEXT COMMENT '修改后的值',
    diff_fields VARCHAR(500) COMMENT '变更字段列表',
    ip VARCHAR(50) COMMENT 'IP地址',
    location VARCHAR(100) COMMENT 'IP归属地',
    user_agent VARCHAR(500) COMMENT '浏览器UA',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '操作状态:1成功 2失败',
    error_msg TEXT COMMENT '错误信息',
    cost_time INT COMMENT '耗时(毫秒)',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_module_type (module, operation_type),
    INDEX idx_business (business_type, business_id),
    INDEX idx_create_time (create_time),
    INDEX idx_trace_id (trace_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';

-- 数据变更详情表(可选,用于记录字段级变更)
CREATE TABLE operation_log_detail (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    log_id BIGINT NOT NULL COMMENT '操作日志ID',
    field_name VARCHAR(50) NOT NULL COMMENT '字段名',
    field_label VARCHAR(100) COMMENT '字段标签',
    old_value VARCHAR(1000) COMMENT '旧值',
    new_value VARCHAR(1000) COMMENT '新值',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_log_id (log_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据变更详情表';

🔧 方案1:注解+AOP(推荐)

自定义注解

/**
 * 操作日志注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
    
    /**
     * 操作模块
     */
    String module();
    
    /**
     * 操作类型
     */
    OperationType type();
    
    /**
     * 业务类型
     */
    String businessType() default "";
    
    /**
     * 业务ID的SpEL表达式
     * 例如:#id、#order.id、#result.data.id
     */
    String businessId() default "";
    
    /**
     * 操作描述
     * 支持SpEL表达式:#{#user.name}修改了订单#{#order.orderNo}
     */
    String description();
    
    /**
     * 是否记录请求参数
     */
    boolean recordParams() default true;
    
    /**
     * 是否记录响应数据
     */
    boolean recordResponse() default false;
    
    /**
     * 是否记录数据变更(对比前后值)
     */
    boolean recordDiff() default false;
    
    /**
     * 是否异步记录
     */
    boolean async() default true;
}

/**
 * 操作类型
 */
public enum OperationType {
    
    /**
     * 新增
     */
    CREATE("新增"),
    
    /**
     * 修改
     */
    UPDATE("修改"),
    
    /**
     * 删除
     */
    DELETE("删除"),
    
    /**
     * 查询
     */
    QUERY("查询"),
    
    /**
     * 导出
     */
    EXPORT("导出"),
    
    /**
     * 导入
     */
    IMPORT("导入"),
    
    /**
     * 授权
     */
    GRANT("授权"),
    
    /**
     * 其他
     */
    OTHER("其他");
    
    private final String label;
    
    OperationType(String label) {
        this.label = label;
    }
    
    public String getLabel() {
        return label;
    }
}

AOP切面实现

/**
 * 操作日志切面
 */
@Aspect
@Component
@Slf4j
public class OperationLogAspect {
    
    @Autowired
    private OperationLogService operationLogService;
    
    @Autowired
    private SpelExpressionParser spelParser;
    
    @Autowired
    private AsyncTaskExecutor asyncExecutor;
    
    /**
     * 环绕通知
     */
    @Around("@annotation(operationLog)")
    public Object around(ProceedingJoinPoint pjp, OperationLog operationLog) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        // 1. 获取当前用户信息
        UserContext currentUser = UserContextHolder.get();
        
        // 2. 获取请求信息
        HttpServletRequest request = getRequest();
        
        // 3. 记录前的数据(用于对比)
        Object oldValue = null;
        if (operationLog.recordDiff()) {
            oldValue = getOldValue(pjp, operationLog);
        }
        
        // 4. 执行方法
        Object result = null;
        Throwable exception = null;
        try {
            result = pjp.proceed();
            return result;
            
        } catch (Throwable e) {
            exception = e;
            throw e;
            
        } finally {
            // 5. ⚡ 记录操作日志(异步)
            long costTime = System.currentTimeMillis() - startTime;
            
            recordLog(pjp, operationLog, currentUser, request, result, 
                oldValue, exception, costTime);
        }
    }
    
    /**
     * 记录日志
     */
    private void recordLog(ProceedingJoinPoint pjp, 
                          OperationLog operationLog,
                          UserContext currentUser,
                          HttpServletRequest request,
                          Object result,
                          Object oldValue,
                          Throwable exception,
                          long costTime) {
        
        Runnable task = () -> {
            try {
                // 构建日志对象
                OperationLogDTO logDTO = new OperationLogDTO();
                
                // 基本信息
                logDTO.setTraceId(MDC.get("traceId"));
                logDTO.setUserId(currentUser != null ? currentUser.getUserId() : null);
                logDTO.setUserName(currentUser != null ? currentUser.getUserName() : "");
                logDTO.setModule(operationLog.module());
                logDTO.setOperationType(operationLog.type().name());
                logDTO.setBusinessType(operationLog.businessType());
                
                // 方法信息
                MethodSignature signature = (MethodSignature) pjp.getSignature();
                logDTO.setMethod(signature.getDeclaringTypeName() + "." + signature.getName());
                
                // 请求信息
                if (request != null) {
                    logDTO.setRequestUri(request.getRequestURI());
                    logDTO.setRequestMethod(request.getMethod());
                    logDTO.setIp(getIpAddress(request));
                    logDTO.setUserAgent(request.getHeader("User-Agent"));
                }
                
                // 请求参数
                if (operationLog.recordParams()) {
                    Object[] args = pjp.getArgs();
                    logDTO.setRequestParams(JSON.toJSONString(args));
                }
                
                // 响应数据
                if (operationLog.recordResponse() && result != null) {
                    logDTO.setResponseData(JSON.toJSONString(result));
                }
                
                // ⚡ 业务ID(SpEL表达式解析)
                if (StringUtils.isNotBlank(operationLog.businessId())) {
                    Long businessId = parseSpel(operationLog.businessId(), pjp, result, Long.class);
                    logDTO.setBusinessId(businessId);
                }
                
                // ⚡ 数据对比
                if (operationLog.recordDiff() && oldValue != null) {
                    Object newValue = getNewValue(pjp, operationLog, result);
                    
                    if (newValue != null) {
                        logDTO.setOldValue(JSON.toJSONString(oldValue));
                        logDTO.setNewValue(JSON.toJSONString(newValue));
                        
                        // 对比差异字段
                        List<String> diffFields = compareObjects(oldValue, newValue);
                        logDTO.setDiffFields(String.join(",", diffFields));
                    }
                }
                
                // 操作状态
                if (exception != null) {
                    logDTO.setStatus(0);  // 失败
                    logDTO.setErrorMsg(exception.getMessage());
                } else {
                    logDTO.setStatus(1);  // 成功
                }
                
                // 耗时
                logDTO.setCostTime((int) costTime);
                
                // 保存日志
                operationLogService.saveLog(logDTO);
                
            } catch (Exception e) {
                log.error("记录操作日志失败", e);
            }
        };
        
        // 异步或同步执行
        if (operationLog.async()) {
            asyncExecutor.execute(task);
        } else {
            task.run();
        }
    }
    
    /**
     * 获取修改前的数据
     */
    private Object getOldValue(ProceedingJoinPoint pjp, OperationLog operationLog) {
        if (operationLog.type() != OperationType.UPDATE) {
            return null;
        }
        
        try {
            // 从参数中获取ID
            Object[] args = pjp.getArgs();
            Long id = null;
            
            for (Object arg : args) {
                if (arg instanceof Long) {
                    id = (Long) arg;
                    break;
                } else if (arg != null && hasIdField(arg)) {
                    id = getIdFromObject(arg);
                    break;
                }
            }
            
            if (id != null) {
                // 查询修改前的数据
                return queryOldValue(pjp, id);
            }
            
        } catch (Exception e) {
            log.error("获取修改前数据失败", e);
        }
        
        return null;
    }
    
    /**
     * 获取修改后的数据
     */
    private Object getNewValue(ProceedingJoinPoint pjp, OperationLog operationLog, Object result) {
        if (operationLog.type() != OperationType.UPDATE) {
            return null;
        }
        
        try {
            // 从参数中获取ID
            Object[] args = pjp.getArgs();
            Long id = null;
            
            for (Object arg : args) {
                if (arg instanceof Long) {
                    id = (Long) arg;
                    break;
                } else if (arg != null && hasIdField(arg)) {
                    id = getIdFromObject(arg);
                    break;
                }
            }
            
            if (id != null) {
                // 查询修改后的数据
                return queryNewValue(pjp, id);
            }
            
        } catch (Exception e) {
            log.error("获取修改后数据失败", e);
        }
        
        return null;
    }
    
    /**
     * 对比对象差异
     */
    private List<String> compareObjects(Object oldObj, Object newObj) {
        List<String> diffFields = new ArrayList<>();
        
        if (oldObj == null || newObj == null) {
            return diffFields;
        }
        
        try {
            Class<?> clazz = oldObj.getClass();
            
            for (Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                
                Object oldValue = field.get(oldObj);
                Object newValue = field.get(newObj);
                
                // 跳过null值
                if (oldValue == null && newValue == null) {
                    continue;
                }
                
                // 比较值
                if (oldValue == null || !oldValue.equals(newValue)) {
                    diffFields.add(field.getName());
                }
            }
            
        } catch (Exception e) {
            log.error("对比对象差异失败", e);
        }
        
        return diffFields;
    }
    
    /**
     * SpEL表达式解析
     */
    private <T> T parseSpel(String spelExpression, ProceedingJoinPoint pjp, 
                           Object result, Class<T> clazz) {
        try {
            Expression expression = spelParser.parseExpression(spelExpression);
            
            StandardEvaluationContext context = new StandardEvaluationContext();
            
            // 设置方法参数
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            String[] paramNames = signature.getParameterNames();
            Object[] args = pjp.getArgs();
            
            for (int i = 0; i < paramNames.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
            
            // 设置返回值
            context.setVariable("result", result);
            
            return expression.getValue(context, clazz);
            
        } catch (Exception e) {
            log.error("SpEL表达式解析失败:{}", spelExpression, e);
            return null;
        }
    }
    
    /**
     * 获取请求对象
     */
    private HttpServletRequest getRequest() {
        try {
            ServletRequestAttributes attributes = 
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            
            return attributes != null ? attributes.getRequest() : null;
            
        } catch (Exception e) {
            return null;
        }
    }
    
    /**
     * 获取IP地址
     */
    private String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        
        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        
        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        
        return ip;
    }
    
    /**
     * 判断对象是否有ID字段
     */
    private boolean hasIdField(Object obj) {
        try {
            obj.getClass().getDeclaredField("id");
            return true;
        } catch (NoSuchFieldException e) {
            return false;
        }
    }
    
    /**
     * 从对象中获取ID
     */
    private Long getIdFromObject(Object obj) {
        try {
            Field field = obj.getClass().getDeclaredField("id");
            field.setAccessible(true);
            return (Long) field.get(obj);
        } catch (Exception e) {
            return null;
        }
    }
    
    /**
     * 查询修改前的值
     */
    private Object queryOldValue(ProceedingJoinPoint pjp, Long id) {
        // 这里需要根据具体业务实现
        // 例如:orderService.getById(id)
        return null;
    }
    
    /**
     * 查询修改后的值
     */
    private Object queryNewValue(ProceedingJoinPoint pjp, Long id) {
        // 这里需要根据具体业务实现
        return null;
    }
}

使用示例

/**
 * 订单Controller
 */
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    /**
     * ⚡ 创建订单(自动记录日志)
     */
    @PostMapping
    @OperationLog(
        module = "订单管理",
        type = OperationType.CREATE,
        businessType = "order",
        businessId = "#result.data.id",
        description = "创建订单:#{#orderDTO.orderNo}"
    )
    public Result<Order> createOrder(@RequestBody OrderDTO orderDTO) {
        Order order = orderService.createOrder(orderDTO);
        return Result.success(order);
    }
    
    /**
     * ⚡ 修改订单(自动记录日志+数据对比)
     */
    @PutMapping("/{id}")
    @OperationLog(
        module = "订单管理",
        type = OperationType.UPDATE,
        businessType = "order",
        businessId = "#id",
        description = "修改订单:#{#id}",
        recordDiff = true  // 记录数据变更
    )
    public Result<Void> updateOrder(@PathVariable Long id, 
                                    @RequestBody OrderDTO orderDTO) {
        orderService.updateOrder(id, orderDTO);
        return Result.success();
    }
    
    /**
     * ⚡ 删除订单(自动记录日志)
     */
    @DeleteMapping("/{id}")
    @OperationLog(
        module = "订单管理",
        type = OperationType.DELETE,
        businessType = "order",
        businessId = "#id",
        description = "删除订单:#{#id}"
    )
    public Result<Void> deleteOrder(@PathVariable Long id) {
        orderService.deleteOrder(id);
        return Result.success();
    }
    
    /**
     * ⚡ 导出订单(自动记录日志)
     */
    @GetMapping("/export")
    @OperationLog(
        module = "订单管理",
        type = OperationType.EXPORT,
        description = "导出订单数据",
        recordParams = true
    )
    public void exportOrders(OrderQueryDTO query, HttpServletResponse response) {
        orderService.exportOrders(query, response);
    }
}

⚡ 高级功能:数据对比

/**
 * 数据对比工具
 */
@Component
public class DataDiffUtils {
    
    /**
     * 对比两个对象的差异
     */
    public static List<FieldDiff> compareObjects(Object oldObj, Object newObj) {
        List<FieldDiff> diffs = new ArrayList<>();
        
        if (oldObj == null || newObj == null) {
            return diffs;
        }
        
        try {
            Class<?> clazz = oldObj.getClass();
            
            for (Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                
                // 获取字段标签(从注解中)
                String fieldLabel = getFieldLabel(field);
                
                Object oldValue = field.get(oldObj);
                Object newValue = field.get(newObj);
                
                // 跳过相同的值
                if (Objects.equals(oldValue, newValue)) {
                    continue;
                }
                
                // 记录差异
                FieldDiff diff = new FieldDiff();
                diff.setFieldName(field.getName());
                diff.setFieldLabel(fieldLabel);
                diff.setOldValue(oldValue != null ? oldValue.toString() : "");
                diff.setNewValue(newValue != null ? newValue.toString() : "");
                
                diffs.add(diff);
            }
            
        } catch (Exception e) {
            log.error("对比对象失败", e);
        }
        
        return diffs;
    }
    
    /**
     * 获取字段标签
     */
    private static String getFieldLabel(Field field) {
        // 可以从自定义注解中获取
        // @FieldLabel("订单金额")
        // private BigDecimal amount;
        
        FieldLabel annotation = field.getAnnotation(FieldLabel.class);
        if (annotation != null) {
            return annotation.value();
        }
        
        return field.getName();
    }
}

/**
 * 字段标签注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldLabel {
    String value();
}

/**
 * 字段差异VO
 */
@Data
public class FieldDiff {
    
    /**
     * 字段名
     */
    private String fieldName;
    
    /**
     * 字段标签
     */
    private String fieldLabel;
    
    /**
     * 旧值
     */
    private String oldValue;
    
    /**
     * 新值
     */
    private String newValue;
}

/**
 * 使用示例
 */
@Data
public class Order {
    
    @FieldLabel("订单ID")
    private Long id;
    
    @FieldLabel("订单编号")
    private String orderNo;
    
    @FieldLabel("订单金额")
    private BigDecimal amount;
    
    @FieldLabel("订单状态")
    private Integer status;
}

📊 日志查询与统计

/**
 * 操作日志查询服务
 */
@Service
public class OperationLogQueryService {
    
    @Autowired
    private OperationLogMapper operationLogMapper;
    
    /**
     * 分页查询操作日志
     */
    public PageResult<OperationLogVO> queryLogs(OperationLogQueryDTO query) {
        Page<OperationLog> page = new Page<>(query.getPageNo(), query.getPageSize());
        
        LambdaQueryWrapper<OperationLog> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(query.getUserId() != null, OperationLog::getUserId, query.getUserId())
               .eq(StringUtils.isNotBlank(query.getModule()), 
                   OperationLog::getModule, query.getModule())
               .eq(StringUtils.isNotBlank(query.getOperationType()), 
                   OperationLog::getOperationType, query.getOperationType())
               .eq(query.getBusinessId() != null, 
                   OperationLog::getBusinessId, query.getBusinessId())
               .between(query.getStartTime() != null && query.getEndTime() != null,
                   OperationLog::getCreateTime, query.getStartTime(), query.getEndTime())
               .orderByDesc(OperationLog::getCreateTime);
        
        Page<OperationLog> result = operationLogMapper.selectPage(page, wrapper);
        
        List<OperationLogVO> voList = result.getRecords().stream()
            .map(this::convertToVO)
            .collect(Collectors.toList());
        
        return PageResult.of(result.getTotal(), voList);
    }
    
    /**
     * 查询某条数据的操作历史
     */
    public List<OperationLogVO> queryDataHistory(String businessType, Long businessId) {
        LambdaQueryWrapper<OperationLog> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(OperationLog::getBusinessType, businessType)
               .eq(OperationLog::getBusinessId, businessId)
               .orderByDesc(OperationLog::getCreateTime);
        
        List<OperationLog> logs = operationLogMapper.selectList(wrapper);
        
        return logs.stream()
            .map(this::convertToVO)
            .collect(Collectors.toList());
    }
    
    /**
     * 统计操作次数
     */
    public Map<String, Long> statisticsOperationCount(Date startTime, Date endTime) {
        List<OperationLog> logs = operationLogMapper.selectBetweenTime(startTime, endTime);
        
        return logs.stream()
            .collect(Collectors.groupingBy(
                OperationLog::getOperationType,
                Collectors.counting()
            ));
    }
    
    /**
     * 统计用户操作排行
     */
    public List<UserOperationStatVO> statisticsUserOperation(Date startTime, Date endTime) {
        return operationLogMapper.statisticsUserOperation(startTime, endTime);
    }
    
    /**
     * 转换VO
     */
    private OperationLogVO convertToVO(OperationLog log) {
        OperationLogVO vo = new OperationLogVO();
        BeanUtils.copyProperties(log, vo);
        
        // 解析差异字段
        if (StringUtils.isNotBlank(log.getOldValue()) && 
            StringUtils.isNotBlank(log.getNewValue())) {
            
            List<FieldDiff> diffs = parseDiff(log.getOldValue(), log.getNewValue());
            vo.setDiffList(diffs);
        }
        
        return vo;
    }
    
    /**
     * 解析差异
     */
    private List<FieldDiff> parseDiff(String oldValueJson, String newValueJson) {
        // 解析JSON并对比差异
        return DataDiffUtils.compareObjects(
            JSON.parseObject(oldValueJson, Object.class),
            JSON.parseObject(newValueJson, Object.class)
        );
    }
}

✅ 最佳实践

操作日志系统设计要点:

1️⃣ 日志内容:
   □ 操作人(谁)
   □ 操作时间(何时)
   □ 操作模块(哪里)
   □ 操作类型(做什么)
   □ 操作详情(怎么做)
   □ 操作结果(成功/失败)
   
2️⃣ 技术实现:
   □ AOP切面(推荐)⭐⭐⭐⭐⭐
   □ 自定义注解
   □ SpEL表达式
   □ 异步记录
   
3️⃣ 高级功能:
   □ 数据对比(前后值)
   □ 敏感数据脱敏
   □ 链路追踪ID
   □ IP归属地
   
4️⃣ 性能优化:
   □ 异步记录(不影响主流程)
   □ 批量插入
   □ 定期归档
   □ 索引优化
   
5️⃣ 日志管理:
   □ 分页查询
   □ 条件筛选
   □ 操作历史
   □ 数据导出
   □ 日志归档
   
6️⃣ 安全防护:
   □ 敏感字段脱敏
   □ 访问权限控制
   □ 防止篡改
   □ 审计日志

🎉 总结

操作日志系统核心:

1️⃣ 自动化:AOP自动记录,无需手动调用
2️⃣ 完整性:记录操作全过程
3️⃣ 可追溯:完整的操作链路
4️⃣ 数据对比:清晰展示前后值变化
5️⃣ 高性能:异步记录不影响主流程

记住:操作日志是系统的"黑匣子",关键时刻能救命! 📝


文档编写时间:2025年10月24日
作者:热爱审计追溯的日志工程师
版本:v1.0
愿每个操作都有迹可循!