前后端分离档案管理系统实战:从SpringBoot到Vue全栈部署

发布时间:2026/10/21 21:11:09
前后端分离档案管理系统实战:从SpringBoot到Vue全栈部署 前后端分离档案管理系统我前后做了差不多一整个迭代周期。最开始只是在某高校的信息化项目里接了个需求后来发现这套东西的完整度、边界划分和踩坑点放到很多中小型项目里都有参考价值。所以把整个系统从设计到部署的实战过程拆成这篇文章覆盖SpringBoot、Vue、MyBatis和MySQL的关键实现以及最容易被忽略的部署细节。如果你是刚接触前后端分离项目的开发想找一个能看完整条链路、能照着动手的实战型系统或者你已经在写业务代码但遇到文件上传、权限拦截、Nginx反向代理这类问题时想找个现成的排查思路这篇文章都值得看完。1. 档案管理系统的需求边界先把业务模块拆清楚档案管理系统不是什么高深业务但它天然带有分类多、状态多、角色多三个特征。如果一开始没有把模块边界划分清楚后期很容易陷入在控制器里堆业务逻辑的泥潭。1.1 功能模块划分我把它拆成了七个一级模块档案分类、档案登记、档案检索、档案借阅、档案销毁、用户权限、系统日志。每个模块的职责非常单一比如档案登记只管录入和修改不掺合借阅流程档案借阅只管借阅单的生命周期不管文件本体存储。档案分类管理维护树形分类结构支持多级目录用于组织档案归属 档案登记管理新增/编辑/删除档案条目上传附件 档案检索按档号、标题、责任者、时间范围、密级组合查询 档案借阅提交借阅申请审批流转借阅记录管理归还处理 档案销毁登记销毁批次走审批流程记录销毁原因 用户权限用户、角色、菜单权限、操作权限的管理 系统日志登录日志、操作日志按时间和用户维度查询这套模块划分直接决定了后面的数据库表设计和接口粒度。每个模块的增删改查都收敛在自己的服务类里避免出现跨模块直接操作对方Mapper的情况。1.2 核心角色与权限矩阵档案系统必须解决谁能看、谁能改、谁能批的问题。我设计了四类角色权限范围从窄到宽角色权限范围普通用户检索档案、提交借阅申请、查看自己的借阅记录档案员档案登记、分类维护、借阅审批、销毁申请管理员全部功能包括用户权限配置、系统日志查看只读访客仅可检索和查看档案基本信息无借阅权限权限控制的落点有两个一是前端路由和按钮级别的显隐二是后端接口的拦截校验。两边必须保持一致但后端校验是真正可靠的那一层前端只是体验优化。我使用的是基于角色的访问控制模型用户表、角色表、菜单表、用户角色关联表、角色菜单关联表一套标准设计不引入太重的权限框架Spring Boot拦截器加自定义注解就能搞定。2. 数据库设计档案系统的表结构和关键字段数据库是整个系统里最不能出错的部分。档案业务不像电商那样有频繁的读写并发但它对数据一致性、可追溯性要求很高。所以字段设计上宁可冗余清晰不要花哨模糊。2.1 档案主表的设计思路档案主表我命名为archive_info核心字段如下CREATE TABLE archive_info ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, archive_code varchar(64) NOT NULL COMMENT 档号, title varchar(200) NOT NULL COMMENT 档案标题, category_id bigint(20) NOT NULL COMMENT 分类ID, level tinyint(4) DEFAULT 0 COMMENT 密级0公开1内部2秘密3机密, responsible_person varchar(50) DEFAULT NULL COMMENT 责任者, archive_date date DEFAULT NULL COMMENT 档案形成日期, file_path varchar(255) DEFAULT NULL COMMENT 附件存储路径, file_name varchar(255) DEFAULT NULL COMMENT 原始文件名, status tinyint(4) DEFAULT 1 COMMENT 状态1在库2借出3销毁, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), UNIQUE KEY uk_archive_code (archive_code), KEY idx_category (category_id), KEY idx_title (title) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT档案信息表;档号archive_code必须唯一它是档案在业务上的身份证号我做的是唯一索引防止重复归档。密级字段用tinyint而不是字符串是为了排序和查询效率前端再映射成中文标签。文件存储我存的是服务端相对路径不直接存完整URL这样换域名、换端口时不用改数据库。2.2 借阅流程的状态机借阅是档案系统里最能体现表结构设计功底的地方。一条借阅记录从提交到归还至少经历五个状态待审批 - 审批通过 - 已借出 - 已归还 - 已归档 \- 审批驳回 / 借阅超期我把借阅记录单独建了张borrow_record表字段包括申请原因、审批人、审批时间、计划归还日期、实际归还日期、状态等。这里要注意一点不要把状态当成普通字段随便update最好在建表时就约定好状态流转的方向在Service层做校验。CREATE TABLE borrow_record ( id bigint(20) NOT NULL AUTO_INCREMENT, archive_id bigint(20) NOT NULL COMMENT 档案ID, user_id bigint(20) NOT NULL COMMENT 借阅人ID, apply_reason varchar(500) DEFAULT NULL COMMENT 借阅理由, approver_id bigint(20) DEFAULT NULL COMMENT 审批人ID, approve_time datetime DEFAULT NULL COMMENT 审批时间, plan_return_date date DEFAULT NULL COMMENT 计划归还日期, actual_return_date date DEFAULT NULL COMMENT 实际归还日期, status tinyint(4) DEFAULT 0 COMMENT 0待审批 1通过 2驳回 3已借出 4已归还 5超期, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_archive (archive_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;在提交借阅时需要在事务里同时检查档案的当前状态是否为在库然后创建借阅记录。借出时再把archive_info的status更新为2。归还时反过来先更新借阅记录状态再恢复档案状态。这两个操作必须在一个事务里完成否则当并发提交时可能把同一份档案借给两个人。2.3 分类表与日志表分类表很简单id、父级id、名称、排序号。用parent_id实现无限级分类前端用递归组件渲染树形结构。有一个细节要注意parent_id默认为0不要用null否则多表关联和判空逻辑都会变得麻烦。操作日志表我单独设计了op_log字段包括用户ID、操作模块、操作类型、操作描述、IP地址、操作时间、耗时毫秒数。写操作日志的时机要选在业务成功后用Spring的AOP切面统一处理不要在每个Service方法里手动写日志代码。3. SpringBoot后端实现分层架构与核心功能拆解后端部分是整个系统的中枢我用的是标准的三层架构Controller接收请求、Service处理业务、Mapper与数据库交互中间用实体类传递数据。这个结构朴实无华但很稳。3.1 项目的分层结构com.example.archive ├── controller # 接口层只负责参数接收和响应封装 ├── service # 业务层事务和核心逻辑都在这里 │ └── impl ├── mapper # MyBatis的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端交互的数据传输对象 ├── common # 统一返回结果、异常处理、常量 ├── config # 配置类跨域、拦截器、文件上传配置 ├── interceptor # 登录拦截器、权限拦截器 └── utils # 工具类我强烈建议把请求参数和实体类分开也就是用DTO而不直接绑实体。理由很简单前端传参经常只是部分字段比如分页参数、筛选条件它们不是一条完整记录。直接用实体接收会非常别扭尤其在字段多的时候。3.2 统一返回结果与全局异常处理前后端分离项目最怕接口返回值格式不一致。前端拿到一个接口返回的是data对象另一个接口返回的是纯字符串解析逻辑就会变得混乱。我设计了一套统一返回结构{ code: 200, message: success, data: {} }后端用Result类统一封装。Controller层的每个方法返回Result类型PageHelper分页时data部分是PageResult对象包含records列表、total总数、current当前页、size每页数量。全局异常处理我用RestControllerAdvice实现分三类处理参数校验异常返回400、业务异常返回500及自定义错误信息、未知异常返回500和兜底提示。这里有一个实战经验业务异常一定要定义自己的RuntimeException子类不要抛笼统的Exception不然后端日志里全是看不出业务的报错。3.3 登录认证与JWT拦截档案系统的安全级别要比普通管理系统高登录认证我用的是JWT结合Spring Boot拦截器的方案。用户登录成功后后端生成token返回给前端前端后续请求在请求头里携带Authorization拦截器统一校验。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/auth/login)) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(401, 未登录或登录已过期); } // 解析token把用户信息放入ThreadLocal LoginUser user JwtUtil.parseToken(token.replace(Bearer , )); UserContext.set(user); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 防止内存泄漏 UserContext.clear(); } }用户信息放入ThreadLocal后Controller和Service层都可以随时获取当前登录人ID这在创建借阅记录、写操作日志时非常方便。token过期时间我设置的是24小时如果要更安全还可以引入刷新机制但对档案系统来说这个级别够了。3.4 档案登记中的文件上传处理文件上传几乎是档案系统的标配功能档案扫描件、电子文档都要挂在档案条目下。Spring Boot处理multipart文件比较直接但有几个坑容易踩文件大小和请求大小需要单独配置存储路径不能在代码里写死要放在配置文件里文件名必须重命名防止中文乱码和重复spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB我建议系统内维护一个file存储目录按年月分文件夹文件保存后把路径写入数据库对应字段。前端上传接口用单独的上传端点成功后拿到文件路径和名称提交档案表单时再一起写入档案表。这样做的优点是档案和附件解耦即使档案表单还没提交附件也已经保存在服务器上前端可以做暂存和预览。3.5 MyBatis的组合查询与分页MyBatis的XML是处理复杂SQL的地方。档案检索的筛选条件并不固定可能是档号密级也可能是标题模糊时间范围。这种情况下最合适的就是动态SQLselect idselectArchivePage resultTypecom.example.archive.entity.ArchiveInfo SELECT * FROM archive_info where deleted 0 if testarchiveCode ! null and archiveCode ! AND archive_code LIKE CONCAT(%, #{archiveCode}, %) /if if testtitle ! null and title ! AND title LIKE CONCAT(%, #{title}, %) /if if testlevel ! null AND level #{level} /if if testcategoryId ! null AND category_id #{categoryId} /if if teststartDate ! null AND archive_date gt; #{startDate} /if if testendDate ! null AND archive_date lt; #{endDate} /if /where ORDER BY create_time DESC /select分页我用的是PageHelper引入依赖后在Service层查询前调用PageHelper.startPage(pageNum, pageSize)即可返回结果自动封装成Page对象。它本质上是拦截了MyBatis的Executor在SQL执行前拼接了limit和count查询。PageHelper有个需要注意的坑startPage之后必须紧跟第一条查询语句中间不能有其他数据库操作否则分页会作用到错误的那条SQL上。3.6 事务控制的细节处理事务分为单表事务和跨表事务。单表事务最简单在Service方法上加Transactional就行。但跨表事务要注意事务粒度Spring默认只在RuntimeException时回滚如果代码里手动catch了异常而未重新抛出事务是不会回滚的。我在借阅处理中用了这样一段逻辑Transactional(rollbackFor Exception.class) public void approveBorrow(Long recordId, Integer approveResult) { BorrowRecord record borrowRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { throw new BusinessException(借阅记录不存在或已被处理); } if (approveResult 1) { // 通过更新借阅记录状态 锁定档案 record.setStatus(1); record.setApproveTime(new Date()); borrowRecordMapper.updateById(record); ArchiveInfo archive archiveInfoMapper.selectById(record.getArchiveId()); archive.setStatus(2); archiveInfoMapper.updateById(archive); } else { // 驳回只用更新借阅记录 record.setStatus(2); record.setApproveTime(new Date()); borrowRecordMapper.updateById(record); } }rollbackForException.class一定要写因为默认的rollback规则只针对RuntimeException如果出现受检异常事务可能不会回滚最终造成借阅记录显示通过但档案状态没变的脏数据。4. Vue前端实现页面组织、状态管理与接口封装前端用的是Vue生态标准方案Vue2 Vue Router Vuex Element UI。这套组合在管理系统中非常成熟组件丰富文档完善适合档案管理这种以表格和表单为主的应用场景。4.1 前端目录结构src ├── api # 接口请求封装 │ ├── archive.js │ ├── borrow.js │ ├── login.js │ └── user.js ├── assets # 静态资源 ├── components # 通用组件 ├── router # 路由配置 ├── store # Vuex状态管理 ├── utils # 工具函数 │ ├── request.js # axios封装 │ └── auth.js # token工具 ├── views # 页面视图 │ ├── dashboard │ ├── archive │ ├── borrow │ └── system └── App.vue4.2 axios封装与请求拦截axios封装是前端项目的基石。我在request.js里做了统一处理请求拦截器里把token加到请求头响应拦截器里解包Result数据结构遇到特定错误码自动跳转登录页。import axios from axios import { Message } from element-ui 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( response { const res response.data if (res.code 200) { return res.data } else if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(res.message)) } else { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } ) export default servicebaseURL设置成/api而不是完整域名有个实际好处开发环境用Vue CLI的proxy代理到后端端口生产环境用Nginx把/api路径代理到后端服务前端代码不需要任何环境判断切换部署环境时只要改代理配置就行。这是前后端分离项目里非常实用的技巧。4.3 动态路由与按钮权限档案系统的菜单权限是跟着角色走的不同角色登录系统后看到的左侧菜单不一样。我在路由设计上用了动态路由登录后从后端拉取当前用户的菜单列表再动态注册路由。// 动态注册路由 export function addDynamicRoutes(menus) { const routes generateRoutes(menus) router.addRoutes(routes) }按钮级别的权限用自定义指令v-permission实现传入需要的权限标识没有权限的元素直接移除。比如档案登记页面的作废按钮只有管理员和档案员能操作前端判断没有权限就不渲染。虽然真正的控制在后端但前端不渲染对普通用户更友好不会出现点击后才提示无权限这种不算严重的体验问题。4.4 档案列表页与表单的复用设计列表页在管理系统中重复度极高每个模块几乎都是搜索区加表格加分页。我封装了一个PagePanel组件把搜索表单、操作按钮、分页统一收进去子页面只传对应的table columns和查询接口方法。这样一个配置化列表页组件能覆盖档案列表、借阅记录列表、用户列表、操作日志列表约八成场景。档案表单页比较特殊它需要处理附件上传、分类树选择、密级选择三个联动组件。分类树我用了Element UI的el-tree数据从后端一次拉全选中节点后回填分类名称和ID。上传组件是el-upload配合自定义action路径由于上传接口已经配好token鉴权组件里需要在请求头手动带上Authorization字段。4.5 前端状态管理的取舍Vuex在这个项目里我只存了三个全局的东西用户信息、token、菜单列表。其他业务数据全部放组件内部或通过接口获取。过度使用Vuex会让状态变更链路变得难以追踪相反档案列表页的表格数据就是接口返回后放在data里刷新重新请求简单直接。借阅流程页面里有个状态流转的展示我用了Element UI的Steps步骤条组件把状态映射到步骤项配合当前状态高亮显示。这个交互效果对档案审批流程特别友好审批人一眼就能看出单子走到哪一步了。5. 数据库初始化与项目配置调整数据库设计完成之后初始化工作也很关键。我把初始化SQL按顺序执行建库、建表、插入管理员账号、插入菜单数据。管理员账号的密码必须用BCrypt加密后再插入不能存明文。5.1 MyBatis驼峰映射配置如果数据库字段使用下划线命名实体类使用驼峰命名MyBatis默认不会自动映射。需要在application.yml里配置mybatis: configuration: map-underscore-to-camel-case: true这个配置必不可少。比如数据库里create_time对应实体类createTime如果不开启驼峰映射查出来的时间字段就会是null排查起来还不容易发现。开启之后MyBatis在ORM映射时自动做转换省掉大量写resultMap的时间。5.2 数据库连接池的选型连接池我用的是Druid主要看中它的监控能力和SQL防注入功能。Spring Boot集成Druid很简单引入依赖后在配置文件里声明数据源类型。Druid的Web监控页在本地开发时非常有用可以实时查看SQL执行次数、慢SQL定位接口性能问题。spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/archive_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpasswordurl参数中serverTimezoneAsia/Shanghai是必须的新版MySQL驱动对时区严格校验不加这个参数会直接报连接错误。characterEncodingutf8指定字符集否则存储中文时容易乱码。5.3 时间字段与Jackson序列化Spring Boot默认使用Jackson序列化时间字段。如果实体类的时间类型是java.util.Date或LocalDateTimeJSON序列化出的默认格式可能不适合前端展示。我在配置文件里统一了时间格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8LocalDateTime类型的字段需要单独配置ObjectMapper或者在每个字段上用JsonFormat注解。我更推荐写一个全局的Jackson配置类统一注册LocalDateTime序列化器避免每个实体类都加注解。6. 部署上线从jar包到Nginx托管部署是整个项目的重要环节。很多项目开发时跑得好好的一到部署就出现各种环境差异这一节我按实际流程详细说明。6.1 后端打包过程后端本地开发用Spring Boot Maven插件打包mvn clean package -DskipTests打包完成后target目录下生成archive-system-0.0.1-SNAPSHOT.jar。这个包自带内嵌Tomcat可以直接用java命令启动。我需要强调的是打包前的环境配置分离要做好。开发环境和生产环境的数据库连接、文件存储路径、日志级别都不一样。我维护了多套配置文件application.yml # 公共配置 application-dev.yml # 开发环境 application-prod.yml # 生产环境启动时通过参数指定环境java -jar archive-system.jar --spring.profiles.activeprod这样本地开发切dev配置服务器上切prod配置互不影响。生产环境还可以把yml文件放到jar包外部通过--spring.config.location指定比如把数据库密码放在服务器文件中不进入git仓库更安全。6.2 使用systemd管理后端服务将jar包上传到服务器指定目录后我不建议直接用nohup启动。仓库环境为了崩溃重启和开机自启建一个systemd服务是更规范的做法[Unit] DescriptionArchive System Service Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/opt/archive ExecStart/usr/bin/java -jar /opt/archive/archive-system.jar --spring.profiles.activeprod Restarton-failure RestartSec10 [Install] WantedBymulti-user.target文件放到/etc/systemd/system/archive.service执行systemctl daemon-reload systemctl enable archive systemctl start archive之后查看日志用journalctl -u archive -f更新发布用systemctl restart archive。相比nohup加kill进程的方式systemd管理进程的健壮性明显更好进程崩溃后会自动拉起。6.3 前端构建与Nginx配置前端构建npm install npm run build构建产物在dist目录把它上传到服务器的Nginx配置中指定的根目录比如/opt/archive-web。Nginx配置如下server { listen 80; server_name yourdomain.com; root /opt/archive-web; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }关键点有两个try_files $uri $uri/ /index.html是实现前端history路由的关键否则刷新非首页时返回404。location /api/的proxy_pass末尾带斜杠会把请求路径中的/api前缀去掉后转发给后端比如请求/api/archive/list会转发为http://127.0.0.1:8080/archive/list后端接口路径不需要额外加/api前缀。6.4 文件上传大小在Nginx层的限制如果档案附件比较大Nginx默认的client_max_body_size限制是1MB会直接导致大文件上传失败。需要在server块里改大server { client_max_body_size 100m; ... }这个配置很容易被遗忘。后端已经把max-file-size配置到了50MB但Nginx层在上传文件时先拦截了请求返回413错误前端看到的是报错耗时排查最后才找到原因。后端、Nginx两层限制都要检查才算是完整的处理链路。7. 联调与测试前后端接口对接的几个常见坑前后端联调阶段是问题的高发期。很多问题单独开发后端或前端时根本暴露不出来一对接就开始冒烟。7.1 跨域配置与联调代理后端在开发环境需要配置跨域支持。我在后端写了一个通用的跨域配置类允许本地前端开发服务器访问Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }生产环境推荐用Nginx同域部署配置跨域反而有安全隐患。这里一个常见的误区是前端用axios设置了withCredentialstrue但后端allowedOriginPatterns没配好导致带cookie的请求被浏览器拦截。由于我用的JWT方案不依赖cookie所以allowedOrigins可以用通配符场景更宽松。7.2 时间字段前后端格式不一致的连锁问题档案列表页用日期范围做筛选前端传来的是yyyy-MM-dd格式字符串。后端如果用LocalDateTime接收会报格式转换错误。一个稳妥的办法是后端定义DTO接收时用String类型自己解析Data public class ArchiveQueryDTO { private String archiveCode; private String title; private Integer level; private String startDate; private String endDate; }在Service里用DateTimeFormatter解析成LocalDate再拼接到查询条件。这比依赖全局Jackson配置更可控因为查询参数和返回体的时间处理逻辑并不完全一样。返回给前端时实体类的时间字段序列化为yyyy-MM-dd HH:mm:ss格式前端展示时按需截取日期部分。7.3 空值与枚举值的前端兼容密级字段使用tinyint但前端展示需要中文。我的做法是后端在实体类里额外加一个非数据库字段levelName查询完成后手动赋值在枚举或常量类中维护映射关系。前端拿到数据后直接用levelName展示不用自己去翻译数字这样列表和详情页都能复用。注意在resultType映射时实体类里非数据库字段MyBatis不会自动查出来但select *返回后手动处理即可不影响ORM映射。7.4 接口联调清单联调阶段我维护了一份检查清单避免遗漏关键点接口入参重点返回结果联调注意事项登录用户名/密码token 用户信息密码错误要提示明确不能只说失败档案分页查询分页参数 筛选条件PageResult模糊查询要确认是左右模糊还是精确档案新增表单字段 文件路径新增ID档号重复要全局校验借阅审批借阅记录ID 审批结果无审批后要刷新档案状态展示文件上传multipart文件文件路径文件大小超限要有对应提示8. 完整测试链路与上线前的安全检查测试阶段我用的是三层本地单元测试、接口测试、手动业务流测试。这里重点说后两层的做法。8.1 关键业务流的测试用例设计档案系统的核心业务流有两条档案登记后检索借阅申请到归还。针对这两条流我设计了至少十条用例新建分类、添加档案、按档号检索、按多个条件组合检索、提交借阅申请、审批通过、查看档案状态变化、审批驳回、逾期归还、逻辑删除档案。其中借阅并发场景很重要。多个用户同时申请借阅同一份档案需要验证是否只有一个人能成功。实现层虽然用事务加状态判断但并发问题往往只在压力环境下暴露。我写了一个测试接口用于并发申请用Jemeter模拟10个用户同时调借阅接口检查最终通过的记录数。8.2 接口防刷与参数校验档案系统虽然不面向公众开放但接口层面的基础安全还是要做。参数校验我引入了spring-boot-starter-validation在DTO字段上标注注解Data public class LoginDTO { NotBlank(message 用户名不能为空) private String username; NotBlank(message 密码不能为空) private String password; }Controller层加Valid注解校验失败的提示自动封装为统一异常返回格式。另外一个容易被忽略的点是档案检索接口的文本输入需要做长度限制防止恶意构造超长参数我这里统一做了长度限制超大长度的查询直接被拦截。8.3 数据备份策略档案系统的数据价值极高线上数据定期备份必不可少。我写了两个定时备份的shell脚本一个备份MySQL数据一个保留最近30天的备份文件。备份策略是全量备份加每天增量对中小型项目来说全量备份的频率可以设为每天零点执行用crontab管理即可。恢复流程也要演练不能只备份不验证恢复。9. 部署过程中的配置细节与常见报错排查部署环节最容易遇到的问题可以列成一张排查表这里整理成文。9.1 后端启动失败的排查路径后端jar包启动失败日志里最常见的几类报错第一是端口占用。Spring Boot默认8080端口被占用会启动失败。解决方法是改端口或杀掉占用进程。但更合理的做法是生产环境直接换端口比如8090避免和服务器上其他服务冲突。第二是数据库连接失败。检查网段、数据库账号权限、MySQL服务是否启动。如果MySQL在服务器本地连接串用127.0.0.1而不是localhost某些驱动对localhost解析为IPv6会延迟。第三是配置文件读取不到外部dataSource。yml缩进错误是高频问题Spring Boot对配置文件的缩进敏感。建议配置文件的检查顺序缩进、编码、数据库账号密码的转义、时区参数。9.2 前端页面白屏排查前端部署后页面白屏是一个非常常见的报错场景。可能的原因和对应排查手段现象可能原因排查方式首页白屏静态资源路径错误控制台看资源加载404检查base路径刷新后404history路由未配置Nginx里加try_files首屏白屏但控制台无报错构建时依赖异常重新npm install并再次build登录后白屏路由守卫死循环检查动态路由注册逻辑与刷新鉴权Vue CLI默认的publicPath是/部署在域名根路径时问题不大。如果部署在子路径比如/archive必须把publicPath配置为/archive/否则资源路径全部404。9.3 文件上传到服务器后访问404开发环境文件上传和访问都很正常部署到生产环境后发现上传的文件打不开。这个问题大多出在本地存储路径上。开发环境通常把文件存到项目目录或者某个临时目录部署后路径变了。我把文件存储路径放在配置文件中统一指定file: upload-dir: /data/archive-files然后做一个静态资源映射把/file/**路径映射到磁盘目录前端直接用filePath字段拼接URL访问文件。注意上传目录的权限要调整确保服务进程有读写权限否则上传时会报权限不足。9.4 数据库连接已关闭的偶发异常Druid连接池空闲连接回收后偶发出现connection is closed的报错。原因是MySQL默认wait_timeout为8小时连接池中空转超时的连接会被数据库主动断开。解决办法是在Druid配置中设置连接保活和空闲回收spring: datasource: druid: test-while-idle: true validation-query: SELECT 1 time-between-eviction-runs-millis: 60000核心是让连接池在空闲连接失效前定期检查并重新连接。这条配置在长时间未操作的页面上很容易被忽略但实际影响非常大。10. 实际运行中的一处优化经历上线运行一段时间后我注意到档案检索页面在数据量到五万条左右时明显变慢单次查询要3秒多。排查下来发现两个原因。第一个原因是没有建立组合索引。原来只有单字段索引多条件组合查询时MySQL只能选一个索引其他条件全表过滤。我在category_id、status、archive_date上建立了联合索引查询速度明显提升。ALTER TABLE archive_info ADD INDEX idx_category_status_date (category_id, status, archive_date);第二个原因是档案列表页每页加载50条数据但SELECT查出来的还是全字段包括file_path这种大字段。我把列表查询改成动态选择字段列列表接口只返回必要字段详情接口才加载完整字段。这个优化对性能和带宽都有帮助尤其是在附件字段越来越大的情况下。优化之后同样条件下查询耗时降到了800毫秒以内。如果你也遇到列表越查越慢的情况优先检查索引和查询字段列往往比提升硬件配置更有效。11. 最后再分享几个实用性建议整个系统从设计到部署的完整流程走完有几件事体会特别深。第一件是项目里的配置文件一定要保持干净。开发环境的密码、测试环境的地址、生产环境的密钥分开管理绝不允许把生产密码提交到仓库里。我吃过这个亏后来把敏感配置全部外置到服务器代码里只保留占位符。第二件是日志策略要提前设计。每个接口的请求参数、处理耗时、异常堆栈都要落到日志里。刚开始我只在异常时打日志导致线上问题排查时缺少上下文信息非常被动。后来加了AOP统一日志切面按操作模块记录关键信息配合systemd的journalctl排错效率提高了很多。第三件是不要迷信具体的框架或版本。这个项目用了SpringBoot和Vue的稳定版本但技术选型的核心在于解决业务问题。档案管理系统的难点不在技术栈的新旧而在于业务状态的准确流转和数据的安全可控。把业务逻辑梳理清楚把事务边界划明白比追逐新工具要重要得多。如果后续想扩展可以往两个方向走一个方向是加入消息队列异步处理文件转码和OCR识别适合档案电子化量大时使用另一个方向是引入对象存储系统处理海量附件的存管。但主体架构不变现有的模块拆分完全扛得住这些升级。这篇内容从需求设计一路讲到部署运维覆盖了我认为档案管理系统开发中最值得记录的决策和问题。开发过程中遇到的每个坑回头看都是对系统理解的加深过程。希望这篇文章对正在做类似前后端分离管理系统的你有一些直接可用的帮助。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询