ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot的档案数字化系统实战:从OCR识别到全文检索与权限安全设计

基于SpringBoot的档案数字化系统实战:从OCR识别到全文检索与权限安全设计 前阵子接了一个档案数字化项目管理系统的开发任务把仓库里积压的几十万卷纸质档案逐步转成电子件再做成一个能检索、能借阅、能审批、能追溯的管理平台。表面看这套系统就是“扫描 存储 搜索”真上手做才发现难点全藏在流程编排、数据一致性、权限控制这些不那么显眼的地方。这篇就把我从需求调研到上线运维踩过的坑、选型的权衡、核心模块的实现思路都梳理一遍给准备做同类系统的朋友一个可参考的底稿。1. 纸质档案管理的真实痛点与数字化系统的需求边界1.1 不数字化业务根本转不动接手前我专门去客户档案室蹲了两天感受非常直接一个拥有几万卷存量档案的单位查一份10年前的合同正常流程是先翻目录本再到铁皮柜里一层层找运气好十几分钟运气不好半天就没了。档案借出去没人还、还回来放错位置、谁看过哪份文件没有记录这些问题在纯纸质管理下几乎无解。客户最初的想法是要一个“能把档案存进电脑”的系统。这个描述太笼统真按这个思路做最后只会得到一个文件管理器。所以我花了大量时间做需求访谈把隐藏在“数字化”三个字背后的真实诉求拆开存量档案能被稳定存储和快速定位而不是堆在硬盘目录里扫描件或照片能自动识别出标题、文号、日期、责任者等关键字段减少人工著录工作量业务人员能按多种条件组合检索例如按文号 年度 档案类型筛出目标文件档案的查看、下载、借阅全程有审批和留痕不能出现“谁拿走了这份档案”这种查无可查的情况电子档案必须和实物档案的编号一一对应目录、卷内文件、页文件三层关系清晰。这些诉求汇总后系统的定位就很明确了它不是一个文件网盘而是一个围绕“档案对象”做全生命周期管理的业务系统。1.2 功能范围怎么划才不会做成四不像档案数字化项目最容易犯的错是把什么功能都往里塞。我这边梳理需求时定了几个边界供同类项目参考第一系统管的是档案目录与电子文件不做无纸化办公的流程审批第二OCR识别是“辅助著录”最终落库字段必须有人工确认环节因为档案字段准确性直接影响后续检索第三系统支持借阅流程但只服务电子档案的线上借阅与审批实物档案的物流跟踪不上线第四第三方系统对接只保留标准接口不直接订制集成到对方的OA里。边界清楚之后技术方案才好做。后面所有模块划分都是在这个范围内展开的。1.3 数字化核心链路一个完整的档案数字化链路大致是实体档案经过高速扫描生成TIFF或PDF经过图像处理去黑边、纠偏、增强再送OCR识别抽出结构化字段人工在界面上校对补录最终归档形成档案主数据。这个链路里面扫描和图像预处理通常由硬件和采集端完成系统侧的核心是把采集端的产物接收进来并把识别、校对、归档、检索这几件事做成可跟踪的流程。我在设计时把流程分成了5个状态待处理、识别中、待校对、已归档、已作废。每个档案对象在数据库里带一个状态字段所有操作都围绕状态流转来做既是业务流程的真实映射也是后面排查问题的抓手。2. SpringBoot在档案管理系统里的角色与技术架构选型2.1 为什么SpringBoot仍然是这类后端项目的主力选择这套系统需要快速交付、长期维护、人员更替后还能接得上。SpringBoot的优势刚好匹配起步依赖帮我们省掉了一堆版本协调问题内置Tomcat让部署简单生态里MySQL、Redis、Elasticsearch、模板引擎、权限框架都有成熟整合方案。更重要的是团队成员对SpringBoot的熟悉程度普遍偏高后续接手的成本低。我在选型时也对比过Spring Cloud微服务方案结论是现阶段没必要。档案系统的并发量通常不会特别夸张单体应用配合合理的模块拆分和缓存设计运维成本远低于微服务。哪怕后续压力上来了SpringBoot单体也可以先做垂直扩展再把OCR、检索这类重模块独立成服务演进路径是通的。2.2 整体技术栈最终确定的技术栈如下层级选型说明开发框架SpringBoot 2.7系列稳定版本避免过高版本在特定环境中的兼容问题持久层MyBatis-Plus常规单表操作不用手写SQL复杂统计用注解SQL数据库MySQL 8.0主要业务数据存储InnoDB引擎缓存Redis 7.x验证码、热数据缓存、分布式锁检索Elasticsearch 7.x档案全文检索与MySQL数据双写文件存储MinIO 本地磁盘备份电子档案原件存储接口安全Sa-Token轻量鉴权方案角色权限控制OCR引擎PaddleOCR自建服务数据不落第三方平台满足涉密要求在线预览kkFileView文档转图片预览支持水印叠加前端我这边用的是Vue3 Element Plus部署时打成静态包放进SpringBoot的static目录一套jar搞定前后端客户部署时不用单独配Nginx省了很多售后沟通。2.3 核心数据表设计档案系统的数据模型说复杂也复杂但核心表就那么几张。我把最关键的表结构放出来大家可以根据自己的业务扩展。第一张是档案主表t_archive它承载的是档案的目录信息。CREATE TABLE t_archive ( id BIGINT AUTO_INCREMENT PRIMARY KEY, archive_code VARCHAR(64) NOT NULL COMMENT 档号, archive_type VARCHAR(32) NOT NULL COMMENT 档案类型如合同/文书/人事, title VARCHAR(500) NOT NULL COMMENT 题名, document_no VARCHAR(128) DEFAULT NULL COMMENT 文号, author VARCHAR(256) DEFAULT NULL COMMENT 责任者, archive_date DATE DEFAULT NULL COMMENT 归档日期, drawer VARCHAR(64) DEFAULT NULL COMMENT 实体存放柜号, security_level TINYINT DEFAULT 3 COMMENT 密级1绝密2机密3秘密4普通, status TINYINT DEFAULT 0 COMMENT 流程状态, page_count INT DEFAULT 0 COMMENT 页数, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_archive_code (archive_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT档案主表;第二张是电子文件表t_archive_file记录每个档案下挂的电子文件一个档案可多文件。第三张是OCR识别任务表t_ocr_task记录扫描件识别任务状态、引擎返回的原始JSON、校对结果、操作人。第四张是操作日志表t_operation_log记录所有关键操作包括谁在什么时间查看了哪份档案、下载了哪个文件、审批了哪条借阅申请。这些表之间通过archive_id关联尽量保持表结构简单不搞复杂的继承关系。档案业务最忌讳把全部字段塞一张宽表因为档案种类多、属性差异大宽表后期会变成字段垃圾场。2.4 模块划分后端我用的是标准的四层分包但针对业务做了聚合controller接收参数、做基础校验、调用serviceservice业务编排事务管理状态流转mapper数据访问复杂查询走XMLintegrate外部接口适配层包括OCR引擎客户端、文件存储客户端、ES索引同步组件。domain包下面放业务实体dto包放接口出参入参枚举类单独收拢。这套结构不花哨但每个人都看得懂后续维护的人不会骂我。3. 核心功能模块的实现细节与设计逻辑3.1 大文件分片上传与断点续传档案扫描件单个文件经常上百MB直接用MultipartFile一次性接收内存和网络都会被压垮一旦中断就得从头传。我这边实现了分片上传方案整体逻辑是前端把文件切成每片5MB计算整个文件的MD5后先调后端接口检查是否已存在秒传逻辑。如果不存在就逐片上传每片带上传批次号和分片序号全部传完后后端合并分片并校验MD5是否一致。分片上传的核心代码可以抽象成下面这样PostMapping(/upload/chunk) public Result uploadChunk(RequestParam(file) MultipartFile chunk, RequestParam(batchId) String batchId, RequestParam(chunkIndex) Integer chunkIndex) { // 1. 将分片写入临时目录目录按 batchId 隔离 Path tmpDir Paths.get(uploadRoot, batchId); chunk.transferTo(tmpDir.resolve(chunkIndex .part)); // 2. 记录分片状态到 Redis redisTemplate.opsForSet().add(RedisKey.chunkKey(batchId), chunkIndex); return Result.ok(); } PostMapping(/upload/merge) public Result merge(RequestParam(batchId) String batchId, RequestParam(fileName) String fileName, RequestParam(md5) String md5) { // 1. 按序号读取所有分片合并输出到正式目录 // 2. 比对MD5不一致则删除半成品返回错误 // 3. 删除临时分片文件清除Redis分片记录 // 4. 创建档案文件记录状态置为待识别 }这个方案有几处容易踩坑一是分片合并时要按序号排序后逐个写入不能靠文件名排序的字符串默认顺序否则10会在9前面二是MD5校验务必保留网络传输偶发分片损坏的概率在实际使用中一点不低三是传完合并阶段建议加一个分布式锁防止前端重复点击触发两次合并。3.2 OCR识别链路异步任务与状态机OCR方案上我最早试用过Tesseract识别中文印刷体的效果一般对扫描件的版式适应性偏弱。后来选型时的场景是档案数据不能出内网所以公有云OCR接口首先排除最终用了PaddleOCR做私有化部署以HTTP服务形式供业务系统调用。系统侧接收完扫描文件后会创建一条OCR任务记录然后异步交给线程池处理。这里有一个关键设计识别任务必须能重试、能追踪、能人工干预。我的做法是引入一张任务表和一套极简状态机初始状态为PENDING提交线程池后置为RUNNING识别成功置为SUCCESS识别失败且可重试时置为FAILED并插入延迟队列等待重试重试超过3次置为DEAD提醒人工处理。异步配置上我没有直接使用默认的SimpleAsyncTaskExecutor因为它在高并发下会无限制创建线程很容易把数据库和内存拖垮。线程池参数我按机器配置的4核8G做了调优Bean(ocrTaskExecutor) public ThreadPoolTaskExecutor ocrTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(200); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix(ocr-worker-); executor.setRejectedExecutionHandler(new CallerRunsPolicy()); executor.initialize(); return executor; }线程名前缀在排查问题时非常有用线上看日志能直接看出来是哪个线程在处理OCR拒绝策略选了CallerRunsPolicy意思是队列满了之后由提交线程自己执行任务起到天然背压的作用不会直接抛异常丢任务。OCR引擎返回的原始结果是一段JSON包含识别文本块以及每个文本块的位置信息。我没让引擎结果直接落档案主表而是先把原始JSON存到OCR任务表里随后用解析规则抽取题名、文号、日期、责任者等字段抽不出来的字段留给人工在“待校对列表”补录。这一步为什么要这么做因为识别一定会出错尤其扫描件盖章遮挡、字体非标准的时候直接落库会把错误数据污染到正式档案里后期去改主数据比登天还难。3.3 全文检索从MySQL优先方案到适当时机引入ES档案检索场景有个典型特征用户经常只记得文号片段、题名里的几个词或者某个人名组合条件也灵活。MySQL的LIKE %关键词%在几十万数据量下就会出现全表扫描的问题更别提多条件OR组合了。我先在小批量验证阶段用MySQL做检索加上ngram全文索引勉强能跑但数据量涨到几十万多关键词检索明显变慢。于是我在架构上预留了ES索引同步方案实现逻辑是档案主表数据变更时通过事件或定时任务把增量数据同步到ES检索接口优先走ES详情数据回表MySQL。下面是简化版的索引定义{ settings: { analysis: { analyzer: ik_max_word } }, mappings: { properties: { archiveCode: { type: keyword }, title: { type: text, analyzer: ik_max_word }, documentNo: { type: keyword }, author: { type: text, analyzer: ik_max_word }, archiveDate: { type: date, format: yyyy-MM-dd }, archiveType: { type: keyword }, securityLevel: { type: integer }, contentOcr: { type: text, analyzer: ik_max_word } } } }这里要特别提醒不要把待校对的OCR结果直接进ES。我在实际项目里吃过亏OCR原始文本质量不高进索引之后检索出来的内容错漏百出用户会以为系统抽风。后来改成只有状态为“已归档”的档案才允许进ES检索体验才稳定下来。如果不想一上来就上ES可以用MySQL的全文索引做过渡但字段类型要设置合理比如用ngram分析器支持中文检索。这个方案胜在零额外组件缺点是很吃CPU、相关度排序能力弱数据量超过50万后建议趁早换。3.4 档案在线预览与水印档案文件的原始格式包括PDF、TIFF、JPG、PNG等。TIFF这种格式浏览器直接打不开在线预览必须先转换。我接的是kkFileView它会调用LibreOffice把Office文档转成PDF再把PDF按页渲染成图片输出到前端。预览页必须加水印这是档案系统的硬性要求。我的做法是在图片预览层做前端水印根据登录用户姓名、工号、当前时间生成水印字符串铺满预览区域。这个方案实现简单但防不住技术大牛直接调接口拿原文件。所以在文件下载接口上我同样加了水印处理下载前动态生成带水印的副本原始文件不直接暴露。代价是下载大文件时多一次生成副本的IO开销但安全性高很多。这里有一个性能细节预览图片每次实时转换会打满CPU必须做缓存。我这边采用两级缓存转换完成后的图片文件存本地临时目录文件路径画像文件MD5 页数作为缓存key内存里再放一层热点文件映射避免同一个文件被反复读取磁盘。4. 权限模型、借阅审批和数据安全档案系统的生命线4.1 数据级权限不是简单靠角色切档案系统的权限比普通管理后台复杂的地方在于即使两个用户都是“档案管理员”角色一个只能看本部门的人事档案另一个能看全库的合同档案角色相同但数据范围不同。单纯用RBAC表结构实现不了这种控制。我这边做了“角色 数据范围”组合模型。角色表还是传统的t_role但角色上挂了数据范围字段枚举包括本人、本部门、本部门及下级部门、全库。查询档案列表时MyBatis的拦截器会往SQL里拼接数据权限条件例如当前用户Id为1001有“本部门”权限就自动追加AND (archive.create_dept_id #{currentDeptId} OR archive.create_user_id #{currentUserId})这么做的麻烦点在于一旦数据权限条件拼错会导致越权数据被查出来这是档案系统最严重的事故。所以我在数据权限拦截器上加了严格的单元测试针对不同范围枚举都准备了阳性、阴性用例。4.2 密级与档案类型的双重控制光有数据范围还不够档案本身有密级。系统里我把密级分成四级绝密、机密、秘密、普通。查看规则是用户密级大于等于档案密级才允许查看。这里的“大于等于”是指密级数值越小权限越大所以逻辑判断是用户密级数值 档案密级数值这块代码建议单独抽一个工具类防止多个业务方法里各写各的判断。另外档案类型也可以做独立的可见性控制。比如人事档案和业务合同可能分属不同业务条线管理用户即便有全库的数据范围也不一定需要看到所有类型的档案。我最终的做法是数据范围控制行粒度档案类型控制列范围两者取交集。4.3 借阅审批流程的状态设计借阅是档案系统的高频业务。我设计的核心表结构包含借阅申请表和审批记录表。借阅申请创建后状态为PENDING提交给指定审批人审批人通过后状态变为APPROVED用户可在有效期内反复预览和下载到期后状态自动变为EXPIRED。实际开发中容易被忽略的地方有两个第一个是审批人超时未处理。我加了一个定时任务每天扫描超过3天未审批的申请单给审批人推送提醒超过7天的自动挂起并通知申请人。没有这个机制借阅申请会堆死在审批人手里用户体感会非常差。第二个是下载次数控制。有些档案只允许借阅人下载一次我就在借阅单上存了remaining_download_count每次下载调用时做原子扣减int remain borrowedRecordService.deductDownloadCount(borrowId); if (remain 0) { throw new BizException(下载次数已用完); }这里务必用数据库原子更新不能先查后改否则并发下肯定超发。4.4 操作审计给每份档案配上行为轨迹档案利用留痕是这家单位合规要求里的核心项任何查看和下载都要能回答“谁、什么时间、看了什么、在哪看的”。我用AOP做了统一审计切面注解标注关键方法AuditLog(action PREVIEW, resourceType ARCHIVE) public ViewResult previewArchive(Long archiveId, Long fileId) { // 业务逻辑 }切面里统一记录操作人、操作时间、操作IP、目标档案ID、文件ID、操作结果。存储上我按月分表t_operation_log_202602这种方式能防止日志表无限膨胀查询近期记录时也更快。审计数据不要只依赖业务代码里手动打点因为容易漏。AOP方案好处是切面统一新增接口时只要记得加注解日志自然就出来了。坏处是注解如果忘了加那么这个操作就漏审计了所以我在代码评审里把“关键操作是否加了审计注解”作为硬指标。5. 实战中遇到的最典型问题与性能优化记录5.1 档案列表深分页查询慢的排查系统上线两个月档案量到了30万条管理后台翻到第500页时接口耗时从200ms涨到3秒多。一眼看过去就是MySQL的LIMIT offset, size问题MySQL查深分页时要先扫描并丢弃前offset条记录offset越大越慢。我做了两个层面的优化第一层管理后台列表查询要求必须带筛选条件禁止无条件翻到深页第二层把深分页场景改成基于游标的分页即排序字段必须是有序的唯一键前端传lastId作为查询条件后端用WHERE id lastId ORDER BY id LIMIT size这个方案可以将查询耗时稳定在几十毫秒。不过游标分页只适用于列表上下翻页不适用于跳页。档案查询场景里用户很少直接跳到第300页绝大多数是搜到一批结果后翻几页所以游标方案完全够用。5.2 OCR批量导入时的内存溢出与任务堆积第一次压测时并发提交300个档案批次每个批次含50页扫描件OCR线程池瞬间塞满。我原本以为有CallerRunsPolicy就能兜住结果因为每页扫描件是几百MB的大型图片识别时Python后端做图像矩阵运算占用大量内存Docker容器内存只有8G直接OOM了几次。排查思路不是只看SpringBoot应用日志而是先看容器整体的CPU、内存和IO。我后来做了三件事一是控制入库速率前端批量导入时限制同时上传批次数量后端接收文件接口做了流量控制超出并发时返回“系统忙碌请稍后重试”二是按单页处理顺序不再把一个档案的所有页一次性塞进识别队列而是逐页分发识别任务每页识别结果落库后再标记完成三是给JVM和OCR服务分别设置合理内存参数SpringBoot的-Xmx调成2GOCR服务限制单进程最大内存防止互相挤占。从这次排障我学到一个原则处理重IO任务的系统瓶颈往往不在应用代码而在资源隔离与背压机制早做流量控制比事后扩容省心得多。5.3 文件存储选型与备份策略档案原件的存储我建议优先使用对象存储本地磁盘用传统路径存储有几个麻烦磁盘满了要人工清理、RAID磁盘损坏恢复麻烦、单个文件目录文件数太多后IO性能下降。我这边用了MinIO它在内网环境部署很轻量兼容S3协议后续要迁移到公共云OSS也方便。不过对象存储不是银弹我也遇到过一个坑MinIO默认的纠删码模式需要至少4块磁盘才能跑起来第一次部署图省事只挂了两块盘结果服务起不来。所以生产环境一定要按官方要求规划磁盘数量。备份策略上档案系统和其他系统不太一样既要有数据库的备份也要有文件存储的备份。我配置的是MinIO存储桶之间定时同步同步任务放在凌晨执行同时对关键档案目录做异机冷备。没有这层保护一旦存储节点出故障几十万份电子档案就全没了这是不能承受的损失。5.4 SpringBoot与周边依赖的版本坑最后说说SpringBoot版本本身。我这里选的是2.7系列有一个很现实的考虑很多档案管理系统要对接的内网组件数据库驱动、打印机、政府单位的老版中间件更新很慢SpringBoot 3.x要求JDK17起步不少内网环境还停留在JDK8。硬上3.x只会给自己找麻烦。另外有几次遇到依赖冲突的问题比如引了spring-boot-starter-web又手动引入了一个旧版的Jackson结果接口返回的日期格式全部异常。排查的时候用mvn dependency:tree看依赖关系把多余的直接排掉这类问题大多能迅速解决。还有一个容易踩的坑是使用MyBatis-Plus时把分页插件和自定义拦截器同时配置分页插件执行顺序不对会导致count查询失效。解决办法是给分页插件设置正确的order并确保自定义拦截器不影响分页SQL改写。5.5 在线预览中文乱码的处理预览PDF时遇到过中文全部变成方块的惨案根因是环境缺少中文字体。LibreOffice渲染时找不到中文字体就会变成豆腐块。排查过程很直接进容器里执行fc-list看字体列表发现里面只有少数英文字体于是把系统里的思源黑体或文泉驿字体文件拷贝进容器并刷新字体缓存问题立即解决。这类环境问题很容易被忽略好记性不如烂笔头我建议所有部署文档里都要包含字体检查这一项尤其是面向内网客户交付时服务器环境五花八门不提前固化检查项上线后会被各种环境问题折磨。6. 上线后的运维经验与几个值得预埋的能力系统上线不代表事情结束反而是一系列运维问题的开始。档案系统有一个特点日常使用频率较低但集中导入期间压力极大。平时可能只有几十个并发用户导入时却可能同时提交几万页扫描件。所以监控指标里不能只看平均负载还要关注高峰期队列积压量。我的监控页面上固定放了几个指标OCR任务积压数、文件上传成功率、ES同步延迟、磁盘剩余容量、MinIO存储桶健康状态。其中OCR积压数是预警重点超过阈值就说明识别速度跟不上了这时候需要考虑增加OCR工作节点。另外建议提前预埋的能力是统计报表。档案系统运营一段时间后客户一定会问今年新增了多少卷档案按类型分布是什么样各科室的利用次数排名这些需求最好在数据模型设计阶段就预留报表查询入口比如在档案主表加创建科室字段、归档年度字段不然等报表需求来时再补数据补起来非常痛苦。最后提一个容易被忽略的点档案系统的数据字典要提前固化和版本管理。档案类型、密级、保管期限、利用方式这些枚举在业务里到处引用如果散落在代码各个位置后期改一处漏一处。我统一用枚举类加数据库字典表双份维护代码里写枚举页面下拉框的数据从字典表读两边通过编码关联这样既保证编译期安全又支持运营人员自助维护字典项。这套系统从立项到稳定运行大概花了一个季度最大的体会是档案数字化项目管理和普通CRUD系统在技术难度上没高到天上去但它对数据准确性、权限安全、操作留痕的要求极为严格这些软性指标才是项目真正的技术难点。如果让我重新做一次我会更早地引入ES、更早地做流量控制、更早地和客户确认密级体系的细粒度规则。这些方向走得越早后面返工越少。希望这篇记录能给正在做同类系统的你省掉几晚加班。
返回列表