很多计算机专业的朋友在准备毕业设计的时候,都会在选型上卡住:既要体现技术栈的完整性,又得保证自己能在有限时间内做完、能讲清楚。SpringBoot加Vue的组合,算得上是这类项目里的“标准答案”。不管是企业级应用还是个人毕设,这套前后端分离的架构都有着非常成熟的学习路径和现成生态。
我最初接触这个健康检查系统的时候,想得比较简单:无非就是体检预约、报告录入、用户管理这几张表。真正动手去做才发现,一个合格的毕设项目,核心不是把页面拼出来,而是搞清楚数据是怎么流转的、权限是怎么控制的、异常是怎么兜底的。这篇内容不聊虚的,直接说清楚这个系统的完整设计思路、表结构怎么拆、后端接口怎么写、前端页面怎么接,以及那些你在实际开发中一定会踩到、但教材里根本不会写的坑。
1. 健康检查系统到底要做什么:核心需求与功能拆解
1.1 系统的真实使用场景
健康检查系统,放到现实场景里,最典型的使用方是企业工会、体检中心或者学校医务室。员工或者学生先预约体检时间,到了现场做各项检查,检查完医生录入结果,最后个人查到自己的体检报告。这个流程天然适合做前端展示加后端管理的结构。
作为毕设,它的受众其实有三类:普通用户负责预约和查看报告,体检医生负责录入检查数据,系统管理员负责维护用户、体检项目和套餐信息。这三类角色的操作边界完全不同,所以权限控制是躲不开的设计点。
1.2 功能模块怎么拆分才合理
从实际操作流程反推,系统可以拆成下面这几个核心模块:
第一个是用户端。用户注册登录后,能看到可预约的体检日期和剩余名额,提交预约后等待医生确认,检查完成后查看自己的体检报告,报告会以分项的形式展示,比如血常规、肝功能、心电图,每一项有对应的指标值和参考范围。
第二个是医生端。医生登录后能看到分配给自己的待检查用户列表,勾选检查项目后录入结果,如果指标有异常,可以写上诊断建议。这个模块还有一个隐藏需求,就是体检状态的多步骤流转:待检查、检查中、已完成。
第三个是管理端。管理员负责维护体检项目的字典数据,配置体检套餐包含哪些项目,还可以查看医院的整体预约统计情况,比如今日检查人次、异常率、项目覆盖率等等。
第四个是通用的系统支撑功能,比如密码加密、登录状态校验、操作日志记录。这些在毕设答辩时很加分,因为它体现了你对“完整项目”的理解,而不只是CRUD。
2. 技术选型和架构设计:为什么是SpringBoot+Vue
2.1 后端用SpringBoot的真实理由
SpringBoot在Java生态里已经是事实上的默认起点。它最大的价值在于把以前SSH时代那些繁琐的配置都约定好了,你只需要引入依赖,它自己就知道怎么装配。对工期紧张、时间有限的毕设来说是件好事,你不需要在环境配置上耗掉大半个月。
用SpringBoot做这个系统,我的建议是选择2.7.x版本的稳定分支,不要一上来就追SpringBoot 3.x。原因很简单:3.x基于JDK 17,很多学校机房或者你自己电脑上的JDK还停留在8或者11,版本对不上会出现非常无聊但烦人的兼容问题。另外,3.x里javax命名空间改成了jakarta,网上大量现成资料里的代码复制过来直接报红,对新手极不友好。
持久层框架我建议直接用MyBatis Plus而不是原生MyBatis。它内置了通用的增删改查方法,单表的CRUD你几乎不用写SQL,省下来的时间可以去做更有价值的事情,比如说权限拦截和逻辑校验。原型设计上,用一个LambdaQueryWrapper就能解决条件查询,代码看起来也简洁很多。
2.2 前端Vue的项目结构和配套组件
前端的选型,Vue 2加Element UI和Vue 3加Element Plus之间,我更推荐后者。Vue 3的Composition API在写业务逻辑的时候比Options API更顺手,组件间的逻辑复用也更清晰,最关键的是新项目没有必要去学一套马上要过时的东西。
用Vue CLI或者Vite初始化项目都可以。Vite在开发环境下的启动速度明显要快,但对于不熟悉前端工程化的同学,Vue CLI提供的图形化配置界面会更直观一点。我个人倾向于Vite加手动配置,因为这样你能真正理解依赖和构建的过程,而不是只知道点按钮。
配套组件那边,UI框架用Element Plus,HTTP请求用Axios,状态管理如果不是特别复杂的场景,用Pinia就够了。路由用Vue Router,拦截器里做登录校验。要注意的是,Vue Router 4的API和Vue Router 3有些差异,特别是通配符路径的写法,在迁移的时候经常有人在这里翻车。
2.3 前后端联调时的接口约定
前后端分离的架构下,接口规范比代码本身更重要。我的做法是约定好统一的返回格式,后端所有接口都返回一个Result对象,里面包含三个字段:状态码code、消息message、数据data。
code为200表示成功,401表示未登录,403表示无权限,500表示业务异常。前端Axios封装的响应拦截器里面统一判断状态码,成功就返回数据,失败就弹出提示。这样前端处理逻辑时不用每个页面写一套判断逻辑,代码会干净很多。
接口地址以/api开头,后面按模块区分,比如/api/auth/login、/api/appointment/create、/api/report/list。版本号可以加上,v1/api/health/user这种格式,反正以后如果要改接口,不用连个前端一起改,显得有条理。
3. 数据库设计:从一张表到一套体系的演变
3.1 核心表结构的设计思路
这个系统的数据库,我开始设计的时候只有四张表:用户表、预约表、检查表、项目表。跑通流程之后发现,权限区分没法实现,套餐功能和体检项目之间的关系也是混乱的。后来又重新调整了设计,最后稳定下来的是六张核心表加三张辅助表。
用户表(sys_user)是最基础的,字段包括id、username、password、real_name、role、phone、create_time。角色字段用字符串类型存ROLE_USER、ROLE_DOCTOR、ROLE_ADMIN,比用数字存更直观,写代码的时候不用做太多的语义转换。
体检项目表(exam_item)存的是每一个体检项的基础数据,比如项目名称、所属分类、正常值范围、单位、参考说明。这里有一个比较关键的字段设计,参考范围要拆成upper_limit和lower_limit两个数值字段,不要直接存成一个字符串,否则后面做异常指标自动标记就会非常麻烦。
套餐表(package)和套餐项目关联表(package_item)解决的是组合关系问题。一个套餐包含多个项目,一个项目可以出现在多个套餐里,典型的多对多关系。关联表里只有一个套餐ID和一个项目ID,通过它来做查询连接。
预约表(appointment)记录用户预约的体检时间和套餐,字段有appointment_no、user_id、package_id、expect_date、status、create_time。预约编号用时间戳加随机数生成,比自增ID做订单号靠谱得多。
检查结果表(exam_result)是整个系统的数据核心,记录每一次体检中每个项目的实测值。字段包括result_no、appointment_id、item_id、item_value、is_abnormal、doctor_comment、check_time。其中is_abnormal可以由后端判断指标值是否在参考区间内后自动填入,这算是一个很好的设计亮点,答辩时值得作为功能创新点来说。
3.2 状态设计:预约到报告的流转逻辑
状态字段的设计最能体现一个系统的严谨性。预约状态我用了四个值:0表示待确认,1表示已确认,2表示已完成,3表示已取消。体检记录的状态是:0待检查、1检查中、2已完成。
状态流转的规则是前端单独维护的。用户提交预约后,状态是待确认;管理员确认预约,状态变成已确认;医生录入完所有检查项的数值后,状态变成已完成。取消操作只能在待确认和已确认状态下执行,已完成状态下不允许取消。这个规则在后端的Service层实现,不要放在Controller层里做判断,因为Service才是业务逻辑正确性的最后一道屏障。
3.3 索引和运维细节
数据库设计里容易被忽略的是索引。预约表的user_id和expect_date字段加普通索引,因为这两个字段用得多,一个是我的预约列表,一个是按日期查容量。exam_result表的appointment_id加索引,查询报告详情时会非常频繁地按这个字段做关联。
字符集统一用utf8mb4,不要用utf8。这不是玄学,utf8mb4才能正确存储emoji和一些特殊字符,比如用户填的体检备注信息里偶尔会有这样的符号。排序规则用utf8mb4_general_ci就够用,不要为了追求高级去选utf8mb4_unicode_ci,实际查询性能差别不大,但前者的兼容性问题更少。
4. 后端核心代码实现:从登录拦截到业务闭环
4.1 登录鉴权和拦截器配置
健康检查系统里有三种角色,每一种角色的访问范围不同,所以登录鉴权是后端的第一道门槛。我采用JWT(JSON Web Token)的方式,登录成功后服务端生成一个token返回给前端,前端存到localStorage里,后续每次请求都在请求头里带上Authorization字段。
生成token的工具类里,核心代码就是用用户的id和角色生成一个带有过期时间的JWT字符串。密钥要放到application.yml的配置项里,不要写死在代码中。过期时间我设的是24小时,足够覆盖一段完整的操作会话,也不会因为太长带来安全风险。
拦截器是继承了HandlerInterceptor,在preHandle方法里校验token的有效性。校验通过就把用户信息放入ThreadLocal中,方便后续业务代码获取当前登录用户。这样做比每次手动从token里解析用户信息再传参要优雅很多,代码也更集中,后期做日志审计也方便。
权限控制的细节在于,拦截器只负责校验是否登录,而某个接口是否允许当前角色访问,我是用的自定义注解加AOP来实现的。在需要权限校验的方法上标注@RequireRole("ADMIN"),然后切面里判断当前用户角色是否符合要求。
4.2 体检报告生成的核心逻辑
报告数据查询是系统里最复杂的一段业务逻辑。一次体检包含多个项目,每个项目有一条结果数据。用户端展示报告的时候,需要一个明细列表、异常项数量、整体健康建议。
我在Service层的实现思路是,先根据预约ID查询出所有的检查结果数据集合,再遍历这个集合,根据item_id去查出对应的项目信息,回填到返回对象里。遍历的过程中做两件事:判断每一条数据的is_abnormal是否为1,统计异常总数;把每条结果和项目信息组合成明细对象。
批量查询替代逐条查询是一个很重要的性能优化。不要在循环里调用单条查询,而是一次性查出所有项目信息后,转成Map,以item_id为key,然后循环里直接从Map里取。这样的数据库交互从N次降到了1次,数据量大的时候差异非常明显。
异常判断的规则是这样的:若体检项目设置了参考上下限,且实测值不在区间内,则is_abnormal标记为1。有些项目如“既往病史”这种是文字描述,没有参考范围,就直接跳过判断。健壮性方面,item_value如果是小数,需要统一转换成BigDecimal去比较,不要用double,避免精度问题。
4.3 文件上传和对象存储
系统里有报告附件上传功能的时候,推荐使用MinIO作为对象存储服务,能够解决本地文件路径不好管理的问题。MinIO是开源的,兼容S3协议,部署也极其轻量,下载一个二进制文件就能跑起来。
用SpringBoot集成MinIO,核心就是配置好endpoint、accessKey、secretKey和bucket名称,然后用MinioClient的putObject方法上传。上传成功后会返回一个对象的路径,拼上MinIO的访问地址就是文件的完整URL,前端用这个URL直接展示图片或者下载附件。
我踩过的坑是上传文件大小限制。SpringBoot对上传文件默认限制是1MB,实际体检报告中可能存在批量上传的附件图,超过这个大小会直接报错。解决办法是在配置文件中显式设置spring.servlet.multipart.max-file-size和max-request-size两个参数,我一般设为10MB和20MB。
4.4 MyBatis Plus的查询封装技巧
使用MyBatis Plus之后,手动写SQL的场景少了很多,但条件查询依然得会用它的QueryWrapper。以预约列表为例,后端接收三个查询条件:用户ID、状态、预约日期区间。构造查询逻辑时,先创建一个LambdaQueryWrapper,然后根据参数是否为空,决定是否加对应的查询条件。
参数为空就不要拼条件,这种判断在MyBatis Plus里用字符串的isNotBlank来做。日期区间查询对应ge和le方法,翻译过来就是大于等于和小于等于。排序统一用orderByDesc按创建时间倒序,让最新的预约排在最前面。
分页用MyBatis Plus内置的Page对象,传入当前页码和每页条数,查询后返回IPage类型。前端传过来的页码参数,后端直接用Page接收,不需要手动做count查询。这里要注意,分页插件一定要在MyBatis Config里注册PaglnnerInterceptor,否则分页不生效,很多人第一次用的时候都会在这个问题上浪费很多时间。
5. 前端Vue页面搭建:从启动项目到路由守卫
5.1 项目初始化和基础环境配置
Vue项目初始化之后,第一步要做的就是安装Element Plus组件库。看到这里可能会觉得这些都很简单没什么可讲的,但实际写过前端的人都知道,依赖版本的对齐其实是最容易坑人的一件事。
我用的版本组合是这样的:Vue 3.2以上,Element Plus 2.2以上,Axios 1.x,Vue Router 4.x。直接处理器自动安装最新的稳定版,然后按需引入Element Plus组件,不需要全量引入。这样做的效果就是打包体积小,启动速度快。全量引入虽然省事,但会让前端首屏加载速度明显变慢,几百个组件全部打包进去,在毕设演示现场如果网络不好,体验会非常糟糕。
5.2 路由和菜单权限怎么控制
前端的路由表和后端的菜单不是一回事。前端路由定义了所有页面路径和组件的对应关系,后端菜单决定的是当前登录用户能看到哪些入口。我的实现方式是路由分为两部分,一部分是无需登录的静态路由,比如登录页、注册页,另一部分是主布局下的业务页面。
在路由配置中设置meta字段来标记需要的角色,然后在路由守卫里做拦截。全局前置守卫在每次跳转前判断目标路由的meta信息,如果没有登录就跳转到登录页并且带上redirect参数,登录了但角色不匹配就跳转到401提示页。
左侧菜单栏的数据结构是后端根据角色动态返回的,管理员看到菜单和管理用户相关功能,普通用户只显示我的预约和我的报告。前端把返回的菜单数据递归渲染成侧边栏,这种方案在答辩时会显得架构能力强。
5.3 关键页面的交互逻辑拆解
以预约页面上选体检套餐的逻辑为例,Vue 3中实现起来非常直接。页面加载时调用后端接口获取套餐列表,每个套餐卡片上显示套餐名称、包含的项目数、价格和推荐指数。点击套餐卡片触发选中事件,下方对应的区域展示套餐包含的所有项目列表,以及每个项目的参考范围。
提交预约时,选择一个期望的日期,后端接口会对该日期下的剩余名额做校验。如果名额已满,会返回异常,前端用ElMessage弹出错误提示。这个交互逻辑关键在于提交按钮要做防重复处理,简单做法是加一个isSubmitting变量,请求发出后置为true,请求结束再置为false,配合Loading效果防止重复提交。
报告页面的展示逻辑是表格嵌套卡片。外层用el-tabs区分体检次数,每次体检的信息渲染成一个独立的表格,表格里每一行展示项目名称、实测值、参考范围、结果状态。结果状态为空时用el-tag显示为正常,异常为红色标签,这样一眼就能看到哪些指标偏高或偏低。
5.4 Axios封装和请求拦截
前端对后端接口的调用,强烈建议封装统一的request模块。我一般会创建src/utils/request.js文件,里面用axios.create方法创建一个自定义实例,设置baseURL为/api,超时时间为30秒,然后通过拦截器处理请求和响应。
请求拦截器负责给每个请求加上Authorization请求头,从localStorage中读取token并拼接上Bearer前缀。响应拦截器做集中错误处理,如果返回体的code是401,清除本地存储并跳转到登录页;code非200,将message消息用ElMessage弹出显示给用户。业务代码里再也不用写一堆try-catch,清爽不少。
封装的另一个好处是,后端调整接口路径时,前端只需要修改一处配置就好,不会影响到页面里的具体调用。
6. 前后端联调和部署:进程中的真实问题
6.1 本地联调时的跨域问题
前端和后端分开运行的时候,一个在8080端口,一个在5173端口,浏览器为了安全会阻止跨域的AJAX请求。这个问题的解决方法没有一个阉割的方案,最标准的是在后端加CORS配置。我写了一个WebMvcConfigurer的实现类,在addCorsMappings方法里配置允许的源、请求头和方法,将allowedOriginPatterns设为*,并开启allowCredentials,再设定好允许的HTTP方法和请求头。
开发环境下也可以让Vue的vite配置代理,把/api开头的请求转发到后端地址。这样浏览器访问的还是5173地址,实际请求由Vite开发服务器转发,既绕开了跨域,也让前端代码保持干净的相对路径。两种方案可以同时配置,但生产环境部署时,CORS配置是必须的,代理只适用于开发阶段。
6.2 SpringBoot项目的Maven构建
后端项目打包需要用Maven执行clean package命令,注意在打包前检查配置文件的profile。数据库连接地址、账号密码如果写死在application.yml里,打包完成后改起来比较麻烦,最好用application-dev.yml和application-prod.yml分开管理,打包时通过启动参数指定激活哪一个环境。
用IDEA配置Maven构建时,有一个常见的坑是,本地配置的Maven仓库路径和IDEA内置的默认路径不一致,导致依赖永远下载不对。建议在IDEA的Maven设置里,明确指定settings.xml文件的位置,同时确认本地仓库路径和settings里配置的一致。拉取依赖失败时,最快的排错方案是删除本地仓库中对应jar包的lastUpdated文件后重新导入。
6.3 部署到服务器后的注意事项
打包好的SpringBoot应用是一个可执行的Jar包,服务器上只要安装了JDK就可以直接启动。启动命令建议用nohup java -jar app.jar > app.log 2>&1 &,这样即使SSH连接断开,服务也不会停止。查看日志用tail -f app.log,能及时发现问题。
前端构建后的产物是dist目录,里面是纯静态文件,放到Nginx的html目录下就行。Nginx配置里需要做两部分:一是将/路径指向前端dist目录,二是将/api路径反向代理到SpringBoot进程监听的端口。这个配置是整个部署环节最容易出错的地方,很多人前端部署好了,一刷新页面就404,因为Nginx没有配置try_files规则,把前端路由重定向到index.html。
7. 常见问题排查和避坑指南
7.1 数据库连接和时区问题
SpringBoot连接MySQL时报时间相关的错误是很常见的,原因是MySQL驱动8.0之后对时区敏感。解决方法是连接URL上加上serverTimezone=Asia/Shanghai,还可以配上useSSL=false避免安全证书的告警。数据库连接池用HikariCP,这个在SpringBoot 2.x里是默认的,注意把maximum-pool-size设置成合理的值,不要太大,默认10就够了。
7.2 前端样式和打包体积问题
Element Plus按需引入后仍然有样式丢失的情况,原因是组件样式和组件逻辑没有同步加载。检查方式简单粗暴,直接在main.js里全量引入样式文件,如果问题消失,说明是按需引入的配置有问题。平时开发时用全量引入可以省心很多,到项目上线前再换成按需引入也不迟。
Vue页面首次加载时白屏并伴随控制台报错是另一种常见情况,截图时经常能看到Uncaught TypeError之类的错误。这类问题九成是某个组件或者变量在模板中使用了未定义或为null的值。排查方法是在模板的对应位置加上v-if判断,或者统一通过可选链操作符访问深度嵌套的属性。
7.3 后端接口报错排查顺口溜
接口五百不用慌,先看日志找方向。空指针、参数错,多半前端传错值。SQL报错有原因,表格字段查仔细。404先看访问路径,拼写错了白费劲。
这是我在实际调试中总结的一套排查有方向的路线。首先打开后端控制台看完整的异常堆栈,找到自己项目包名下的那一行代码,几乎所有的错误都能定位到具体方法。如果日志里没信息,检查是不是被全局异常处理器吞掉了。不要一上来就方向不明地瞎试,效率很低。
我个人做完这个项目,对前后端分离架构的理解比几个月前要清晰很多。最大的感受是,大部分所谓的开发难题并不是某个技术本身有多深,而是数据流和状态流转的边界没有想清楚。先把流程理顺,再把功能拆细,代码写起来反而是顺畅的。
最后再分享一个实在的小技巧。答辩前的演示环境一定要提前准备好,不光是代码能运行,登录的测试账号、演示用的数据,都要提前录入好。现场临时去注册账号、去预约体检,这些操作在紧张的环境下特别容易出错。我在第一次预演的时候就吃过这个亏,从那次起所有演示数据都固定放在数据库里,只保留一条干净的测试链路。这个习惯,做任何项目都适用。