资讯动态

SpringBoot+Vue3+MyBatis高校健康管理系统前后端分离实战

发布时间:2026/10/16 9:52:56 来源:尧图企业网站定制
做Java Web开发这些年被问得最多的一个需求就是“这种前后端分离的管理系统到底怎么搭”尤其是SpringBootVue3MyBatis这套组合几乎成了国内Web开发从入门到实战的标准路径。今天我就以一套高校健康信息管理系统的源码实现为主线把前后端分离项目的完整落地过程拆开来讲——从数据库设计、后端接口、前端页面到联调部署每一步我都会说明为什么这样做以及实操中踩过的坑。这套源码用的是SpringBootVue3MyBatisMySQL业务场景是高校日常健康信息采集与管理角色包含学生、辅导员、管理员三类功能覆盖每日信息填报、异常上报、数据看板、公告发布。适合正在做课程设计、毕业设计或者想系统学习全栈开发的同学参考。1. 这套系统到底解决什么问题从需求拆解到技术选型逻辑1.1 业务场景与角色边界先说业务。高校健康信息管理这个场景表面上是个“信息采集系统”本质上是一个高频、真实、带强时间约束的数据填报场景学生在每天某个时间点之前完成信息上报辅导员要实时掌握自己分管学生的填报状态管理员则要看到全校维度的汇总情况。这三大类角色的诉求完全不同数据库表设计、接口拆分、前端页面组织都要围绕这个差异来。学生端最核心的诉求是“快”打开页面、填完提交、查看自己历史记录整个流程越短越好。所以学生端页面我做成一个表单页加一个记录列表不做多余的信息堆砌。辅导员端的核心诉求是“全”要看到管辖范围内所有学生的填报进度能一键筛选未填报的人能对异常记录做处理标记。管理员端的核心诉求是“看得见”用户管理、部门维护、通知公告下发以及基于全院系的上报率统计图表。这三类角色如果不做权限隔离代码会乱成一锅粥。我按角色类型在用户表里用一个role_type字段区分然后前端路由用meta信息控制页面准入后端接口用拦截器校验角色权限。你说为什么不用更细粒度的权限模型因为这类业务场景就三类角色权限边界非常清晰RBAC的细粒度模型角色菜单按钮权限会引入大量冗余配置对这个体量的系统是过度设计。把role_type加进JWT里拦截器里做一次判断够用且好维护。1.2 为什么是SpringBootVue3MyBatis这套组合选型这事很多人会纠结“是不是不够新”。我的观点是技术选型从来不是追新而是匹配场景和团队维护成本。后端用SpringBoot理由很直接Java生态最成熟的快速开发框架起步快社区资料多。一个启动类加几个注解就能把Web容器、配置绑定、依赖注入全部搞定。相比SSH时代的XML地狱SpringBoot的自动化配置把开发者的心智从“配置框架”解放到“写业务”。ORM选MyBatis而不是JPA是我在这个场景下的明确选择。健康信息管理这类系统后端不可避免要写多表关联查询、按条件动态统计、时间范围聚合。MyBatis直接写SQL虽然多写几行XML但每条SQL的执行逻辑一目了然真出性能问题也容易定位。JPA确实在单表CRUD上更省事但在一对多关联、复杂统计场景下你反而要和“怎么让框架生成高效SQL”作斗争。对于以SQL为中心的管理系统MyBatis的可控性优势大于JPA的开发效率优势。前端选Vue3很大程度是因为组合式API。这个项目的页面逻辑里表单校验、列表加载、分页切换、状态重置这些逻辑要频繁复用组合式API可以用一个自定义Hook把表格页的全部逻辑收拢起来一个页面组件只要几十行代码就能跑起来。配合Vite的秒级冷启动开发体验比Vue2时代的Webpack好一个档次。UI层用Element Plus后台管理界面这种“表单表格弹窗”的密集场景用它可以少写大量样式代码。1.3 源码的整体工程结构整个项目按前后端分离拆成两个工程目录后端是标准的Maven多模块单模块结构前端是Vite创建的Vue3工程。后端分包按照controller、service、mapper、entity、common、config、interceptor来组织前端按views、router、stores、api、components、utils来组织。这个分包方式看着平淡但它是经过实战验证的“安全生产目录”。很多新手项目乱就乱在分层不清晰逻辑写在Controller里、SQL拼在Service里、前端请求散落在每个组件中。我在这个源码里强制约束了分层规范Controller只做参数接收和结果返回Service里写业务规则Mapper只碰数据库操作前端所有请求必须走api目录下的封装函数任何组件里不得直接出现axios实例。2. 数据库设计状态上报与审批流的核心表结构2.1 用户、院系与角色的三张基础表这个系统的基础数据是“人”和“组织”。用户表、院系表、角色不做成三张表做关联而是在用户表里直接挂department_id和role_type。为什么这么设计因为高校院系结构是固定的两层学院-班级/专业没有多级动态扩展的需求单独做一张组织树反而让查询复杂化。用户表结构大致如下idbigint主键自增usernamevarchar(50)唯一索引passwordvarchar(100)存BCrypt加密后的密码real_namevarchar(50)role_typetinyint1学生2辅导员3管理员department_idbigint关联院系表phonevarchar(20)statustinyint1启用0禁用create_time、update_timedatetime这里有两个容易被忽略但很重要的细节。第一个是密码字段长度BCrypt加密后的字符串长度是60很多新手设计成varchar(32)存进去就被截断导致登录永远失败。第二个是role_type的魔法值问题我建议在项目常量类里统一定义比如UserRoleEnum.STUDENT.getValue()而不是在代码里到处写死“1”“2”“3”。不然三个月后你自己都看不懂这个数字代表什么角色。2.2 每日上报记录表唯一索引是防重复的第一道防线业务规则是“每人每天最多上报一条”这类约束必须在数据库层面兜底。如果只在Java代码里先查再插并发场景下两条请求同时通过查询就会产生两条重复数据。所以health_report表里我加上了一个唯一索引UNIQUE KEY uk_user_date (user_id, report_date)上报记录表的核心字段idbigint主键user_idbigint上报人IDreport_datedate上报日期temperaturedecimal(4,1)体温记录health_statustinyint1正常2异常locationvarchar(255)当前所在地contact_infovarchar(255)紧急联系人/联系方式remarkvarchar(500)备注create_timedatetime上报时间这个表设计的关键在于把“上报日期”和“上报时间”拆开而不是混成一个datetime字段。为什么因为业务上要查的是“今天谁没上报”“某一天的上报率”用report_date做分组和条件查询可以走索引速度非常快。如果用create_time去做日期过滤所有查询都得套DATE()函数索引失效数据量上来后就是全表扫描。2.3 异常记录与通知公告表异常记录表记录的是“某条上报数据被标记为异常后的人工处理流程”。字段包括id、user_id、report_id、exception_type、description、status1待处理2已处理、handle_user_id、handle_time、handle_remark。这表本质是一个轻量工单表不复杂但要注意status的流转一定要在后端Service层控制不要在Controller里随手update。否则哪天改了状态枚举连带三四处代码都要跟着改。通知公告表比较简单id、title、content、publisher_id、create_time、is_top。考虑到只有管理员能发公告甚至不需要单独的发布权限表。查询公告时按is_top降序、create_time降序排列即可。我见过有人在公告表里加了一个target_role字段来区分“发给学生还是发给老师”这个需求在这个系统里其实不存在——公告无非全校可见不需要定向推送。2.4 逻辑删除还是物理删除用户表和部门表我用了逻辑删除is_deleted字段上报记录和异常记录用物理删除。理由用户和部门是主数据删除后历史记录里的关联信息会失去意义逻辑删除可以保留数据线索上报记录是流水数据删除后只是少一条历史不涉及外键完整性物理删除更干净。这个取舍不是绝对的但你要在项目一开始就定好规则而不是每个表看心情加is_deleted那才是灾难。3. 后端工程落地SpringBootMyBatis的常见坑与我的处理方式3.1 分层的边界在哪里后端我严格按照Controller-Service-Mapper三层来写。很多人会问一个简单的查询Service里就是一行调用不写Service直接Controller调Mapper行不行短期能跑长期一定出问题。因为业务规则会不断叠加比如上报接口一开始只是插入一条记录后来要加“当天重复提交校验”再加“异常状态判断”再加“推送通知”。如果一开始跳过Service层这些规则后面要么堆在Controller里变肿要么回头重构。我建议哪怕最简单的方法也走一遍Service保持调用链的完整。在这个项目里我特别强调Service层的“事务边界”。默认情况下Spring事务只对RuntimeException回滚这个坑值得单独说如果在Service方法里catch了异常再抛一个自定义BizException而BizException没有继承RuntimeException事务不会回滚。所以我在common包里定义BizException时明确让它继承RuntimeException。3.2 MyBatis XML中那些让你排查半天的写法问题MyBatis的核心是XML里的SQL。这个项目里我把所有SQL写在XML中Mapper接口只定义方法签名。为什么不用注解SQL因为这个系统有大量动态条件查询注解里写script标签可读性极差XML的where、if、foreach才是处理动态SQL的正道。第一个容易踩的坑是动态where的首个条件前多了AND。很多人手写WHERE 11来规避这在数据量小的系统里问题不大但属于不太优雅的处理方式。更规范的做法是用where标签它会自动处理第一个条件前的AND关键字。第二个坑是模糊查询的写法LIKE % #{keyword} %这样写才是预编译的安全且能用索引新手常写成LIKE CONCAT(%, #{keyword}, %)效果一样但不好读。第三个坑是排序字段。ORDER BY ${sortColumn}是防不住的动态SQL注入点我在源码里的处理方式是定义排序字段白名单只允许create_time或id这两个值传入其他一律回退默认排序。3.3 分页查询不要自己写Limit列表页和统计页都要分页。MyBatis分页我直接用PageHelper用法很简单查询前调用PageHelper.startPage(pageNum, pageSize)紧接着的Mapper查询会自动带上LIMIT。但有两个细节第一PageHelper只对紧随其后的第一条查询生效这个大家都知道但很多人不知道如果查询前有别的数据库操作分页就失效了。第二在多表关联查询和嵌套ResultMap的场景下PageHelper的count查询可能统计不准确所以我做列表查询时尽量避免复杂的嵌套映射多表关联就拆成单表查询后内存组装或者写专门的VO查询类。3.4 登录鉴权用拦截器而不是Spring Security登录鉴权我用的方案是JWTHandlerInterceptor。为什么不引入Spring Security对这个体量的系统Security的过滤器链和配置复杂度远远超出需求而且它和SpringBoot版本选型耦合度高一旦版本不匹配配置半天调不通纯属给自己加负担。用拦截器逻辑透明所有代码都在掌控之中。实现要点有几个。第一登录接口在拦截器里要放行不然用户还没登录就被拦了。第二登录成功后生成JWT在Token里只放userId和roleType不放密码等敏感信息。第三拦截器里解析Token如果失败直接返回401状态码和统一错误格式而不是抛出异常让全局处理器去处理。第四用户信息用ThreadLocal保存这样Controller和Service里随时可以取当前登录用户ID这在填写上报记录时特别方便——不需要前端传userId后端从Token里拿防止伪造。统一返回格式我用ResultT结构是code、msg、data三件套。200表示成功401表示未登录500表示系统错误业务异常用自定义code。所有接口返回这个结构前端就不需要为每个接口单独写错误判断逻辑。全局异常处理用RestControllerAdvice捕获BizException返回业务错误捕获参数校验异常返回参数错误捕获Exception返回系统错误。4. 前端 Vue3 工程化从路由守卫到组件拆分4.1 工程初始化与目录约定前端我用Vite创建命令是npm create vitelatest。很多初学者会纠结要不要用TypeScript我的建议是如果项目是课程设计或者个人学习JavaScript完全够用如果团队协作并且有规范偏好再上TypeScript。这个源码我没用TS不是TS不好而是这套业务逻辑比较简单类型系统带来的约束收益不高反而增加了模板代码。目录结构比我前文说的更具体一点。views下面按角色分子目录studentViews、teacherViews、adminViews每个角色页面互不混用。api目录按业务域拆分文件user.js、report.js、notice.js、statistics.js。stores目录用Pinia这里主要存两个全局状态当前登录用户信息和侧边栏折叠状态。组件复用层面我封装了一个CommonTable组件和一个CommonFormDialog组件管理员端的用户管理、上报记录管理等好几个页面都能复用它们。4.2 Axios拦截器与Token生命周期Axios封装是整个前端的命脉。我在utils/request.js里创建了一个axios实例然后设置了两层拦截器。请求拦截器做的事情比较简单从localStorage取Token有就加到请求头Authorization字段。这里有一个大坑Token的存储位置。我见过不少项目把Token存在localStorage里页面刷新后还在但遇到XSS攻击时Token会泄露。更稳妥的做法是用HttpOnly Cookie但这个方案会带来CSRF防护需求复杂度上来了。考虑到课程设计和中小型项目的实际威胁模型localStorage其实足够注意别用eval、别把用户输入直接innerHTML就行。响应拦截器要做的事情更多拿到后端返回的data后先判断业务code。如果是401说明Token过期或无效直接清掉本地Token并跳转登录页同时用Element Plus的Message给用户提示如果是200但业务code不是200说明业务执行失败统一弹错误提示。这样设计后页面里写请求代码根本不需要处理错误分支只需要关心成功后的渲染。4.3 路由守卫前后端都要有防线路由守卫的逻辑分三块。第一块beforeEach里判断目标路由是否需要登录需要登录但没Token就跳登录页。第二块有Token但访问了无权限的角色页面通过meta.roles做拦截比如学生角色访问adminViews下的路由直接重定向到403页面。第三块登录页在已登录状态下访问跳转到对应角色的首页。前端做路由守卫的作用是用户体验而不是安全。真正防越权的是后端拦截器前端躲开只是让界面不出现一堆无权限报错。我在源码的注释里专门强调了这一点因为很多初学者把前端权限当安全防线一旦绕过前端直接请求后端接口就裸奔了。4.4 核心页面填报表单与表格看板学生端的填报页面是这个系统最体现交互细节的一个页面。逻辑不复杂但要做好两个状态今天是已经填报还是未填报。页面加载时请求后端判断今日是否已报如果已报表单全部禁用只显示“今日已填报”的状态如果未报表单可用。这里有一个体验细节提交成功后的按钮立刻变成loading状态并置灰防止用户连点提交造成重复请求虽然后端有唯一索引兜底但前端也要主动避免无效请求。管理端的表格看板页面我封装了一个usePageTable组合式函数把搜索条件、分页参数、列表数据、加载状态、查询方法、重置方法全部收进去。页面组件里只需传一个获取列表的API函数和搜索表单字段就能自动组装出完整表格页。这就是组合式API相对选项式API的优势逻辑复用是自然的函数式组合而不是mixin的命名空间污染。5. 前后端联调跨域、日期格式、空值这些小事如何逼疯人5.1 开发环境的跨域用Vite代理而不是后端开CORS前后端分离项目联调时第一个遇到的就是跨域。前端跑在5173端口后端跑在8080端口端口不同就产生了跨域。我推荐开发环境用Vite的proxy配置解决所有以/api开头的请求Vite在开发服务器层面转发到8080端口的后端服务。// vite.config.js export default { server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样配置后前端代码里请求路径直接写/api/report/submit浏览器看到的请求是同源的不会触发跨域拦截。很多人图省事在后端加一个CrossOrigin放开所有来源这在开发环境可以但生产环境如果也把CORS全放开任何网站都能往你的接口发请求非常危险。我在源码里后端的CORS配置是可选的生产环境走Nginx同源部署完全不需要后端放跨域。5.2 Long类型精度丢失前端拿到的ID不对这个坑极其隐蔽。数据库的bigint类型对应Java的Long类型没问题但JSON序列化后前端JavaScript的Number类型只能精确表示2^53以内的整数。MySQL自增ID一旦超过这个范围比如雪花ID或者数据量大的系统前端拿到的ID尾数就是错的然后拿着这个错误的ID去请求详情或删除后端自然查不到数据。解决方案在后端全局给Long类型的序列化做一个处理在application.yml里配置Jackson把Long序列化为String。当然这会影响所有Long字段如果只是个别字段需要就在实体字段上加JsonSerialize(using ToStringSerializer.class)。这个系统的用户ID在数据量不大时不会超过精度范围但既然做了通用源码我顺手就在全局配置里处理了一劳永逸。5.3 日期时间格式前后端各管一段后端LocalDateTime序列化默认输出的是2025-01-01T10:30:00这种ISO格式Element Plus的日期时间组件默认格式是2025-01-01 10:30:00。如果不统一前端表格里显示的日期要么带T要么和组件解析不一致导致回显错误。我在后端全局配置了Jackson的日期格式为yyyy-MM-dd HH:mm:ss同时指定time-zone: GMT8防止时区偏差导致时间少8小时。前端这边有两个需要注意的点。第一个Element Plus的date-picker组件要设置value-formatYYYY-MM-DD这样绑定值就是一个纯日期字符串提交给后端时不需要再format。第二个后端查询统计的时候接收前端传的日期参数用DateTimeFormat(pattern yyyy-MM-dd)注解明确告诉Spring怎么把字符串转成Date对象。5.4 空值字段的前端兜底联调最容易出现的另一个现象是后端返回的字段里有null前端组件里直接row.remark.length就报错了。后端接口设计时我建议全局配置Jackson的NON_NULL序列化时忽略null字段这样响应体干净紧凑前端也不会解出多余的null。但有些字段前端需要知道“有没有值”来做展示逻辑比如异常记录的处理备注null代表未处理空字符串代表处理时没填备注这个语义差异前端要区分。所以更稳妥的办法是前端代码里用可选链row.remark?.length和空值兜底row.remark || --来处理展示而不是依赖后端屏蔽null。我在项目的表格列渲染里统一写了一个formatValue函数null和空串都显示为占位符看着规整也不会出现控制台报错。6. 项目源码使用与部署从本地跑起来到生产服务器6.1 拿到源码后第一步怎么跑起来先说明这套源码的本地运行前置条件JDK要求17以上SpringBoot 3.x的硬性要求MySQL要求8.0Node要求18以上。第一步导入数据库项目根目录下有个sql目录里面是建库建表脚本和初始化数据。执行脚本时我建议用命令行或者Navicat直接source不要复制粘贴到图形化工具的查询窗口里执行——脚本比较长窗口执行容易一半中断。第二步改后端配置。打开application-dev.yml把数据库连接地址、用户名、密码改成你自己的。特别注意MySQL连接串里的时区参数serverTimezoneAsia/Shanghai不配这个参数高版本MySQL驱动会报时区错误。第三步启动后端IDE里运行主类或者命令行mvn spring-boot:run。第四步启动前端先npm install装依赖然后npm run dev浏览器访问http://localhost:5173。这里说一个新手经常卡住的问题npm install很慢或者报错。我的建议是先确认npm源如果默认源访问慢切换成国内镜像源再装。如果node_modules装了一半中断把整个目录删掉重装不要想着修复node_modules的状态是没法保证的。踩过太多次这种坑了。6.2 Maven多环境配置数据库密码这种事绝对不能写死在代码里提交到仓库。我的做法是后端配置拆三个文件application.yml放公共配置application-dev.yml放本地开发配置application-prod.yml放生产配置。生产配置里的数据库密码用环境变量占位比如${DB_PASSWORD}部署时在服务器环境变量里注入真实值。打包命令是mvn clean package生成的jar包里有完整的内嵌Tomcat。如果你用IDE直接启动用dev环境部署服务器用java -jar xxx.jar --spring.profiles.activeprod指定生产环境。这里有个细节SpringBoot 3.x和Maven插件版本有兼容性要求pom.xml里spring-boot-maven-plugin版本要和SpringBoot父版本一致不然打包后运行会报“no main manifest attribute”错误。6.3 Nginx部署与前端路由history模式部署这块我单独说一下因为前端路由的坑太典型了。前端用Vue Router的history模式后路由路径不是带#的hash模式用户在浏览器直接访问http://服务器IP/student/dashboard这个地址时Nginx找不到这个物理路径会返回404。解决办法是在Nginx配置里加上try_files回退location / { root /usr/share/nginx/dist; index index.html; try_files $uri $uri/ /index.html; }Nginx的另一个配置是API反向代理。前端构建后部署在服务器的80端口后端jar包跑在8080端口需要把/api/开头的请求转发到后端location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里有一个容易配错的地方proxy_pass后面如果带了路径http://127.0.0.1:8080/请求的/api前缀会被去掉不带路径则完整透传。我用的是完整透传方式因为后端所有Controller都是挂在/api下面的保持一致。启动后端时用nohup java -jar 项目名.jar app.log 21 放后台跑然后把日志写入app.log排查问题全靠它。6.4 服务器初始化数据与定时任务生产环境第一次启动时需要执行初始化SQL创建表和管理员账号。我建议初始化管理员密码直接用脚本里生成的BCrypt哈希值而不是明文避免被人看到源码就知道所有系统初始密码。另外健康信息管理场景需要每日零点自动重置当天的填报状态虽然接口层面是通过“查询今日是否已填报”来判断的不需要真的重置数据但如果你要跑定时统计可以在后端加一个Scheduled注解的定时任务每天凌晨算出前一天的各部门上报率并缓存起来。定时任务看起来简单但要注意两点第一默认单线程调度多个任务要用线程池配置EnableAsync和Async否则一个任务阻塞会影响其他任务第二生产环境多个后端实例部署时要防止定时任务重复执行简单方案是加一个数据库锁或者你这个体量就用分布式锁框架这个项目加锁也可以用数据库唯一键实现总之不要裸跑。7. 开发完这套系统后的几点实在话做完整套系统最深的体会有三点。第一先跑通再优化。我在最初版本里根本没加Redis缓存、没有消息队列、没有复杂权限模型就是一个极简的SpringBootVue3 CRUD系统。先把主流程跑通学生能登录、能填报、管理员能看统计之后所有优化都是在主流程之上的锦上添花。很多同学做项目一开始就想把所有技术栈堆上去结果卡在集成问题上项目进度一拖再拖。第二日志是定位问题的最好朋友。整个后端代码里我几乎没有用System.out.print全部用SLF4J的log.info/log.error。遇到接口返回500不要急着猜先看日志栈。前端也一样遇到页面不刷新打开浏览器控制台看Network面板和后端返回数据。这个习惯能省下大量联调时间。第三不要过度设计。这个系统本质上是一个典型的管理信息系统它的核心价值在于数据能够准确、及时地流转而不是用了多少新潮技术。如果你在做一个课程设计或毕设我的建议是把基础功能做扎实比堆一堆只会跑Demo的新技术更能拿高分。后续想扩展可以往这些方向走对接企业微信或钉钉的消息通知、引入二维码扫码填报、增加Excel导入导出、用ECharts做更丰富的趋势分析。这些扩展都不需要推翻现有架构你只需要在Service层和前端新增模块即可。这套源码你拿去跑起来对着代码看我这篇文章很快就能看明白整个链条。如果遇到环境问题跑不起来优先看后端的启动日志和前端的构建报错十有八九是端口、数据库连接或Node版本这三个方向。祝顺利跑通。

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

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

免费获取报价 →
↑