ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前后端分离科研管理系统实战:SpringBoot+Vue+MyBatis全栈设计与部署

前后端分离科研管理系统实战:SpringBoot+Vue+MyBatis全栈设计与部署 前后端分离这套东西这几年基本成了JavaWeb项目的标配。手头上刚好有一套完整的科研管理系统SpringBoot Vue MyBatis MySQL前后端完全拆开源码和部署文档都齐整。写这篇东西不是给你贴代码而是把这套系统的设计思路、核心实现、部署过程中的坑从头到尾捋一遍。不管你是拿它做毕设、课程设计还是想参考着搭一套自己的管理系统都能少走不少弯路。1. 项目整体设计与技术选型1.1 科研管理系统到底做了什么先把这个项目的最初定位说清楚。科研管理系统的使用场景是高校院系或者科研院所核心用户有三类普通教师/科研人员、科研秘书/管理员、系统管理员。它解决的核心问题就是让科研项目从申报、立项、中期检查到结题验收的全流程数据不再靠Excel传来传去而是统一在一个系统里管理。功能模块我做过梳理大概可以分成以下几块用户管理登录、角色区分、个人信息维护项目管理项目申报、立项审批、中期检查、结题验收整个生命周期都覆盖成果管理论文、专利、软著、获奖这些科研成果的登记和审核经费管理项目经费的预算、到账、使用情况记录消息通知审批通过、项目到期等关键节点的站内提醒统计报表按学院、按成果类型、按年份做数据统计这些模块单独看都不复杂但串起来之后权限设计、状态流转、数据统计这些小细节就会让你忙活好一阵。1.2 为什么选这套技术栈选型这件事确实值得先讲讲。前后端分离这个决定核心诉求是让前端开发和后端开发互不阻塞。前端用Vue起一个独立的工程比如8080端口后端用SpringBoot起一个API服务比如8081端口两边只通过JSON数据打交道前端要改页面完全不用动后端代码后端要加接口也不用等前端页面出来。尤其在这种管理系统里页面迭代很频繁这个优势会被放得很大。SpringBoot这门后端框架选它的理由不需要多讲内嵌Tomcat、自动配置、生态成熟对着版本号拉一个maven工程出来就能跑。MyBatis在这套系统里的价值体现在两个地方一是SQL可以写在XML里复杂关联查询、动态条件拼接好维护二是MyBatis的缓存机制一级缓存默认开启二级缓存按需配置对这种读多写少的管理系统很友好。MySQL做存储则是最稳的选择部署简单而且大家对其熟悉遇到权限问题、连接问题搜索解决办法最快。版本搭配上我实际使用的是后端SpringBoot 2.7.x前端Vue 2.x配合Element UIMyBatis用SpringBoot整合的mybatis-spring-boot-starter 2.xMySQL用8.0。这套组合经过一年多项目实战检验稳定得很网上讨论也多遇到问题基本都搜得到现成方案。1.3 数据库设计与核心表结构数据库设计是整个系统最值得花时间的地方。当时我先画了ER图理清实体之间的关系后再动手建表。科研模块的核心表主要有一张项目主表、一张用户表、一张成果表再加若干关联表。用户表的设计要点是角色字段用role字段区分三种角色。这里不搞复杂的RBAC权限表因为项目就三类人做细粒度权限反而增加系统复杂度。密码字段存MD5加密后的值虽然现在更推荐BCrypt但MD5配合盐值在这个量级的系统里也能用。项目主表是系统的心脏设计上需要关怀的是状态字段。我用status字段表示当前所处的阶段0草稿、1待审批、2立项通过、3进行中、4已结题、5已驳回。整个生命周期靠这个字段驱动每变更一次状态在业务层做校验和日志记录。成果表的设计主要有论文类型、成果名称、作者、发表时间等字段同时设置project_id外键关联项目表。这里要注意主外键关联虽然简单但统计报表时需要多表JOIN查询建议给外键加索引。经费表单独立出来记录每一笔费用的类型、金额、发生时间经费和项目通过project_id关联。统计已用经费时用一条SUM()查询就能搞定不需要额外维护冗余字段。2. 前端实现Vue的核心模块与踩坑2.1 项目结构与路由设计Vue前端部分当时我用Vue CLI脚手架创建项目配合Vue Router和Element UI。目录结构这样说吧在src/views下按功能模块分目录src/api下集中放所有接口请求src/utils放工具函数。这样的结构从一开始就定下来后期加模块加页面不迷路。路由设计上登录页和主布局页面分离。所有功能页面都在主布局的子路由下主布局包含左侧菜单栏和顶部导航栏。每个功能模块对应一个children子路由这样整体结构清晰新增模块就是在路由表里加一项。路由守卫这块值得重点说。前后端分离后页面路由是由前端控制的但哪些页面需要登录才能访问就需要统一的拦截逻辑。我在路由配置里给需要权限的页面加了meta: { requiresAuth: true }标记然后在beforeEach里统一判断。实现逻辑是没有token的直接重定向到登录页有token但是访问登录页就跳转到首页。这套逻辑从项目初期搭好后续基本不用改。值得注意的是Vue Router的history模式这种模式URL很干净没有#号但部署到Nginx后会出现刷新404的问题。原理是前端路由是浏览器端的服务器上根本没有对应的物理路径刷新时服务器找不到该路径就返回404。解决方法是Nginx配置try_files $uri $uri/ /index.html;让所有匹配不到的路由都回退到前端入口文件。这个坑部署时几乎必踩我建议写进自己的部署笔记里。2.2 核心页面写起来是什么感觉以科研项目管理页面为例子这是整个前端最核心的部分。项目列表用了el-table组件列字段包括项目名称、负责人、申报时间、当前状态、操作按钮等。状态列我用了el-tag标签不同状态对应不同颜色草稿灰色、待审批橙色、进行中蓝色、已结题绿色、已驳回红色。用户扫一眼颜色就知道项目所处阶段这个体验细节还是挺重要的。项目申报表单是我觉得设计上最花心思的地方。它不是一个单纯的大表单而是按照基本信息、人员信息、经费预算拆成了多个el-form区块最终提交的时候统一校验。这里用到了Element UI的表单校验rules里配置了必填校验规则比如项目名称不能为空、申报人不能为空、预算金额必须大于零。表单校验不通过提交按钮会弹提示用户就知道哪里没填对。前端有一个场景需要展示PDF格式的立项文件和结题报告。Element UI没有直接支持PDF预览的组件当初搜这个问题的时候也看到有问vue image能显示pdf吗的这里统一说下方案PDF预览用浏览器天然支持的方式搞定后端接口返回PDF的在线地址前端直接用iframe或者embed标签嵌入。不需要额外引入pdf.js之类的库除非你需要在页面上翻页或者操作PDF内容否则原生标签足够简单可靠。然后是文件上传功能用的el-upload组件配置了action属性指向后端的文件上传接口。上传成功后会拿到后端返回的文件ID和访问URL这个URL会随同表单一起提交到后端保存。这里有个细节el-upload默认在上传成功后会把它自己维护的文件列表也展示在界面上。如果表单被提交了这个列表要清空不然下次编辑会显示上一次的文件处理方式是在表单提交成功后调用this.$refs.upload.clearFiles()。2.3 axios封装与权限控制前端和后端通信统一走axios我把axios实例单独封装在一个文件里做四件事设置基础请求地址、设置超时时间、请求拦截器加token、响应拦截器统一处理错误。给axios设置基础请求地址时我在环境变量文件.env.development和.env.production里分别配置了不同的后端地址。开发环境指向http://localhost:8081/api生产环境指向http://localhost:80/apiNginx会把/api反向代理到后端服务。这样做的好处是代码里所有接口请求都不用写完整域名换环境时只需要改环境变量。请求拦截器里从localStorage取出token放到请求头的Authorization字段。后端接口过滤器会读取这个头解析成功后把用户信息塞进请求上下文。这里要注意token过期的情况响应拦截器里如果收到401状态码就清空本地token并跳转登录页。权限控制这块前端的思路是根据角色动态生成菜单。用户登录成功后后端会返回当前用户的信息以及角色前端根据角色判断哪些菜单显示、哪些隐藏。比如管理员能看到用户管理菜单项目负责人看不到。这个方案在角色固定且数量少的场景下比动态路由要简单可控。动态路由是有它的好处系统庞大时这个方案更方便但工作量和风险都相应增加在科研管理系统这个量级静态路由加按钮级别的v-if判断完全够用。3. 后端实现SpringBoot与MyBatis的配合3.1 后端分层结构与统一返回体后端工程结构我按Controller、Service、Mapper、entity四层划分。Controller负责接收HTTP请求和参数校验Service层处理业务逻辑Mapper层和数据库打交道entity是对应数据库表的实体类。另外单独有个common包放统一返回类、异常处理类、工具类。统一返回体的设计是我觉得后端最值得关注的细节。所有接口不管成功失败返回结构都是{ code: 200, message: 操作成功, data: {...} }code字段表示业务状态码200成功401未登录403无权限500系统错误。message是给前端展示的提示信息data才是真正的数据。这样设计的好处是前端的响应拦截器只需要判断code是200还是其他值就能决定是直接取数据还是弹出错误提示不用每个接口单独做判断。后端还有一个值得一提的设计是日志。我在审批、驳回、结题这些关键动作上都加了操作日志。实现方式是写一个AOP切面拦截标注了Log注解的方法自动记录操作人、操作时间、操作内容、IP地址。这样系统上线后出了问题排查时能找到谁在什么时间做了什么操作这是个平时不显眼但关键时刻非常关键的模块。3.2 MyBatis配置、Mapper XML与动态SQLMyBatis在这套系统里最出彩的地方就是XML里的动态SQL。拿项目列表查询举例这个接口支持按项目名称模糊查询、按状态精确查询、按申报时间范围查询这三个条件可能都传也可能都不传。如果用传统JDBC拼接SQL代码会变得又臭又长。MyBatis的if标签可以让XML里的SQL像拼积木一样灵活拼装select idselectProjectList resultTypecn.project.entity.Project SELECT * FROM project where if testkeyword ! null and keyword ! AND project_name LIKE CONCAT(%, #{keyword}, %) /if if teststatus ! null AND status #{status} /if if teststartTime ! null AND create_time gt; #{startTime} /if if testendTime ! null AND create_time lt; #{endTime} /if /where ORDER BY create_time DESC /selectwhere标签会自动去掉多余的AND这就避免了where后面直接跟AND的经典SQL错误。审批列表查询、成果列表查询、经费统计很多都是这套模式写顺手之后新加一个查询模块的效率非常快。关于MyBatis缓存我想多说一句。一级缓存是SqlSession级别的同一个SqlSession中执行两次相同的查询第二次会从缓存中取。但SpringBoot整合MyBatis后每次请求都会创建新的SqlSession所以一级缓存其实作用有限。二级缓存是Mapper级别的跨SqlSession生效但管理系统的数据变更比较频繁配置二级缓存后容易出现数据不一致的问题我实际开发时是关闭了二级缓存靠MySQL自身的查询缓存和合理的索引优化来扛。面试题里经常问的MyBatis一级缓存二级缓存这个是核心理解点需要明白缓存的作用范围才能正确配置。3.3 文件上传、MinIO存储与本地方案的选择科研管理系统离不开附件上传立项申请书、结题报告、专利证书扫描件都是文件。这里有两个方案可选一是存本地磁盘二是用MinIO这类对象存储服务。本地存储的方案很简单文件上传接口把文件写到服务器的指定目录然后数据库里存文件的访问路径。缺点是文件多了以后磁盘空间紧张而且如果是多台服务器部署文件同步会是个问题。MinIO的方案我把它看作是轻量级的私有云存储。MinIO是一个开源的对象存储服务兼容S3协议可以部署在私有服务器上。将MinIO整合到SpringBoot工程的操作也不复杂引入minio的Java SDK配置服务地址、账号、密钥然后在文件上传接口里调用SDK的putObject方法把文件流传到MinIO返回的是文件的访问URL。相比本地存储MinIO的好处是文件管理集中、支持桶的权限控制、容量可以横向扩展。我在这个项目里的选择是开发环境先用本地存储放到一个uploads目录下生产环境再切到MinIO。这样做的考虑是减少部署依赖本地存储只要一个目录就行MinIO需要多部署一个服务。封装好存储接口后切换存储方案只需要改一个实现类不影响上层业务代码。文件上传还有一个必须处理的点是大小限制。SpringBoot默认上传文件大小限制是1MB这个很容易踩第一次上传PDF就会报MaxUploadSizeExceededException。需要在配置文件里调大spring.servlet.multipart.max-file-size50MB spring.servlet.multipart.max-request-size50MB3.4 异常处理与分页查询后端开发时最容易被忽视但又影响体验的是全局异常处理。如果不做统一处理数据库异常、空指针异常、自定义业务异常会以不同形式抛给前端前端处理逻辑会非常零散。我用了RestControllerAdvice做全局异常拦截配合ExceptionHandler注解定义不同异常的返回结构。业务异常是我自己写的一个BusinessException类继承RuntimeException带一个code和message。业务逻辑里需要校验的地方直接抛出这个异常比如if (project.getStatus() ! 0) { throw new BusinessException(500, 只有草稿状态的项目才能修改); }全局异常处理器捕获到BusinessException后把code和message封装到统一返回体里返回给前端。这样前端的提示信息就能准确反映业务问题而不是笼统地显示系统错误。分页查询这块可能很多人会去用PageHelper插件配置一个拦截器然后PageHelper.startPage(pageNum, pageSize)后跟着的查询自动带上LIMIT。PageHelper确实方便但我在这个项目里是手写的分页。原因是系统查询逻辑本身不复杂手写分页只多了两个参数pageNum和pageSizeSQL里拼上LIMIT #{offset}, #{pageSize}再写一个count查询统计总数。返回的分页结构是{ total: 100, list: [...] }手写分页灵活性更高SQL容易控制。4. 完整部署流程从代码到上线4.1 环境准备MySQL安装与Navicat连接部署前先把后端依赖的MySQL搞定。Linux服务器上安装MySQL 8.0推荐用rpm方式安装好处是安装速度快、路径规范、systemd管理服务方便。下载rpm包后通过rpm -ivh按顺序安装再启动mysqld服务。初始化时系统会生成一个临时密码在/var/log/mysqld.log日志里第一次登录必须用这个密码然后强制修改为自定义密码。修改密码的SQL是ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_password;这里有个非常常见的问题Navicat连接MySQL时提示caching_sha2_password错误。原因是MySQL 8.0默认的认证插件是caching_sha2_password而旧版本Navicat只支持mysql_native_password。解决方式就是上面那句SQL把认证插件改回来。这个问题几乎每个用MySQL 8.0的人都会遇到我在部署时提前把这段命令写进了部署文档省得现场查。另外要注意数据库字符集。建库时显式指定utf8mb4CREATE DATABASE scms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4比utf8多支持emoji字符现在这个版本选utf8mb4是标准姿势。接着导入SQL文件导入时若提示某字段超长或乱码基本都是字符集没设置对重新建库一般能解决。4.2 后端打jar包与关键配置后端部署我用的是标准Maven打包流程。在工程根目录执行mvn clean package -DskipTests完成后在target目录下生成一个jar包用java -jar命令启动即可。但打包前有几个配置必须检查。端口冲突问题服务器上8080端口可能被其他服务占用在application.yml里把端口换成8081或自定义端口。数据库连接配置把localhost改成服务器的内网IP账号密码改成自己设置的。还有连接参数MySQL 8.0推荐带上时区不然会报时区错误spring: datasource: url: jdbc:mysql://localhost:3306/scms?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8 username: root password: your_passworduseSSLfalse是去掉SSL连接因为内网环境不需要加密传输还可以省掉SSL握手的时间。allowPublicKeyRetrievaltrue是配合MySQL8的认证方式。有些教程里没提这个参数连接的时候就会报Public Key Retrieval is not allowed。启动jar包用nohup java -jar scms-server.jar --spring.profiles.activeprod scms.log 21 --spring.profiles.activeprod是激活生产环境配置nohup和保证SSH断开后进程不终止日志输出到scms.log文件。查看日志用tail -f scms.log确认启动成功的标志是看到Started Application in xx seconds。4.3 前端打包部署的两种方案前端打完包之后怎么和后端配合我用的方案是Nginx反向代理。前端构建出静态文件Nginx监听80端口页面请求交给Vue的静态文件接口请求通过反向代理转发到后端jar包的端口。Nginx核心配置是这样server { listen 80; server_name your_domain; root /usr/local/scms/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }两个关键点。第一location /api/的proxy_pass末尾要带/这样请求/api/login会被转发成http://127.0.0.1:8081/api/login不带/则会把完整的/api/login拼上容易出问题。第二location /的try_files就是前面说的解决前端路由刷新404的方案作用是把不存在的路径请求统一回退到index.html。还有一种方案是Vue打包后直接放进SpringBoot的静态资源目录。做法是把dist目录里的文件拷贝到src/main/resources/static下重新打包后端这时访问jar包端口就能看到前端页面。这个方案部署最简单一个jar包搞定所有内容但这样就没有真正分离了前端每次改动都要重新打包后端适合单机小规模使用。真要体验前后端分离Nginx方案是正解。4.4 部署后的验证与调优部署完成后系统性验证一遍。登录入口测一遍项目申报创建一条数据测一遍文件上传传一个PDF测一遍审批流程走一遍。特别要盯着浏览器F12的Console和Network面板有接口报错或者跨域问题一目了然。跨域是前后端分离部署最常见的问题。浏览器直接请求后端接口时如果前端域名和后端域名不一致浏览器会拦截响应。解决方式有几种在SpringBoot里配置CORS跨域过滤器允许指定来源请求。但这个方案说白了比较死板生产环境用了Nginx反向代理后前端和后端对外是同一个域名就不存在跨域问题了/api/的请求全部由Nginx转发。所以我的建议是开发环境用Cors配置解决生产环境让Nginx反代去处理两边各得其所。调优上一个是JVM参数启动jar包时加上-Xms256m -Xmx512m防止默认堆内存过大把服务器内存吃满。一个是MySQL的连接池Spring Boot默认的HikariCP连接池最大连接数默认是10并发量上来后要调大比如maximum-pool-size: 50。5. 常见问题与排查技巧实录5.1 数据库连接类问题这类问题在部署阶段出现频率最高整理一下能节省不少现场排查时间现象原因解决办法连接报Public Key Retrieval is not allowedMySQL 8.0认证插件和JDBC驱动不兼容连接URL加allowPublicKeyRetrievaltrue连接报Access denied for user用户密码错误或者权限不足检查密码执行GRANT ALL ON scms.* TO user%;连接报Communications link failure服务器防火墙拦截或者数据库未监听检查3306端口放行netstat -tunlp看端口监听中文乱码字符集不一致数据库、表、连接URL统一用utf8mb4连接报SSL connection errorMySQL SSL配置问题连接URL加useSSLfalse这里要特别提一下GRANT命令的坑。MySQL 8.0里GRANT ALL ON *.* TO root%这条命令在部分版本会报错因为root用户的host字段默认是localhost。正确操作是先CREATE USER root% IDENTIFIED BY password;再执行授权。数据库账号这块搞错一个授权命令就能折腾半小时提前验证好能避免很多麻烦。5.2 SpringBoot与Vue集成类问题现象原因解决办法前端能打开但接口全部404后端没启动或者Nginx反代路径配置错误先直接访问后端端口看接口通不通再看Nginx配置的proxy_pass请求接口报405前端请求方式GET/POST和后端接口定义不一致检查GetMapping/PostMapping注解后端能启动但访问404静态资源路径问题或者映射配置缺失确认Controller的RequestMapping路径检查是否有拦截器把请求拦截了上传文件报MaxUploadSizeExceededExceptionSpringBoot默认1MB限制配置spring.servlet.multipart.max-file-size50MBVue打包后图片和JS加载不出来publicPath配置错误vite配置base: ./Vue CLI配置publicPath: ./刷新页面404Vue Router history模式Nginx加try_files $uri $uri/ /index.html;publicPath这个坑在Vue CLI老版本里叫assetsPublicPath新版本叫publicPath很多人打包后页面白屏基本都是这个配置没改成相对路径。如果前端部署在服务器根目录这个可以不改但如果部署在子路径或者直接打进jar包里就必须设置成./。5.3 MyBatis缓存与性能类问题运行阶段可能遇到的是查询变慢和缓存数据不一致。查询慢优先看SQL执行计划用EXPLAIN SELECT ...分析SQL是否走了索引。比如按项目名称模糊查询用了LIKE %xxx%这种情况即使有索引也无法命中模糊查询前缀带%导致索引失效。解决方案是在设计阶段就明确哪些字段需要模糊查询对字段本身建立前缀索引或者使用全文索引。缓存数据不一致MyBatis的一级缓存我在前文说过作用有限二级缓存如果开启了在数据变更频繁的表上很容易出问题。拿成果表举例insert一条新成果后如果其他查询还读取的是缓存中的旧数据列表就会少一条。排查方式是关闭二级缓存或者在每次insert/update/delete后显式调用clearCache()。我的建议是管理系统里干脆关闭二级缓存查询优化靠索引和SQL书写数据一致性永远比轻微的性能提升重要。5.4 后台管理开发的通用心得几次项目做下来有些东西是通用的换了项目也照样用得上。统一返回体结构前端只需要判断code一个字段这个规范一定要在项目开始前定义好后面所有接口都按这个格式来不然写到后面接口返回格式五花八门前端要挨个适配痛点非常突出。权限控制尽量做在后端。前端隐藏菜单只是用户体验的一部分防不了懂技术的人直接调接口。后端每个接口都要校验当前用户的角色和资源权限。我的做法是写一个权限校验的工具类在Controller方法上标注RequirePermission(project:approve)通过AOP统一拦截校验。前端菜单隐藏、后端接口校验两层配合才是完整的权限方案。日志的重要性我前面也说过。一个管理类系统上线后用户反馈我明明提交了项目怎么不见了。如果没有操作日志这种问题排查只能靠猜。有了日志一条SQL查出来谁在什么时候提交的、后来被谁改过问题就能定位到具体操作。这套科研管理系统从开始搭建到部署上线最大的体会是系统的复杂度不在某个单独的技术点上而是各个模块之间的衔接和约束。前后端之间靠规范的接口契约衔接后端和数据之间靠清晰的表结构衔接前端页面之间靠统一的路由和状态衔接。每一层衔接顺畅整个系统自然就稳。如果你打算自己搭一套类似的管理系统别急着写代码先把表结构和接口约定设计清楚写起来会快得多。
返回列表