简介:本资源是一套基于Delphi 7与SQL Server开发的档案管理信息系统(AMIS)完整源码工程,面向高校计算机专业学生、桌面应用初学者及档案信息化实践者,解决传统纸质档案数字化管理难、检索效率低、权限控制弱等实际问题。压缩包共106个文件,含22个Pascal源码(.pas)与18个窗体描述(.dfm),构成核心业务逻辑与GUI界面;24个编译单元(.dcu)支持快速构建,2个SQL脚本(.sql)定义数据库结构,另含可执行程序(.exe)、配置文件(.cfg/.ini)及图标资源(.bmp/.gif),整体仅1.51MB,轻量易部署。已有195人学习下载,资源结构清晰,涵盖档案录入、多条件检索、角色权限控制、版本管理与统计报表等六大功能模块,提供从数据库设计、ADO数据访问到VCL界面交互的全链路实现参考,是理解传统桌面级信息管理系统架构的优质学习样本。
1. 档案管理信息系统:不是电子表格的升级版,而是让归档动作从“月底突击”变成“随手完成”的工作流引擎
某高校教务处曾用 Excel 管理近十年的学籍档案,直到一次迎评检查前夜——3 位老师通宵核对 2786 份毕业生材料,发现 142 份缺失签字扫描件、89 份页码错乱、还有 7 份被误存为 Word 草稿。这不是个例。真实场景里,“档案管理”从来不是静态存档,而是贯穿文件生成、审批、移交、借阅、销毁全生命周期的动态协同过程。这个「档案管理信息系统」正是为解决这类问题而生:它不追求炫酷大屏,但强制每份文件带元数据(形成时间、责任部门、密级、保管期限),自动校验归档完整性(比如“学位论文”必须含封面+正文+查重报告+答辩决议四类附件),支持按年度/部门/事由多维组合检索,且所有操作留痕可溯。适合中小单位信息科快速部署,也适合作为政务系统中独立的档案子模块嵌入。它用 Spring Boot + Vue3 实现前后端分离,MySQL 存结构化数据,MinIO 托管原始文件,整套源码无商业依赖,下载即跑,改两行配置就能对接你单位现有的 OA 登录体系。
2. 技术栈与架构设计:为什么选 Spring Boot + Vue3 + MinIO 而不是全栈框架?
这个系统没堆砌新技术,每个选型都对应一个具体痛点。比如用 Spring Boot 不是因为它“新”,而是它的@ConfigurationProperties能把 MinIO 连接参数和档案分类规则写进application.yml,运维改配置不用碰代码;Vue3 的 Composition API 让“借阅申请表单”和“移交清单生成器”复用同一套元数据校验逻辑,避免 JS 里写一堆if (type === 'contract') {...}的玄学分支;MinIO 则彻底甩开本地文件路径硬编码——以前用FileUtils.copyFile(new File("/data/archive/2023/...")),一换服务器就翻车,现在所有文件操作走minioClient.putObject(),连上传进度条都能用UploadOptions控制。
2.1 后端核心分层:Controller → Service → Mapper → Entity 的真实职责边界
很多新手以为 Controller 只是转发请求,其实这里藏着关键业务逻辑。以“移交档案包”接口为例:
@PostMapping("/transfer") public Result<TransferResult> submitTransfer(@RequestBody TransferRequest request) { // 1. 校验移交人权限(非管理员需所属部门匹配) if (!authService.hasDeptAuthority(request.getSubmitterId(), request.getDeptCode())) { return Result.fail("无权向该部门移交档案"); } // 2. 触发完整性预检(调用独立校验服务,非简单 if 判断) ValidationReport report = validationService.checkCompleteness(request.getFiles()); if (!report.isValid()) { return Result.fail("移交失败:", report.getErrors()); } // 3. 生成唯一移交批次号(格式:TR-20240521-00123) String batchNo = transferService.generateBatchNo(); // 4. 异步执行文件落盘 + 元数据入库(避免大包上传阻塞 HTTP 连接) transferService.asyncSaveTransfer(batchNo, request); return Result.success(new TransferResult(batchNo, report.getTotalFiles())); }提示:
ValidationReport是自定义返回对象,包含isValid()布尔值、getErrors()错误列表、getTotalFiles()统计数。不要直接返回Map<String, Object>,否则前端得写一堆res.data.errors?.length > 0的防御性判断。
2.2 前端路由与状态管理:如何让“待审核”“已归档”“已销毁”三类视图共享同一套数据流
Vue3 用 Pinia 替代 Vuex 后,状态管理更轻量。但关键不在“用没用 Pinia”,而在怎么拆分 store。本项目将档案状态流拆成两个 store:
useArchiveListStore():管理列表页的筛选条件(年份、密级、关键词)、分页参数、当前选中项 ID 数组;useArchiveDetailStore():专注单个档案的详情、操作历史、附件列表,通过archiveId作为 key 缓存不同档案的完整数据。
这样设计后,用户在“待审核”列表点击某条记录,跳转到详情页时,useArchiveDetailStore().loadById(id)会先查缓存,命中则秒开;未命中才发请求。而切换回列表页再点另一条,完全不影响前一个详情页的状态——这解决了传统单页应用里“点来点去数据互相覆盖”的经典翻车场景。
2.3 文件存储策略:MinIO 的桶(Bucket)命名规则与权限隔离实践
MinIO 不是简单替代upload/目录。本系统按“保管期限+密级”创建桶,例如:
bucket-retention-30y-public(30 年保管期,公开级)bucket-retention-10y-confidential(10 年保管期,机密级)
每个桶设置独立的policy.json,限制仅允许后端服务账号上传,禁止浏览器直传(防越权)。关键代码在MinIOConfig.java中:
@Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint("http://minio:9000") .credentials("minioadmin", "minioadmin") // 生产环境务必改! .build(); } // 桶初始化逻辑放在 @PostConstruct 方法中 @PostConstruct public void initBuckets() { String[] buckets = {"retention-30y-public", "retention-10y-confidential"}; for (String suffix : buckets) { String bucketName = "bucket-" + suffix; try { if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); // 绑定预设策略(策略文件需提前上传到 MinIO 的 policy/ 目录) minioClient.setBucketPolicy(SetBucketPolicyArgs.builder() .bucket(bucketName) .config(getPolicyContent(suffix)) .build()); } } catch (Exception e) { log.error("初始化桶 {} 失败", bucketName, e); } } }getPolicyContent()方法读取classpath:policies/{suffix}.json,内容示例(retention-10y-confidential.json):
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam::123456789012:user/backend-service"]}, "Action": ["s3:GetObject", "s3:PutObject"], "Resource": ["arn:aws:s3:::bucket-retention-10y-confidential/*"] } ] }注意:MinIO 默认开启
--console-address,但生产环境必须关闭控制台或加 Nginx 鉴权,否则minioadmin/minioadmin账号等于裸奔。
3. 目录结构与核心文件说明:看清骨架,才能下刀改功能
项目采用标准 Maven 多模块结构,但删减了无用的 parent pom 和 test 模块,实际只有archive-server(后端)和archive-web(前端)两个根目录。这种精简不是偷懒,而是避免新手被spring-cloud-starter-alibaba-nacos-config这类名字吓退——它根本没用 Nacos,配置全在application-prod.yml里。
3.1 后端archive-server目录关键路径解析
src/main/java/com/example/archive/ ├── ArchiveApplication.java # 启动类,@SpringBootApplication 注解在此 ├── config/ # 配置类集中地 │ ├── MinIOConfig.java # MinIO 客户端 Bean 创建 │ ├── MyBatisPlusConfig.java # MP 分页插件、SQL 性能分析器(开发环境启用) │ └── WebMvcConfig.java # 全局日期格式化、跨域配置 ├── controller/ # 接口层,命名严格按业务域 │ ├── ArchiveController.java # 档案主流程(新增/查询/借阅) │ ├── TransferController.java # 移交专项(含批量移交、撤回) │ └── ReportController.java # 统计报表(导出 Excel 用 EasyExcel) ├── entity/ # POJO,字段名与数据库列名一致,含 Lombok 注解 │ ├── Archive.java # 主档案表(id, title, dept_code, create_time...) │ ├── ArchiveFile.java # 附件关联表(archive_id, file_name, minio_path...) │ └── TransferBatch.java # 移交批次表(batch_no, submitter_id, status...) ├── mapper/ # MyBatis Plus Mapper 接口 │ ├── ArchiveMapper.java # 继承 BaseMapper<Archive> │ └── TransferBatchMapper.java # 同上 ├── service/ # 业务逻辑层,接口+实现分离 │ ├── impl/ │ │ ├── ArchiveServiceImpl.java # 主服务实现,含复杂事务(如借阅+生成借阅码) │ │ └── TransferServiceImpl.java # 移交服务,含异步任务调度 │ └── ArchiveService.java # 接口定义 └── util/ # 工具类,非通用工具(如 MinIO 工具类、档案编号生成器) ├── MinIOUtil.java # 封装 putObject/getObject/deleteObject └── ArchiveNoGenerator.java # 档案号生成规则:DEPT-YEAR-SEQ(如 JWC-2024-00123)提示:
ArchiveNoGenerator.java的generate()方法接受deptCode和year参数,内部用RedisTemplate.opsForValue().increment("seq:" + deptCode + ":" + year, 1)保证部门内年度序号唯一,比数据库自增更抗并发。
3.2 前端archive-web目录关键路径解析
src/ ├── api/ # 接口请求封装,按模块组织 │ ├── archive.js # 档案主接口(list, detail, update) │ ├── transfer.js # 移交接口(submit, cancel, listByBatch) │ └── auth.js # 权限相关(login, getUserInfo) ├── assets/ # 静态资源 │ ├── icons/ # SVG 图标(按功能分类:archive.svg, transfer.svg) │ └── styles/ # 全局样式(variables.scss 定义主题色) ├── components/ # 可复用业务组件 │ ├── ArchiveList.vue # 档案列表(含筛选栏、操作列) │ ├── FileUploader.vue # 智能附件上传(自动识别 PDF/DOCX/IMG,限制单文件 50MB) │ └── StatusBadge.vue # 状态徽章(根据 status 字段渲染不同颜色) ├── router/ # 路由配置 │ └── index.js # 路由守卫:未登录跳 login,无权限跳 403 ├── stores/ # Pinia store │ ├── archiveList.js # 列表状态管理 │ └── archiveDetail.js # 详情状态管理 ├── utils/ # 工具函数 │ ├── request.js # Axios 封装(自动携带 token,错误统一处理) │ └── date.js # 日期格式化(兼容 YYYY-MM-DD 和 YYYY-MM-DD HH:mm:ss) └── views/ # 页面级组件 ├── HomeView.vue # 首页(统计卡片 + 快捷入口) ├── ArchiveListView.vue # 档案列表页(使用 ArchiveList.vue 组件) └── ArchiveDetailView.vue # 档案详情页(含 Tab 切换:基本信息/附件/操作日志)3.3 数据库表结构设计要点:为什么archive表不直接存文件路径?
archive表只存元数据,文件路径存在archive_file关联表,这是为支持“一份档案多个附件”和“附件版本管理”。关键字段如下:
| 表名 | 字段名 | 类型 | 说明 |
|---|---|---|---|
archive | id | BIGINT PK | 主键,雪花算法生成 |
archive_no | VARCHAR(32) | 档案号(JWC-2024-00123),业务主键 | |
title | VARCHAR(200) | 标题(全文索引) | |
dept_code | VARCHAR(20) | 归属部门编码(关联部门字典表) | |
retention_years | TINYINT | 保管年限(3/10/30/永久) | |
security_level | TINYINT | 密级(1=公开, 2=内部, 3=机密, 4=绝密) | |
archive_file | id | BIGINT PK | 主键 |
archive_id | BIGINT FK | 关联 archive.id | |
file_name | VARCHAR(200) | 原始文件名(含扩展名) | |
minio_path | VARCHAR(500) | MinIO 中路径(bucket-name/object-key) | |
file_size | BIGINT | 字节数(用于前端显示“2.3 MB”) | |
version | INT DEFAULT 1 | 版本号(同一文件多次上传递增) |
注意:
archive_file.minio_path格式为bucket-retention-10y-confidential/jwc/2024/00123/cover.pdf,其中jwc/2024/00123/是按部门+年份+档案号生成的逻辑路径,便于 MinIO 控制台人工排查。
4. 运行与部署:从本地启动到 Docker Compose 一键上线
别被“信息系统”四个字唬住。这套代码设计目标就是“开发者电脑上 10 分钟跑起来,运维小哥照着文档 30 分钟部署上线”。它不依赖 Kubernetes 或云厂商特有服务,MySQL 和 MinIO 都用 Docker 最简方式启动。
4.1 本地开发环境启动(Windows/macOS/Linux 通用)
前提:已安装 JDK 17、Node.js 18+、Docker Desktop
步骤 1:启动 MySQL 和 MinIO
# 创建专用网络,避免端口冲突 docker network create archive-net # 启动 MySQL(密码 root,建库脚本见 resources/sql/init.sql) docker run -d \ --name archive-mysql \ --network archive-net \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root \ -e MYSQL_DATABASE=archive_db \ -v $(pwd)/mysql-data:/var/lib/mysql \ -d mysql:8.0.33 # 启动 MinIO(账号 minioadmin/minioadmin) docker run -d \ --name archive-minio \ --network archive-net \ -p 9000:9000 -p 9001:9001 \ -e "MINIO_ROOT_USER=minioadmin" \ -e "MINIO_ROOT_PASSWORD=minioadmin" \ -v $(pwd)/minio-data:/data \ quay.io/minio/minio server /data --console-address ":9001"步骤 2:初始化数据库
运行resources/sql/init.sql(含建表语句和基础字典数据,如部门表sys_dept、密级字典sys_dict)。可用 DBeaver 或 MySQL Workbench 执行。
步骤 3:后端启动
修改archive-server/src/main/resources/application-dev.yml:
spring: datasource: url: jdbc:mysql://archive-mysql:3306/archive_db?useSSL=false&serverTimezone=Asia/Shanghai username: root password: root minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin然后在 IDEA 中右键ArchiveApplication.java→ Run。
步骤 4:前端启动
cd archive-web npm install # 修改 src/api/request.js 中 baseURL 为 http://localhost:8080 npm run dev访问http://localhost:5173即可看到登录页(默认账号 admin/admin)。
4.2 生产环境 Docker Compose 部署
docker-compose-prod.yml文件精简到 40 行,只保留必要服务:
version: '3.8' services: archive-db: image: mysql:8.0.33 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} MYSQL_DATABASE: archive_db volumes: - ./mysql-data:/var/lib/mysql networks: - archive-net archive-minio: image: quay.io/minio/minio:latest restart: unless-stopped command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${MINIO_USER} MINIO_ROOT_PASSWORD: ${MINIO_PASS} volumes: - ./minio-data:/data ports: - "9000:9000" - "9001:9001" networks: - archive-net archive-server: image: archive-server:1.0.0 restart: unless-stopped depends_on: - archive-db - archive-minio environment: SPRING_PROFILES_ACTIVE: prod SPRING_DATASOURCE_URL: jdbc:mysql://archive-db:3306/archive_db?... MINIO_ENDPOINT: http://archive-minio:9000 ports: - "8080:8080" networks: - archive-net archive-web: image: nginx:alpine restart: unless-stopped volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./dist:/usr/share/nginx/html ports: - "80:80" networks: - archive-net构建镜像命令(在archive-server目录下):
mvn clean package -DskipTests docker build -t archive-server:1.0.0 .前端构建(在archive-web目录下):
npm run build cp -r dist ../nginx-dist最后执行:
# 创建 .env 文件填入 DB_PASSWORD、MINIO_USER、MINIO_PASS echo "DB_PASSWORD=your_strong_password" > .env echo "MINIO_USER=minioadmin" >> .env echo "MINIO_PASS=minioadmin" >> .env docker-compose -f docker-compose-prod.yml up -d提示:
nginx.conf需配置反向代理/api到后端,避免跨域。示例片段:location /api { proxy_pass http://archive-server:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
4.3 常见问题排查:那些让你怀疑人生却只需改一行配置的坑
现象:前端上传文件时提示 “Network Error”,控制台显示 500 Internal Server Error
原因:后端application.yml中minio.endpoint写成了http://localhost:9000,但 Docker 容器内localhost指向自身,而非宿主机的 MinIO。
解决:在docker-compose-prod.yml中,archive-server服务的environment下添加MINIO_ENDPOINT: http://archive-minio:9000,并确保application-prod.yml读取该环境变量。
现象:登录成功后,首页统计卡片全部为空,Network 面板看到/api/report/summary返回 401
原因:前端src/api/request.js的拦截器里,token 存在localStorage.getItem('token'),但后端WebMvcConfig.java的跨域配置漏了Access-Control-Expose-Headers: Authorization,导致前端无法读取响应头中的新 token。
解决:在WebMvcConfig.java的addCorsMappings方法中,添加.exposedHeaders("Authorization")。
现象:MySQL 启动后,后端报错Unknown system variable 'tx_isolation'
原因:MySQL 8.0.33 默认禁用tx_isolation系统变量,但旧版 JDBC 驱动(如 5.1.x)仍尝试读取它。
解决:升级pom.xml中的 MySQL 驱动版本:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>现象:MinIO 控制台能登录,但后端调用putObject报错NoSuchBucketException
原因:MinIOConfig.java的initBuckets()方法在 Spring Boot 启动时执行,但此时 MinIO 容器可能尚未完全就绪(特别是首次启动),导致bucketExists返回 false,后续makeBucket失败。
解决:增加重试机制,在initBuckets()中用for (int i = 0; i < 5; i++)循环,每次失败Thread.sleep(2000),5 次后仍失败才抛异常。
现象:档案详情页附件列表为空,但 MinIO 控制台能看到文件
原因:archive_file.minio_path字段存的是bucket-name/object-key,但MinIOUtil.getObject()方法拼接 URL 时,错误地用了http://minio:9000/bucket-name/object-key,而 MinIO 的 getObject API 要求GET /bucket-name/object-key,且需签名。
解决:MinIOUtil.java中getObjectUrl()方法应调用minioClient.getPresignedObjectUrl()生成带签名的临时链接,而非拼接字符串。
5. 二次开发实战:如何在 30 分钟内为“合同档案”增加电子签章验证功能
这是我在某单位落地时的真实需求:合同类档案移交后,需验证 PDF 是否含有效数字签名(非简单水印),且验证结果要存入数据库、前端展示绿色对勾或红色叉号。整个改造涉及后端新增校验服务、前端新增状态展示、数据库加字段,但无需动核心流程。下面带你一步步做。
5.1 数据库扩展:为archive_file表增加签章验证字段
执行 SQL(在生产环境先备份):
ALTER TABLE archive_file ADD COLUMN signature_valid TINYINT DEFAULT NULL COMMENT '签章验证状态:0=无效,1=有效,NULL=未验证', ADD COLUMN signature_info VARCHAR(500) DEFAULT NULL COMMENT '签章详情(如签发者、有效期)';注意:
TINYINT DEFAULT NULL而非NOT NULL,因为历史数据无需补验,留空表示“不适用”。
5.2 后端新增 PDF 签章验证服务
引入 Apache PDFBox 依赖(pom.xml):
<dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.29</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency>创建PdfSignatureService.java:
@Service public class PdfSignatureService { @Autowired private MinIOUtil minIOUtil; /** * 验证 PDF 文件是否含有效数字签名 * @param fileId archive_file.id * @return SignatureResult 包含 valid(布尔)和 info(字符串) */ public SignatureResult verifySignature(Long fileId) { try { ArchiveFile file = archiveFileMapper.selectById(fileId); if (file == null || !"pdf".equalsIgnoreCase(FilenameUtils.getExtension(file.getFileName()))) { return new SignatureResult(false, "非 PDF 文件,跳过验证"); } // 1. 从 MinIO 下载 PDF 到内存(不落盘) InputStream is = minIOUtil.getObject(file.getMinioPath()); PDDocument document = PDDocument.load(is); // 2. 获取签名字段 List<PDSignature> signatures = document.getSignatureDictionaries(); if (signatures.isEmpty()) { return new SignatureResult(false, "PDF 未检测到数字签名"); } // 3. 验证第一个签名(实际项目可遍历全部) PDSignature signature = signatures.get(0); SignatureInterface signatureInterface = new BouncyCastleDigest(); boolean isValid = signatureInterface.verifySignature(document, signature); String info = String.format("签发者:%s,有效期:%s", signature.getName(), signature.getSignDate() != null ? signature.getSignDate().getTime() : "未知"); return new SignatureResult(isValid, info); } catch (Exception e) { log.error("PDF 签章验证失败,fileId={}", fileId, e); return new SignatureResult(false, "验证异常:" + e.getMessage()); } } } // 结果类 @Data @AllArgsConstructor public static class SignatureResult { private boolean valid; private String info; }5.3 前端在详情页集成签章状态展示
修改views/ArchiveDetailView.vue,在附件 Tab 下新增:
<!-- 在附件列表下方 --> <div v-if="activeTab === 'files'" class="mt-6"> <h3 class="text-lg font-medium text-gray-900 mb-4">附件签章验证</h3> <div class="space-y-4"> <div v-for="file in archiveDetail.files" :key="file.id" class="flex items-center justify-between p-4 bg-gray-50 rounded-lg"> <div class="flex items-center"> <DocumentIcon class="h-5 w-5 text-gray-400 mr-2" /> <span class="font-mono">{{ file.fileName }}</span> </div> <div class="flex items-center space-x-2"> <!-- 状态徽章 --> <span v-if="file.signatureValid === null" class="px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded"> 未验证 </span> <span v-else-if="file.signatureValid" class="px-2 py-1 text-xs bg-green-100 text-green-800 rounded flex items-center"> <CheckCircleIcon class="h-4 w-4 mr-1" /> 有效 </span> <span v-else class="px-2 py-1 text-xs bg-red-100 text-red-800 rounded flex items-center"> <XCircleIcon class="h-4 w-4 mr-1" /> 无效 </span> <!-- 验证按钮 --> <button @click="verifySignature(file.id)" :disabled="file.signatureValid !== null" class="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50" > {{ file.signatureValid === null ? '开始验证' : '已验证' }} </button> </div> </div> </div> </div>在<script setup>中添加方法:
const verifySignature = async (fileId) => { try { const res = await api.transfer.verifySignature({ fileId }); // 更新当前文件的 signatureValid 和 signatureInfo 字段 const file = archiveDetail.files.find(f => f.id === fileId); if (file) { file.signatureValid = res.data.valid; file.signatureInfo = res.data.info; } } catch (e) { ElMessage.error('验证失败:' + e.response?.data?.message || e.message); } };同时在api/transfer.js中新增接口:
// 验证单个文件签章 export const verifySignature = (data) => request.post('/api/transfer/verify-signature', data);后端TransferController.java新增接口:
@PostMapping("/verify-signature") public Result<SignatureResult> verifySignature(@RequestBody Map<String, Long> req) { Long fileId = req.get("fileId"); if (fileId == null) { return Result.fail("fileId 不能为空"); } SignatureResult result = pdfSignatureService.verifySignature(fileId); // 更新数据库 ArchiveFile file = new ArchiveFile(); file.setId(fileId); file.setSignatureValid(result.isValid() ? (byte) 1 : (byte) 0); file.setSignatureInfo(result.getInfo()); archiveFileMapper.updateById(file); return Result.success(result); }5.4 验证与收尾:确保改动不破坏原有逻辑
- 回归测试:手动测试“非 PDF 文件点击验证”是否返回友好提示;上传一个无签名的 PDF,确认显示红色叉号;上传一个带 Adobe Sign 的 PDF,确认显示绿色对勾。
- 性能观察:PDF 验证耗时约 200~800ms(取决于文件大小),因此前端按钮加了
:disabled防止重复点击,后端未做异步——因为合同类档案数量有限,且验证是主动触发,非批量。 - 安全加固:
PdfSignatureService中PDDocument.load(is)后,必须document.close(),否则内存泄漏。已在finally块中补全。
从那以后我每次给档案系统加新校验能力,都先问自己三个问题:1)这个校验是否必须实时?(否→可异步)2)失败时能否降级为人工复核?(是→前端必须提供“跳过验证”按钮)3)结果是否要参与归档完整性判定?(本例否,所以signature_valid字段不参与checkCompleteness()校验)。希望帮到你。
本文还有配套的精品资源,点击获取