简介:一套基于Java和Spring Boot的企业级档案管理系统源码包,面向Java开发学习者及企业信息化管理人员,适用于档案收集、整理、存储、检索与更新等日常场景。系统采用模块化设计,分管理员与员工双权限体系,管理员端覆盖员工管理、客户信息维护、设备维修保养、配件采购及合同归档等功能,能帮助读者直观理解权限控制与业务模块的整合方式。整套源码共440个文件,以131个Java后端文件、50个Vue前端组件、21个JS脚本和17个XML配置为核心,另附SQL数据库脚本及SVG图标、构建批处理等辅助内容,压缩包约8.61MB,结构规整便于查阅。已有111人下载学习,适合希望借助完整项目案例提升Spring Boot全栈开发能力、需要参考真实业务流设计与权限模型的开发者。
1. 档案管理系统源码值不值得下:先说清楚它解决什么问题
档案管理系统这个题目,在计算机类的课程设计、毕业设计里出现频率极高,因为它不是一个简单的 CRUD,还牵扯到文件上传、权限控制、审批流程和操作留痕。这套基于 Java 和 Spring Boot 的档案管理系统源码,把这些骨架都搭好了,业务链路完整,代码里没有故意炫技的东西。
你下载回来,从改表结构到换文件存储路径,再到加自己的检索维度,每一层都有清晰的落点。适合做课设、毕设的计算机专业学生,也适合刚工作不久、想拿一个完整业务闭环练手的 Java 工程师。
还没配好 JDK 的直接看第四章;已经跑过几个 Spring Boot demo 的,从第二章表设计开始读就行。
2. 技术骨架拆解:数据模型、权限模型与文件上传链路的选型逻辑
2.1 为什么选 Spring Boot + MyBatis:先从技术选型看代码边界
档案管理系统这类 OA 项目,最吃开发效率的不是“增删改查”,而是三件事:业务状态怎么流转、文件上传下载的通路有多顺、权限能细到什么粒度。Spring Boot 恰好把这三件事都覆盖到了:文件上传依靠 servlet 容器内置的 multipart 处理,不用自己写底层文件流;权限可以用拦截器,也可以上 Spring Security;数据库访问层选 MyBatis 还是 Spring Data JPA 都行,取决于需求边界。
这套源码选用的是经典三层结构 Controller - Service - Mapper,实体类对应数据库表,没有引入重量级组件。为什么用 MyBatis 而不是 JPA?因为档案检索条件太琐碎:按年份查、按分类查、按密级查、按部门查,还要拼关键词模糊匹配。MyBatis 动态 SQL 在这种组合查询下的表达力非常直观,一个 XML 映射就能看清楚条件拼接的边界。你在答辩或代码 review 时,能把这句选型理由讲出来,比单纯说“框架是老师给的”要有说服力得多。
最容易让新人困惑的是 Spring Boot 的自动配置到底“自动”了什么。比如上传文件,它不需要你手动注册 MultipartResolver,在 yml 里写一个spring.servlet.multipart.max-file-size就生效;数据源同理,给出连接串和账号密码,连接池自动初始化。先不要急着研究底层原理,把 pom.xml 里的spring-boot-starter-parent版本和mybatis-spring-boot-starter版本确认好,再进主启动类看@SpringBootApplication扫描路径,后面的代码读起来会顺很多。
提示:下载源码后第一个要打开的不是 demo 页面,而是 sql 目录里的建表脚本。表设计能反映作者的真实业务边界。
2.2 三张核心表的设计逻辑:档案、借阅、用户怎么串联
这类系统的数据库脚本通常放在项目根目录的 sql 文件夹下,从create table的顺序能看出设计思路:先建部门表、用户表,再建档案表,最后建借阅表。拿到源码后,第一件事不是让项目跑起来,而是把建表 SQL 从头读一遍。
档案主表archive的字段设计遵循一个常见模式:
| 字段名 | 类型 | 作用 |
|---|---|---|
| id | bigint | 主键,自增 |
| archive_no | varchar(64) | 业务编号,按规则生成,用户可见 |
| title | varchar(255) | 标题,列表页核心展示字段 |
| category | varchar(32) | 分类,合同/人事/财务/公文等 |
| secret_level | varchar(16) | 密级,公开/内部/秘密 |
| file_path | varchar(255) | 存储路径,不存文件本体 |
| status | int | 1 正常 / 0 下架 |
| create_by | bigint | 上传人,关联用户表 |
| create_time | datetime | 建档时间 |
用户表sys_user里除了常规的 username、password、real_name,一般还会带一个dept_id部门字段,这个字段很关键,它让权限从“登录拦截”上升到“数据范围拦截”。借阅表archive_borrow记录档案 id、借阅人 id、借出时间、预计归还时间、审批状态,是整个系统里流转状态最多的一张表。
看这三张表的关系,能明白一个设计习惯:文件内容不落数据库,数据库只存路径。档案可能是一份几十 MB 的扫描 PDF,塞进 blob 字段后,列表查询会随表膨胀急剧变慢;只存路径可以保持表体积小、查询稳定,以后把本地路径换成 OSS key 也只是改一处拼接逻辑的事。我见过有人把文件转成 base64 存 text 字段,那才是真要命——一次 list 查询把整表大字段拖出来,慢到没法用。
2.3 文件存储与权限双层过滤:前端可感知的关键设计
文件上传下载链路值得单独拆开讲,因为它决定系统能不能在真实环境里跑。上传时 Controller 拿到MultipartFile,通过transferTo写到磁盘,数据库记录相对路径;下载时取出 file_path,拼出绝对路径,用ResponseEntity<byte[]>或StreamingResponseBody返回。如果源码里出现了 aliyun-sdk 或 minio 依赖,说明作者预留了云存储接口;没有的话,那就是本地存储方案。
权限部分,从“登录才能访问”到“只能看本部门”,中间隔着两层逻辑。第一层是登录拦截器,校验 session 里有没有 loginUser,未登录请求一律重定向;第二层是数据范围控制,查询列表时根据当前用户的 dept_id 拼条件。我平时处理查询范围,习惯把deptId做成查询对象里的一个隐藏字段,服务层从当前登录用户上下文里自动填充,而不是让前端传——前端传 deptId 就等于把数据范围的控制权交给调用方了,这属于肉眼可见的设计缺陷。
这两层设计的边界很清晰:登录拦截管“你是谁”,数据范围管“你能看什么”。接二次开发需求时先分清这两个位置,再决定改哪层。如果出现跨部门调档需求,把数据范围判断抽出来,改成根据调档申请授权即可,逻辑是现成的。
3. 核心业务落地:档案录入、多条件检索、借阅审批的代码细节
3.1 档案录入接口:先保存文件还是先写库,顺序有讲究
档案录入接口看起来和普通新增没有区别,但它多了一步文件处理。源码里 Controller 的写法大致是这样的。
@PostMapping("/archive/add") public Result addArchive(@RequestParam("file") MultipartFile file, @RequestParam String title, @RequestParam String category, @RequestParam(required = false) String secretLevel) { // 1. 参数校验:文件不能为空 if (file.isEmpty()) { return Result.error("上传文件不能为空"); } // 2. 用 UUID 重命名,避免同名覆盖和路径穿越 String originalFilename = file.getOriginalFilename(); String suffix = ""; if (originalFilename != null && originalFilename.contains(".")) { suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); } String storeName = UUID.randomUUID().toString().replace("-", "") + suffix; // 3. 按年月建子目录,把文件落到磁盘 String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")); File dir = new File(uploadDir + datePath); if (!dir.exists()) { dir.mkdirs(); } File target = new File(dir, storeName); file.transferTo(target); // 4. 组装档案实体并入库 Archive archive = new Archive(); archive.setArchiveNo(generateArchiveNo()); archive.setTitle(title); archive.setCategory(category); archive.setSecretLevel(secretLevel); archive.setFilePath(datePath + "/" + storeName); archive.setStatus(1); archive.setCreateBy(currentUserId()); archiveMapper.insert(archive); return Result.success(archive.getId()); }这里有两个顺序要看清。第一,先落文件再写库:写库失败,磁盘上最多多一个没有索引的孤儿文件,清理成本低;反过来先写库再落文件,一旦写文件超时或磁盘满,数据库里就出现一条指向不存在文件的死记录。第二,transferTo是 Spring MVC 封装好的方法,处理了临时文件在容器之间的移动,比手写FileOutputStream分块复制可靠。至于为什么用 UUID 重命名,是因为原始文件名可能包含中文、空格和路径符号,直接拼路径容易踩编码和路径穿越的坑。
@RequestParam("file")里的值必须和前端表单的 name 一致。很多毕设卡在这一步:前端input type="file"的 name 写成了uploadFile,后端注解写"file",结果参数一直是 null,还以为是框架出了问题。@RequestParam(required = false)的secretLevel表示密级可为空,是否必填取决于业务规则。我一般不建议设成非必填,档案管理里密级是审核重点,否则日志审计时缺一项不好解释。
3.2 多条件组合检索:MyBatis 动态 SQL 与分页
档案列表检索是全系统最常用的功能,典型的 Mapper XML 长这样。
<select id="selectArchiveList" resultType="com.example.archive.entity.Archive"> SELECT a.id, a.archive_no, a.title, a.category, a.secret_level, a.create_by, a.create_time FROM archive a <where> <if test="keyword != null and keyword != ''"> AND (a.title LIKE CONCAT('%', #{keyword}, '%') OR a.archive_no LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="category != null and category != ''"> AND a.category = #{category} </if> <if test="startTime != null"> AND a.create_time >= #{startTime} </if> <if test="endTime != null"> AND a.create_time <= #{endTime} </if> </where> ORDER BY a.create_time DESC </select>这个 XML 的细节值得逐一拆。<where>标签自动去掉第一个 AND 前缀,解决了条件为空时的 SQL 语法问题。CONCAT('%', #{keyword}, '%')是安全写法:#{}生成预编译占位符,${}直接拼字符串,后者在 keyword 里带单引号时会构成 SQL 注入入口。XML 里的>=和<=是转义写法,直接写>会被解析器当成标签边界。
分页方面,源码常见 PageHelper,Service 层写法如下。
public PageResult<Archive> pageList(ArchiveQuery query) { // startPage 必须紧贴查询方法调用,中间不能有任何其他 SQL 操作 PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<Archive> list = archiveMapper.selectArchiveList(query); PageInfo<Archive> pageInfo = new PageInfo<>(list); return PageResult.success(pageInfo.getList(), pageInfo.getTotal()); }PageHelper 的原理是把分页参数放进线程的 ThreadLocal,下一次 Mapper 调用时取出拼 LIMIT。所以startPage与查询之间绝对不能有别的 SQL,我见过有人因为中间打日志触发了一次查询,分页参数被消费掉,列表直接把全表数据返回了。新手最容易犯错的是在循环里调用分页查询,线程复用后 ThreadLocal 残留,下一页会把上一页的条件带过去。
3.3 借阅审批状态机:数据一致性靠事务兜底
档案借阅的状态流转是这个系统的业务深度所在。通常设计为四态:待审批、已通过、已驳回、已归还,用一个audit_status字段表达。源码里没有引入工作流引擎,我认为这是合理的——状态线性可枚举时,引入 Activiti 或 Flowable 属于过度设计,一张表的字段加事务控制已经很干净。
审批动作的核心代码如下。
@Transactional(rollbackFor = Exception.class) public void auditBorrow(Long borrowId, Integer auditResult, Long auditorId) { ArchiveBorrow borrow = borrowMapper.selectById(borrowId); if (borrow == null || !borrow.getAuditStatus().equals(0)) { throw new BusinessException("该申请不在待审批状态,不能重复审批"); } borrow.setAuditStatus(auditResult); // 1 通过,2 驳回 borrow.setAuditorId(auditorId); borrow.setAuditTime(LocalDateTime.now()); borrowMapper.updateById(borrow); if (auditResult == 1) { archiveMapper.updateStatus(borrow.getArchiveId(), 2); // 2 表示已借出 } }@Transactional(rollbackFor = Exception.class)值得讲透。Spring 事务默认只对运行时异常回滚,如果方法抛了受检异常如IOException,默认不会回滚,数据可能只写了一半。这里显式声明所有异常都回滚,是审批写多张表场景的标准做法。代码里先查 borrow 再判断状态,用equals(0)而不是== 0,避免 Integer 缓存比较的坑;状态不对时直接抛业务异常,前置校验是状态机防止非法转移的基本手段。
改造这套系统时,注意前端按钮显示逻辑必须和后端状态保持一致。如果前端只有“提交申请”和“通过”,而后端还支持“驳回”,演示就会出现按钮缺失的尴尬。状态机最忌讳前后端各维护一套状态枚举,我一般用统一枚举类,前端从后端接口拉取状态列表,从根上避免两套文案不同步。
4. 本地部署全过程:环境、数据库初始化、参数配置与验证
4.1 环境准备:JDK、Maven、IDEA 的版本匹配
打开源码包后,第一件事看 pom.xml 的<parent>标签。如果是spring-boot-starter-parent2.3.x 或 2.7.x,JDK 8 或 11 都可以;如果是 3.0 以上的版本,JDK 必须 17。我遇到过不少这样的情况:拿老毕设源码,用 JDK 8 跑 Spring Boot 3.0,启动时报UnsupportedClassVersionError,这是版本不匹配,不是代码问题。
JDK 环境变量配置是初级读者最容易卡住的一环。Windows 上安装 JDK 后,JAVA_HOME要指向安装目录的根路径,比如C:\Program Files\Java\jdk-11.0.18,path 变量里追加%JAVA_HOME%\bin。配置完后在命令行执行java -version,能正常输出版本号才算成功。Maven 同理,MAVEN_HOME指向解压目录,path 加上%MAVEN_HOME%\bin,再执行mvn -v验证。
IDEA 方面注意两点。第一,社区版对 Spring Boot 项目的 Maven 管理、代码补全支持完全够用,不需要专门装旗舰版。第二,如果实体类用了 Lombok 的@Data,必须确认依赖引入成功并开启注解处理。设置路径是Settings -> Build -> Compiler -> Annotation Processors -> Enable annotation processing。这个开关没打开,编译时全报“找不到 getter/setter”,很多新人以为是源码有问题,其实就是这个选项没勾上。
注意:MySQL 8.0 的连接串必须带
serverTimezone参数,否则启动阶段直接抛时区异常,这是环境问题不是代码问题。
4.2 初始化数据库:建库、导入、初始账号
典型的 sql 脚本会先创建库,再切库建表。
CREATE DATABASE IF NOT EXISTS archive_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE archive_system; -- 然后依次执行 schema.sql 和 data.sql -- source /your/path/archive_schema.sql; -- source /your/path/archive_data.sql;字符集必须用 utf8mb4,而不是 utf8。utf8mb4 是 MySQL 真正的完整 UTF-8 支持,能存 emoji 和特殊符号;utf8 在 MySQL 5.7 之后只支持最多 3 字节字符,遇到生僻字或表情符直接报错或乱码。很多老源码建表时就写了 utf8,导入前手动改掉是最好的时机。
初始数据里一般有一个 admin 账号,密码通常是 admin 或 123456,具体看 README 说明。第一次登录后的动作顺序建议是:改密码、建一个新用户、用新用户登录走一遍借阅流程。为什么这么建议?因为很多源码演示数据只有 admin 一个账号,如果答辩要演示多用户审批场景,临时建用户存在慌乱的预期,先建好,演示时直接切换。
4.3 核心配置:上传大小、连接串、端口
打开src/main/resources/application.yml,重点确认这几个配置项。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/archive_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 100MB max-request-size: 100MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.archive.entity这些参数的含义和常见问题整理如下:
| 配置项 | 作用 | 常见坑 |
|---|---|---|
| server.port | HTTP 端口 | 8080 被占时改 8081,前端 axios baseURL 也要同步改 |
| spring.datasource.url | 数据库连接串 | MySQL 8.0 驱动必须带 serverTimezone,否则启动报时区 |
| multipart.max-file-size | 单文件大小上限 | 默认 1MB,传扫描 PDF 一定会挂 |
| multipart.max-request-size | 单次请求总大小 | 批量导入时若超限,后端报 MaxUploadSizeExceededException |
| mapper-locations | MyBatis XML 扫描路径 | 路径不匹配时启动正常,调用报 Invalid bound statement |
还有两个环境差异要提醒。如果你的 MySQL 是 5.7,连接串的驱动类要从com.mysql.cj.jdbc.Driver换回com.mysql.jdbc.Driver,或者直接不写驱动类让 Spring Boot 自己推断。如果数据库密码里带@或#,连接串里要 URL 编码,否则#被解析成 URL 片段,表现为连接报错但配置看起来没问题。密码带特殊字符时,我一般改用spring.datasource.hikari.data-source-properties单独注入,绕开 URL 编码问题。
4.4 启动与验证:从控制台日志到一次完整业务
在 IDEA 里找到带@SpringBootApplication注解的主类,文件名一般是ArchiveApplication,右键运行。控制台出现Started ArchiveApplication in xx seconds才算完整成功。注意Tomcat started on port 8080出现不等于启动完成,它只是容器起来了,Spring 上下文还没完全初始化。
启动后的验证,我按这个清单走:
- 浏览器访问
http://localhost:8080/,跳转到登录页,说明静态资源和登录接口正常。 - 用初始账号登录,进档案列表页,看是否能查出一条演示数据;列表空白需先确认 sql 脚本是否执行完整。
- 录入一份文件,看文件是否落到配置的上传目录,数据库里多出一条记录且路径正确。
- 发起借阅申请,用另一个账号审批,观察借阅表状态和档案状态是否同步变化。
这个清单走完核心链路才算通。如果某一步异常,先看浏览器的开发者工具 Network 面板,再对症定位:404 基本是接口路径错了,500 是后端异常,302 循环是拦截器配置问题。分清层次再翻代码,比漫无目的打断点快得多。
5. 避坑指南:这套源码最容易翻车的六个问题
5.1 Invalid bound statement (not found)
现象:启动正常,一调用档案列表接口,后台抛Invalid bound statement (not found),页面 500。
原因:MyBatis 找不到对应的 Mapper XML。通常是 yml 里mapper-locations与实际存放路径不一致,或者 XML 文件没有打进 classpath。这种源码的 XML 经常和 Java 类放在同一目录,但 Maven 默认构建只打包 resources 下的文件。
解决:把 XML 移到src/main/resources/mapper/下,确认配置为classpath:mapper/*.xml。如果必须把 XML 放 Java 包目录,到 pom.xml 的<build><resources>里加上**/*.xml的包含规则。改完执行mvn clean compile再重启,不要用 IDEA 增量构建,否则可能加载旧 classpath。
5.2 数据库连接串报时区错误
现象:启动时抛The server time zone value '...' is unrecognized,错误信息里时区名是乱码。
原因:MySQL 8.0 驱动强制要求显式时区。如果源码连接串里少了serverTimezone,或者有人改数据库密码时把参数误删,就会触发这个异常。
解决:连接串改成jdbc:mysql://localhost:3306/archive_system?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。顺手确认 MySQL 服务端时区,执行SELECT @@global.time_zone;,输出不是 SYSTEM 就能稳定运行。
5.3 上传大文件报 413 或 500
现象:传几 MB 的文档没事,传 50MB 的扫描件就挂。后端报MaxUploadSizeExceededException,前端可能收到 413。
原因:Spring Boot 的 multipart 默认单文件上限 1MB,很多毕设源码没有改这个配置。如果前面还挂了 Nginx,Nginx 默认client_max_body_size也是 1MB,请求还没到后端就被打回来了。
解决:配置文件里把max-file-size和max-request-size调大,同时把 Nginx 的client_max_body_size 100m;加上。注意区分:Spring Boot 超限报 500,Nginx 超限报 413。看到 413 查 Nginx,看到 500 再查 Spring 配置,排查方向别搞反。
5.4 PageHelper 分页错乱
现象:第二页返回了全表数据,或者第一页和第二页内容重叠。
原因:PageHelper.startPage()与 Mapper 调用之间插入了其他 SQL。分页参数放在 ThreadLocal 里,下一次 SQL 执行时消费掉,中间多了任何查询或日志触发 MyBatis 都会把它吞掉。
解决:把startPage写在目标查询紧邻的上一行,中间不插入其他数据库操作。如果是在循环里分页,先把当前页参数取出来,循环内直接用LIMIT拼接,别用 ThreadLocal 型分页插件。排查时在 startPage 前打印调用栈能看到是谁插了一脚。
5.5 登录后 302 循环,接口拿不到 session
现象:登录成功跳到首页,但每次后端请求都 302 回登录页,Network 面板里能看到重定向环路。
原因:拦截器排除路径没配全,或者前端 ajax 请求没携带 cookie。前后端分离开发时,前端跑 8081、后端跑 8080,跨端口请求默认不带凭证,session 自然失效。
解决:后端拦截器把登录、注册、静态资源都加进排除列表;前端请求配置withCredentials: true,axios 写法是axios.defaults.withCredentials = true。后端如果开了 CORS,还要把allowedCredentials设成 true,否则浏览器会拒绝携带 cookie 的跨域响应。这类问题看 Network 面板比猜代码快得多。
5.6 中文乱码,全站显示问号
现象:登录后用户姓名、档案标题全部显示为?。
原因:数据库表是 utf8 字符集,但导入脚本时连接编码不一致。Windows 命令行导入 SQL 时,cmd 默认 GBK,用mysql -uroot -p < archive_schema.sql直接灌,中文全部变成问号。
解决:在 mysql 客户端里先执行SET NAMES utf8mb4;,再执行source导入;也可以直接用 Navicat 或 DataGrip 打开 sql 文件执行,这些客户端会自动按文件编码读取。建库语句统一用utf8mb4,连接串带characterEncoding=utf8,三处保持一致,乱码基本消掉。
6. 进阶改造:用三个改动把通用源码做出明显区分度
拿到一套能跑通的源码,直接交毕业设计也能过,但要拉开差距还得加改造。我建议先加三个不复杂的功能,每个都可以在现场演示。
第一个:给档案表补一个file_md5字段,上传前先通过MessageDigest算 MD5,重复文件直接复用,列表页还能显示“此文件已借阅 N 次”。这个能力叫文件去重,实现成本不高,但演示效果直接。第二个:档案列表页加一个统计面板,按分类统计数量、按月展示建档趋势,后端一条GROUP BY category就能出数据。这个功能能让评委看到你有数据意识,不是只会查列表。第三个:接入 Spring Boot Admin,引入spring-boot-admin-starter-server和spring-boot-admin-starter-client,把当前应用的健康状态、内存、请求延迟在监控面板里展示。对毕设答辩来说,这是很加分的增量,它证明你接触过生产环境运维视角的东西。
改造顺序上我有一个教训。刚开始改这类源码时,我直接动了表结构并同步改了所有 Mapper 查询,结果接口全挂在字段名不匹配上。后来我总结了一套顺序:先加字段,再改实体和 Mapper,然后重启看日志,最后才动前端页面。因为前端页面跟 Controller 接口路径强绑定,接口一变,前端全部要跟着动;而字段不一致的报错通常比接口改动好定位得多,优先级应该靠前处理。
还有一步我每次都会做:在 IDEA 的 Terminal 里执行mvn dependency:tree,把依赖树完整扫一遍,确认 Spring Boot 版本、MyBatis Starter、MySQL 驱动、Lombok 这几个关键依赖没有版本冲突。我当年把 Spring Boot 从 2.3 升到 2.7 时,顺手把 Lombok 升到最新版,结果编译直接报方法签名错误;回滚 Lombok 之后又发现 MySQL 驱动被拉成 8.0.33,时区配置全要重调。从那以后,每次拿到新源码,我都强制先跑一次mvn dependency:tree,看清楚依赖底座再动手。这套档案管理系统源码的坑不算多,但数据模型、权限边界和文件链路这三块,值得你花一个晚上彻底弄透,希望帮到你。
本文还有配套的精品资源,点击获取