拓冰建站拓冰建站
首页 / 资讯中心 / 正文

康复中心管理系统实战:SpringBoot+Vue+MySQL从数据建模到部署

简介基于JAVAVueSpringBootMySQL的康复中心管理系统毕业设计资源面向计算机相关专业毕业生及需要精细化权限管理的开发人员覆盖访客、房间、入住退房、缴费、药品档案、康复训练、网上咨询等核心业务同时内置用户、部门、角色、菜单、日志、数据字典、文件管理及图表展示等基础模块实现基于角色的按钮级访问控制适合有严格权限约束需求的医院或护理机构场景。资源包采用zip格式共364个文件覆盖186个Java源码、87个Vue页面组件、42个JavaScript脚本及SQL初始化脚本、YAML配置等整体仅1.2MB目录划分清晰便于直接导入开发工具运行。目前已有185人学习下载适合需要快速搭建同类型系统或参考完整前后端交互流程的读者。除完整的前后端代码外资料中还包含大量BTL模板文件如实体、控制器、接口等生成模板可辅助理解代码生成器思路对二次开发或改造具有较强的参考价值。1. 康复中心管理系统为什么说它不是普通的 CRUD 项目很多同学把康复中心管理系统当成一个“患者增删改查 预约表”来做最后答辩被问倒或者上线跑两周就出乱子。实际上康复中心的核心业务是“评估—计划—执行—再评估”的闭环一个患者从入院时的功能评估到治疗师制定康复计划再到每次训练的记录和阶段复评中间涉及大量状态流转和跨角色协作。用 VueSpringBootMySQL 做这个选题真正的难点不在技术栈本身而在你怎么把康复业务流程抽象成数据表关系和状态机并在前后端之间通信时把这些状态守住。这个系统适合两类人一是要做毕业设计、需要一个“业务逻辑完整但规模可控”的 Java Web 项目的同学二是刚入职小公司、被安排做内部管理系统的初级工程师。它能让你在一个项目里同时练到 MySQL 表设计、SpringBoot 分层架构、Vue 组件通信、权限控制以及最容易被忽视的——异常场景处理。本文按我实际做这类项目的顺序来写先理清业务模型和表结构再做后端接口和前端页面最后讲部署验证和常见坑。你不用照抄代码但你可以照着这个思路去搭自己的版本。2. 系统设计与数据建模把康复流程转成表关系关键在“状态”2.1 患者、康复计划、评估记录三个核心表的关系怎么定康复中心的管理对象是“患者”但围绕患者的不是一张表能解决的。你需要至少三类核心数据患者基本信息、康复计划、评估记录。康复计划是一次性的还是分阶段的评估记录是每次训练后都写还是只在阶段节点写这决定了表怎么设计。我采用的方案是患者表存静态信息姓名、性别、年龄、诊断、入院时间、主治医师康复计划表存动态信息患者ID、计划名称、当前阶段、开始日期、结束日期、状态评估记录表存每次评估的得分和结论患者ID、计划ID、评估时间、评估项、得分、评估师。注意评估记录必须带计划ID否则你没法回答“这个计划执行到一半效果如何”这类答辩必问题。用 MySQL 实现时外键逻辑关系建议用逻辑外键而非物理外键。原因有两个一是物理外键在后来迁移数据、批量导入时容易报错二是毕业设计答辩时面试老师更关注你是否理解关联关系而不是数据库自身的约束。你可以在 Service 层通过多次查询或 JOIN 来保证一致性。-- 患者表 CREATE TABLE patient ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0男 1女, age INT, diagnosis VARCHAR(255), doctor VARCHAR(50), status TINYINT NOT NULL DEFAULT 1 COMMENT 1在院 0出院, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 康复计划表 CREATE TABLE rehab_plan ( id BIGINT AUTO_INCREMENT PRIMARY KEY, patient_id BIGINT NOT NULL, plan_name VARCHAR(100) NOT NULL, current_phase VARCHAR(20) DEFAULT INITIAL, start_date DATE, end_date DATE, status TINYINT DEFAULT 0 COMMENT 0草稿 1进行中 2已完成 3已终止, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表逻辑里status 字段是核心。患者表的 status 控制患者是否在院康复计划的 status 控制计划生命周期。很多初学会把“删除”做成物理删除但在这个场景里患者出院不代表历史记录消失所以用状态位代替 DELETE 是更稳妥的做法。后面做统计报表时这些历史状态就是数据资产。2.2 状态流转的设计草稿、进行中、已完成、已终止康复计划不是建了就完事。一个计划可能被治疗师暂停、调整、终止也可能按阶段自动推进。这里我用状态机来管理草稿(0) → 进行中(1) → 已完成(2)任一步骤可跳转到已终止(3)。终止是“人为中断”已完成是“按计划走完”在业务上必须区分。在后端 Service 层你不需要引入复杂的状态机框架用一个枚举类加一个更新方法就能守规矩。很多翻车现场是这样的前端直接调 UPDATE 接口改 status 字段导致进行中的计划被误改成已完成。守住状态流转的入口比在数据库写 CHECK 约束更实际。public enum PlanStatus { DRAFT(0), ACTIVE(1), FINISHED(2), TERMINATED(3); private final int value; PlanStatus(int value) { this.value value; } public boolean canTransitTo(PlanStatus target) { if (this DRAFT) return target ACTIVE; if (this ACTIVE) return target FINISHED || target TERMINATED; return false; } }调接口时Service 里先判断当前状态能否跳转不能跳转就直接抛业务异常。这里的参数说明不允许从草稿直接跳转到已完成也不允许已完成再跳回进行中。实际开发中我发现很多同学忽略了枚举的 canTransitTo 方法直接在 Controller 里写 if else代码一多就乱。放到枚举里后续加状态、改规则只动一处。2.3 用预留字段和逻辑删除给答辩加分这个系统很容易被答辩老师评价为“功能太简单”。一个有效的应对方法是在设计表时预留扩展字段并且在文档里说明这些字段未来的用途。比如患者表加一个 rehab_goal康复目标字段计划表加一个 progress_note阶段小结。这些字段在初版系统里不一定用得上但它们证明了你有“面向未来设计”的意识。逻辑删除我用 delete_flag 而非直接删行。在 SpringBoot 的查询里所有 SELECT 都加WHERE delete_flag 0或者用 MyBatis-Plus 的TableLogic注解统一处理。注意如果你用 MyBatis-Plus逻辑删除字段的全局配置要在 application.yml 里声明mybatis-plus: global-config: db-config: logic-delete-field: deleteFlag logic-delete-value: 1 logic-not-delete-value: 0这里的参数说明logic-delete-field 指定实体类里的字段名value 是已删除标记值。用框架统一控制的好处是你不需要在每个 Mapper XML 里手写条件也不会漏改某个查询导致脏数据。但要注意联表查询时逻辑删除字段的过滤条件并不会自动带上去这是 MyBatis-Plus 的一个隐藏坑——我在避坑章节会展开讲。3. 后端 SpringBoot 实现把接口拆成“患者管理”和“计划流转”两拨3.1 项目分层Controller、Service、Mapper 之间不要绕圈子SpringBoot 项目的分层是常规动作但很多人把 Service 写得像 Controller 的二次封装。正确的做法是Controller 只做参数接收和简单校验业务规则放 Service数据库操作走 Mapper。这样做的直接好处是事务注解 Transactional 可以作用在 Service 方法上而不会因为 Controller 被反复调用而失控。针对康复中心这个业务我把接口按角色拆成三类面向治疗师的“患者管理”接口录入患者、查看患者列表、面向计划执行的“计划管理”接口创建计划、修改阶段、提交评估、面向管理员的“统计报表”接口在院人数、计划完成率。接口拆分得粗一点前端调用时不容易乱。RestController RequestMapping(/api/patient) public class PatientController { Autowired private PatientService patientService; PostMapping public Result createPatient(RequestBody Valid PatientDTO dto) { patientService.createPatient(dto); return Result.success(); } GetMapping(/list) public Result listPatients(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size) { return Result.success(patientService.pagePatients(page, size)); } }这段代码里的 Valid 注解很多人会漏掉。DTO 里的字段校验比如姓名不能为空、年龄范围必须放在 DTO 层而不是 Service 层手写判断。用 spring-boot-starter-validation 提供的 NotBlank、Min、Max 就能覆盖大多数场景。参数说明page 和 size 用 defaultValue 兜底避免前端没传参时直接报 400。3.2 用 MyBatis-Plus 写一个带条件的分页查询从 Page 到条件构造器康复中心管理系统的患者列表通常需要筛选按姓名模糊搜索、按状态在院/出院过滤、按入院日期排序。用 MyBatis-Plus 的 LambdaQueryWrapper 加 Page 是最常见的做法。public IPagePatientVO pagePatients(int page, int size, String name, Integer status) { PagePatient pageParam new Page(page, size); LambdaQueryWrapperPatient wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(name), Patient::getName, name) .eq(status ! null, Patient::getStatus, status) .orderByDesc(Patient::getCreateTime); return patientMapper.selectPage(pageParam, wrapper); }这里的参数说明like 方法第一个参数是 boolean 条件为 false 时不拼接该条件eq 同理。用这种写法你不需要为每个查询组合写一个 SQL。orderByDesc 建议放在最后避免和分页参数混淆。特别注意delete_flag 为 1 的数据在这里不会出现前提是你的实体类上有 TableLogic 注解。3.3 登录与权限控制给前端一个 JWT而不是把用户ID裸奔毕业设计里最常见的权限实现是前端判断用户角色然后隐藏按钮但这在真实场景里就是裸奔。SpringBoot 后端应该做的是登录接口校验用户名密码签发 JWT前端后续请求在 Header 里带 Token后端用一个拦截器解析 Token并把用户ID放进请求上下文。PostMapping(/login) public Result login(RequestBody LoginDTO dto) { User user userService.verify(dto.getUsername(), dto.getPassword()); if (user null) { return Result.error(用户名或密码错误); } String token JwtUtil.generateToken(user.getId(), user.getRole()); return Result.success(Collections.singletonMap(token, token)); }JWT 的 payload 里建议只放 userId 和 role不放密码等敏感信息。生成 Token 时要设置过期时间我一般设 24 小时。前端在 axios 响应里捕获 401 状态码并跳回登录页这是最低限度的会话管理。至于 refreshToken毕业设计阶段可以不做但你要知道它的存在。4. 前端 Vue 实现从路由守卫到康复计划的动态表单4.1 项目初始化和路由设计嵌套路由让菜单和页面层级一致Vue 前端我用 Vue CLI 或 Vite 都行关键是路由结构要和菜单结构一一对应。康复中心的后台菜单一般是仪表盘统计卡片、患者管理列表详情、计划管理计划列表计划编辑、系统设置用户管理。const routes [ { path: /login, component: Login }, { path: /layout, component: Layout, redirect: /dashboard, children: [ { path: dashboard, component: Dashboard }, { path: patient, component: PatientList }, { path: patient/:id, component: PatientDetail }, { path: plan, component: PlanList }, { path: plan/edit/:id, component: PlanEdit } ] } ];路由设计的关键在于患者详情和计划编辑都带参数 id通过this.$route.params.id获取不要在详情页里自己存一个全局变量。嵌套路由的 children 路径不加斜杠否则会被当成根路径。我在 vue-router 4 和 vue-router 3 之间踩过坑v4 里path: patient/:id对应组件访问方式没变但this.$route的响应式特性在组合式 API 里要改用法。4.2 axios 封装统一处理 Token 和错误码前端对接后端的一个重要步骤是封装 axios 实例。你不能在每个页面里都写axios.get然后各自处理 401那样代码会变成灾难。import axios from axios; const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { // 业务错误 return Promise.reject(new Error(res.msg)); } return res; }, error { if (error.response error.response.status 401) { router.push(/login); } return Promise.reject(error); } );参数说明timeout 设 10 秒是防止后端接口卡死导致前端白屏拦截器里处理的是res.code ! 200这种业务码而不是 HTTP 状态码。很多同学的错误是后端返回{code: 500, msg: 服务器异常}但 HTTP 状态码是 200结果前端最后的error.response.status 401永远走不到。建议后端在业务异常时也返回 HTTP 200只在严重系统错误时才返回 500这样前端逻辑更清晰。4.3 动态表单在康复计划编辑页怎么用康复计划编辑页不是固定的表单而是“选择患者—选择阶段—填写训练项目—提交评估”的多步骤流程。这里最值得讲的是 Vue 的 v-model 在动态数组里的绑定方式。template div v-for(item, index) in plan.trainingItems :keyindex el-input v-modelitem.name placeholder训练项目名称 / el-input-number v-modelitem.duration :min5 :max120 / el-button clickremoveItem(index)删除/el-button /div el-button clickaddItem添加训练项/el-button /template这里有个 Vue 的经典坑如果你用plan.trainingItems[index].name xxx这种方式去赋值响应式会失效但通过 v-model 直接绑定item.name是没问题的因为 item 本身是数组里的对象引用。另外删除数组元素时用splice(index, 1)不要用deletedelete 会留下空洞。表单数据在组件销毁前要保存到 Vuex 或 sessionStorage否则切换路由后数据丢失。5. 避坑指南从 MySQL 时区到 Vue 响应式六个让人翻车的细节5.1 MySQL 8.0 的时区问题导致时间字段差了 8 小时现象后端插入的患者 create_time 是正确的但前端显示时少了 8 小时。原因MySQL 8.0 默认时区是 UTC驱动连接时没有指定 serverTimezone。解决在 JDBC 连接串上加serverTimezoneAsia/Shanghai同时确认 MySQL 服务端时区设置。spring: datasource: url: jdbc:mysql://localhost:3306/rehab?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai注意如果你的数据库服务器本来就设了 CST 时区再配 serverTimezone 可能会重复偏移。最稳妥的办法是连接串和 MySQL 全局变量都设为 Asia/Shanghai然后重启应用。5.2 MyBatis-Plus 逻辑删除在联表查询时不自动生效现象你删除了一个患者患者列表和计划关联查询正常但统计报表里仍能看到已删除患者的计划。原因逻辑删除的过滤条件只作用于单表主查询联表查询的 ON 条件不会自动带delete_flag 0。解决在自定义 SQL 里手动加条件或者用 MyBatis-Plus 的SqlParser注解处理。这条最坑的是它不会报错只会给你错误的数据。5.3 Vue 数组下标更新不触发视图现象在康复计划编辑页修改某个训练项目名称页面没反应但打印数据是新的。原因Vue 的响应式系统在普通对象属性添加和数组下标赋值上有兼容限制。解决用this.$set(item, name, newValue)或直接通过 v-model 绑定。现在用 Vue 3 的话Proxy 解决了这个问题但如果你还在用 Vue 2 维护老项目这是个必踩的坑。5.4 SpringBoot 的 Transactional 不生效的三种情况现象你在 Service 方法标记了事务但中途抛异常后数据还是被写进去了。原因一是方法被同类内部调用事务代理没生效二是方法不是 public三是异常被 try-catch 吞掉。解决确保事务注解加在 public 方法上并且不要在本类中调用带事务的方法异常要往外抛或手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。如果你在康复计划状态流转时遇到数据写一半的情况优先检查这三条。5.5 MySQL 排序不稳定导致分页数据重复或缺失现象患者列表按 create_time 分页翻到第二页时出现第一页的数据。原因create_time 精确到秒但多个患者同一秒创建排序时没有唯一键兜底。解决orderBy 里加主键 id 作为第二排序字段。wrapper.orderByDesc(Patient::getCreateTime) .orderByDesc(Patient::getId);这个坑在做报表和导出时尤其致命。同一个时间戳默认按主键排但主键是无序的如果你不在 orderBy 里显式加 idMySQL 的查询优化器可能在不同查询间改变顺序。5.6 跨域配置导致登录后所有请求都失败现象前端能打开登录页但登录后其他接口全部报跨域错误。原因你只在后端配置了允许跨域的CrossOrigin但因为拦截器先于跨域处理执行请求被拦截后直接返回了 401前端看到的还是跨域错误。解决在 SpringBoot 的配置类里统一配置 CORS 过滤器而不是在 Controller 上加注解。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns 用通配符比 allowedOrigins(*) 更安全后者在 allowCredentials(true) 时会被浏览器拒绝。这条是血泪经验我当时排查了两个小时才发现是拦截器顺序的问题。6. 部署与进阶用 Docker 组合编排把整个系统在服务器上跑起来再用接口测试验证核心链路6.1 打包和双端启动的两种姿势后端打包用 Maven 的mvn clean package -DskipTests产物是一个 jar。前端npm run build产物是 dist 目录。两种启动方式一是前端 dist 放到 Nginx 里后端 jar 单独跑 Java 进程二是把前端 dist 复制到 SpringBoot 的 static 目录下打成单 jar 直接跑。我推荐第二种毕业设计演示时最省事不用跟老师解释反向代理。实际部署时Java 启动命令建议带上 JVM 参数限制内存java -Xms256m -Xmx512m -jar rehab-server.jar --spring.profiles.activeprod这里的参数说明-Xms 和 -Xmx 设置堆内存初始值和最大值--spring.profiles.activeprod 指定加载 application-prod.yml 里的生产配置数据库连接、日志级别等。注意云服务器内存只有 2G 的话堆内存设 512m 就够了给 MySQL 留足余量。6.2 用 Docker Compose 实现一键启动如果你的服务器装了 Docker用 docker-compose 把 MySQL 和 App 编排起来是更规范的做法也方便换机器演示。version: 3 services: mysql: image: mysql:8.0 container_name: rehab-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: rehab ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql command: --character-set-serverutf8mb4 --default-time-zone08:00 app: build: . depends_on: - mysql ports: - 8080:8080环境变量里 MYSQL_DATABASE 会在容器首次启动时自动建库省去手动导入 SQL 的步骤。command 里的 default-time-zone 直接覆盖了 MySQL 时区问题一劳永逸。app 服务里的 build 对应项目目录下的 Dockerfile如果你不需要镜像也可以改成image: openjdk:8-jre加挂载 jar 的方式。6.3 用接口测试验证核心链路登录、建档、计划流转部署完成后别急着打开页面点来点去。用 curl 或 Postman 跑一遍核心链路确认后端的逻辑没有在部署环境里失效。# 1. 登录拿 token TOKEN$(curl -s -X POST http://localhost:8080/api/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} | jq -r .data.token) # 2. 创建患者 curl -X POST http://localhost:8080/api/patient \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {name:测试患者,gender:0,age:65,diagnosis:脑卒中} # 3. 创建康复计划 curl -X POST http://localhost:8080/api/plan \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {patientId:1,planName:上肢功能训练计划,startDate:2025-05-01,endDate:2025-06-01}三个请求跑通说明登录鉴权、数据入库、状态初始化都没有问题。这里的 jq 命令只是解析 JSON 用没有装的话可以用 Python 或手写提取 token。注意第 3 步创建计划时接口应该默认把 status 设为 0草稿这是我在 2.2 节设计的逻辑你需要在接口实现中确认这一点。我自己的习惯是每完成一个功能模块先用这种方式验证接口再填前端页面。等到所有功能做完拿这套链路走一遍作为验收清单通过之后再写使用说明文档。这样做的好处是答辩演示时不用临时点来点去用 curl 命令一秒钟就能展示核心链路比打开浏览器等页面加载要稳得多。最后做一个小的进阶优化把 JWT 过期时间缩短到 2 小时并在前端 axios 拦截器里加一个静默刷新逻辑。实现方式是在登录时返回 refreshToken拦截器在收到 401 时用 refreshToken 换新 token然后重放原请求。这一步能体现你对“会话管理”的完整理解比多做几个 CRUD 页面更有价值。希望这篇实战笔记帮你在选题和落地上少走弯路做完以后你对 SpringBoot 和 Vue 的理解会比背十套面试题都扎实。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门