news 2026/10/31 2:19:35

电子文件档案管理系统毕设拆解:业务链路、数据权限与二次开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
电子文件档案管理系统毕设拆解:业务链路、数据权限与二次开发

简介:面向本科毕业设计场景的电子文件档案管理系统全套资料,围绕电子文档的上传、分类检索、权限控制、版本管理与安全审计等需求展开,适合计算机相关专业学生完成课程实践、毕业设计或学习 Java 桌面应用与数据库协同开发。压缩包共 151 个文件,约 3.24MB,包含 93 个 class、22 个 form、18 个 java 源文件,以及 SQL 脚本、properties 配置、jar 依赖和 5 份 doc 文档;其中源码与窗体文件对应系统功能模块,SQL 脚本用于数据库建表,doc 文档承载毕业论文与英文翻译,便于从实现到论文写作整体参考。已有 606 人学习。资料的价值在于将需求分析、实体关系设计、文件信息表与用户权限表设计、登录鉴权、访问日志、缓存与异步处理等知识点落到可运行代码中,注释较完整,可帮助读者理解桌面端档案管理系统的目录组织、功能调用关系与数据库连接方式,也方便在现有结构上做功能扩展和答辩准备。

1. 一套「电子文件档案管理系统」毕设包,值钱的不是代码量

答辩现场最常见的一幕:电子文件档案管理系统能登录、能上传、能下载,老师只问一句「归档和网盘上传有什么区别」,人就卡住了。这类毕业设计包通常配得挺全,全套代码、数据库脚本、逐行注释、毕业论文加英文翻译一应俱全,问题往往出在功能和业务对不上号。这套电子文件档案管理系统的价值不在代码行数,而在于它把「收集—整理—归档—保管—利用」这条链路落成了表结构和状态机,论文里的需求分析能和库里的字段一一对得上。适合三种人:赶毕设的本科生、想拿一套带权限和审批的后台脚手架练手的初级开发、要给同类系统做二次开发的人。下面按「它是什么 → 怎么跑 → 怎么改」的顺序拆。

2. 技术栈与目录结构:先把骨架和建表脚本摸清楚

拿到这种整包资源,我一般不看 README,先看三样东西:pom.xml、建表 SQL、controller目录。三样看完,项目能干什么、怎么跑、坑大概率在哪,心里基本有数了。这一章就按这个顺序拆,顺带把「档案系统」和普通「文件管理后台」在数据模型上的差别讲透,因为答辩和二次开发都绕不开这一点。

2.1 技术栈与依赖清单:先确认是单体还是前后端分离

这套包主流有两种形态:一种是 Spring Boot 单体 + Thymeleaf 模板 + Layui,页面直接由后端渲染;另一种是 Spring Boot 提供 REST 接口 + Vue2 + Element UI 独立前端。判断方法很简单,看根目录有没有package.json、vue.config.js或者vite.config.js,有就是分离的,没有就是服务端渲染。

把依赖整理成一张表,方便你对照手上的包核对:

层次常见选型在包里的位置怎么确认
运行环境JDK 8pom.xml的maven.compiler.source搜source和target
后端框架Spring Boot 2.x 或 SSMpom.xml的<parent>看 groupId 是不是spring-boot-starter-parent
持久层MyBatis / MyBatis-Plussrc/main/resources/mapper/*.xml有 XML 说明是原生 MyBatis
数据库MySQL 5.7 / 8.0sql/*.sql看脚本里的utf8mb4和引擎
视图层Thymeleaf + Layui 或 Vue2templates/或src/views/两边目录名不一样
文件存储本地磁盘目录application.yml的file.upload-path搜upload关键字

版本号这类信息以包里pom.xml为准,别照抄我表里的写法。有几处细节值得注意:如果是 SSM(Spring + SpringMVC + MyBatis),会多出spring-mvc.xml、applicationContext.xml和web.xml,启动方式也从main方法变成丢进 Tomcat;如果是 Spring Boot,配置文件一般是application.yml,启动类是带@SpringBootApplication注解的那个类。这两种结构的调试思路完全不同,先分清能省下大量时间。

三条命令就能把关键信息捞出来:

# 1. 看后端框架和 JDK 版本 grep -A3 "<parent>" pom.xml grep -nE "java.version|maven.compiler" pom.xml # 2. 判断是不是前后端分离 ls -1 | grep -E "package.json|vue.config.js|vite.config.js" # 3. 看数据源和文件存储配置 grep -nE "url:|username:|password:|upload|path" src/main/resources/application.yml

第一条命令里的-A3表示匹配行后再输出 3 行,用来完整看到parent的 groupId 和 version;第二条过滤出前端工程标志文件,输出为空就是服务端渲染;第三条把数据源和上传路径一次性列出来,后面配置环节直接用得上。如果application.yml不存在,就去src/main/resources下找*.properties,字段名是spring.datasource.url这种完整形式。

2.2 目录结构与文件职责:注释密度高,但别全信

典型的目录长这样,我按职责标注了每一层该放什么:

archive-system ├── sql/ # 建库脚本 + 初始化数据,跑项目第一步就是它 │ ├── archive_db.sql # 建表语句 │ └── init_data.sql # 字典、菜单、默认管理员账号 ├── src/main/java/com/example/archive │ ├── controller/ # 只做参数接收、校验、返回封装 │ ├── service/ # 业务逻辑,事务边界在这一层 │ │ └── impl/ │ ├── mapper/ # 接口,对应 resources/mapper 下的 XML │ ├── entity/ # 与数据库表一一映射 │ ├── dto/ # 接收前端入参 │ ├── vo/ # 返回给前端的裁剪视图 │ ├── config/ # 拦截器、跨域、静态资源映射 │ ├── common/ # Result 统一返回、异常、常量 │ └── utils/ # 文件工具、档号生成器 ├── src/main/resources │ ├── application.yml │ ├── mapper/ # MyBatis 的 SQL XML │ ├── static/ # 前端静态资源 │ └── templates/ # Thymeleaf 页面 └── pom.xml

重点看config目录,权限拦截器、静态资源映射、跨域配置基本都在这,二次开发改动最频繁的也是这里。utils里一般会有个FileUtils或者UploadUtils,里面藏着文件落盘规则和扩展名白名单,安全相关的逻辑集中在这个文件,值得单独读一遍。

提示:这套包的卖点之一是逐行注释,但注释和代码不同步是常态,尤其是状态字段的取值含义。凡是注释里写「1 表示已归档」的地方,都去sql/里的建表语句或者枚举类核一遍,以数据库和枚举为准,别以注释为准。

2.3 数据库设计:从表名读出业务

普通文件管理后台,一张file表就够;档案系统不行,它得管档号、密级、保管期限、立卷部门、借阅留痕。这是两者最本质的差别,也是答辩最容易追问的地方。核心表通常有这么几张:

  • arc_category:档案分类(门类、一级类目、二级类目),树形结构,一般带parent_id和ancestors字段
  • arc_file:电子文件主表,存元数据 + 物理路径
  • arc_file_version:版本表,同一份文件多次修改留痕
  • arc_borrow:借阅申请表,带审批状态
  • arc_log:操作日志,谁在什么时候看了、下载了哪份档案
  • sys_user/sys_role/sys_dept:权限三件套,dept_id同时兼作数据权限字段

主表结构大致如下,字段设计基本能反映业务深度:

CREATE TABLE `arc_file` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `file_no` VARCHAR(64) NOT NULL COMMENT '档号:全宗号-年度-分类号-4位流水号', `title` VARCHAR(200) NOT NULL COMMENT '文件题名', `category_id` BIGINT NOT NULL COMMENT '分类,关联 arc_category.id', `security_level` TINYINT NOT NULL DEFAULT 1 COMMENT '密级:1公开 2内部 3秘密', `retention` TINYINT NOT NULL DEFAULT 10 COMMENT '保管期限(年),30 视为永久', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待归档 1已归档 2已借出 3已销毁', `storage_path` VARCHAR(500) NOT NULL COMMENT '相对存储路径,不存绝对路径', `file_md5` CHAR(32) NOT NULL COMMENT '文件指纹,用于秒传和去重', `dept_id` BIGINT NOT NULL COMMENT '立卷部门,数据权限过滤字段', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_file_no` (`file_no`), KEY `idx_md5` (`file_md5`), KEY `idx_dept_status` (`dept_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电子文件主表';

几个字段值得单独说。file_no上加了唯一索引,这是档号生成的最后一道防线,并发下靠它兜底而不是靠代码判断。file_md5用来做秒传:同一份文件第二次上传时不必重复占磁盘空间,直接把storage_path指过去就行。storage_path存相对路径而不是D:/xxx/yyy.pdf,换机器、换系统、迁到 Linux 都不用改数据。dept_id和status建了联合索引,因为列表页最常见的查询就是「本部门 + 已归档」。字符集必须是utf8mb4,档案题名里出现生僻字和特殊符号很正常,utf8会截断,这个问题在后面排查章节还会遇到。

3. 核心模块拆解:一份电子文件从入库到借出的完整链路

档案系统的业务链路其实不长,但每一步都有坑:上传要防绕过、检索要防越权、借阅要防并发。这一章挑三个最能体现设计思路的模块拆开讲,代码按常见的 Spring Boot + MyBatis 写法给出,包里的实际实现可能略有差异,但逻辑骨架基本一致。

3.1 上传与元数据落库:先算指纹,再决定要不要写盘

上传接口是整套系统最值得读的一段代码,因为它同时要处理类型校验、去重、档号生成和事务边界:

@PostMapping("/upload") public Result<Long> upload(@RequestParam("file") MultipartFile file, ArchiveFileForm form) throws IOException { // 1. 白名单校验扩展名,不能只信前端传的 Content-Type String ext = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase(); if (!ALLOW_EXT.contains(ext)) { return Result.fail("不支持的文件类型:" + ext); } // 2. 先算 MD5,命中已有记录就直接复用物理文件,实现秒传 String md5 = DigestUtils.md5Hex(file.getInputStream()); ArchiveFile exist = archiveFileMapper.selectByMd5(md5); String relativePath = (exist != null) ? exist.getStoragePath() : storageService.save(file, form.getYear(), md5); // 3. 生成档号:全宗号-年度-分类号-4位流水号 String fileNo = fileNoGenerator.next(form.getFondsCode(), form.getYear(), form.getCategoryCode()); // 4. 元数据落库,物理文件此时已经在磁盘上 ArchiveFile entity = new ArchiveFile(); entity.setFileNo(fileNo); entity.setTitle(form.getTitle()); entity.setCategoryId(form.getCategoryId()); entity.setSecurityLevel(form.getSecurityLevel()); entity.setRetention(form.getRetention()); entity.setFileMd5(md5); entity.setStoragePath(relativePath); entity.setStatus(FileStatus.PENDING.getCode()); entity.setDeptId(SecurityUtils.getDeptId()); // 从登录态取,绝不从前端传 archiveFileMapper.insert(entity); return Result.ok(entity.getId()); }

逻辑上分四步,每步都有讲究。第一步用扩展名白名单而不是黑名单,黑名单永远漏;Content-Type由客户端决定,可以随便伪造,不能作为唯一依据。第二步先算 MD5 再决定是否落盘,是为了避免重复文件把磁盘撑爆,档案系统里同一份红头文件被不同部门反复上传的情况非常普遍。第三步的档号生成器是关键,fondsCode(全宗号)、year、categoryCode(分类号)拼起来,流水号一般用「当天最大号 +1」或者数据库序列,并发下靠uk_file_no唯一索引兜底,插入失败就重试一次,比加分布式锁划算得多。第四步的deptId一定要从登录态取,前端传什么都不能信。

注意:物理落盘和数据库插入不在同一个事务里。文件写成功但 insert 失败,会留下孤儿文件;反过来 insert 成功但文件没落盘,列表点进去就是 404。常见做法是先落盘再入库,入库失败时在catch里删文件,同时挂一个定时任务每天扫一遍磁盘,把数据库里查不到的孤立文件清掉。

3.2 分页检索与数据权限过滤:别让前端决定你能看什么

列表页看着最简单,实际最容易被刷。真正的风险是:用户改一下 URL 里的deptId,就能看到别的部门的档案。正确做法是把数据权限条件强制拼在服务端查询里,前端传的任何部门、密级参数一律忽略:

public PageResult<ArchiveFileVO> page(ArchiveQuery query) { Page<ArchiveFile> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<ArchiveFile> wrapper = Wrappers.lambdaQuery(); // 业务筛选条件:全部使用 xxx != null 才拼接,避免空条件污染 SQL wrapper.like(StringUtils.hasText(query.getTitle()), ArchiveFile::getTitle, query.getTitle()) .eq(query.getCategoryId() != null, ArchiveFile::getCategoryId, query.getCategoryId()) .ge(query.getBeginTime() != null, ArchiveFile::getCreateTime, query.getBeginTime()) .orderByDesc(ArchiveFile::getCreateTime); // 数据权限:非管理员强制追加部门和密级上限,忽略前端传参 if (!SecurityUtils.isAdmin()) { wrapper.eq(ArchiveFile::getDeptId, SecurityUtils.getDeptId()) .le(ArchiveFile::getSecurityLevel, SecurityUtils.getMaxSecurityLevel()); } archiveFileMapper.selectPage(page, wrapper); return PageResult.of(page, ArchiveFileVO::from); }

LambdaQueryWrapper的第一个布尔参数是「条件是否生效」,写成query.getTitle() != null这种形式能让空值自动跳过,比在 XML 里堆<if>标签清爽。数据权限的le(小于等于)用得比较多:用户密级上限是 2(内部),那他能看 1 和 2,看不到 3(秘密)。selectPage会同时在两条 SQL 上生效——一条count、一条limit,所以索引要能同时覆盖过滤和排序,idx_dept_status就是为此建的。

这里还有个容易被忽略的点:导出 Excel 的接口往往另写一套查询,忘了加数据权限,结果列表看不到的数据能从导出里拿到。二次开发时如果新增了任何「批量查询」入口,都要回到这个方法确认权限条件是否复用。

3.3 借阅审批:把状态流转收进枚举,别散落在 if 里

借阅是档案系统区别于网盘的核心功能,状态流转必须收敛。散落在各个 Service 里的if (status == 1)最后一定失控,正确做法是定义枚举 + 单一入口的流转方法:

public enum BorrowStatus { PENDING(0, "待审批"), APPROVED(1, "已通过"), REJECTED(2, "已驳回"), BORROWED(3, "借阅中"), RETURNED(4, "已归还"), OVERDUE(5, "已逾期"); private final Integer code; private final String desc; // 构造、getter 省略 }

审批方法本身要做两件事:校验当前状态、条件更新文件状态:

@Transactional(rollbackFor = Exception.class) public void approve(Long borrowId, boolean pass, String remark) { ArchiveBorrow borrow = borrowMapper.selectById(borrowId); // 只有 PENDING 能被审批,防止前端连点两次把状态改回去 if (!BorrowStatus.PENDING.getCode().equals(borrow.getStatus())) { throw new BizException("当前状态不允许审批"); } borrow.setStatus(pass ? BorrowStatus.APPROVED.getCode() : BorrowStatus.REJECTED.getCode()); borrow.setRemark(remark); borrowMapper.updateById(borrow); if (pass) { // 条件更新:只有「已归档」的文件才能被借出,影响行数为 0 说明被抢了 int rows = archiveFileMapper.updateStatus(borrow.getFileId(), FileStatus.ARCHIVED.getCode(), FileStatus.BORROWED.getCode()); if (rows == 0) { throw new BizException("该文件已被借出或状态已变更"); } } logService.record("BORROW_APPROVE", borrowId, remark); // 操作留痕,档案系统的硬要求 }

updateStatus对应的 SQL 是update arc_file set status = #{to} where id = #{id} and status = #{from},把状态判断交给数据库,靠行锁保证原子性。这比「先 select 判断再 update」可靠得多,后者在并发下必然出现两个人都借到同一份文件的情况。@Transactional的rollbackFor要显式写Exception.class,否则受检异常不会回滚,这个坑后面还会提。最后那行日志不能省,档案系统要求「每一次利用都有记录」,答辩时老师很可能就盯着这个问。

4. 跑起来:从建库到登录成功的三步启动

代码看得差不多了,接下来是动手。这套包的启动难度不高,坑主要集中在配置和初始化数据上,按下面三步走,一般十几分钟能见到登录页。

4.1 环境核对与配置项

先把环境对齐:JDK 8(版本不匹配会报Unsupported class file major version)、Maven 3.6+、MySQL 5.7 或 8.0。MySQL 8 要注意驱动类名是com.mysql.cj.jdbc.Driver,连接串还得带时区,否则启动时报The server time zone value is unrecognized。

配置文件长这样,逐项对着改:

server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/archive_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 50MB # 单文件上限,档案扫描件往往较大 max-request-size: 100MB # 单次请求总上限,多文件上传时按倍数调 file: upload-path: D:/archive/data # 物理存储根目录,Linux 改成 /data/archive allow-ext: pdf,doc,docx,xls,xlsx,jpg,png,tif

几个参数得单独解释。serverTimezone=Asia/Shanghai不写会导致时间差 8 小时,列表页的创建时间会莫名其妙错位。allowPublicKeyRetrieval=true是 MySQL 8 用caching_sha2_password插件时需要的,不加会报公钥检索失败。upload-path指向的目录必须真实存在且有写权限,代码一般不会帮你创建,Windows 下写成D:/archive/data(正斜杠)比反斜杠安全,反斜杠在 yml 里容易被当转义符。

4.2 建库导数据与首次启动

命令按顺序执行,每一步失败都要停下来看报错,别一路往下冲:

# 1. 建库,字符集必须 utf8mb4,否则中文题名会被截断 mysql -uroot -p -e "CREATE DATABASE archive_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 2. 导入结构和初始化数据(脚本里通常已包含 use archive_db) mysql -uroot -p archive_db < sql/archive_db.sql # 3. 编译打包,第一次会下载依赖,慢是正常的 mvn clean package -DskipTests # 4. 启动,dev 环境一般会打印更详细的 SQL 日志 java -jar target/archive-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev

第二步最容易出问题:脚本用的是utf8而不是utf8mb4,导入时中文变问号;或者脚本里写死了archive_db之外的库名,导入到别的库里去了。导入完执行show tables;确认表数量对得上,再看select count(*) from sys_user;,默认管理员账号就在这张表里,密码一般是 MD5 或 BCrypt 加密后的串,明文写没写在论文附录里,去论文的「系统实现」章节找一下就有了。

第四步如果打成 war 包,就不能用java -jar了,得丢进 Tomcat 的webapps目录,端口也由 Tomcat 决定。判断方法看pom.xml里的<packaging>,是jar还是war。

4.3 冒烟验证清单

启动成功不等于能用,按这条链路走一遍,五分钟能发现问题:

  1. 打开http://localhost:8080,用初始化账号登录,看是否跳转到首页而不是白屏
  2. 进「档案分类」,新建一个一级类目和一个子类目,刷新看树形结构是否正常
  3. 进「档案管理」,上传一个 PDF,观察upload-path下有没有真实生成文件
  4. 用上传时填的题名做模糊搜索,确认能查到,且分页总数正确
  5. 换一个不同部门的账号登录,确认看不到刚才那条记录
  6. 发起借阅申请,用管理员账号审批通过,回列表看状态是否变成「已借出」
  7. 进「操作日志」,确认上面每一步都有记录

第 3 步和第 5 步最能暴露问题。文件没落盘通常是路径配置错或者目录不存在;跨部门能看到数据,说明数据权限拦截没生效,回到 3.2 那节检查dept_id是不是从登录态取的。

5. 常见问题排查:五个翻车现场的原因与修复

这套包我断断续续帮人调过几次,报错集中在几个固定位置,基本可以按「症状」直接对号入座。

5.1 启动阶段:连不上库和端口占用

现象一:启动直接退出,日志里是Access denied for user 'root'@'localhost',或者Unknown database 'archive_db'。

原因通常是两种:密码里含有@、#、$这类特殊字符,写在 yml 里被解析错了;或者建库那一步根本没执行,直接把 SQL 导进了一个不存在的库。还有一种隐蔽情况,本地装了多个 MySQL 实例,127.0.0.1:3306连的不是你以为的那个。

解决方式:先用命令行验证连接,mysql -h127.0.0.1 -P3306 -uroot -p能进去说明账号没问题;密码带特殊字符时在 yml 里用单引号包起来,写成password: 'ab#123';实在判断不清就show variables like 'port';确认端口,Windows 上用netstat -ano | findstr 3306看谁在占。

现象二:Web server failed to start. Port 8080 was already in use.,或者启动日志里出现Circular view path。

端口占用多半是上一次的进程没退干净,IDE 里点了停止但 Java 进程还在。Windows 用netstat -ano | findstr :8080拿到 PID 再taskkill /F /PID xxx,Linux 用lsof -i:8080加kill -9。

Circular view path是 Thymeleaf 项目的经典坑:Controller 的@GetMapping返回值写成了"archive/list",但这个 Controller 的@RequestMapping本身就是/archive,视图解析时拼出/archive/archive/list,找不到模板又转回来,死循环。解决办法是返回值写相对路径"list",或者把@RequestMapping改成更具体的前缀。

5.2 上传与下载:路径、中文名和预览

现象三:上传提示成功,列表里也有记录,但点下载报 404,upload-path目录下什么都没有。

原因大概率和路径拼接有关。代码里常见两种写法:new File(uploadPath + "/" + fileName)和Paths.get(uploadPath, fileName),前者在 Linux 下如果uploadPath结尾带了斜杠,会拼出//,某些环境能跑某些不能;后者更稳。另外相对路径的问题更常见——upload-path: ./data是相对「启动时的工作目录」的,在 IDE 里跑和用java -jar跑,工作目录不一样,文件就跑到两个地方去了。

解决方式:把upload-path一律配成绝对路径;启动后打一行日志把new File(uploadPath).getAbsolutePath()打出来,看它到底落在哪;storage_path入库存相对路径,读取时统一拼上根目录,别在数据库里存绝对路径。

现象四:中文文件名下载后变成%E6%A1%A3%E6%A1%88.pdf这种乱码,或者干脆下载得到一个 0 字节文件。

前者是响应头里Content-Disposition的编码问题。文件名含中文时,浏览器对filename=的解析各家不一样,标准做法是同时给filename和filename*=UTF-8''两份:

String encoded = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-Disposition", "attachment; filename=\"" + encoded + "\"; filename*=UTF-8''" + encoded);

replaceAll("\\+", "%20")这行不能少,URLEncoder会把空格编成+,在 URL 语义里+表示空格,但部分浏览器会原样保留,导致文件名多出一堆加号。

至于 0 字节文件,多半是上传时就存了个空文件:MultipartFile被读过一次之后流就到底了,代码里如果先算 MD5 再把流交给写文件逻辑,第二次读就是空。解决办法是file.getBytes()存成字节数组复用,或者用file.getInputStream()时套一层BufferedInputStream并注意不能重复消费。

5.3 权限与数据:看得见和看不见

现象五:普通用户登录后,列表里能看到所有部门的档案;或者反过来,管理员登录也只剩自己部门的数据。

前者的原因是数据权限拦截器没生效,常见于两种情况:拦截器注册了但addPathPatterns写错,把/archive/**写成了/archives/**,压根没拦到;或者拦截器生效了,但 Service 里的查询走了另一个没加条件的方法(比如导出、统计),漏网了。

后者的原因更隐蔽,通常是拦截器里判断管理员的逻辑写反了,或者角色标识硬编码成了某个具体字符串,初始化数据里的角色编码和代码里的常量不一致——这种「对不上号」的问题最消耗时间。

排查思路:在拦截器里打一行日志,把当前用户、角色、部门、生效的 SQL 条件都打出来;打开 MyBatis 的 SQL 日志(logging.level.com.example.archive.mapper: debug),看最终执行的语句到底带没带dept_id条件。看到真实 SQL 之后,这类问题基本一眼就能定位,比盯着代码猜快得多。

提示:改权限相关代码之后,一定要用至少三个账号验证——超级管理员、部门管理员、普通用户,且普通用户要跨部门测一次。只用一个账号测权限,等于没测。

6. 二次开发:把这套毕设改成能写进简历的项目

直接拿毕设去面试,容易卡在「你做了什么」这个问题上。我的建议是挑两三个改造点做深,做完能讲清楚为什么改、怎么权衡,比把功能堆满更有说服力。按投入产出排个序:

改造点现状建议做法工作量
存储层抽象本地磁盘硬编码在工具类里抽StorageService接口,本地和对象存储两套实现半天
全文检索like '%关键词%'全表扫MySQL 全文索引起步,数据量大再上 ES1~2 天
文件在线预览只能下载转 PDF 后用 pdf.js 渲染,Office 文件走转换服务2~3 天
前端分离Thymeleaf 模板混写抽 REST 接口 + Vue3 重写列表页3~5 天

最值得先做的是存储层抽象,改动小、收益立竿见影。现在代码里到处是new File(uploadPath + ...),抽一个接口出来:

public interface StorageService { /** 保存文件,返回相对路径(入库用这个,不返回绝对路径) */ String save(MultipartFile file, String bizPath); /** 按相对路径读取,供下载和预览复用 */ InputStream open(String relativePath); /** 删除,物理删除失败只记日志不抛异常,避免影响主流程 */ void delete(String relativePath); }

本地实现照旧写磁盘,再补一个对象存储实现,通过@ConditionalOnProperty按配置切换。这样面试时能讲的东西就多了:为什么返回相对路径、删除失败为什么不抛异常、两套实现怎么保证行为一致。改造完记得回归第 4.3 节那份冒烟清单,尤其是上传、下载、删除这三条,存储层动过之后最容易在这翻车。

第二个建议是做全文检索。like '%xx%'在数据量上万之后会明显变慢,因为前置通配符用不上索引。可以先用 MySQL 的ngram分词建全文索引,几十万级数据足够用,改造量只有一条 DDL 加一段MATCH ... AGAINST查询。真要上 ES,就把索引同步做成「入库后发事件 + 异步写索引」,别在业务事务里同步调 ES,一旦 ES 挂了整条上传链路都得跟着挂。

还有个小技巧:改这套代码之前,先把sql/目录整个备份一份,再新建一个dev分支。毕设包的表结构往往和实体类有细微偏差(比如数据库里是create_time,实体类写的是createDate,靠@TableField映射着),改字段的时候很容易改漏一处,等到运行时才报Unknown column。我一般会在改之前先跑一遍全表select,把每个字段的实际值打出来对一遍。

说个踩过的坑收尾。有次图省事,直接在原项目上改存储路径,改完上传正常,下载 404,查了两个小时才发现是数据库里已经存了一批旧的绝对路径,新代码按相对路径拼,拼出来是个不存在的路径。从那以后我每次动存储相关逻辑,都强制先跑一遍数据订正 SQL,把所有历史记录刷成统一的相对路径格式,再动代码。希望这些能帮你少走点弯路,包里的代码、脚本、注释和论文都在,照着上面的顺序走一遍,跑起来不难。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 2:19:19

超市进销存管理系统源码拆解:从用例图到库存联动与二次开发

简介&#xff1a;这份资源是面向超市运营管理者、软件开发初学者及课程设计人员的超市进销存管理系统源码与设计资料&#xff0c;围绕商品进货、销售管理、库存统计、订单管理四大业务模块&#xff0c;解决手工记账效率低、库存数据滞后、订单难追踪等问题。压缩包共103个文件&…

作者头像 李华
网站建设 2026/10/31 2:19:14

Swift内购支付工具:StoreKit 1与2统一封装,解决审核被拒与丢单问题

简介&#xff1a;面向iOS开发者的Swift内购支付实践工程&#xff0c;聚焦应用内购买&#xff08;IAP&#xff09;从产品配置到交易回调的完整链路&#xff0c;适合已具备Swift基础、希望打通StoreKit接入与收据验证环节的中级开发者参考。压缩包共31个文件、约66KB&#xff0c;…

作者头像 李华
网站建设 2026/10/31 2:17:46

C语言学习笔记——常见操作符和表达式求值

C语言学习笔记——常见操作符和表达式求值 Hello大家好&#xff0c;又是我Mii&#xff0c;最近几天Mii又学习了常见操作符和表达式求值&#xff0c;所以Mii我又来写一篇文章来分享Mii学习到的知识了&#xff0c;话不多说直接开写(&#xff40;・ω・) 上次Mii学习了常亮和变量的…

作者头像 李华
网站建设 2026/10/31 2:17:10

继承和接口(Java)

继承继承是一种由已有的类创建新类的机制。利用继承可以先定义一个共有属性的一般类&#xff0c;根据该一般类再定义具有特殊属性的子类&#xff0c;子类继承一般类的属性和行为&#xff0c;并根据需要增加它自己的新的属性和行为。特点&#xff1a;java不支持多重继承&#xf…

作者头像 李华
网站建设 2026/10/31 2:16:26

山区落石目标检测数据集 助力边坡落石风险智能监测 AI深度学习第10837期

文章目录山区落石目标检测数据集&#xff0c;助力边坡落石风险智能监测数据概括应用价值小白入手训练代码山区落石目标检测数据集&#xff0c;助力边坡落石风险智能监测 边坡落石检测是山区道路地质安防的重要方向&#xff0c;这份山区落石目标检测数据集&#xff0c;面向边坡…

作者头像 李华