企业级档案操作留痕全流程实操指南:从搭建到上线可直接复用

一、前期准备

所需工具可直接通过以下地址或命令安装:

二、核心表结构设计

直接执行以下SQL创建留痕日志表,可直接复用:

```sql CREATE TABLE `sys_archive_operation_log` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `archive_id` bigint NOT NULL COMMENT '关联档案ID', `operate_user_id` bigint NOT NULL COMMENT '操作人ID', `operate_user_name` varchar(32) NOT NULL COMMENT '操作人姓名', `operate_type` varchar(16) NOT NULL COMMENT '操作类型:VIEW/EDIT/DELETE/DOWNLOAD/UPLOAD/PREVIEW', `operate_content` text COMMENT '操作详情:修改字段新旧值、操作描述', `operate_ip` varchar(64) NOT NULL COMMENT '操作IP地址', `operate_ua` varchar(256) DEFAULT NULL COMMENT '操作端UA', `operate_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', PRIMARY KEY (`id`), KEY `idx_archive_id` (`archive_id`), KEY `idx_operate_time` (`operate_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='档案操作留痕日志表'; ```

三、后端核心逻辑实现

3.1 切面依赖与自定义注解

首先引入AOP切面依赖,无需修改现有业务代码即可实现埋点:

```xml org.springframework.boot spring-boot-starter-aop ```

自定义操作留痕注解,用于标记需要留痕的业务接口:

```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ArchiveLog { // 操作类型,对应表中operate_type字段 String operateType(); // 档案ID所在请求参数的位置,默认第0位 int archiveIdIndex() default 0; } ```

3.2 全局切面实现

企业级档案操作留痕全流程实操指南:从搭建到上线可直接复用

以下代码可直接复制使用,所有切面逻辑用try-catch包裹,异常不会影响主业务流程

```java @Aspect @Component public class ArchiveLogAspect { @Autowired private HttpServletRequest request; @Autowired private ArchiveOperationLogMapper logMapper; @Autowired private ArchiveMapper archiveMapper; // 切点:所有标注@ArchiveLog的方法 @Pointcut("@annotation(com.xxx.archive.annotation.ArchiveLog)") public void logPointcut() {} @AfterReturning(pointcut = "logPointcut()", returning = "result") public void afterOperate(JoinPoint joinPoint, Object result) { try { // 获取注解配置 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); ArchiveLog logAnnotation = signature.getMethod().getAnnotation(ArchiveLog.class); String operateType = logAnnotation.operateType(); int archiveIdIndex = logAnnotation.archiveIdIndex(); // 获取档案ID Long archiveId = (Long) joinPoint.getArgs()[archiveIdIndex]; // 获取当前登录用户(需提前在登录拦截器将用户信息存入request attribute) LoginUser loginUser = (LoginUser) request.getAttribute("loginUser"); // 组装日志基础信息 ArchiveOperationLog log = new ArchiveOperationLog(); log.setArchiveId(archiveId); log.setOperateUserId(loginUser.getUserId()); log.setOperateUserName(loginUser.getUserName()); log.setOperateType(operateType); // 编辑操作自动对比新旧字段值 if ("EDIT".equals(operateType)) { Archive oldArchive = archiveMapper.selectById(archiveId); Archive newArchive = (Archive) joinPoint.getArgs()[archiveIdIndex + 1]; String content = compareFieldDiff(oldArchive, newArchive); log.setOperateContent(content); } // 获取操作IP和UA log.setOperateIp(getIpAddress(request)); log.setOperateUa(request.getHeader("User-Agent")); // 写入日志表 logMapper.insert(log); } catch (Exception e) { e.printStackTrace(); } } // 对比两个档案对象的字段差异 private String compareFieldDiff(Archive oldObj, Archive newObj) throws IllegalAccessException { StringBuilder sb = new StringBuilder(); Field[] fields = Archive.class.getDeclaredFields(); // 排除不需要对比的系统字段 List excludeFields = Arrays.asList("id", "createTime", "updateTime", "createBy", "updateBy"); for (Field field : fields) { if (excludeFields.contains(field.getName())) continue; field.setAccessible(true); Object oldVal = field.get(oldObj); Object newVal = field.get(newObj); if (!Objects.equals(oldVal, newVal)) { sb.append(field.getName()).append(":旧值=").append(oldVal).append(",新值=").append(newVal).append(";"); } } return sb.toString(); } // 兼容反向代理场景获取真实IP private String getIpAddress(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } if (ip != null && ip.contains(",")) { ip = ip.split(",")[0].trim(); } return ip; } } ```

使用时只需在业务接口上添加注解即可,示例:@ArchiveLog(operateType = "EDIT", archiveIdIndex = 0)

四、前端补充埋点

针对预览、打印等前端触发无后端接口的操作,可通过全局指令上报留痕,Vue示例代码如下:

```js // 注册全局留痕指令 Vue.directive('archive-log', { bind: function (el, binding) { el.addEventListener('click', () => { const { archiveId, operateType } = binding.value // 调用后端留痕上报接口 axios.post('/api/archive/log/report', { archiveId, operateType }).catch(err => console.log('留痕上报失败', err)) }) } }) // 页面使用: ```

五、验证与兜底配置

5.1 上线前验证

必须覆盖所有操作场景测试:查看、编辑、删除、下载、上传、预览,每个操作后查询留痕表,确认所有字段填充正确,编辑操作的新旧值对比准确

5.2 日志备份兜底

留痕日志不可删除,需配置定时备份,Linux执行crontab -e添加以下配置,每天凌晨1点自动备份:

```shell 0 1 /usr/bin/mysqldump -uroot -p你的数据库密码 库名 sys_archive_operation_log > /data/backup/archive_log_`date +\%Y\%m\%d`.sql ```

六、常见问题排查

  • 获取不到操作人:检查登录拦截器执行顺序是否早于切面,是否将登录用户信息存入request attribute
  • IP获取为127.0.0.1:检查Nginx反向代理配置是否添加proxy_set_header X-Forwarded-For $remote_addr;
  • 编辑操作新旧值对比为空:检查是否错误排除了需要对比的字段,请求参数的新值是否和数据库字段匹配
AI咨询
热线电话

028-85154420

15388110056

全国售前咨询电话

扫码咨询
安答联动微信公众号二维码

微信扫码关注安答联动

申请试用
热线电话
申请试用

安答联动档案管理系统