做毕设时我选了SpringBoot+Vue这套“医院资源管理系统管理平台”,说难不难,说简单也真不简单——它几乎是资源类管理系统里最能练手的组合,把人员、床位、设备、药品、号源这几类资源塞进一个完整业务模型里,写一遍下来,从CRUD到权限、事务、报表统计就都摸了一遍。
这篇文章写给正在做毕设、课设,或者只是单纯想用Java+MySQL+SpringBoot+Vue练全栈的同学。我会按我实际开发时的思路,把项目拆解、数据库设计、后端核心逻辑、前端页面落地、部署和答辩避坑全部捋一遍。你可以直接拿这套框架改成门诊管理系统、体检预约系统,甚至实验室设备借用系统,换皮不换骨。
1. 项目整体拆解:医院资源管理系统到底在管什么
1.1 需求模块与角色权限
医院资源管理系统,更准确说是一个面向中小型医院或院区的综合管理后台。核心目标不是看病,而是把“资源”管起来,让患者挂得上号、医生排得了班、床位查得到状态、设备不闲置、药品库存不断供。
我第一版把系统拆成了六个业务模块:
- 基础档案:科室、医生、患者、职工信息维护。
- 排班与挂号:医生排班表发布,患者在线预约、取消预约。
- 病房与床位:床位分配、退床、按科室统计占用率。
- 设备管理:设备台账、借用归还、维修状态。
- 药品与库存:药品入出库、库存预警、效期管理。
- 统计报表:挂号和收入数据可视化,给管理员看趋势。
权限上采用经典的三层角色:管理员拥有全部菜单;医生能看到自己的排班、处理号源;护士/前台负责挂号登记和床位操作。如果再细分,还能加一个“患者”角色用于自助预约,我当时就是靠这个角色把登录流程做成多角色跳转的,答辩时很加分。
这个项目适合毕设的关键点在于:它不像电商系统那样卷订单并发,也不像纯管理系统那样一眼到底。挂号、库存、床位天然带状态流转,能讲出业务深度,又不至于超出两三个月开发量。
1.2 技术栈选型:为什么是SpringBoot+Vue+MySQL
技术栈用这套,一方面是主流,另一方面是真省事。
后端选SpringBoot,因为它把配置自动化做到了极致。我不需要像SSH项目那样写一堆XML,一个spring-boot-starter-web就搞定Web环境,用Maven管理依赖,内置Tomcat,打包成jar直接跑,这对不爱折腾服务器配置的学生太友好了。版本建议用2.7.x,别一上来追SpringBoot 3,JDK要求、javax迁移到jakarta这些坑,会让毕设进度直接卡一周。
持久层我用的是MyBatis-Plus,不是纯MyBatis。纯MyBatis写单表增删改查太琐碎,MyBatis-Plus提供BaseMapper,最简单的CRUD零SQL实现,复杂查询再用@Select注解或Wrapper条件构造器解决。这样既不想完全丢掉SQL思维,又想提高效率,是很务实的选择。
前端选Vue,配合Element UI。Vue的响应式数据绑定让页面状态管理非常直接,Element UI则相当于给前端提供了一套现成的后台管理组件,表格、表单、弹窗、日期选择器全是封装好的。我用的Vue 2 + Element UI,图的是资料多、组件稳、网上踩坑记录一搜一大把;如果你想用Vue 3 + Element Plus也可以,核心思路一样,只是部分API写法有变化。
数据库选MySQL 8.0,免费、轻量、学校机房都有。MySQL 5.7和8.0在连接驱动和时区配置上有区别,建议新项目直接用8.0,遇到“Public Key Retrieval is not allowed”就在JDBC连接串加allowPublicKeyRetrieval=true。
这套组合还有一个隐藏优势:招人问起来不丢面儿,Java后端 + Vue前端 + MySQL数据库,是所有全栈岗位最标准的“打招呼方式”。
2. 数据库设计与核心表结构
2.1 核心表与关系
数据库是整个项目的灵魂,表结构设计合理,后端能少写一半判断。我建议先设计表再写代码,不要边写边加字段,否则后面改接口会越改越乱。
我的核心表设计如下:
- sys_user:用户表,存放所有登录账号,包含username、password、real_name、role_id、department_id、status。
- sys_role:角色表,常见就管理员、医生、护士、患者四种。
- department:科室表,记录科室名称、位置、电话、简介。
- doctor:医生表,扩展用户表,关联科室,包含职称、擅长领域、简介、排班状态。其实也可以直接用user表加role区分,但单独拆出来更便于扩展排班和挂号。
- schedule:排班表,记录医生在某天某个时段出诊,包含doctor_id、schedule_date、time_slot、total_slots、remain_slots、status。这个是挂号系统的核心资源表。
- patient:患者表,记录姓名、身份证、手机号、病历号等。
- appointment:预约挂号表,记录患者预约哪个排班,包含appointment_no、patient_id、schedule_id、status(待就诊/已就诊/已取消/已过期)、create_time。
- bed:床位表,包含bed_no、department_id、status(空闲/占用/维修)、patient_id(当前占用患者)、enter_time。
- equipment:设备表,包含equipment_no、name、department_id、status(正常/维修/报废)、buy_date。
- drug:药品表,包含drug_code、name、specification、unit、price、stock、warning_stock、expire_date。
- drug_log:药品出入库流水表,记录操作类型、数量、操作人。
表之间的关系很简单:schedule关联doctor和department,appointment关联schedule和patient,bed关联department和patient,drug_log关联drug和sys_user。这个模型简化了真实医院系统的复杂度,但把资源调度的核心逻辑完整保留了。
CREATE TABLE `schedule` ( `id` bigint NOT NULL AUTO_INCREMENT, `doctor_id` bigint NOT NULL COMMENT '医生ID', `schedule_date` date NOT NULL COMMENT '出诊日期', `time_slot` tinyint NOT NULL COMMENT '时段:1上午 2下午 3晚', `total_slots` int NOT NULL DEFAULT '20' COMMENT '总号源数', `remain_slots` int NOT NULL DEFAULT '20' COMMENT '剩余号源数', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1正常 2停诊 3已结束', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_doctor_date` (`doctor_id`, `schedule_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生排班表';这里有一个几乎所有新手都会犯的错:把排班的日期和时段存成字符串,比如“2025-06-01上午”,这样看似简单,但后续统计“周一上午出诊医生数”、按日期范围筛选,全都要靠LIKE去模糊匹配,性能差还容易出bug。正确做法是日期用date类型,时段用tinyint枚举,查询用范围条件,数据干净又高效。
2.2 关键字段设计心得与注意事项
设计字段时,有四个细节特别值得注意。
第一,金额一律用decimal,不能用float或double。药品价格、挂号费涉及精确计算,浮点数会出现0.1+0.2=0.30000000000000004的经典问题,记账对不上就是大事故。比如挂号费字段定义decimal(10,2),入库和查询都不用担心精度丢失。
第二,状态字段用tinyint,不要用varchar。比如排班status,1正常、2停诊、3已结束,程序里用常量或者枚举类维护,数据库查询和索引都更高效。如果直接存“正常”“停诊”,中文乱码、删除、改名都是麻烦。
第三,所有表都要有逻辑删除标识deleted,默认0,删除改1。我见过同行直接用DELETE删用户表数据,结果关联的外键报错、历史统计少数据,最后还得从binlog里捞。做毕设虽然数据量不大,但养成逻辑删除的习惯,答辩时跟老师解释“防止误删、保留审计轨迹”,这就是专业意识。
第四,时间类型统一用datetime。create_time默认CURRENT_TIMESTAMP,update_time用ON UPDATE CURRENT_TIMESTAMP自动更新。有些教程习惯用varchar存时间字符串,排序、范围查询全乱套,不建议学。
索引方面,不需要建太多,但要建在查询频率高的字段上。挂号查询的黄金条件就是“日期+科室”,所以在schedule表建(department_id, schedule_date)联合索引;appointment表按patient_id查本人的预约记录,建patient_id索引。细节做到这个程度,面试被问到索引优化也有东西可讲。
3. 后端核心实现:从登录鉴权到资源调度
3.1 登录鉴权与统一返回
后端我分了四层:controller、service、mapper、entity。controller只做参数接收和调用service,service写业务逻辑,mapper操作数据库。刚开始写项目时容易把业务代码全堆在controller里,最后Controller两百行,Service空荡荡,这是典型的坏味道。
登录鉴权,我没有用复杂的Shiro,而是用JWT + 拦截器,自己手写一遍,代码量不大但原理非常清楚。流程是这样的:
- 用户提交用户名密码,service层校验MySQL中的账号状态。
- 校验通过后生成JWT,把userId、roleId、userName放进去,设置过期时间两小时,返回前端。
- 前端把token存到localStorage,每次请求在axios拦截器里放到header的Authorization字段。
- 后端拦截器解析token,如果过期或非法直接返回401,前端再跳转登录页。
// 生成token String token = Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("roleId", user.getRoleId()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 120)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();这段代码里signWith的密钥一定要放在配置文件里,不要写死到class。另外,JWT是无状态的,token一旦签发,在过期前无法主动撤销,所以“退出登录”只是前端删token,后端拦截器不再认它,但这在毕设场景里足够用。
统一返回结果我也封装了一个Result类,包含code、message、data三个字段。code为200表示成功,401表示未认证,500表示业务异常。所有controller都返回Result,前端拿到code再判断。这样好处是:所有接口的响应格式一致,前端axios响应拦截器里可以统一处理错误码,不用每个页面写重复的提示逻辑。
3.2 排班、挂号这类核心业务的接口设计
排班和挂号是最能体现业务能力的地方,我重点说这两个。
3.2.1 排班发布
管理员/医生端,选择科室、医生、日期、时段,设定总号源数,生成一条schedule记录。这里要防止重复排班,同一医生同一天同一时段只能有一条有效记录。我在数据库加了唯一索引,同时service里用count查询做二次校验。
// 检查该时段是否已排班 long count = scheduleMapper.selectCount(new LambdaQueryWrapper<Schedule>() .eq(Schedule::getDoctorId, doctorId) .eq(Schedule::getScheduleDate, date) .eq(Schedule::getTimeSlot, timeSlot) .eq(Schedule::getStatus, 1)); if (count > 0) { throw new BizException("该医生在此时间段已有排班,请勿重复添加"); }3.2.2 预约挂号
患者选了一个排班,点“预约”,后端要做的不是直接insert一条appointment,而是要保证“号源不超卖”。这里我用的是MySQL乐观锁思路:更新排班表剩余号源时带上remain_slots = remain_slots - 1的条件判断。
@Transactional public Appointment bookAppointment(Long scheduleId, Long patientId) { Schedule schedule = scheduleMapper.selectById(scheduleId); if (schedule == null || schedule.getStatus() != 1) { throw new BizException("该排班已停诊或不存在"); } if (schedule.getRemainSlots() <= 0) { throw new BizException("号源已满,请选择其他时段"); } // 扣减号源 int rows = scheduleMapper.updateRemainSlots(scheduleId); if (rows != 1) { throw new BizException("号源被抢光,请重新尝试"); } // 生成预约记录 Appointment appointment = new Appointment(); appointment.setAppointmentNo(generateAppointmentNo()); appointment.setPatientId(patientId); appointment.setScheduleId(scheduleId); appointment.setStatus(APPOINTMENT_STATUS_WAIT); appointmentMapper.insert(appointment); return appointment; }为什么要先扣号源再插入预约?因为如果把insert放前面,两个请求同时进来,都发现remain_slots=1,都insert成功,最后号源更新成0,就出现了两条预约抢一个号的情况。先扣号源,并且用update返回受影响行数来判断是否成功,数据库行锁会保证同一时刻只有一个事务能扣成功,这就是“原子性”。
这个方法我加了@Transactional注解,号源扣减和预约单生成在同一个事务里,任何一个失败就整体回滚。这是整个后端最值得在答辩时展开讲的一个方法,因为它把事务、并发、资源一致性都包含了——我当时被问到“如果同一时刻100个人抢20个号怎么办”,就靠这段代码解释清楚了。
3.2.3 建立“排班看板”查询接口
排班看板是前端首页的核心,后端要提供按科室、日期、医生姓名多条件分页查询,返回每个排班的预约进度。我用MyBatis-Plus的LambdaQueryWrapper连接条件,配合Page分页,代码量很小。
public IPage<ScheduleVO> getSchedulePage(ScheduleQuery query) { Page<Schedule> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Schedule> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(query.getDepartmentId() != null, Schedule::getDepartmentId, query.getDepartmentId()) .eq(query.getDoctorId() != null, Schedule::getDoctorId, query.getDoctorId()) .ge(query.getStartDate() != null, Schedule::getScheduleDate, query.getStartDate()) .le(query.getEndDate() != null, Schedule::getScheduleDate, query.getEndDate()) .orderByAsc(Schedule::getScheduleDate, Schedule::getTimeSlot); IPage<Schedule> schedulePage = scheduleMapper.selectPage(page, wrapper); // 再封装成VO,补充医生姓名、科室名称 return convertToVO(schedulePage); }这里我用了LambdaQueryWrapper的条件构造,query里的字段为null时自动跳过这个条件,前端传什么就查什么,不用写一堆if拼SQL。如果需要关联查询医生姓名,可以用voList,也可以在SQL里join,MyBatis-Plus有自定义分页的写法,毕设里选择在service里二次查询姓名即可,简单直观。
3.3 权限控制与接口安全
后端我只放行了登录接口和静态资源,其余接口全部走拦截器校验token,再根据角色判断是否有访问某个接口的权限。具体做法是定义一个@RequireRole注解,标注在controller方法上,拦截器里解析token拿到roleId,再和注解值对比。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { int value() default 1; }比如删除药品库存的接口只允许管理员调用,我在方法上标@RequireRole(ADMIN_ROLE_ID),拦截器发现不是管理员就返回“无权限”。这种自定义注解的方式,比在service里自己写if判断看起来规范得多,也体现了你对Spring AOP和注解机制的理解。
接口安全还有一个细节:update、delete接口不要直接用前端传来的id无限信任,还要做一遍数据归属校验。比如护士只能修改自己所在科室的床位,就要先查bed的department_id,不是自己科室就拒绝。这部分写进接口里,免得出现越权操作。
4. 前端页面与交互落地
4.1 Vue工程结构与路由权限
前端我用Vue CLI创建工程,目录结构很标准:
src ├── api // 接口请求封装 │ ├── modules │ │ ├── schedule.js │ │ ├── appointment.js │ │ └── drug.js │ └── request.js // axios实例封装 ├── assets ├── components ├── router │ └── index.js ├── store │ └── modules ├── views │ ├── login │ ├── dashboard │ ├── schedule │ ├── reservation │ ├── bed │ ├── drug │ └── statistics └── utils └── auth.js路由配置里我用了meta.roles做权限控制。每个路由meta包含允许访问的角色数组,router.beforeEach里判断当前用户roleId是否在允许列表里,不在就直接踢回首页或者提示无权限。
{ path: '/drug', component: () => import('@/views/drug/index'), meta: { title: '药品库存管理', roles: [1] } }axios封装的核心是请求拦截器和响应拦截器。请求拦截器从localStorage取token,放进header;响应拦截器在code为401时清理本地缓存并跳转登录页,在code为500时统一弹出错误提示。这样每个页面里的接口调用都不用重复处理错误了,代码清爽很多。
4.2 核心页面实现:排班看板、预约挂号、数据统计
4.2.1 排班看板
排班看板页面是我花时间最多的地方。页面加载时调schedule分页接口,用el-table展示日期、时段、科室、医生、总号源、剩余号源、状态。状态列用el-tag根据status字段切换颜色:
<el-table-column label="排班状态" width="100"> <template slot-scope="scope"> <el-tag :type="formatStatusType(scope.row.status)"> {{ formatStatusText(scope.row.status) }} </el-tag> </template> </el-table-column>筛选区用日期选择器+科室下拉框。科室下拉框的数据是进入页面时从department接口拉的,选完条件点查询,重新调用接口。这个页面还加了一个“重置”按钮,把所有筛选条件清掉并刷新列表,这是后台管理页面里容易被忽略但对用户体验很重要的功能。
4.2.2 预约挂号
挂号页分成两步:第一步选择一个排班,第二步填写患者信息。排班列表旁边直接显示“剩余号源”,少于5个时数字变红提示紧张。点击“立即预约”时,前端确认弹窗后调用后端预约接口。成功之后刷新排班列表,可以看到剩余号源减一。
这里有个交互细节:要防止用户短时间内反复点击“立即预约”导致重复提交。我在按钮上加了loading状态,点击后立即禁用,等接口返回成功或失败再恢复,从源头拦住手滑和连点。后端的接口本身也是幂等性设计——同一排班同一患者重复预约,在service层会先检查是否已有未取消的预约,有就直接抛异常,双层保险。
4.2.3 数据统计
统计页我只用了ECharts,不引入重型BI组件。页面加载时调统计接口,后端返回一个折线图数据(近7天挂号量)和一个饼图数据(各科室挂号占比),前端用setOption渲染图表。
ECharts最常见的问题是容器宽度不对导致图表变形,解决办法是在图表渲染前给它一个固定高度,el-card里放一个带height: 400px的div,并在mounted里初始化图表。如果tab切换后图表宽度计算错误,可以在activated钩子里调用chart.resize(),这个我在写多tab页面时踩过坑,后来加了resize监听才彻底解决。
4.3 前端常见细节心得
前端还有一个容易忽略的点:接口返回的时间。后端返回的LocalDateTime默认是ISO格式字符串,如果格式不统一,前端显示会很难看。我后端在配置里加了Jackson的全局时间格式,统一成yyyy-MM-dd HH:mm:ss,前端就不用每次手动格式化。
Element UI的表格默认是英文的,日期选择器、分页组件都有语言包。在main.js里引入zh-cn语言包就能解决,否则老师演示时看到英文分页“Prev/Next”会很突兀。这个两行代码就能修复,但很多人都会忽略。
组件主题色我也顺手改了一下,在scss里覆盖Element UI的$--color-primary变量,换成医疗行业常见的蓝色,整体界面显得更正式。这些小细节在答辩展示时都会让老师眼前一亮。
5. 部署运行与答辩避坑指南
5.1 从零跑起来的部署步骤
毕设最怕的不是写代码,而是演示前一天环境崩掉。我总结一套最稳妥的启动顺序,照着做基本不会出大问题。
- 安装并启动MySQL 8,执行项目里的database.sql脚本初始化表结构和测试数据。
- 用IDEA打开后端工程,等待Maven下载依赖。改application.yml里的数据库账号密码、端口号、JWT密钥。
- 运行Application.java启动后端。看到“Started Application in xxx seconds”说明启动成功。
- 前端工程用VS Code打开,执行npm install安装依赖,如果下载慢就配置淘宝镜像源。
- 执行npm run serve启动开发服务器,浏览器访问localhost:8080,能在登录页看到系统就说明前后端连通了。
后端启动失败时,优先看控制台报错。90%的启动失败来自MySQL连接问题:host连不上、密码错误、数据库不存在、时区报错。时区报错就在连接串加serverTimezone=Asia/Shanghai。端口被占用就改server.port,或者用netstat -ano | findstr :8080查出来杀掉进程,别跟端口死磕。
前端npm install失败比较常见的是node-sass版本和Node版本不兼容。我建议直接不用node-sass,换成sass或者干脆用Element UI时不用scss自定义主题,这样能省很大力气。如果一定要用,就用项目对应版本的sass-loader,别追新版本。
5.2 常见问题与排查速查表
我整理了一个速查表,基本覆盖了开发和部署阶段遇到的高频问题。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 前端访问后端报跨域 | 未配置CORS | 后端加WebMvcConfigurer重写addCorsMappings,允许前端地址和所有请求头 |
| 登录成功但用户信息拿不到 | 前端请求没带token或token key错误 | 检查axios请求拦截器,确认header字段名和后端读取名一致 |
| 预约挂号失败,提示号源已满 | 测试数据号源被耗尽 | 在数据库手动把schedule表remain_slots调大,或去排班页新增排班 |
| MySQL连接报Public Key Retrieval错误 | MySQL 8加密方式导致 | JDBC连接串加allowPublicKeyRetrieval=true |
| schedule查询不到数据 | 前端传的日期是字符串“2025-06-01”,接口匹配不上 | 后端日期参数用@DateTimeFormat(pattern="yyyy-MM-dd") |
| npm run build内存溢出 | Node默认内存不足 | package.json里build脚本改成“build”: “vue-cli-service build --max_old_space_size=4096” |
| Element UI按需引入后组件样式缺失 | 没引入对应组件样式或者按需插件配置错 | 改为全量引入Element UI最简单,毕设不差这点体积 |
还有一个最隐蔽的坑:前端本地调试时访问图片、上传文件是OK的,一旦部署到服务器,静态资源路径可能失效。这种情况通常是用了相对路径,我建议所有接口前缀写成/api,在axios的baseURL里配置,后端统一用/api做context-path,Nginx代理时再转发到Java服务,路径问题全部集中在代理配置里解决。
5.3 答辩和课设展示的加分技巧
代码写完只是及格,把项目讲出深度才能拿高分。我建议答辩前重点准备三个话题。
第一,为什么选择这个课题。不要只说“医院资源管理系统很常见”,要说“它涵盖多角色权限、资源并发控制、状态机流转、数据可视化,能与实际业务场景结合,能系统考核工程能力”。这句话一出来,老师就知道你不是随便选的题目。
第二,讲清核心难点。把3.2节的挂号扣减号源那段逻辑重点讲,提“并发超卖、乐观锁、事务原子性”。这段代码是你项目里技术含量最高的部分,也是最容易被问到的地方。
第三,展示扩展性思考。可以主动说“后续可以引入Redis做验证码和更高效的库存预扣,用RabbitMQ做预约通知”,“或者用Docker Compose一键部署前端、后端、数据库”。哪怕你只是了解,没实际做,也能展示你具备工程视野。
演示时准备好“降级方案”。如果现场网络不稳定导致ECharts图表加载不出来,就提前准备好几张截图放PPT里。如果数据库被人误改了数据,就先把初始化脚本跑一遍恢复测试数据。演示页面不要只点静态页面,要有意识地操作一次完整的业务流——管理员建排班、患者预约、护士分配床位、统计报表数字变化,这条链路走通了,整个演示就很完整、很有说服力。
最后分享一个我最初踩坑之后的习惯:做任何修改前,先备份数据库。在项目根目录放一个reset.sh或者手动执行sql脚本,任何时候测试数据乱了都能一键恢复。我遇到过答辩前五分钟数据库被同学乱删数据的情况,要不是有备份脚本,当场就得社死。这个习惯,希望你从现在开始也养成。