简介:基于JAVA+Vue+SpringBoot+MySQL实现的康复中心管理系统,是面向毕业设计场景的完整项目源码。系统覆盖访客管理、房间管理、入住退房、缴费管理、药品档案、康复训练与网上咨询等核心业务模块,同时内置用户、部门、角色、菜单、日志等通用后台功能,并通过基于角色的访问控制实现按钮级权限分配,适合需要精准权限设计的医院管理类项目参考。资源包共364个文件,包含186个Java后端代码、87个Vue前端页面、42个JS脚本,以及SQL数据库脚本、YAML配置和说明文档等,压缩包约1.2MB,结构清晰便于导入开发工具后直接学习。目前已有185人学习下载,适合有一定Java与Vue基础的本科毕业生作为毕设参考。除了完整源码与该项目的数据库初始化文件,还可从项目讨论帖获取更多设计思路,便于快速理解模块划分、权限流程与前后端交互方式,节省从零搭建系统的时间。
1. 康复中心管理系统:Java 毕业设计里的全栈闭环样板
康复中心的护理主管每天要处理几十条访客登记、房间排期和缴费记录,手里的工具是 Excel 加纸质登记表,训练计划和药品消耗常常对不上账。拆过不少 java 毕业设计项目,这套基于 Vue、SpringBoot、MySQL 的康复中心管理系统是完整度较高的一类:访客管理、房间管理、入住退房、缴费管理、药品档案、康复训练、网上咨询全部串成闭环,权限精确到按钮级别,适合正在做毕设、想演示权限控制的同学,也适合想给中小机构快速搭管理后台的开发者。
项目的真正亮点是流程闭环与工程化的 RBAC 权限模型,资源包还自带代码生成模板,新模块不用从零手写 CRUD。接下来按技术栈结构、核心业务、权限实现、运行避坑、二次开发的顺序拆解,每步落到代码和参数。
2. 技术栈与项目结构:Vue + SpringBoot + MySQL 的前后端分离骨架
2.1 技术选型:为什么毕业设计最怕「能跑但讲不清」
选技术栈这件事,毕业设计和企业项目是两套逻辑。企业项目看维护成本和团队熟悉度,毕业设计看的是答辩时能不能把每个组件存在的理由讲清楚。SpringBoot 在这套组合里承担 REST API 层,内置 Tomcat,免去繁琐的 XML 配置,一个 main 方法就能起服务;Vue 负责页面和路由,配合 Element UI 这类组件库,后台管理界面半天就能搭出雏形;MySQL 则是最稳妥的数据库选择,安装简单、文档多、遇到问题一搜就有答案。
这套组合在答辩时比较加分,因为评委大概率会问「为什么这么选」,答案很明确:SpringBoot 让后端开发聚焦业务而不是环境配置,Vue 的双向绑定和组件化让前端代码复用率高,MySQL 的事务和索引能满足康复中心这种中小体量的并发。反过来,如果你用 SSH(Struts + Spring + Hibernate)这种老组合,虽然也能跑,但技术栈停留在五年前是减分项。SpringBoot 的自动配置、Vue 的响应式数据流、MyBatis Plus 的 CRUD 封装,这三个点讲清楚,技术分基本就稳了。
数据访问层选择 MyBatis Plus 是另一个值得说的点。它把单表 CRUD 直接封装成 BaseMapper 的方法,写康复中心这种以单表操作为主的业务,几乎不用手写 XML 映射。我在实际项目里见过太多「为写 SQL 而写 SQL」的代码,一个简单的 findById 都要写三行注解,其实用 MyBatis Plus 的 selectById 一行就够,只有多表关联和复杂统计才需要自定义 SQL。这套项目把「简单操作走封装、复杂查询走自定义」的分寸把握得比较合适。
2.2 目录结构:从 btl 模板到 controller 的资源包怎么组织
资源包的奇特之处在于,核心文件不是一堆写死的业务代码,而是一套 .btl 结尾的代码生成模板。btl 是 Beetl 模板引擎的扩展名,这套模板接收数据库表结构作为输入,输出标准的 Controller、Service、实体类和 Vue 页面。我用表格整理一下资源包里这些模板文件的职责。
| 模板文件 | 生成目标 | 说明 |
|---|---|---|
| tree.btl | 树形页面 | 用于部门、菜单这类层级数据 |
| table.btl | 列表页组件 | 生成表格列、搜索栏、分页 |
| tableIndex.btl | 列表页入口 | 页面路由与数据加载入口 |
| add.btl / edit.btl | 新增/编辑弹窗 | 表单校验与提交逻辑 |
| entity.btl | Java 实体类 | 映射数据库表字段 |
| mpController.btl | Controller 层 | 提供 REST 接口 |
| mplus.btl | Mapper 层 | 基于 MyBatis Plus 的数据访问 |
| mpServiceImpl.btl | Service 实现 | 业务逻辑与事务 |
| api.btl | 前端 API 封装 | axios 请求统一出口 |
第一次看到这套结构时有点意外,因为大部分 Java 毕业设计是「代码加 SQL 脚本」的形态,而这里是「模板加生成器」的形态。这意味着你不只是拿到一个能跑的系统,还拿到了一条增量开发的流水线。实际跑起来的项目代码是模板的产物,后端目录是标准的 Spring Boot 分层:controller 接收请求、service 处理业务、mapper 操作数据库、entity 对应表结构,前端则分 views、api、router 三层。生成器和模板配合,新增一张表就能生成一组可用代码,这对后面扩展模块的意义我在第六章会展开。
2.3 数据库设计:核心表与关联关系
再往下看数据库。康复中心核心业务离不开这几张表:访客表、房间表、入住信息表、缴费记录表、药品档案表、康复训练计划表、咨询消息表,再加上权限体系的用户表、角色表、菜单表、部门表。访客和房间是多对多的关系,因为一个访客在不同时间可能入住不同房间,所以中间用入住信息表关联,同时冗余了入住时间、退房时间、状态这些字段。
以缴费记录表为例,它的设计直接反映了业务闭环:
CREATE TABLE payment_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, visitor_id BIGINT NOT NULL COMMENT '访客ID', stay_id BIGINT NOT NULL COMMENT '入住单ID', item_name VARCHAR(64) NOT NULL COMMENT '收费项目:床位费/训练费/药品费', amount DECIMAL(10,2) NOT NULL COMMENT '金额', pay_method TINYINT NOT NULL DEFAULT 1 COMMENT '1现金 2微信 3支付宝', status TINYINT NOT NULL DEFAULT 0 COMMENT '0未结清 1已结清', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_visitor (visitor_id), INDEX idx_stay (stay_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个容易被忽略的设计点。第一,item_name 不是外键而是一个冗余字段,因为缴费记录一旦生成就不应该随着收费项目表的改名而联动变化,这是典型的「快照」思路,对账时以记录里的 item_name 为准。第二,status 字段默认 0,配合缴费模块的结清操作做更新,而不是直接删记录,财务流水可追溯。索引方面,visitor_id 和 stay_id 是高频查询条件,必须加索引,否则联表查缴费历史时就是全表扫描。
房间表的建模也值得一提。room 表除了房间号、楼层、床位数量这些基础字段,还维护了一个 status 状态位,取值包括空闲、已入住、维修中、已预定。这个状态位的变更必须走统一的 service 方法,不能由各个模块各自 update,否则会出现「缴费模块把房间置为空闲,但入住模块还认为有人住」的经典脏数据。状态统一收口,是我看这套库表设计时觉得比较成熟的地方。
3. 核心模块拆解:从访客登记到康复训练的完整闭环
3.1 访客管理与房间管理:状态流转的实现
访客管理是系统的入口模块,主要做三件事:登记访客基础信息(姓名、身份证、联系方式、病史摘要)、维护访客状态(在院、离院、预约待入住)、查看访客历史记录。这个模块的 Controller 是典型的 Restful 风格,核心代码我拆一段:
@RestController @RequestMapping("/api/visitor") public class VisitorController { @Autowired private VisitorService visitorService; @PostMapping public Result<?> add(@RequestBody Visitor visitor) { // 登记时校验身份证唯一,防止同一访客重复建档 if (visitorService.checkIdCardExists(visitor.getIdCard())) { return Result.fail("该身份证已存在档案"); } visitor.setStatus(VisitorStatus.REGISTERED); visitorService.save(visitor); return Result.ok(); } @PutMapping("/{id}/check-in") public Result<?> checkIn(@PathVariable Long id, @RequestBody StayInfo stay) { // 入住前检查房间是否空闲 if (!roomService.isRoomFree(stay.getRoomId(), stay.getStartTime())) { return Result.fail("该房间在当前时间已被占用"); } return visitorService.checkIn(id, stay); } }checkIn 这个接口是状态流转的关键。它不直接改 visitor 表,而是做三件事:校验访客存在且状态为 REGISTERED、校验房间在入住时间段内空闲、在入住信息表插入一条记录并把访客状态改为 IN_HOSPITAL。这三步必须在一个事务里完成,否则会出现「房间分配成功了但访客状态没更新」这种脏数据。注意 checkIn 接口里传入的 StayInfo 对象包含了入住时间和预计退房时间,房间的空闲判断不是只看当前时刻,而是看整个时间段内有没有重叠的入住记录,这是房间排期最常见的业务细节。
3.2 入住退房与缴费:事务处理的边界
入住退房模块是最容易写出事务 Bug 的地方。退房时系统要同时做:计算本次入住费用、生成缴费记录、释放房间、更新访客状态。任何一个步骤失败,整个流程都要回滚。我用 @Transactional 把退房逻辑包起来:
@Transactional(rollbackFor = Exception.class) public void checkout(Long stayId) { StayInfo stay = stayInfoMapper.selectById(stayId); // 1. 计算费用:床位费按天 + 训练费按次数 + 药品费按实际消耗 BigDecimal amount = chargeService.calculate(stay); // 2. 生成缴费记录(状态为未结清) paymentService.createPayment(stay, amount); // 3. 释放房间 roomService.release(stay.getRoomId()); // 4. 更新访客状态为已离院 visitorService.updateStatus(stay.getVisitorId(), VisitorStatus.LEFT); // 5. 更新入住单状态 stay.setStatus(StayStatus.FINISHED); stayInfoMapper.updateById(stay); }注意 rollbackFor = Exception.class,这是血泪经验。Spring 的 @Transactional 默认只在 RuntimeException 上回滚,如果某一步抛出的是检查性异常,事务不会回滚,你会得到一份「半边成功」的数据。检查性异常在这个场景里很常见,比如金额计算时发现药品价格未配置,抛出的如果是自定义 BizException 且继承自 Exception,不加 rollbackFor 就会翻车。缴费模块本身独立,支持现金、微信、支付宝三种方式,记录流水后可以打印收据,收据号和 payment_record 表主键一一对应,方便后期对账。退房后如果发现费用算错,不要直接改原记录,而是生成一条负数冲正记录,再补一条正确的,这才是财务系统的正路。
3.3 药品档案与康复训练:状态和流水要分开建模
药品档案模块相对独立,维护的是药品基础信息:药品名称、规格、库存量、有效期、供应商。这里的难点是库存扣减。康复训练模块则是把「训练计划」和「执行记录」分开建模:训练计划表存康复师为访客制定的目标和阶段,执行记录表存每次实际完成的训练内容、时长、消耗热量。
这两个模块放在一起的原因是它们都涉及「状态」和「流水」的配合。药品库存是一个状态,每次发药是一条流水;访客的康复进度是一个状态,每次训练记录是一条流水。如果在流水表里直接改库存字段而不是新增记录,后期统计「这个月消耗了多少药品」就无从查起。我一般会把状态表和流水表分离,状态表只存当前值,流水表只追加不修改,查询统计走流水表,页面展示走状态表,各司其职。康复训练计划表的字段大概会长这样:
CREATE TABLE training_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, visitor_id BIGINT NOT NULL, goal VARCHAR(255) COMMENT '训练目标,如恢复下肢肌力', phase TINYINT COMMENT '阶段:1评估 2强化 3巩固 4出院前', trainer_id BIGINT COMMENT '负责康复师', start_date DATE, end_date DATE, status TINYINT COMMENT '0进行中 1已完成 2中止' );每次训练完成后,执行记录表插入一条数据,同时更新计划表的 status 和当前进度字段。这个设计在答辩时可以作为一个讲点:为什么执行记录不做 update?因为训练数据是患者病历的一部分,需要留痕,改字段等于篡改记录,追加流水才是合规做法。
3.4 网上咨询:不用消息队列的异步实现
网上咨询模块模拟的是访客或家属在线提问、康复师回复的场景。这个模块在毕业设计里不需要上 RabbitMQ 或 Kafka,用一张咨询消息表加状态位就足够。
public class ConsultMessage { private Long id; private Long visitorId; private Long responderId; // 康复师/医生ID private String content; private Byte direction; // 1访客发 2回复 private Byte readStatus; // 0未读 1已读 private LocalDateTime createTime; }前端通过定时轮询或 WebSocket 推送新消息。如果答辩时想突出亮点,我建议用 WebSocket,因为轮询的代码面试官看一眼就知道是初级水平,WebSocket 虽然代码量多一点,但能讲清楚「长连接」「服务端主动推送」这两个概念,属于性价比很高的加分项。回复消息时把 readStatus 置为 0,访客端打开会话时批量置 1,未读角标的实现就完成了。如果不想引入 WebSocket 依赖,定时轮询也能用,但轮询间隔不要太短,10 秒一次足够,否则数据库压力全在冗余查询上。
4. 权限系统的实现:按钮级 RBAC 的落地细节
4.1 RBAC 模型在 SpringBoot 中的映射
权限模块是这个系统最有工程价值的模块,也是答辩时最容易被深挖的部分。五张表:用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。用户表不含任何权限字段,角色表不含任何菜单字段,全部通过关联表连接,这是标准的 RBAC 模型。菜单表里不仅放目录和页面,还把按钮也作为一条菜单记录存放,通过 type 字段区分「目录 / 菜单 / 按钮」三种类型。比如「访客管理」是一个菜单记录,「新增访客」「删除访客」各自是挂在它下面的按钮记录,权限标识分别是 visitor:add、visitor:delete。
后端拦截通过 Spring AOP 加自定义注解实现。定义一个 @RequiresPermission 注解,在需要权限的接口上标注权限标识,例如 @RequiresPermission("visitor:add"),然后在一个切面里统一校验当前用户是否拥有该权限标识:
@Aspect @Component public class PermissionAspect { @Before("@annotation(requiresPermission)") public void check(JoinPoint point, RequiresPermission requiresPermission) { // 从 SecurityContext / ThreadLocal 取当前登录用户的权限集合 Set<String> permissions = getCurrentUserPermissions(); String required = requiresPermission.value(); // 权限标识不存在或未授权则抛出异常 if (!permissions.contains(required)) { throw new AccessDeniedException("无操作权限"); } } }这里面有一个值得展开的细节:权限标识的设计。visitor:add、visitor:edit、visitor:delete 这种「模块:操作」格式,比存一个「是否是管理员」的布尔字段靠谱得多。原因是角色是动态的,今天定义一个「接待专员」角色可以新增访客但不能删访客,用布尔字段就得改代码,用权限标识只需要在角色菜单关联表里勾选几个菜单节点。登录流程上,用户登录成功后返回一个 token,后续每次请求携带 token,后端解析出用户 ID 再查权限集合放入 ThreadLocal,切面从这里取权限比对。资源包里自带菜单管理和角色管理界面,新角色从创建到分配权限全程可视化,不需要写一行 SQL。
4.2 前端路由与按钮权限的联动
前端权限控制通常被忽略,因为很多人觉得「后端挡住了就行」。但实际上,如果前端不做权限控制,用户虽然调不了接口,但能看到他无权操作的按钮,体验很差。这个系统的做法是:登录后后端返回该用户的菜单树和按钮权限标识数组,前端用 Vue 的动态路由把菜单映射为路由,按钮则用自定义指令 v-permission 控制显隐。
// 路由守卫:未登录跳转登录页 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (!token && to.path !== '/login') { next('/login'); } else { next(); } }); // 按钮权限指令:无权限时移除 DOM 节点 Vue.directive('permission', { inserted(el, binding) { const required = binding.value; // 例如 'visitor:delete' const owned = store.state.permissions; if (!owned.includes(required)) { el.parentNode && el.parentNode.removeChild(el); } } });使用时直接<el-button v-permission="'visitor:delete'">删除</el-button>,没有权限时按钮不会渲染。有一个血泪经验是:指令的 inserted 钩子在组件挂载时执行一次,如果权限集合是在按钮渲染之后才异步加载的,按钮会被误删。解决方法是确保路由守卫里先拉取权限再放行页面,或者指令内部改成观察权限集合变化再决定是否移除,二选一,别两个都不做。
提示:前端权限只是体验优化,真正的安全边界必须由后端接口的切面强制校验,前端控制按钮显隐挡不住直接调接口的人。
4.3 菜单管理、数据字典与日志的配套
权限体系不能只有授权,还要有配套。菜单管理负责维护整个后台的导航结构,支持树形拖拽排序;数据字典把「性别」「支付方式」这类枚举值抽出来,页面上通过字典类型编码读取,改动字典不用重新部署前端;日志管理用 AOP 记录每次关键操作的操作人、IP、请求参数和结果,答辩时可以说这是「审计功能」,康复中心属于医疗机构,操作留痕是合规要求。
这三块拼起来,权限系统才完整。菜单管理给用户入口,数据字典给页面数据规范,日志管理给安全审计兜底。我在拆项目时发现,很多毕业设计的权限模块只有「登录加角色表」,没有按钮级控制,答辩时被问到「如何控制某个用户不能删除数据」就卡住了。这个系统的做法值得抄:把权限标识当作配置数据而不是硬编码,新增权限不需要改 Java 代码,在菜单表插一条记录再给角色勾选即可。数据字典的典型实现是 dict_type 和 dict_data 两张表,前者定义字典编码,后者存具体键值对,前端通过编码拉取下拉选项,业务表里只存字典值不存显示文本,这样改显示名只动字典,不动业务表。
5. 运行与避坑:本地启动的完整流程和问题排查
5.1 环境准备与启动:四条命令跑起来
本地跑这个项目,前置环境是三件套:JDK 1.8 或 11、Node.js 14 以上、MySQL 5.7 或 8.0。启动分四条线:
# 1. 准备数据库:建立康复中心库并导入初始化 SQL mysql -uroot -p < init.sql # 2. 改后端配置:application.yml 中的数据源和端口 # url: jdbc:mysql://localhost:3306/rehab_center?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai # 3. 启动后端服务 mvn spring-boot:run # 4. 启动前端(cd 到 frontend 目录) npm install npm run dev后端默认跑在 8080,前端 dev 模式默认跑在 3000 或 8081,前端通过 vite/webpack 的 proxy 把 /api 开头的请求转发到后端 8080,这样避免了跨域问题。如果直接用 axios 调后端地址而没走代理,浏览器控制台会报跨域错误,这是新手最容易遇到的第一道坎。
注意:前端和后端不要都占用 8080,建议前端 dev 用 3000,后端保持 8080,代理转发到 8080。
5.2 常见坑:五条具体的踩坑记录
这一节把常见的问题按「现象 → 原因 → 解决」整理出来,每一条都是实际跑项目时真实会遇到的情况。
坑一:启动报 Access denied for user 'root'@'localhost'现象:后端一启动就连不上 MySQL,控制台抛 Access denied。原因:初始化 SQL 用了 root 账号,但本地 root 密码不是配置文件里写的那个。解决:改 application.yml 里的 password,或者新建一个专用账号并授权:CREATE USER 'rehab'@'localhost' IDENTIFIED BY '123456'; GRANT ALL PRIVILEGES ON rehab_center.* TO 'rehab'@'localhost';
坑二:页面能打开但列表接口 404现象:前端登录后所有列表页都拿不到数据,Network 面板里 /api 请求返回 404。原因:前端代理只配了 dev 环境,而后端接口前缀是 /api,代理路径没对上。解决:把 vite.config.js 里的 proxy 目标路径改成和后端 context-path 一致,例如proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } },改完重启前端。
坑三:代码生成器生成的代码编译不过现象:用模板新生成一个模块后,mvn compile 报一堆「找不到符号」。原因:生成器依赖数据库表注释里的某些约定,比如字段注释里的特殊字符被转成了奇怪的方法名,或者生成时依赖的父类不在当前项目里。解决:生成前检查表名和字段名的命名规范,统一用下划线命名,避免使用 desc、order 这类关键字作为字段名,生成后先看 entity 再动其他文件。
坑四:MySQL 8.0 的时区报错现象:插入时间字段时快 8 小时,或直接报 The server time zone value 'CST' is unrecognized。原因:MySQL 8 的驱动要求显式指定时区,默认的 CST 无法被驱动识别。解决:连接串加 serverTimezone=Asia/Shanghai,MySQL 5.7 没有这个问题,这是 8.0 特有的坑。
坑五:登录后菜单空白现象:账号能登录但左侧菜单一个都没有。原因:该账号的角色没有关联任何菜单,或菜单表的 type 字段把目录和按钮混在一起,前端过滤时把目录也过滤掉了。解决:先到角色管理里给该角色勾选菜单,再检查菜单表的 type 字段,目录、菜单、按钮三种类型要区分开,前端按 type 分别渲染。
5.3 验证权限是否生效:一条 SQL 加一个请求
跑通之后,验证权限控制是否真的生效只需要两步。第一步,在数据库里新建一个测试角色,只分配「访客查询」权限,不分配「访客删除」;第二步,用该角色的账号登录前端,你会看到删除按钮不渲染,直接手动调 DELETE 接口会收到 403 拒绝响应。这比看代码更能证明 RBAC 是闭环的。如果 403 没生效,优先检查切面是否被 Spring 容器扫描到,AOP 切面类如果没加 @Component 或者没有在启动类扫描范围内,注解就会静默失效,这是权限类项目最常见的翻车点。
6. 二次开发改造:怎么用代码生成器长出新的业务模块
拿到这套资源后,最有价值的不是原样运行,而是利用代码生成模板快速长出新业务模块。假设新增「设备报修」模块:先把设备表和报修记录表建好,按模板约定生成 entity、mapper、service、controller 和 Vue 页面,然后改三处。
第一处是 Controller,默认只有基础 CRUD,业务校验要自己补,比如报修单创建时校验设备状态是否为使用中;第二处是 Service,在 save 里追加审计日志,把操作人、时间写进日志表,康复中心的设备维修涉及医疗安全,留痕不能省;第三处是前端列表页,生成的是通用表格,把设备名那列换成下拉搜索框,状态列换成 tag 标签展示。
// 改造示例:报修单创建时追加状态校验和审计日志 @Transactional(rollbackFor = Exception.class) public void createRepairOrder(RepairOrder order) { Device device = deviceMapper.selectById(order.getDeviceId()); if (!DeviceStatus.IN_USE.equals(device.getStatus())) { throw new BizException("该设备不在使用中,无法报修"); } order.setStatus(RepairStatus.PENDING); repairOrderMapper.insert(order); // 审计日志:记录操作人和操作内容 logService.record("create_repair", "创建报修单: " + order.getId(), SecurityUtils.getUserId()); }验证走完整链路:登录、创建报修单、分配维修工、维修完成、关闭工单,每步确认权限标识已加到对应接口上,测试角色能被正确拦截。模块之间不要复用外键去直接查数据,而是通过 service 接口互相调用,报修模块要显示设备位置就调 deviceService.getById,这样模块松耦合,后面替换设备模块时不用动报修模块。从那以后,我每次给这类管理系统加模块都强制走一遍「建表 → 改模板 → 生成 → 补业务校验 → 加权限标识 → 日志留痕」的流程,任何一步跳过,后期维护都会加倍偿还。希望这些拆解对你跑通和改造这套康复中心管理系统有帮助。
本文还有配套的精品资源,点击获取