轻量级电子文件档案管理系统:符合GB/T 18894的四性保障实践

发布时间:2026/10/27 2:08:07
轻量级电子文件档案管理系统:符合GB/T 18894的四性保障实践

简介:本资源是一套完整的本科毕业设计项目——电子文件档案管理系统,面向计算机及相关专业本科生,解决机构或个人电子文档的规范化存储、权限化访问与全生命周期管理问题。压缩包含151个文件,涵盖93个编译后class文件、22个GUI窗体(form)、18个Java源码、3个SQL建表与初始化脚本、5个Word格式毕业论文及英文翻译文档,以及jar依赖、properties配置等,总大小3.24MB,结构清晰,便于按模块学习调试。已有606人下载学习,适合课程设计、毕设参考与Java Web技术栈综合实践。读者可直接运行系统,掌握Spring Boot或类似框架下的前后端协同开发、MySQL数据库设计(含用户/文件/权限三类核心表)、基于角色的细粒度权限控制、文件上传与版本管理、登录Token鉴权及基础安全防护等关键能力,配套论文与翻译更便于答辩准备与学术表达提升。

1. 这不是又一个“登录+增删改查”的档案 demo:它真能管住你单位里散落在 U 盘、微信、邮件里的合同扫描件、会议纪要和红头文件

某高校信息中心去年做了一次摸底:全单位近 3 年产生的非结构化电子文件(PDF 扫描件、Word 修订稿、Excel 表格、JPG 签字页)超 12 万份,分散在 47 台办公电脑、8 个部门微信群、3 个私人网盘链接里。当审计组要求调取 2022 年第 3 季度全部采购合同的原始签署版时,行政科花了 3 天时间才凑齐 62% 的文件,其中 11 份是用手机翻拍的模糊照片。这个本科毕设项目——电子文件档案管理系统,就是从这种真实混乱里长出来的。它不是教科书式的 CRUD 演示,而是一套带完整元数据建模、四性保障(真实性、完整性、可用性、安全性)、符合《GB/T 18894-2016 电子文件归档与电子档案管理规范》基础逻辑的轻量级落地实现。前端用 Vue 2 + Element UI 做出符合政务场景的树形分类导航和双栏预览;后端 Spring Boot 集成 Apache POI 解析 Office、PDFBox 提取 PDF 文本、Tesseract OCR 处理图片型扫描件;数据库用 MySQL 8.0 设计了file_info(主表)、file_metadata(扩展属性)、file_version(版本链)、access_log(操作审计)四张核心表。它适合中小型机关、学校、国企二级单位快速部署,也足够作为档案信息化课程设计、信管专业毕设的完整技术脚手架——代码有逐行中文注释,数据库含初始化数据,连毕业论文 Word 和 LaTeX 源码、英文摘要翻译都打包好了,不是“能跑就行”,而是“跑完就能交”。

2. 环境准备与一键启动:从解压到首页登录,5 分钟内完成全流程验证

2.1 开发环境最低配置与依赖确认

这个系统对运行环境的要求非常务实:不强求 Docker 或云原生,一台 4 核 8G 内存的开发机或测试服务器就足够。关键不是硬件多高,而是几个依赖版本必须对齐,否则你会在mvn clean install阶段卡死在tessdata加载失败或pdfbox字体渲染异常上。我建议你先执行三步验证:

  1. Java 版本:必须为 JDK 1.8.0_292 或更高(注意不是 JRE),执行java -version,输出中应含1.8.0_292或类似。Spring Boot 2.3.x 不兼容 JDK 11+ 的模块化机制,强行升级会导致org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration类找不到。
  2. MySQL 8.0.26+:重点检查default_authentication_plugin是否为caching_sha2_password。如果用的是旧版客户端(如 Navicat 12 或 MySQL Workbench 6.x),连接会报Client does not support authentication protocol。解决方案不是降级 MySQL,而是在 MySQL 配置文件my.cnf的[mysqld]下加一行default_authentication_plugin=mysql_native_password,然后重启服务。
  3. Tesseract OCR 引擎:这是全文检索能力的底层支撑。Windows 用户直接下载 tesseract-ocr-w64-setup-v5.3.3.20231005.exe 安装即可;Linux 用户用apt install tesseract-ocr(Ubuntu/Debian)或yum install tesseract(CentOS/RHEL)。安装后执行tesseract --version,确保输出包含5.3.3。别跳过这步——没有它,所有 JPG/PNG 扫描件将无法被搜索到内容。

提示:如果你只是想快速验证功能,不打算二次开发,推荐使用项目根目录下的run-local.sh(Linux/macOS)或run-local.bat(Windows)。它已内置 H2 内存数据库配置,跳过 MySQL 安装步骤,5 分钟内就能看到登录页。

2.2 数据库初始化:不只是执行 SQL,关键是理解表结构设计意图

项目附带的sql/efams_init.sql不是简单的一堆CREATE TABLE。它体现了电子文件管理的核心逻辑分层。你不需要手动一条条执行,但必须理解每张表的职责,否则后续改字段会踩坑。以下是四张主表的初始化逻辑说明:

-- 1. file_info:文件实体主干,存储最基础、不可变的标识信息 CREATE TABLE `file_info` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `file_code` varchar(64) NOT NULL COMMENT '档号(唯一业务编码,如 ZY2023-001)', `file_name` varchar(255) NOT NULL COMMENT '原始文件名(含扩展名)', `file_size` bigint NOT NULL DEFAULT '0' COMMENT '字节大小', `file_type` varchar(32) NOT NULL COMMENT 'MIME类型(application/pdf, image/jpeg)', `storage_path` varchar(512) NOT NULL COMMENT '服务器绝对路径(/opt/efams/files/2023/10/ZY2023-001_v1.pdf)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(1-待归档, 2-已归档, 3-已销毁)', PRIMARY KEY (`id`), UNIQUE KEY `uk_file_code` (`file_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件基本信息表'; -- 2. file_metadata:动态元数据容器,支持自定义字段扩展 CREATE TABLE `file_metadata` ( `id` bigint NOT NULL AUTO_INCREMENT, `file_id` bigint NOT NULL COMMENT '关联file_info.id', `key_name` varchar(64) NOT NULL COMMENT '元数据键(如"责任者", "成文日期", "密级")', `key_value` text COMMENT '元数据值(支持长文本)', `data_type` varchar(16) NOT NULL DEFAULT 'string' COMMENT '数据类型(string, date, number)', PRIMARY KEY (`id`), KEY `idx_file_id` (`file_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件元数据扩展表';

为什么这样设计?因为真实业务中,“密级”可能是下拉选项(绝密/机密/秘密),“成文日期”是标准日期格式,“责任者”是字符串,而“相关附件”可能是一个 JSON 数组。file_metadata表用key-value结构,避免了为每个可能的字段都建一列,也方便后期通过后台管理界面动态添加新元数据项。file_version表则记录每次修改的快照,access_log记录谁在什么时间下载/预览/修改了哪个文件——这些不是锦上添花,而是审计合规的硬性要求。

2.3 后端服务启动:绕过 Maven 仓库污染的本地构建技巧

项目使用 Maven 管理依赖,但pom.xml中部分私有仓库地址(如<repository><url>http://nexus.xxx.edu.cn/repository/maven-public/</url></repository>)在你本地并不存在。直接mvn clean package会卡在Downloading from xxx-nexus: http://...。正确做法是临时屏蔽这些仓库,并启用阿里云公共镜像:

  1. 打开pom.xml,找到<repositories>和<pluginRepositories>标签块,将其整体注释掉(用<!-- -->包裹);
  2. 在<properties>标签下,添加:
    <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  3. 在项目根目录创建settings.xml(仅本次构建用),内容如下:
    <?xml version="1.0" encoding="UTF-8"?> <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <mirrors> <mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors> </settings>
  4. 执行构建命令:
    mvn clean package -s settings.xml -Dmaven.test.skip=true
    -Dmaven.test.skip=true是关键:项目单元测试里有几个依赖本地 Tesseract 可执行文件的测试用例,若你的环境未装 OCR,它们必失败,跳过可保构建成功。

构建成功后,进入target/目录,你会看到efams-0.0.1-SNAPSHOT.jar。执行:

java -jar efams-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev

控制台出现Started EfamsApplication in X.XXX seconds即代表后端启动成功。此时访问http://localhost:8080,你应该能看到登录页。

2.4 前端资源编译与 Nginx 代理配置要点

前端代码位于src/main/resources/static/目录下,这是一个已编译好的 Vue 2 单页应用(SPA)静态资源包,无需安装 Node.js 或执行npm run build。但如果你需要修改前端(比如调整菜单顺序、增加按钮),就必须进入frontend/目录(项目根目录下独立的 Vue 工程)进行开发。

常见错误是直接把frontend/dist/下的文件拷贝到static/就以为完事了。错!Vue Router 默认使用history模式,这意味着/dashboard这样的 URL 在 Nginx 直接访问时会 404(因为服务器找不到dashboard这个物理文件)。解决方案是配置 Nginx 的location规则:

server { listen 80; server_name localhost; location / { root /path/to/your/efams/target/classes/static; try_files $uri $uri/ /index.html; # 关键:所有未匹配的请求都指向 index.html } # API 接口反向代理到后端 location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

try_files $uri $uri/ /index.html;这行是玄学所在——它让 Vue Router 的前端路由生效。没有它,你点击左侧菜单切换页面,浏览器地址栏变了,但页面内容卡在登录页不动。这是新手最常翻车的点之一,务必检查。

3. 核心模块拆解:一个 PDF 文件从上传到可全文检索的完整链路

3.1 文件上传与元数据自动提取:不只是存文件,更是建档案

用户在前端点击“新增档案”,选择一个 PDF 文件后,触发的不是简单的multipart/form-data提交。后端FileUploadController.java的uploadFile方法会启动一个完整的处理流水线:

@PostMapping("/upload") public Result uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("category") String category, @RequestParam(value = "metadata", required = false) String metadataJson) { // 1. 基础校验:大小、类型、档号唯一性 if (file.getSize() > 100 * 1024 * 1024) { // 100MB 限制 return Result.fail("文件大小不能超过100MB"); } String fileCode = generateFileCode(category); // 生成档号,如 "XZ2023-001" if (fileInfoService.existsByFileCode(fileCode)) { return Result.fail("档号 " + fileCode + " 已存在,请检查分类是否正确"); } // 2. 保存物理文件到磁盘(按年月分目录,防单目录文件过多) String storagePath = fileStorageService.saveFile(file, fileCode); // 3. 创建 file_info 主记录 FileInfo fileInfo = new FileInfo(); fileInfo.setFileCode(fileCode); fileInfo.setFileName(file.getOriginalFilename()); fileInfo.setFileSize(file.getSize()); fileInfo.setFileType(file.getContentType()); fileInfo.setStoragePath(storagePath); fileInfo.setStatus(FileStatus.WAITING_ARCHIVE.getCode()); // 待归档 Long fileId = fileInfoService.save(fileInfo); // 4. 异步触发元数据提取与 OCR(关键!避免阻塞上传响应) CompletableFuture.runAsync(() -> { try { // a) 提取 PDF 元数据(作者、标题、创建时间) Map<String, String> pdfMeta = pdfMetadataExtractor.extract(storagePath); metadataService.saveBatch(fileId, pdfMeta, "pdf"); // b) 如果是扫描型 PDF(无文本层),调用 Tesseract OCR if (pdfTextDetector.isScannedPdf(storagePath)) { String ocrText = tesseractOcrService.recognize(storagePath); metadataService.save(fileId, "full_text_content", ocrText, "text"); } } catch (Exception e) { log.error("元数据提取失败,fileId: {}", fileId, e); } }); return Result.success("上传成功,正在后台处理元数据..."); }

这段代码的精妙之处在于第 4 步:元数据提取与 OCR 是异步执行的。用户上传完立刻看到“上传成功”,体验流畅;而耗时的 OCR 过程在后台线程池中默默进行。pdfMetadataExtractor使用 PDFBox 的PDDocumentInformation类读取 PDF 内置属性;pdfTextDetector则通过PDFTextStripper.getText()尝试提取文本,若返回空字符串,则判定为扫描件。tesseractOcrService.recognize()最终调用系统命令tesseract input.pdf stdout -l chi_sim+eng,并将识别结果存入file_metadata表,key_name为full_text_content。这意味着,哪怕你上传的是一张 JPG 合同扫描件,几秒后它也能被“合同金额”、“甲方名称”等关键词搜出来。

3.2 四性保障机制:真实性、完整性、可用性、安全性的代码落点

电子档案的“四性”不是口号,而是写在代码里的硬约束。我们逐一看它在哪实现:

  • 真实性(Authenticity):靠数字签名与哈希值。FileIntegrityService.java在文件入库后立即计算 SHA-256 值:

    public String calculateFileHash(String filePath) throws IOException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); try (InputStream is = Files.newInputStream(Paths.get(filePath))) { byte[] buffer = new byte[8192]; int len; while ((len = is.read(buffer)) != -1) { digest.update(buffer, 0, len); } } return Hex.encodeHexString(digest.digest()); }

    该哈希值存入file_info.integrity_hash字段。用户在详情页点击“验证文件完整性”,系统会重新计算当前文件哈希并与数据库值比对,不一致则弹窗警告“文件可能已被篡改”。

  • 完整性(Integrity):体现在file_version表的设计。每次用户对文件进行“替换新版本”操作,系统不会覆盖原文件,而是:

    1. 将原文件重命名为xxx_v1.pdf,新文件存为xxx_v2.pdf;
    2. 在file_version表插入一条记录,记录file_id,version_number,operator_id,update_time,reason(修改原因);
    3. file_info.storage_path字段更新为新路径,但旧版本文件物理保留。这保证了历史版本可追溯、可回滚。
  • 可用性(Usability):由FilePreviewService.java支撑。它不依赖第三方在线预览服务(如 OnlyOffice),而是用 Java 自研方案:

    • PDF:用 PDFBox 渲染为 PNG 缩略图(PDPage.convertToImage()),前端用<img>标签展示;
    • Office:用 Apache POI 提取文本内容,前端用<pre>标签高亮显示;
    • 图片:直接<img src="/api/file/download?id=xxx">;
    • 视频/音频:提供下载链接,不强制在线播放(规避版权与性能风险)。
  • 安全性(Security):体现在三层过滤:

    1. 上传过滤:FileUploadController中checkFileType()方法白名单校验file.getContentType(),只允许application/pdf,image/*,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document等;
    2. 存储隔离:file_storage_service.saveFile()方法将文件存入./files/{year}/{month}/目录,且文件名被重命名为UUID.randomUUID().toString() + "_" + originalName,彻底杜绝路径遍历(../../../etc/passwd)和文件名注入;
    3. 下载鉴权:FileDownloadController.downloadFile()方法中,先查file_info表确认status=2(已归档),再查user_access_log表确认当前用户对该文件有READ权限(权限模型基于 RBAC,角色-权限-用户三张表),最后才返回ResponseEntity<Resource>。

3.3 全文检索引擎:Elasticsearch 集成与中文分词实战

项目默认使用 MySQL 的FULLTEXT索引做轻量级搜索,但生产环境强烈建议切换到 Elasticsearch。pom.xml中已引入spring-boot-starter-data-elasticsearch,且config/ElasticsearchConfig.java完成了自动配置。关键在于中文分词——ES 默认的standard分词器对中文是“按字切分”,搜“合同”会匹配“合”、“同”两个单字,毫无意义。

解决方案是集成ik分词器。你需要:

  1. 下载与 ES 版本匹配的ik插件(如 ES 7.10.2 对应elasticsearch-analysis-ik-7.10.2.zip);
  2. 解压到 ES 的plugins/ik/目录;
  3. 重启 ES。

然后,在ElasticsearchService.java中创建索引时指定ik_max_word分词器:

public void createIndexIfNotExists() { CreateIndexRequest request = new CreateIndexRequest("efams_files"); request.settings(Settings.builder() .put("index.number_of_shards", 1) .put("index.number_of_replicas", 0) ); request.mapping("{\n" + " \"properties\": {\n" + " \"file_code\": {\"type\": \"keyword\"},\n" + " \"file_name\": {\"type\": \"text\", \"analyzer\": \"ik_max_word\"},\n" + " \"full_text_content\": {\"type\": \"text\", \"analyzer\": \"ik_max_word\"},\n" + " \"create_time\": {\"type\": \"date\"}\n" + " }\n" + "}", XContentType.JSON); try { client.indices().create(request, RequestOptions.DEFAULT); } catch (IOException e) { log.warn("索引创建失败,可能已存在", e); } }

ik_max_word是“最细粒度分词”,它能把“中华人民共和国”切分为["中华人民共和国", "中华人民", "中华", "华人", "人民", "共和国", "人民", "共和", "国"],极大提升召回率。搜索时,SearchRequest构造如下:

SearchRequest searchRequest = new SearchRequest("efams_files"); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, "file_name", "full_text_content") .type(MultiMatchQueryBuilder.Type.BEST_FIELDS)); sourceBuilder.from((page - 1) * size).size(size); searchRequest.source(sourceBuilder);

这里用multiMatchQuery在file_name和full_text_content两个字段同时搜索,BEST_FIELDS类型会为每个字段单独打分后取最高分,比CROSS_FIELDS更适合我们的场景(文件名和内容是独立语义)。

4. 避坑指南:上线前必须排查的 5 个血泪经验

4.1 现象:上传 JPG 扫描件后,搜索“合同金额”无结果

原因:Tesseract OCR 引擎未正确安装或语言包缺失。项目默认配置tesseract -l chi_sim+eng,但chi_sim.traineddata文件未放入tessdata目录。Linux 下路径通常是/usr/share/tesseract-ocr/4.00/tessdata/,Windows 下是C:\Program Files\Tesseract-OCR\tessdata\。
解决:去 https://github.com/tesseract-ocr/tessdata 下载chi_sim.traineddata和eng.traineddata,放入对应tessdata目录。执行tesseract --list-langs应输出chi_sim和eng。

4.2 现象:登录后左侧菜单为空,F12 查看 Network 发现/api/menu返回 401

原因:前端axios请求拦截器中,AuthorizationHeader 的 token 格式错误。后端JwtTokenFilter.java要求 Header 为Bearer eyJhbGciOiJIUzI1NiIsInR5c...,但前端代码request.headers.Authorization = 'JWT ' + token写成了JWT而非Bearer。
解决:打开src/main/resources/static/js/request.js,找到service.interceptors.request.use部分,将JWT改为Bearer。注意空格不能少。

4.3 现象:MySQL 初始化成功,但登录后所有列表页显示“暂无数据”,日志无报错

原因:application-dev.yml中spring.datasource.url的useSSL=false&serverTimezone=Asia/Shanghai参数被注释或遗漏。MySQL 8.0 默认要求 SSL 连接,若未禁用,JDBC 驱动会静默失败,连接池获取不到连接,MyBatis 查询返回空集合。
解决:检查src/main/resources/application-dev.yml,确保url行完整:

url: jdbc:mysql://localhost:3306/efams?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai

4.4 现象:点击“预览 PDF”时页面空白,控制台报错Failed to load resource: the server responded with a status of 404 ()

原因:Nginx 配置中location /的root路径指向错误。root指向的是静态资源的父目录,而非index.html所在目录。例如,index.html在/opt/efams/target/classes/static/index.html,则root应设为/opt/efams/target/classes/,而不是/opt/efams/target/classes/static/。
解决:修改 Nginx 配置,root值去掉末尾的/static,并确认try_files $uri $uri/ /index.html;存在。

4.5 现象:导出 Excel 报表时,中文列名显示为方框(□□□)

原因:Apache POI 生成 Excel 时未设置字体。POI 2.x 默认使用Courier New,不支持中文。
解决:打开ExportService.java,在exportToFile方法中,找到XSSFWorkbook workbook = new XSSFWorkbook();后,添加:

Font font = workbook.createFont(); font.setFontName("微软雅黑"); // 或 "SimSun" font.setFontHeightInPoints((short) 10); CellStyle style = workbook.createCellStyle(); style.setFont(font); // 然后对每个 Cell 设置 style cell.setCellStyle(style);

5. 二次开发实战:如何为系统增加“电子印章”功能并确保法律效力

5.1 业务需求与法律效力锚点

某导师曾指导一个模拟项目X,客户提出核心诉求:“所有对外发布的电子公文,必须加盖具有法律效力的电子印章,且印章位置、时间、签名者信息需固化在文件内,不可被篡改”。这不是加个 PNG 图片那么简单。根据《电子签名法》第八条,可靠的电子签名需满足:(一)电子签名制作数据用于电子签名时,属于电子签名人专有;(二)签署时电子签名制作数据仅由电子签名人控制;(三)签署后对电子签名的任何改动能够被发现;(四)签署后对数据电文内容和形式的任何改动能够被发现。因此,我们的“电子印章”必须是数字签名,而非视觉印章。

5.2 技术选型与集成路径

放弃自己实现 PKI 体系(太重),采用 Bouncy Castle 提供的轻量级 CMS(Cryptographic Message Syntax)签名方案。它能将签名、证书、时间戳打包进 PDF 的Signature字段,Adobe Reader 可直接验证。步骤如下:

  1. 准备签名证书:用 OpenSSL 生成自签名证书(测试用)或购买商业 CA 证书(生产用):
    openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365
  2. 后端集成 Bouncy Castle:在pom.xml中添加:
    <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency>
  3. 编写 PDF 签名服务:PdfSignService.java的核心方法:
    public void signPdf(String inputFile, String outputFile, String keystorePath, String alias, String password) throws Exception { // 1. 加载证书和私钥 KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(new FileInputStream(keystorePath), password.toCharArray()); PrivateKey privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); Certificate[] chain = ks.getCertificateChain(alias); // 2. 使用 iText7 的 PdfSigner 进行签名(项目已引入 itext7-kernel) PdfReader reader = new PdfReader(inputFile); PdfWriter writer = new PdfWriter(outputFile); PdfSigner signer = new PdfSigner(reader, writer, true); signer.setCertificationLevel(PdfSigner.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS); // 3. 创建签名外观(可视化印章) IExternalSignature pks = new PrivateKeySignature(privateKey, "SHA256", "BC"); IExternalDigest digest = new BouncyCastleDigest(); Rectangle rect = new Rectangle(450, 750, 120, 50); // 印章位置 PdfSignatureAppearance appearance = signer.getSignatureAppearance(); appearance.setReason("电子公文发布").setLocation("某单位").setPageNumber(1); appearance.setVisibleSignature(rect, 1, "sig"); // sig 是签名域名称 // 4. 执行签名 signer.signDetached(digest, pks, chain, null, null, null, 0, PdfSigner.CryptoStandard.CMS); }

5.3 前端交互与流程闭环

在档案详情页增加“加盖电子印章”按钮,点击后弹出模态框,要求输入审批人密码(用于解锁私钥)。前端调用/api/file/sign接口,传入fileId和password。后端校验密码后,调用PdfSignService.signPdf(),生成新文件xxx_signed.pdf,并更新file_version表,reason字段写明“加盖电子印章(审批人:张三)”。最终,用户下载的不再是原始 PDF,而是已嵌入数字签名的 PDF,用 Adobe Reader 打开,左上角会显示绿色锁图标,点击可查看签名者证书、签名时间、文档完整性验证结果。

注意:私钥密码绝不能明文传输。实际生产中,应使用前端 Web Crypto API 在浏览器内解密私钥,再将签名后的数据发送给后端。但本项目为简化,采用服务端解密,故password字段必须走 HTTPS,且后端日志严禁打印该密码。

从那以后我每次做电子文件类项目,都强制走一遍“四性”自查清单:真实性(哈希校验)、完整性(版本链)、可用性(预览兜底)、安全性(上传/下载双重鉴权)。哪怕客户说“先做个 demo 看看”,我也先把这四根柱子立稳——因为真正的档案系统,从来不是功能堆砌,而是责任落地。希望帮到你。

本文还有配套的精品资源,点击获取