资讯动态

基于SpringBoot+Vue3+MyBatis的档案管理系统设计与部署实战

发布时间:2026/10/19 2:58:26 来源:尧图企业网站定制
做档案管理系统这个项目其实比想象中要麻烦得多。传统Excel管理档案随着数据量增长文件一多就乱查找、权限、版本全都是问题。我这次把一套完整的Java SpringBootVue3MyBatis档案管理系统源码从零梳理了一遍从需求拆分、表结构设计、前后端接口联调到打包部署该踩的坑一个没落下。这套技术栈是目前国内中小型管理系统里最常遇到的一套组合前后端分离 MySQL存储既有代表性又足够轻量。不管你是准备毕业设计还是想快速上手企业级项目开发都值得花二十分钟把这里面的设计思路完整走一遍。1. 项目整体设计思路与需求拆解1.1 档案管理系统不只是增删改查很多人看到档案管理系统这个标题第一反应是不就是对一张档案表做增删改查吗真正做起来才会发现业务上至少有五个维度需要同时处理。第一是分类。档案必须有可配置的档案分类比如人事档案、合同档案、技术文档分类还要支持树形层级否则后期检索直接失控。第二是权限。不同角色能看到的档案范围必须不一样普通用户只能看自己创建或借阅的档案管理员才能看全部。第三是借阅归还流程。档案实体借出去之后要有状态流转是在库还是已借出借了多久、谁借的、什么时候还都需要记录。第四是操作留痕。谁在什么时间改过哪份档案、删除过哪个分类必须能追溯不然出了问题根本没法查。第五才是录入、修改、查询这些基础操作。所以系统最终拆出来的功能模块比单纯一张表要复杂登录认证、用户权限、档案分类、档案管理、借阅管理、操作日志基本是六大部分。1.2 为什么一定要用前后端分离早几年做管理系统基本都是SpringBoot Thymeleaf模板一把梭后端同时管数据和页面渲染。这种方式对于小项目很快但页面交互一复杂就非常难受。这套项目采用前后端分离后端只提供JSON接口前端用Vue3独立构建页面两边可以完全并行开发。档案管理系统的查询页面经常要调整交互比如查询条件加一个、表格列改一列分离之后只改前端就行后端接口完全不用动。Vue3的Composition API处理复杂列表页、弹窗表单、文件上传这些场景比Vue2的Options写法更直观逻辑可以按功能聚合而不是按数据、方法、计算属性硬拆。另外前后端分离还有一个隐性好处接口可以复用。将来如果要做移动端或者小程序后端接口无需重写直接对接新前端就行。这也是现在企业里普遍采用这种架构的真实原因。2. 系统核心模块与数据库设计2.1 核心功能模块划分动手写代码之前先把模块边界划清楚。这里不能想到哪儿写到哪儿否则后面接口会越拆越乱。模块核心功能系统登录账号密码登录、JWT签发、退出登录用户与权限用户管理、角色分配、菜单权限控制档案分类分类树维护、档案类型关联档案管理档案新增、编辑、删除、条件查询、详情预览借阅管理借阅登记、归还处理、借阅记录查询操作日志用户操作记录、登录日志每个模块对应独立的Controller、Service、Mapper文件不做大杂烩。这一点很重要很多新手喜欢把所有接口塞进一个Controller看起来省事后面维护一次就哭一次。2.2 数据库表结构设计数据库是整个系统的地基。我把核心表拆成六张系统用户表、角色表、用户角色关联表、档案分类表、档案主表、借阅记录表另外再加一张操作日志表。用户表要特别注意密码字段不能存明文我用VARCHAR(100)来存BCrypt加密后的密码。档案主表是核心字段包括档案编号、标题、分类ID、内容、文件URL、状态、创建人、创建时间等。档案编号要有唯一性我单独加了唯一索引。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), status TINYINT DEFAULT 1, deleted TINYINT DEFAULT 0, create_time DATETIME, update_time DATETIME ); CREATE TABLE archive_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0, name VARCHAR(100) NOT NULL, sort_order INT, deleted TINYINT DEFAULT 0 ); CREATE TABLE archive ( id BIGINT PRIMARY KEY AUTO_INCREMENT, archive_no VARCHAR(50) NOT NULL, title VARCHAR(200) NOT NULL, category_id BIGINT, content TEXT, file_url VARCHAR(255), status TINYINT, create_by BIGINT, create_time DATETIME, update_time DATETIME, deleted TINYINT DEFAULT 0, UNIQUE KEY uk_archive_no (archive_no), KEY idx_category (category_id), KEY idx_create_time (create_time) );所有业务表都加了deleted字段做逻辑删除而不是物理DELETE。为什么档案数据往往需要审计追溯物理删掉之后痕迹就没了。逻辑删除虽然每次查询都要带上WHERE deleted 0但对数据安全来说完全值得。主键统一用BIGINT自增看起来不够炫但足够稳定。档案编号单独走业务编码规则比如类型前缀日期流水号避免用户拿主键当业务号用。2.3 索引和冗余设计心得档案表最核心的查询场景是根据档案编号、标题、分类、时间范围做分页查询。所以索引至少要有三个archive_no唯一索引、category_id普通索引、create_time普通索引。实际开发中我建议列表接口不要在数据库做太多关联查询。档案表要显示创建人姓名和分类名称我的做法是直接LEFT JOIN用户表和分类表。这两张表数据量不会太大关联成本可控。冗余字段反而是个坑比如在档案表里冗余一个分类名称一旦分类改名所有档案都得批量更新很容易出漏网之鱼。借阅记录表单独建一张记录借阅人、借出时间、应还时间、实际归还时间、状态每次归还都更新记录。千万不要在档案表上堆一堆当前借阅人借出时间字段否则历史记录全丢。3. 后端核心实现SpringBoot MyBatis3.1 分层结构与Mapper XML配置后端项目我按标准的三层架构拆包Controller负责接口和参数校验Service负责业务逻辑Mapper负责数据库操作。实体类、DTO、VO分开存放避免把查询参数和返回结果混在一起。com.example.archive ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo └── config用MyBatis时有一个非常容易踩的坑Mapper接口和XML文件必须能对得上。接口方法名、XML里语句的id、命名空间一丁点都不能错。XML文件我统一放在src/main/resources/mapper/目录下然后在application.yml里配置扫描路径mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.archive.entity如果项目启动后提示Invalid bound statement九成是这里没配好或者XML文件没有编译到target目录。3.2 用户认证与JWT权限控制登录模块我用JWT做无状态认证。用户提交用户名和密码后端用BCrypt校验密码校验通过后生成一个带过期时间的Token返回给前端。前端每次请求在Header里带Authorization: Bearer token后端通过拦截器统一校验。JWT的好处是后端不用保存会话状态水平扩展时非常方便。核心代码就是注册一个拦截器Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(401, 未登录或登录已过期); } Long userId JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, userId); return true; } }然后配置拦截器排除登录接口和静态资源registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns(/api/auth/login, /upload/**, /error);权限控制则是基于角色的用户登录后后端查询出其角色集合和权限标识在需要控制的接口上加自定义注解和AOP或者直接在Service里判断角色。小型档案系统用AOP做接口级权限控制就够了不需要引入重型权限框架。3.3 档案分页与条件查询档案列表是我最重视的接口。查询条件有四个关键字、分类ID、创建时间范围、当前页码。分页用MySQL的LIMIT实现在Service里计算offset和pageSizeMapper里用动态SQL拼接条件。Mapper XML核心片段select idselectArchivePage resultTypecom.example.archive.vo.ArchiveVO SELECT a.*, c.name AS category_name, u.real_name AS create_by_name FROM archive a LEFT JOIN archive_category c ON a.category_id c.id LEFT JOIN sys_user u ON a.create_by u.id WHERE a.deleted 0 if testkeyword ! null and keyword ! AND (a.archive_no LIKE CONCAT(%, #{keyword}, %) OR a.title LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null and categoryId ! 0 AND a.category_id #{categoryId} /if if teststartTime ! null AND a.create_time gt; #{startTime} /if if testendTime ! null AND a.create_time lt; #{endTime} /if ORDER BY a.create_time DESC LIMIT #{offset}, #{pageSize} /select注意时间比较在XML里要写成gt;和lt;直接写会报XML解析错误。这个问题排查过一次就长记性了。分页接口要返回总数所以我另外写了一个selectArchiveCount同一个条件再查一次COUNT。这个方案在数据量不大时完全没问题等将来档案量到百万级再考虑换成MySQL分析和缓存优化。3.4 文件上传与静态资源映射档案系统必然要上传PDF、图片、Word文件。后端接口用MultipartFile接收文件存储到服务器指定目录文件名用UUID重新生成避免中文名和重名问题。PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(请选择文件); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) ext; Path dir Paths.get(uploadPath); if (!Files.exists(dir)) { Files.createDirectories(dir); } file.transferTo(Paths.get(uploadPath, fileName).toFile()); return Result.success(/upload/ fileName); }文件存好之后要能通过URL访问Spring Boot默认不会把本地磁盘目录映射成静态资源需要在配置类里手动加Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourcePatterns(file: uploadPath /); }这里有个重要细节uploadPath最好配置成绝对路径不要用相对路径。我在本地调试时用./upload看起没问题但用java -jar部署的时候工作目录一变文件就找不到了。4. 前端核心实现Vue3 组件库4.1 Vue3工程初始化与核心依赖前端我用Vite创建的Vue3项目。相比WebpackVite开发环境下冷启动快很多改代码的响应速度也是即时反馈体感舒服太多。创建一个新项目的命令是npm create vitelatest archive-ui -- --template vue cd archive-ui npm install vue-router4 pinia element-plus axios组件库选了Element Plus和Vue3配合很顺畅表格、表单、分页、弹窗这些管理系统的常见组件开箱即用。状态管理用Pinia比Vuex写法简洁足够处理用户信息和全局状态。4.2 Axios请求封装与Token注入所有后端请求我都统一封装在一个request.js模块里不直接散落在页面中调用Axios。拦截器做两件事请求头带上Token响应里处理公共错误码。import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( res { const code res.data.code if (code 200) return res.data.data if (code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(登录已过期)) } ElMessage.error(res.data.msg) return Promise.reject(new Error(res.data.msg)) }, err { ElMessage.error(网络异常请稍后重试) return Promise.reject(err) } ) export default service这里的关键是统一处理401跳转页面里就不会到处写判断登录过期的逻辑。4.3 档案列表页与分页逻辑档案列表页是前端最复杂的页面我用了Vue3的reactive来管理查询参数和分页数据把表格数据、加载状态、搜索表单放在一起逻辑才清晰。import { reactive, ref, onMounted } from vue const queryForm reactive({ keyword: , categoryId: null, startTime: , endTime: }) const page reactive({ current: 1, size: 10, total: 0 }) const tableData ref([]) async function loadData() { const params { ...queryForm, page: page.current, pageSize: page.size } const res await getArchivePage(params) tableData.value res.records page.total res.total } onMounted(loadData)每次搜索时把page.current重置为1再调用loadData()分页事件时只更新页码和条数。表格用el-table分页用el-pagination组件本身的current-change和size-change事件与page参数绑定。4.4 档案新建/编辑表单与上传组件档案表单用el-form加校验规则字段比较多我用一个formRef统一校验。文件上传用el-upload组件的http-request方法自定义上传逻辑这样能拿到后端返回的文件URL再把它赋值给表单字段。async function handleUpload(options) { const formData new FormData() formData.append(file, options.file) const url await uploadFile(formData) form.fileUrl url options.onSuccess(url) }编辑回显时文件URL要从后端返回的档案详情中取出来用一个el-image预览或el-link展示文件名。注意el-upload的默认行为会把文件立刻上传所以在编辑场景下我要先判断fileUrl是否已有值有值就只做展示不让组件再发请求。5. 本地运行与部署调试全流程5.1 从MySQL初始化到后端启动很多拿着源码跑不起来的人问题都出在初始化步骤上。我习惯按顺序做在MySQL里创建空数据库字符集选utf8mb4排序规则选utf8mb4_general_ci。执行项目里的sql/init.sql脚本它会自动建表并插入默认管理员账号。修改后端application.yml里的数据库地址、用户名、密码。在项目根目录执行mvn spring-boot:run或者用IDE直接启动主类。这里有个容易被忽视的坑utf8mb4和utf8mb4_general_ci一定要选对否则中文用户名、中文标题可能写入失败。以前老项目用utf8遇到特殊字符会直接报错其实都是字符集惹的祸。5.2 前端启动与代理配置前端启动前要装依赖然后配置文件里的接口地址。开发环境我一般不在前端写http://localhost:8080而是用Vite的代理转发server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/archive/page代理会自动转发到后端的/api/archive/page跨域问题在开发阶段就消掉了。5.3 前后端分离部署的跨域问题前后端分离项目部署后跨域问题会比开发环境更明显。生产环境我用Nginx做反向代理把前端静态文件和/api接口请求放在同一域名下从根源上消除跨域。Nginx配置核心是location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }前端打包后的dist目录交给Nginx托管。这里我再强调一遍如果后端同时开启了CrossOrigin和Nginx代理会出现重复的响应头偶尔引发奇怪的登录失效问题。所以生产环境我建议后端不要开全局CORS让Nginx统一处理。6. 常见问题与排查技巧实录6.1 Mapper XML 提示 Invalid bound statement这个问题我在好几个项目里都看到过。现象是启动后调用查询接口MyBatis报Invalid bound statement (not found)。原因基本是三类XML里namespace没有写Mapper接口全类名。XML文件没放在配置的mapper-locations路径下。Maven构建时没有把resources目录下的XML同步到target。第三个最隐蔽。如果你发现target目录里根本没有XML文件就在pom.xml里加一段配置把src/main/resources下的xml和properties文件强制打包build resources resource directorysrc/main/resources/directory includes include**/*.xml/include include**/*.properties/include /includes /resource /resources /build6.2 前端表单提交后端一直报 400 或 415前后端联调时最常见的问题之一是前端把表单数据作为application/x-www-form-urlencoded提交后端接口却用RequestBody接收JSON于是报415。反过来前端用Axios默认提交JSON后端用RequestParam接收也会300错误。我的规范是POST的JSON接口统一用RequestBody前端提交时显式用JSON.stringify和Content-Type: application/json。我封装好的request.js里会统一处理页面里不要再手动改请求头。另一个坑是时间字段。前端传2024-01-01后端用LocalDateTime接收会报无法转换。解决方法是前端传字符串后端用DateTimeFormat注解或者在DTO里直接用String接收后再转换。6.3 文件上传提示文件大小超限Spring Boot默认最大上传文件是1MB上传一个几MB的PDF就报错。需要在配置中放大上限spring: servlet: multipart: max-file-size: 20MB max-request-size: 25MB如果项目用了Nginx还需要改Nginx的client_max_body_size否则请求会在Nginx层就直接被拒。这个大小要和后端配置协调我一般设置成后端的两倍。6.4 数据库时间查询结果差了8小时这个问题在国内开发环境下非常典型。MySQL连接串里如果只写了serverTimezoneUTC时间默认就是UTC时区比北京时间晚8小时。解决方案是改成spring: datasource: url: jdbc:mysql://localhost:3306/archive?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时如果后端是Spring Boot 2.x以上LocalDateTime序列化到前端时也需要统一格式我通常在application.yml里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8最后分享一点个人体会这个项目整体做下来我最大的收获不是学会了某个框架API而是真正理解了需求拆分比代码实现更重要。像档案分类树、借阅状态机、逻辑删除这些看似很基础的设计如果一开始没想清楚后面返工的成本会非常高。我自己的体验是先花半天把表结构设计好再花半天把接口文档列出来剩下写编码的时间其实很顺畅。前后端分离项目最容易卡住联调环节但只要请求封装、代理配置、Token方案这三件事从一开始就统一规范流程整体会顺很多。后续如果要在这个源码上继续扩展我建议优先加两层能力一层是Redis缓存热点档案和用户登录态减少数据库压力另一层是全文检索引擎对付大量档案的模糊搜索会快得多。但当前这套SpringBootVue3MyBatis的基础架构已经把核心流程完整跑通踩坑经验也都摆在上面了照着动手做一遍前后端分离的整套套路基本就吃透了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑