今年我盯上了一个挺典型的Java Web全栈项目师生健康信息管理系统。技术栈也很直接——SpringBoot2 Vue3 MyBatis-Plus MySQL8.0附带完整的项目文档。说实话这套组合在国内中小型管理系统开发里已经算“标准答案”了从后端接口到前端页面、从数据库设计到部署上线基本覆盖了Java Web开发的大部分核心技能点。这篇内容适合正在做课程设计、毕业设计或者想快速上手一整套前后端分离项目的开发者参考。系统要解决的痛点很明确学校要管理教职工和学生的体检数据、体测记录、异常预警用Excel维护容易乱、权限不好划分、统计报表也难做医院或体检机构的专业系统又太重不适合校园自管。所以自己搭一套轻量级Web端管理系统正是这类项目最常见的落地场景。下面我就把这套系统的设计思路、核心实现、部署经验和填坑记录一条线讲清楚。1. 系统定位与总体设计1.1 这套技术栈是怎么定下来的先说解决“为什么”的问题。很多人拿到项目直接开敲但技术选型背后的理由才是决定项目能走多远的关键。SpringBoot2为什么不追新用SpringBoot3因为SpringBoot3要求JDK17而很多学校机房、公司服务器还在用JDK8。SpringBoot2生态成熟资料多任何报错几乎都能搜到解决方案开发和部署成本最低。管理类系统不需要特别新的特性稳定压倒一切。Vue3对比Vue2组合式API和script setup语法让代码逻辑更集中同样的页面代码量能少三分之一。再加上Vite的冷启动速度比Webpack快太多开发体验好很多。刚开始项目不用上TypeScript先用JavaScript把业务跑通等后续的复杂度上来后再补TS反而是更务实的选择。MyBatis-Plus很多人纠结Spring Data JPA和MyBatis-Plus的区别。JPA适合领域驱动、以实体模型为核心的场景但国内多数管理系统是SQL驱动复杂查询多、报表统计多MyBatis-Plus既能享受单表CRUD自动化的便利又能随时写原生SQL应对复杂多表关联控制力更强。条件构造器更是让动态条件查询不需要拼SQL这点在实际开发里太香了。MySQL8.0性能比5.7更好支持窗口函数、JSON类型、公共表表达式做统计报表时的SQL写法能简化不少。需要注意的是MySQL8.0默认认证插件是caching_sha2_password老版本工具或驱动连接时容易报错这点后面单独说。前端UI我选了Element Plus它和Vue3配合紧密后台管理系统常用的表格、表单、弹窗、树形控件都齐开发速度能快不止一倍。1.2 功能模块与数据库设计这个系统的核心业务是管理员维护师生基础档案校医或教师录入体检数据系统自动做异常判断学生和教师可以查询自己的健康报告管理员还能看全校的统计报表。角色权限就按管理员、教师、学生三种来分权限模型用最基本RBAC用户表加一个role字段就够了不需要单独维护角色-菜单关联表避免把简单系统做复杂。数据库设计上我坚持几个原则不用物理外键外键约束靠应用层保证。原因很简单学校数据量不大但后期可能要拆表、做归档物理外键会拖累写入性能也不利于后续迁移。所有业务表加逻辑删除字段deleted默认0删除改成 UPDATE防止手滑删数据。每张表都带create_time和update_time字段配合MyBatis-Plus的自动填充省心省事。核心表结构大致如下表名用途关键字段user登录账户id, username, password, role, status, deletedstudent_profile学生档案id, user_id, student_no, name, gender, class_name, grade, birth_dateteacher_profile教师档案id, user_id, teacher_no, name, gender, department, titlehealth_record体检记录id, user_id, record_date, height, weight, vision_left, vision_right, blood_pressure, heartbeat, resultwarning_log异常预警id, user_id, record_id, warning_type, warning_desc, status, handle_user主键统一用自增整型简单直接。体检记录表建立(user_id, record_date)唯一索引防止同一个人同一天重复录入。统计报告要按学段、年级、班级分类汇总所以class_name字段单独拎出来不靠关联查询去拼统计SQL能简单不少。这是做了多次类似系统后总结的经验关联越少查询越快代码越不容易出错。2. 后端SpringBoot2与MyBatis-Plus实操2.1 工程骨架与标准目录结构很多教程上来就让你用IDEA的Spring Initializr但对于有一定经验的开发者我更推荐把整个后端的骨架理解清楚再动手创建。标准目录结构长这样src/main/java/com/example/health/ ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto │ ├── req │ └── resp ├── vo ├── config ├── common │ ├── result │ ├── exception │ └── constant └── utils这个结构看着简单但每层职责必须清晰。Controller只做参数接收和返回实际逻辑写在Service层Mapper层只做数据库交互。Entity对应数据库表结构DTO用于接收前端请求参数和返回响应数据VO可以用于组装页面需要的聚合数据。区别在于Entity字段和表完全一致但DTO/VO可以根据业务灵活增减不能混用否则维护起来会非常痛苦。这里必须提一个坑SpringBoot2集成JSP的目录问题。老项目很多用JSP但SpringBoot默认打包方式是JarJSP对Jar包支持不友好页面会404。如果你非要在SpringBoot2里集成JSP页面必须放在src/main/webapp/WEB-INF/views下pom.xml里加入tomcat-embed-jasper依赖还要在application.yml配置prefix和suffix。但我个人的建议是既然已经用Vue3做前端了就踏踏实实走前后端分离JSP那套东西完全可以抛弃。别给自己增加不必要的复杂度。2.2 MyBatis-Plus关键配置分页、逻辑删除、xml映射MyBatis-Plus的依赖版本要选对。SpringBoot2.x对应的starter版本一般用3.5.x系列不要拿最新3.5.x去配SpringBoot3也没有问题但注意驱动切换。依赖配好以后核心配置类如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页插件必须加否则Page对象不会自动拼接LIMIT语句。实际开发中建议把分页参数统一封装到PageQuery类里避免每个接口都写Page参数代码会干净很多。逻辑删除配置也很简单。在实体类的deleted字段上标注TableLogic TableField(value deleted) private Integer deleted;然后在application.yml里指定全局配置mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这样以后所有删除操作都自动变成UPDATE查询自动追加deleted0条件有效防止误删数据。再讲一个高频问题mapper.xml与Mapper接口不在同一个文件夹下时怎么配置其实更准确的说法是尽量让它们放在同一个包路径下然后做两件事mybatis-plus: mapper-locations: classpath*:com/example/health/mapper/**/*.xml同时在pom.xml的build节点里加上resources配置让xml文件在打包时被包含进去build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build很多同学开发时跑得好好的一打包部署就报Invalid bound statement (not found)就是漏了这一步。2.3 登录鉴权与接口规范健康管理系统中体检数据涉及隐私权限控制必须做。这里我选了JWT 拦截器方案没有引入Spring Security是因为这种中小型系统没必要上重武器JWT加一个拦截器足够用。登录流程用户提交用户名密码UserService.login()校验账号密码密码存的是MD5加盐后的值更高要求可以用BCrypt按项目实际情况来。校验通过后用jjwt生成tokentoken里放userId和role设置有效期24小时。返回给前端前端每次请求在请求头Authorization里带上token。拦截器里校验逻辑public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BizException(401, 未登录或登录已过期); } try { Claims claims JwtUtil.parseToken(token); UserContext.set(claims.get(userId), claims.get(role)); return true; } catch (Exception e) { throw new BizException(401, token无效); } }UserContext用ThreadLocal实现请求结束后记得clear避免线程池复用导致数据串号。接口规范方面统一返回结构{ code: 200, message: 操作成功, data: {} }分页返回用PageResultT。全局异常处理用RestControllerAdvice把业务异常、参数校验异常、系统异常分别处理不要让任何异常赤裸裸地抛给前端。接口文档推荐集成Knife4j比原生Swagger UI好看交文档作业时也能加分不少。3. 前端Vue3 后台管理界面开发3.1 基于Vite创建Vue3工程前端环境建议先统一Node版本最好用Node 18及以上。用Vite创建工程只需要一条命令npm create vitelatest health-web -- --template vue如果你更习惯TypeScript模板换成vue-ts也可以。热词里总有“若依vue3 ts报错”这类问题恰恰说明TS版本对Node版本、类型检查配置非常敏感项目一把梭之前先把Node版本锁死别让环境差异耽误时间。前端目录结构我习惯这样分src/ ├── api ├── assets ├── components ├── router ├── store ├── views │ ├── login │ ├── dashboard │ ├── student │ ├── health │ ├── report │ └── system ├── utils └── App.vueElement Plus按需引入配合unplugin-auto-import和unplugin-vue-components插件就不需要在main.js里全量引入样式了打包体积能小不少。管理后台的登录页总想要点视觉亮点热词里“vue3 登录页面 点线动态的背景”就是这个需求。我的做法是用一个自绘Canvas组件画粒子点阵和连线几十行代码就能实现不额外引入大库。核心思路是生成若干个随机粒子用requestAnimationFrame做移动粒子之间的距离小于阈值时画一条线。这套逻辑在登录页放在背景层视觉档次立刻拉高。关于“Vue3里面用JSX”这个问题我的态度是常规模板语法已经能覆盖90%的管理后台场景。JSX适合组件库开发或逻辑非常复杂的场景业务页面里硬用JSX反而增加阅读负担。如果只是局部需要动态渲染用h函数也能解决。别为了炫技把代码搞复杂。3.2 axios封装、路由守卫与Pinia前后端联调第一步是解决跨域。开发环境最优雅的方式是配置Vite代理而不是让后端改CrossOrigin。在vite.config.ts里server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, } } }这样前端请求/api/user/login实际上会转发到后端的http://localhost:8080/api/user/login开发环境就完全没有跨域问题。生产环境用Nginx反代同样把/api转发给后端服务方案一致。axios封装一定要做拦截器。请求拦截器注入tokenservice.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config })响应拦截器统一处理错误码service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElementPlus.ElMessage.error(res.message || 请求失败) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { ElementPlus.ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )路由守卫控制页面访问权限router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })状态管理用Pinia这是Vue3官方推荐。主要存token和用户基础信息。注意一个细节Pinia仓储默认不持久化页面刷新就没了所以token要同步存一份到localStorage初始化Store时从本地读回来。也可以引入pinia-plugin-persistedstate但小项目手写两行代码就够不用过度依赖插件。3.3 图表统计与打印导出实现健康管理系统的核心价值在于统计报表。我用ECharts做了三个核心图表身高体重趋势曲线按年份展示某个学生的身高、体重变化视力情况对比按班级对比视力正常率和近视率BMI分布饼图按全校或年级展示偏瘦、正常、超重、肥胖的比例。后端接口直接返回聚合统计数据前端拿到option配置塞进ECharts即可。这里有个经验统计接口尽量给后端做别在前端拿全量数据自己算。因为前端算一版、后端算一版口径很容易不一致最终要让数据库SQL的输出为准。体检报告单的打印功能用print-js或vue3-print-nb都能实现。重点不是库本身而是打印样式。我给报告单写了一套media printCSS专门做A4纸适配media print { body * { visibility: hidden; } .print-area, .print-area * { visibility: visible; } .print-area { position: absolute; left: 0; top: 0; width: 100%; } }分页时用page-break-after: always控制每张报告单独立一页。打印前还要把表格边框、字体大小这些细节调好否则打出来很容易变形。这些细节文档上不一定写但用户一定会关注。3.4 前后端联调与接口约定联调阶段最怕接口地址、字段名对不上。我的做法是后端先跑起来Knife4j接口文档生成好前端在src/api目录下每个模块建一个JS文件统一管理接口// src/api/healthRecord.js import request from /utils/request export function getHealthRecordPage(data) { return request({ url: /api/health-record/page, method: post, data }) } export function addHealthRecord(data) { return request({ url: /api/health-record/add, method: post, data }) }这样每个前端页面对应一组接口方法改接口只需要动一个文件。联调时如果返回字段对不上优先让后端统一修改返回结构而不是前端到处适配。因为前端适配一次就要做一次数据清洗代码会越来越乱。维护一份简单的接口变更记录表也很有用接口名、参数、返回示例、变更时间。两个人在同一套系统上协作这份记录能省掉不少沟通成本。4. 环境部署与排坑记录4.1 MySQL8.0安装细节MySQL8.0的安装每个平台各有难点Windows下载MySQL Installer选择Custom模式把MySQL Server 8.0和Workbench勾选上。安装完成后给root设置一个复杂密码注意Windows服务名尽量不包含空格。Docker一条命令搞定。docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD你的密码 \ -e MYSQL_DATABASEhealth_db \ -v /opt/mysql8/data:/var/lib/mysql \ mysql:8.0开发环境用Docker跑MySQL最省事不用装一堆本地服务启动和销毁都干净。LinuxUbuntuapt install mysql-server-8.0装完后执行mysql_secure_installation做安全初始化然后创建应用数据库和账号。用旧版Navicat连接MySQL8.0时常见报错Authentication plugin caching_sha2_password cannot be loaded这是认证插件导致的两种解决办法一是升级Navicat到16以上版本二是把root或者应用账号的认证插件改成mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 新密码; FLUSH PRIVILEGES;但说实话为了数据库更安全生产环境建议直接用8.0默认插件把工具版本升上去。开发环境图省事改一下认证方式也可以看自己取舍。后端jdbcUrl一定要配上这几个参数jdbc:mysql://localhost:3306/health_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueserverTimezone不配会差8小时allowPublicKeyRetrieval不配在8.0下容易报连接失败useSSLfalse避免SSL握手慢。这三个参数都是实战挖过的坑属于必配项。4.2 联调与部署中的高频问题我把这一个多月开发调试过程中遇到的问题汇总成速查表报错现象原因解决办法启动报 ClassNotFoundException: com.mysql.cj.jdbc.DriverMySQL驱动版本不对确认mysql-connector-java8.x依赖存在驱动类名必须用com.mysql.cj.jdbc.Driver页面登录后马上跳到login页请求头没带token或token过期检查axios请求拦截器确认拦截器放行登录接口和静态资源MyBatis-Plus分页不生效没有配置分页插件加PaginationInnerInterceptorBeanSQL执行时报关键字错误如desc、order字段名和MySQL关键字冲突用TableField(\desc) 注解指定反引号别名打包后XML映射找不到resources配置漏了在pom.xml的resources里包含**/*.xmlVue3在Edge浏览器里最小化按钮异常通常是浏览器扩展或全局样式覆盖先无痕模式验证再排查全局position: fixed元素临时去掉或加z-index分层另外一个经常在社区里看到的问题“Vue3怎么连接后端”。本质上就是三步axios封装、代理配置、接口调用不是多神秘的事情。如果连不上先F12看Network请求到底发出去没有再分步排查是跨域、路径还是参数问题别一上来就怀疑框架。关于热词里的“若依vue3 ts报错”虽然本项目不基于若依开发但可以给所有用Vue3TS框架的人一个忠告碰到海量报错时优先检查Node版本是否满足要求再检查tsconfig.json里的strict配置最后看依赖锁文件是否一致。很多TS报错不是代码问题是环境不一致导致的。4.3 部署上线实操上线部署这块我走的是“前后端分离但简化部署”的路线。后端打jar包mvn clean package -DskipTests前端构建npm run build两种部署方式我都试过简单方式把dist目录里的文件拷贝到后端项目的src/main/resources/static下重新打包。这样只启动一个SpringBoot进程就同时提供接口和页面适合内网小规模使用。需要注意接口路径必须是/api开头避免和前端路由冲突。规范方式后端jar包跑在8080端口前端dist目录放到Nginx的html目录Nginx配置/api反向代理到后端服务其他路径都走静态资源。这也是生产环境最常见的方案后面扩展或者拆分都方便。我在一台CentOS服务器上实际操作过Nginx核心配置长这样server { listen 80; server_name your-domain-or-ip; location / { root /usr/share/nginx/html; index index.html; 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; } }启动后端用nohupnohup java -jar health-system.jar --spring.profiles.activeprod app.log 21 生产环境的数据库账号不要用root单独建一个应用账号只授予项目库的增删改查权限。HikariCP连接池参数也稍微调一下maximum-pool-size设10左右就够了别默认值跑到底。部署完第一件事就是检查日志看能不能正常连库、表是否自动创建。如果用了定时体检提醒之类的功能记得在Linux上配置crontab或者在代码里集成XXL-Job这个按需扩展。写到最后我再分享一点个人体会。师生健康信息系统这类项目表面看是CRUD真正花时间的其实在统计口径、打印格式和权限边界。开发前先把这三个问题问清楚后面能少改一半的代码。做完之后把接口文档和数据库设计文档同步整理好比多写两百行代码更有价值——这也是标题里“含文档”三个字的分量所在。如果你手头也正要搭类似的管理系统不妨按这个思路推进先设计好表和接口再跑通后端再切前端页面最后再打磨统计报表和打印效果整个过程会顺很多。