简介基于若依的Java档案管理系统设计源码适合有Java与Vue基础的学习者用于理解企业级后台系统的开发模式与权限控制实现。项目围绕档案业务覆盖录入、查询、管理等常见场景展现前后端分离架构中Java后端、Vue前端以及Shell脚本部署的完整协作方式。资源共812个文件以367个Java源文件和116个Vue组件为核心辅以JavaScript脚本、XML配置、SCSS样式及批处理与部署配置等压缩包约99.27MBJava源码承载业务逻辑Vue文件呈现交互界面配置脚本用于环境切换目录按业务与配置分模块组织。已有970人浏览学习多用于个人项目练手或毕业设计参考。包内自带若依环境使用手册可结合启动脚本与多环境配置文件快速完成本地部署、打包及参数修改无论是初次接触若依脚手架还是希望借助完整案例推进业务系统落地都能从中获得真实可运行的参考资料。1. 基于RuoYi的Java档案管理系统设计源码先想清楚它解决的三个真问题基于RuoYi的Java档案管理系统设计源码核心思路是把若依RuoYi这套Spring Boot加Vue的前后端分离脚手架当成底座在上面搭建档案登记、分类存储、借阅审批、归还销档的完整业务闭环。不少团队接到档案系统需求后的第一反应是从零写结果两三个月过去登录、权限、菜单、部门树耗掉一大半工期真正的档案业务反而没时间打磨。用RuoYi做起点登录、用户、菜单、数据权限都已经有了你把精力集中到档案表设计、编号生成、借阅状态机、文件存储这几块两到三周跑通核心流程是比较现实的。适合谁看准备拿若依框架做管理类系统的Java开发、只要平时写过Spring Boot但对数据权限和状态流转不太熟悉的初中级工程师以及低代码平台之外的私有化部署团队。2. 表结构设计与RuoYi扩展档案、借阅、菜单三张核心表怎么落库2.1 先从数据权限反推档案表谁的档案谁能看RuoYi的权限体系里用户归属部门sys_dept角色关联菜单和按钮数据权限通过DataScope注解在SQL层拼接过滤条件。档案管理系统和普通的客户管理系统不一样档案天然带有“密级”和“归属部门”双重属性。我在这里见过两种翻车做法一是档案表里只放create_by二是不管任何权限。档案表冗余部门字段是Ruoyi体系的默认做法常见解决方案是在档案主表上挂dept_id和dept_name。 原因很直接档案的查看范围多数按部门隔离DataScope注解会把当前用户可见的部门ID列表拼进SQL。如果档案表没有dept_idDataScope就无列可用权限成了摆设。建表脚本如下CREATE TABLE archive_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, archive_code VARCHAR(32) NOT NULL COMMENT 档案编号, title VARCHAR(200) NOT NULL COMMENT 档案标题, category_id BIGINT NOT NULL COMMENT 档案分类ID, secret_level TINYINT DEFAULT 0 COMMENT 密级0公开 1内部 2机密, dept_id BIGINT NOT NULL COMMENT 归属部门ID, dept_name VARCHAR(100) COMMENT 冗余部门名称, file_count INT DEFAULT 0 COMMENT 附件数量, status TINYINT DEFAULT 0 COMMENT 状态0在库 1借出 2销毁, create_by VARCHAR(32), create_time DATETIME, update_by VARCHAR(32), update_time DATETIME, remark VARCHAR(512), KEY idx_archive_dept (dept_id), KEY idx_archive_category (category_id), KEY idx_archive_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT档案主信息表;建表时把dept_id放到非空约束里强制前端传入。archive_code统一由后端生成不要直接接受页面手工输入后续讲解编号生成时会展开。secret_level用TINYINT而不是字符串配合字典翻译。状态字段专门建索引因为档案列表页最常见的查询条件就是“在库”和“借出”。2.2 档案主表、附件表、借阅表的分工边界很多档案系统的第一版只做一张表档案基本信息、附件路径、借阅人、借出时间、归还时间全塞进去。后面一旦要做附件多版本、按档案查借阅历史就只能靠字符串拆解这是典型的“表单思维害了表设计”。拆表原则很简单主表只存放不变或低频变化的信息附件和借阅是独立的生命周期CREATE TABLE archive_file ( id BIGINT AUTO_INCREMENT PRIMARY KEY, archive_id BIGINT NOT NULL COMMENT 档案主表ID, file_name VARCHAR(255) NOT NULL COMMENT 原始文件名, file_path VARCHAR(500) NOT NULL COMMENT 存储路径, file_size BIGINT DEFAULT 0 COMMENT 文件大小字节, file_type VARCHAR(50) COMMENT 扩展名, upload_by VARCHAR(32), upload_time DATETIME, KEY idx_file_archive (archive_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT档案附件表; CREATE TABLE archive_borrow ( id BIGINT AUTO_INCREMENT PRIMARY KEY, archive_id BIGINT NOT NULL COMMENT 档案主表ID, borrow_user VARCHAR(32) COMMENT 借阅人账号, borrow_name VARCHAR(50) COMMENT 借阅人姓名, borrow_time DATETIME COMMENT 借出时间, expect_return DATETIME COMMENT 预计归还时间, actual_return DATETIME COMMENT 实际归还时间, audit_status TINYINT DEFAULT 0 COMMENT 审批状态0待审 1通过 2驳回, audit_by VARCHAR(32), audit_time DATETIME, audit_remark VARCHAR(500), KEY idx_borrow_archive (archive_id), KEY idx_borrow_status (audit_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT档案借阅申请表;主表状态status和借阅表审批状态audit_status是两套状态别混在一起。常见做法是借阅通过后同时更新主表状态为借出归还时再改回在库。archive_file单独拆出来还有一个好处档案扫描件可能几十个文件主表file_count字段只是用来展示的冗余不要当计数依据——统计附件数量一律SELECT COUNT(*) FROM archive_file WHERE archive_id?。2.3 菜单和按钮的SQL脚本用代码管理资源RuoYi的菜单表sys_menu是典型的树形结构建议用SQL脚本管理菜单初始化而不是在页面上手工点。这样代码评审时能看到权限边界。INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, perms, visible, status) VALUES (档案管理, 0, 1, archive, archive/archiveIndex, C, archive:list, 0, 0); SET menu_id LAST_INSERT_ID(); INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, perms) VALUES (档案登记, menu_id, 1, , , F, archive:add), (编辑档案, menu_id, 2, , , F, archive:edit), (删除档案, menu_id, 3, , , F, archive:remove), (借阅审批, menu_id, 4, , , F, archive:approve);menu_type字段里C代表菜单F代表按钮权限。保存到sys_menu后给角色分配菜单时按钮权限自动挂靠未勾选状态不额外插入角色表记录就不会被误用。一个常见问题是要不要插入sys_role_menu关联数据答案是不要——RuoYi后台的角色分配页面会自动维护这张关系表脚本里硬塞反而会在环境重建时出现角色权限残留。3. 核心流程复现档案编号、上传存储与借阅审批的状态机实现3.1 档案编号生成Redis自增加数据库兜底双保险档案编号几乎都长这样分类前缀加年月日再加四位流水例如HT-202503-0007。最蠢的写法是直接取当前表里最大值加一并发情况下两台服务器同时拿到0007编号就重复了。常见的可靠做法是当天日期做Redis Key用INCR命令生成流水号同时给数据库唯一索引做兜底。万一Redis数据被清掉应用能从表里查出当天最大流水号再回填Redis。Service public class ArchiveCodeService { Autowired private StringRedisTemplate redisTemplate; Autowired private ArchiveInfoMapper archiveInfoMapper; public String generateCode(String categoryPrefix) { String datePart LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); String key archive:seq: datePart; Long seq redisTemplate.opsForValue().increment(key); // 防止Redis过期后从头计数导致与库里已有编号冲突 if (seq ! null seq 1L) { String maxCode archiveInfoMapper.selectMaxCodeByDate(datePart); if (StringUtils.isNotBlank(maxCode)) { int existSeq Integer.parseInt(maxCode.substring(maxCode.length() - 4)); seq redisTemplate.opsForValue().increment(key, existSeq); } } redisTemplate.expire(key, Duration.ofDays(2)); return categoryPrefix - datePart - String.format(%04d, seq); } }这段逻辑的关键在增量值。第一次执行并且Redis没有数据时先让计数为1再查表里当天最大值如果库里已经有0009就直接INCR一次增量9把当前值推到10下一次生成就得到0010。expire设置两天是因为跨天之后旧Key没有利用价值但保留两天能防止任务半夜执行时时间边界导致计数错乱。数据库兜底靠的是archive_code字段上加唯一索引真撞了直接抛异常由上层捕获后重试。3.2 附件上传与本地存储、MinIO切换RuoYi默认的上传工具类FileUploadUtils把文件写到本地磁盘小内网可用但团队也会看重对象存储的可靠性。我的建议是控制器不直接依赖具体存储实现定义Service接口配置文件决定走哪个分支。public interface ArchiveFileStorage { String store(MultipartFile file, String archiveCode); void delete(String filePath); } Service ConditionalOnProperty(name archive.storage.type, havingValue local) public class LocalFileStorage implements ArchiveFileStorage { Value(${archive.storage.local-path}) private String localPath; Override public String store(MultipartFile file, String archiveCode) { String dir localPath / archiveCode; File folder new File(dir); if (!folder.exists()) folder.mkdirs(); String fileName System.currentTimeMillis() _ file.getOriginalFilename(); File dest new File(folder, fileName); file.transferTo(dest); return archiveCode / fileName; } }ConditionalOnProperty是Spring Boot内置条件装配配置archive.storage.typelocal时加载本地实现改成minio就加载MinIO实现。上传控制层只调storage.store()返回值存入archive_file.file_path。这样换来一个好处后续把本地历史文件迁移到对象存储只需要新写一个迁移Job业务代码完全不用动。注意本地路径不要放在resources或target目录下那两处打包或执行mvn clean会被清掉。实际部署我一般放在/data/archive-storage由启动脚本提前创建权限交给运行用户。3.3 借阅审批状态机一纸状态枚举挡住重复借出借阅流程如果只用if/else判断后面加需求时迟早会翻车。常见做法是先用枚举把全部合法流转列出来业务代码里只允许枚举定义内的转换。public enum BorrowStatus { PENDING(0), APPROVED(1), REJECTED(2), RETURNED(3); public boolean canTransitTo(BorrowStatus target) { if (this PENDING) { return target APPROVED || target REJECTED; } if (this APPROVED) { return target RETURNED; } return false; } }canTransitTo把非法流转直接挡在入口管理员把已归档的档案再驳回调用方就抛出业务异常。审批通过时同时调一次档案主表的状态更新把archive_info.status从0改成1用事务保证要么借阅记录和档案状态都更新成功要么都回滚。Transactional(rollbackFor Exception.class) public void approve(Long borrowId) { ArchiveBorrow borrow borrowMapper.selectById(borrowId); if (borrow null || !borrow.getAuditStatus().equals(BorrowStatus.PENDING.getCode())) { throw new ServiceException(该借阅申请不在待审批状态); } borrow.setAuditStatus(BorrowStatus.APPROVED.getCode()); borrow.setAuditBy(SecurityUtils.getUsername()); borrow.setAuditTime(new Date()); borrowMapper.updateById(borrow); ArchiveInfo archive archiveInfoMapper.selectById(borrow.getArchiveId()); archive.setStatus(1); archiveInfoMapper.updateById(archive); }Transactional让主表状态和借阅状态同生共死避免出现“借阅审批已通过但档案状态还是在库”的脏数据。同一档案并发被两个人申请借出时光靠这个还不够需要在主表更新时加条件防重UPDATE archive_info SET status 1 WHERE id #{id} AND status 0判断影响行数如果等于0说明档案已经被借走直接抛异常。这是数据库层的乐观锁写法比应用层SELECT FOR UPDATE更简单。3.4 档案检索条件查询与全文索引的取舍档案列表页最常见的检索条件是分类、状态、编号模糊查。RuoYi自带的分页插件PageHelper直接能用。public TableDataInfo list(ArchiveQueryVO query) { PageDomain pageDomain TableSupport.buildPageRequest(); PageHelper.startPage(pageDomain.getPageNum(), pageDomain.getPageSize()); ListArchiveInfo list archiveInfoMapper.selectArchiveVoList(query); return getDataTable(list); }XML里写联表把分类名、部门名翻译好别在Java里循环补字段。至于全文搜索如果档案标题、摘要只有几千条直接LIKE %关键词%够用上万条以后建议引入Lucene或ElasticSearch。档案系统我建议第一版不要上ES理由在于数据量增长不可预测但检索需求相对固定先做好分页和索引更实际。4. 部署与参数调整从开发机到测试环境的RuoYi配置清单4.1 多环境Profile配置一次打包到处跑RuoYi的application.yml默认带application-druid.yml等分文件推荐再加一套application-demo.yml和application-prod.yml用spring.profiles.active控制。档案系统的核心配置项如下。# application-prod.yml spring: datasource: druid: url: jdbc:mysql://172.16.0.10:3306/ruoyi_archive?useUnicodetruecharacterEncodingutf8mb4useSSLfalse username: archive_app password: ${DB_PASSWORD} redis: host: 172.16.0.11 port: 6379 password: ${REDIS_PASSWORD} archive: storage: type: local local-path: /data/archive-storage code: prefix: HT server: port: 8080${DB_PASSWORD}这种写法是把密码从配置文件里抽到环境变量生产环境不把明文库密码写进配置文件。characterEncodingutf8mb4必须配否则档案标题带生僻字时存储会乱码。连接池用Druid时maxActive改成50initialSize保持5不要照抄默认的20档案系统并发量往往不高但文件写入占用连接时间较长小连接池反而更快。4.2 前端联调与API代理配置RuoYi的前端Vue项目本地启动时通过vue.config.js代理解决跨域。档案附件上传特别依赖这个配置因为上传时七牛或本地直传签名时常直接走后端接口。// vue.config.js devServer: { host: 0.0.0.0, port: 80, proxy: { /prod-api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/prod-api: } } } }pathRewrite重写之后前端请求/prod-api/archive/upload后端收到的是/archive/upload。RuoYi默认上下文是prod-api新同事最容易漏配导致401或404。改后端的server.servlet.context-path不如保持默认值因为RuoYi的权限拦截器、Swagger配置都默认与prod-api对齐。4.3 定时任务与逾期归还提醒借阅出去的档案超期未归还是档案室最常见的问题。RuoYi自带Quartz集成建议把逾期检查做成独立Job。RuoYi的定时任务是在sys_job表里维护调用bean所以必须继承TaskService的接口或使用Component标注的Bean方法。Component(archiveRemindTask) public class ArchiveRemindTask { Autowired private ArchiveBorrowMapper borrowMapper; public void checkOverdue() { ListArchiveBorrow overdueList borrowMapper.selectOverdueList(new Date()); for (ArchiveBorrow borrow : overdueList) { // 推送站内信或邮件 notifyUser(borrow); } } }在RuoYi定时任务管理页配置调用目标字符串填archiveRemindTask.checkOverdue执行周期cron表达式填0 0 9 * * ?意思是每天早上9点整扫描一次。不要在任务方法里写Thread.sleepQuartz的一个线程池如果被阻塞其他任务都会排队。5. 常见问题与避坑档案系统落地过程踩过的五个坑5.1 漏配DataScope导致越权查看现象两个平行部门的人登录后档案列表都能看到对方的全部档案。原因查询接口的Mapper层SQL没有拼上数据权限条件DataScope注解漏加或者加了注解但USER_COLUMN与DEPT_COLUMN写得和表字段不一致。解决Controller方法上补DataScope(deptAlias ai)SQL里写成ai.dept_id同时保证archive_info表别名是ai。排查时直接在数据库把当前用户登录态下的SQL打印出来看末尾有没有AND ai.dept_id IN (...)。5.2 大附件上传被网关掐断现象上传超过10MB的扫描件前端显示进度条走完但随后请求失败小文件正常。原因Nginx默认client_max_body_size是1m和Spring Boot默认没有关系。解决Nginx配置文件增加client_max_body_size 50m;和proxy_read_timeout 300s;。Spring Boot侧对应调spring.servlet.multipart.max-file-size。别只改一边Nginx能过但Tomcat超限报错信息不一致时会绕圈子。5.3 并发借阅同一份档案现象档案明明已经被借出另一张借阅单还是审批通过操作日志出现两条生效记录。原因审批和主表状态更新的判断不是原子操作两个请求同时读到status 0。解决审批时用带条件更新的UPDATE archive_info SET status 1 WHERE id #{id} AND status 0返回影响行数为0就回滚抛异常。这个办法不需要引入分布式锁性能上也够用。5.4 时间字段时区问题导致审批记录乱序现象列表页显示借出时间比归还时间还晚排序列乱。原因数据库连接串少了serverTimezoneAsia/ShanghaiSpring Boot默认时区与MySQL不一致DATETIME字段被Jdbc驱动按默认时区转换。解决连接串加参数并在MySQL侧执行SET time_zone 8:00。再有乱序就检查是不是前端Date.parse对2025/03/01和2025-03-01的解析差异统一用时间戳。5.5 删除档案只删了记录物理文件还留在磁盘现象档案删除后磁盘占用没有下降且新登记同编号档案时偶尔出现旧文件可下载。原因业务代码只执行了DELETE FROM archive_info没有删关联表和存储文件。解决删除接口上加事务先查附件列表删除存储文件再删archive_file记录最后删主表记录。文件删除失败不能吞掉异常应当记录日志并把主表标记为deleted_pending由后续补偿任务再处理。6. 进阶用十分钟做一份带封面目录的借阅电子目录6.1 利用RuoYi模板引擎生成档案封面目录档案管理系统的行政人员经常要按批次打印目录清单一条条复制到Word里效率很低。常见做法是后端直接用Apache POI生成EXCEL导出。还有一种更省事的方案做一个HTML模板用Thymeleaf渲染出目录页再配合浏览器的打印功能输出PDF。把目录页存成模板文件渲染时传入档案列表每行显示档案编号、标题、密级、借阅人、借出时间、状态。这个方案不需要引入额外的报表引擎十几行代码就能完成。生成HTML后再用JavaScript调用window.print()用户选择“另存为PDF”就能存档不需要浏览器插件。6.2 验证方法从功能到数据的自查清单验收时不要只盯着页面按钮。我的习惯是检查三件事。第一用不同部门账号登录确认档案列表是否隔离这比看100个菜单权限截图都有用。第二并发开两个浏览器同时提交同一份档案的借阅申请确认只有一单能审批通过。第三手工删掉存储目录下的一个文件再走一遍档案查询和附件下载确保报错信息对用户友好而不是抛500。这三步走完系统的数据权限、并发一致性、错误兜底基本就稳了。我自己的教训是第一次做档案系统时只关注了CRUD接口是否跑通忽略了权限拼接的验证。后来上线前让两个部门互查数据才发现DataScope的别名配置漏了一处差点在生产环境暴露敏感档案。从那以后凡是涉及数据和文件读取的改动我都固定走一遍“低权限账号越权尝试”流程再普通的功能改动也不跳过。希望这篇笔记能帮你少走这段弯路。本文还有配套的精品资源点击获取