Spring Boot医疗服务系统:多角色权限与数据隔离实战解析 📅 发布时间:2026/9/17 16:25:38 👁 浏览次数: 简介一套基于 Spring Boot 的医疗服务系统毕业设计项目面向 Java 方向本专科毕业生解决从选题、系统设计到毕业答辩的完整参考需求。系统按管理员、乡村医生、普通村民三类角色划分涵盖首页公告、乡村诊室、健康档案、学习培训、考核信息、医疗地图、医疗药品、购买信息、留言板等前后台模块业务线条清晰适合理解典型医疗场景下的权限设计与数据流转。压缩包约 71.74MB内含 Spring Boot 完整源码、LW 文档、项目答辩 PPT 和操作演示视频代码基于 Java Spring Boot运行环境需使用 JDK1.8、MySQL 5.7、Tomcat 7。目前已有 166 人学习适合需要快速搭建同类系统、参考角色权限设计或直接进行二次开发的读者。包内同时给出运行环境与核心目录结构可帮助节约部署时间并支撑毕业设计文档撰写与答辩准备。1. 医疗服务系统拆解这个 Spring Boot 毕业设计到底做了什么村卫生室想把纸质档案转成电子档村民要能查自己的健康档案乡村医生要维护档案、处理紧急事故管理员还要管药品、培训、考核和公告。这个 Spring Boot 医疗服务系统把这三类人群放进同一套后台前台页面再叠加公告、医疗地图、医疗药品、留言反馈和医生咨询入口。真正写起来你会发现难点不在 CRUD而在于三套角色的权限边界、村民只能看自己档案的数据隔离、药品下单时的库存扣减一致性。适合正在做 java 毕业设计、需要快速跑通一个完整 springboot 项目的人也适合想看多角色后台权限如何组织的初级工程师源码解压后即可按后文步骤导入运行。2. Spring Boot 项目骨架与技术栈选型从 JDK 1.8 到 MySQL 5.7 的版本组合2.1 这套版本组合为什么是“过渡期黄金搭配”环境清单里写的是 JDK 1.8、springboot、tomcat7、mysql 5.7、Maven 3.3.9。JDK 1.8 是 Spring Boot 2.x 最稳妥的底座lambda、泛型、Stream 都能用又不至于像 JDK 17 那样引入模块化限制。这里的服务器写 tomcat7更多是早期教程沿用下来的外部容器说法springboot 项目实际用java -jar启动时会拉起内嵌的 Tomcat不需要单独部署一个 tomcat7 到机器上。如果学校硬性要求扔进外部 Tomcat 7 运行那要把打包方式改成 war并把内嵌 Tomcat 的 scope 设置为 provided否则会出现 servlet 容器冲突这个问题答辩前最好自查一遍。MySQL 5.7 是全项目里最不能换的一环。MySQL 8.0 默认启用caching_sha2_password认证插件老版本的 mysql-connector-java 驱动和 Navicat11 连上去直接报Client does not support authentication protocol requested by server。5.7 不存在这个问题而且它支持 utf8mb4健康档案里存村民的既往病史、过敏史这类长文本不会出现字符集截断。Maven 3.3.9 配合 JDK 1.8 是经典组合高于 3.8 反而可能因为中央仓库策略变化导致部分旧依赖下载异常。2.2 解压后的资源结构与核心配置源码解压后通常是标准 Maven 工程重点看这几个位置src/main/java/com/medical/ ├── controller/ # 前台页面与后台接口的 Controller ├── service/ # 业务逻辑层 ├── mapper/ # MyBatis 的 Mapper 接口 ├── entity/ # 实体类对应数据库表 └── config/ # 拦截器、WebMvc 配置 src/main/resources/ ├── application.yml # 数据源与 MyBatis 配置 ├── mapper/ # Mapper XML 文件 ├── static/ # CSS/JS/图片 └── templates/ # 前台页面模板数据库连接集中在application.yml这是导入后第一个要改的文件server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/medical_service?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.medical.entity这里的driver-class-name用的是com.mysql.jdbc.Driver对应 mysql-connector-java 5.x这也是坚持 MySQL 5.7 的原因之一。如果 pom 里依赖的是 8.x 驱动这个类名要换成com.mysql.cj.jdbc.Driver否则启动时ClassNotFoundException。serverTimezoneAsia/Shanghai是必须的不加的话 MyBatis 写update_time这类 datetime 字段会报时区异常。mapper-locations指向resources/mapper下的 XML 文件实际项目里如果用的是注解 SQL这段可以省略。2.3 数据库初始化先建库再导 SQLNavicat11 直接连接本地 MySQL 5.7先手动建库再导入项目自带的 sql 文件。推荐命令行方式字符集问题比图形界面少mysql -uroot -p CREATE DATABASE IF NOT EXISTS medical_service DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE medical_service; SOURCE D:/毕业设计/medical_service.sql;utf8mb4_general_ci排序规则对中文兼容性最好如果 sql 文件里有 emoji 或者生僻姓名utf8mb4 不会丢字。用 Navicat11 导入时注意右键数据库选择“运行 SQL 文件”而不是复制粘贴执行大脚本否则中间报错很难定位。导入完成后用SHOW TABLES;验证核心表大致如下表名用途关联角色sys_user登录账号、密码、角色标识三类用户统一存储health_record健康档案主表医生维护、村民查看emergency_event紧急事故上报与处理村民发起、医生处理medical_drug医疗药品与库存管理员维护、村民购买purchase_info药品购买订单村民下单、管理员审核clinic_info乡村诊室及地图坐标管理员维护、前台展示training_info / assessment_info学习培训与考核信息管理员发布、医生参与notice_info公告信息管理员发布message_board留言板村民留言、管理员回复建库这一步做完系统的数据底座就立住了。下一步进入代码层看三端角色权限是怎么处理的。3. 三端角色权限设计管理员、乡村医生、普通村民的登录与数据隔离3.1 一张用户表还是三套独立账号表很多新手会把管理员、医生、村民拆成三张表最后登录时反而要查三次数据库session 里还要额外存一个 userType 才能区分。推荐做法是统一收进sys_user表用role字段区分0 管理员、1 乡村医生、2 普通村民。医生和村民的扩展信息再分别用 profile 表关联比如医生所属诊室、村民的身份证号。角色和菜单权限的对应关系在页面路由和接口拦截两层同时控制前端控制“看得到”后端控制“改得了”。常见做法是登录成功后返回角色前端按角色渲染不同菜单后端拦截器只做登录态验证细粒度的资源权限放在 Service 层判断。角色后台可见菜单数据操作边界管理员全部菜单可增删改所有模块管理医生、村民账号乡村医生公告、诊室、健康档案、紧急事故、培训、考核、购买信息维护本人负责诊室的档案处理紧急事故普通村民健康档案、紧急事故、留言板只能查看本人的档案记录3.2 登录拦截器只挡未登录远远不够先看最基础的登录态拦截用 Spring Boot 的HandlerInterceptor实现public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从 session 取登录用户取不到就跳登录页 Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } // 这里再补一步角色校验注解或路径前缀匹配 return true; } }注册进 WebMvc 配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/, /login, /register, /css/**, /js/**, /images/**, /api/public/**); } }excludePathPatterns里的静态资源路径必须放行否则登录页的 CSS 和 JS 全被拦截页面裸奔。/api/public/**是特意留出来的公开接口前缀例如医疗药品展示、公告列表游客也能看。真正做角色管理时在preHandle里根据loginUser.getRole()判断当前路径是否允许访问比在每个 Controller 里重复写 if 判断要干净得多。3.3 数据隔离村民只能查自己档案Service 层收口权限拦截解决的是“能不能进”数据隔离解决的是“进来看什么”。村民查询健康档案时最危险的做法是前端传一个villagerId给后端后端不做任何校验直接查。攻击者把参数改成别人的 ID就能看到别人的病历。正确做法是从 session 里取当前登录用户的 ID再传给 MapperGetMapping(/my) public Result myRecords(HttpSession session) { // 从会话取用户而不是从请求参数取 User user (User) session.getAttribute(loginUser); if (user null) { return Result.error(未登录); } ListHealthRecord records healthRecordService.listByVillagerId(user.getId()); return Result.ok(records); }这里的关键点有两个。一是user.getId()必须来自服务端 session不能信任前端传值二是 Mapper 层的 SQL 要强制带villager_id #{villagerId}条件配合索引使用。乡村医生的数据边界同理医生只能维护自己诊室关联的档案管理员负责所有账号和药品数据的最终审核。数据隔离做扎实之后下一章看健康档案和紧急事故这两个核心业务模块的代码组织。4. 健康档案与紧急事故Spring Boot 后端 CRUD 的核心实现4.1 健康档案表设计一人一档身份证号做唯一约束健康档案是这个系统里数据敏感度最高的模块设计上要把握两个原则一个村民在系统里只能有一条有效档案档案变更必须记录最后操作医生。字段类型说明record_idint主键自增villager_idint关联 sys_user 的村民 IDid_cardvarchar(18)身份证号建唯一索引height / weightvarchar(10)身高体重字符串便于记录小数blood_typevarchar(10)血型allergy_historytext过敏史past_historytext既往病史diagnosistext当前诊断记录doctor_idint最后操作的医生 IDupdate_timedatetime档案更新时间存text类型的字段不要参与排序和分组也不需要建立普通索引档案查询的主路径是villager_id。身份证号加唯一索引是为了防止医生重复建档这个约束要在建表 SQL 里直接体现而不是靠 Service 层先查再插——并发情况下先查再插会穿透。4.2 Controller-Service-Mapper 三层怎么落新增档案的接口RestController RequestMapping(/api/record) public class HealthRecordController { Autowired private HealthRecordService healthRecordService; PostMapping(/add) public Result add(RequestBody HealthRecord record, HttpSession session) { // 从会话取当前登录的医生不允许前端传 doctorId User doctor (User) session.getAttribute(loginUser); if (doctor null || doctor.getRole() ! 1) { return Result.error(无权限); } record.setDoctorId(doctor.getId()); record.setUpdateTime(new Date()); healthRecordService.addRecord(record); return Result.ok(); } }Controller 层只做参数接收、身份判断和结果封装不写 SQL。事务放在 Service 层Service public class HealthRecordServiceImpl implements HealthRecordService { Autowired private HealthRecordMapper healthRecordMapper; Override Transactional(rollbackFor Exception.class) public void addRecord(HealthRecord record) { // 唯一约束冲突由数据库兜底 healthRecordMapper.insert(record); } }对应的 Mapper XMLinsert idinsert parameterTypeHealthRecord INSERT INTO health_record ( villager_id, id_card, height, weight, blood_type, allergy_history, past_history, diagnosis, doctor_id, update_time ) VALUES ( #{villagerId}, #{idCard}, #{height}, #{weight}, #{bloodType}, #{allergyHistory}, #{pastHistory}, #{diagnosis}, #{doctorId}, #{updateTime} ) /insert#{}预编译参数是底线要求不要用${}拼接字段值身份证号这种敏感字段一旦发生 SQL 注入就是数据泄露。Transactional(rollbackFor Exception.class)里的rollbackFor必须写Spring 默认只对RuntimeException回滚如果你在业务里抛Exception事务不会自动回滚档案插入了但其他关联操作失败数据就错乱了。4.3 紧急事故处理状态字段必须条件更新村民在前台上报紧急事故医生处理整个流程依赖一个状态字段。常见做法是status用 tinyint0 待处理、1 处理中、2 已处理。医生点击“接单”时如果用“先查询再更新”的写法两个医生同时接单可能都会通过状态判断产生脏数据。UPDATE emergency_event SET status 1, handle_doctor_id #{doctorId}, handle_time NOW() WHERE id #{eventId} AND status 0这条 SQL 的核心是WHERE status 0条件。执行后如果影响行数为 0说明事故已经被其他医生接走Service 层直接抛提示不需要锁表也不需要乐观锁版本号。NOW()用数据库时间而不是 Java 时间避免应用服务器和数据库时钟不一致导致追踪问题。处理完成后把状态置为 2同时写入处理意见。5. 医疗药品购买与库存扣减事务与并发下单的源码级处理5.1 购买链路涉及的表和接口村民在前台看到药品列表点击购买后生成一条purchase_info记录管理员后台查看购买信息并处理。这条链路涉及三个动作检查商品状态、扣减库存、生成订单。如果按照“先查库存够就减”的顺序写两个用户同时买最后一盒药时两次查询都看到库存充足都执行了扣减最终库存变成负数这就是典型的超卖。购买信息表的核心字段是purchase_id、drug_id、user_id、quantity、status、create_time。status建议 0 待发货、1 已发货、2 已完成、3 已取消不要在状态字段里存中文扩展性差且浪费存储。5.2 扣库存用条件 UPDATE不用“先查再减”下单业务放在一个事务方法里Service public class DrugOrderService { Autowired private MedicalDrugMapper drugMapper; Autowired private PurchaseInfoMapper purchaseMapper; Transactional(rollbackFor Exception.class) public void createOrder(Long drugId, Integer quantity, Long userId) { // 条件更新扣库存影响行数为 0 表示库存不足或药品下架 int rows drugMapper.deductStock(drugId, quantity); if (rows 0) { throw new RuntimeException(库存不足或药品已下架); } // 库存扣减成功后生成购买记录 PurchaseInfo info new PurchaseInfo(); info.setDrugId(drugId); info.setUserId(userId); info.setQuantity(quantity); info.setStatus(0); purchaseMapper.insert(info); } }对应的库存扣减 SQLUPDATE medical_drug SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{drugId} AND stock #{quantity}这里的技巧在AND stock #{quantity}这一句。MySQL 执行 UPDATE 时会锁住这一行在锁内判断库存是否足够不够就影响 0 行不会把库存扣成负数。两个并发请求同时进来时后一个 UPDATE 会等锁锁释放后重新判断条件库存不足就走不到更新从机制上杜绝超卖。单机部署这个写法完全够用如果以后拆微服务、分库分表再引入 Redis 预扣库存或者乐观锁版本号也不迟毕业设计阶段不要过度设计。还有一个易踩的坑库存扣减成功但purchaseMapper.insert抛出异常时事务必须回滚。rollbackFor Exception.class保证 checked exception 也能触发回滚这一点已经在第四章提过在订单场景同样适用。你可以自己验证故意把购买信息表名改成不存在的表发起下单观察medical_drug的库存是否恢复原值。5.3 医疗地图点位后台维护、前台渲染医疗地图模块的数据结构比药品简单得多clinic_info表存诊室名称、地址、经度、纬度、联系电话管理员在后台维护点位前台页面加载时调一个公开接口拿全部点位 JSON[ { clinicId: 1, clinicName: 西河村卫生室, address: 西河村村委会东侧, lng: 116.3912, lat: 39.9073 } ]前端拿到点位数组后遍历渲染地图标记。这个模块的常见问题有两个一是经纬度存成字符串导致地图 API 解析异常建表时用decimal(10,6)存储二是点位接口没有做缓存每次打开首页都全量查询数据量上来后建议加一层本地缓存或把接口改成按区域查询。地图渲染只是展示层工作真正的核心逻辑还是点位数据的增删改查保持后台接口对齐即可。6. 从源码到可运行部署排错清单与自测链路6.1 环境变量、Maven 导入与两个高频报错先把 JDK 1.8 和 Maven 3.3.9 的环境变量配好命令行里分别执行java -version和mvn -version确认无误。然后 IDEA 里File - New - Project from Existing Sources选择解压目录识别为 Maven 项目后等待依赖下载。IDEA 默认 Maven 配置如果是 3.8建议在 Settings 里指到本地 Maven 3.3.9同时设置阿里云镜像仓库依赖下载会快很多。两个最高频的启动报错。第一个是数据库连接失败Access denied for user rootlocalhost去application.yml里改用户名密码确认 MySQL 5.7 服务已启动。第二个是Client does not support authentication protocol requested by server说明本机装的是 MySQL 8.0 而不是 5.7。如果暂时不想换版本执行一次ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码;把认证方式降级但终极方案仍然是换回 5.7因为整个 sql 脚本和驱动版本都按 5.7 写的。启动成功后访问http://localhost:8080/Jenkins 之类的工具先关掉避免和 8080 端口冲突。端口被占用时执行netstat -ano | findstr 8080查 PID再taskkill /PID 编号 /F结束进程。6.2 启动后的三链路自测项目能跑起来不等于功能正确按下面三条链路完整走一遍能覆盖系统绝大多数模块。管理员登录后台发布一条公告新增一个医疗药品并设置库存为 1在医疗地图中录入一个诊室点位。普通村民注册账号查看公告和地图找到药品执行购买。用两个浏览器同时下单这盒库存为 1 的药品一个成功一个提示库存不足说明扣库存逻辑生效。管理员在后台把村民的账号分配给乡村医生所属诊室医生登录后为该村民建立健康档案村民端刷新即可看到档案内容同时用村民身份直接访问医生接口应被拦截或返回无权限验证数据隔离。再让村民提交一条紧急事故医生端接单、处理、完结管理员端能看到完整的处理记录和时间戳。最后打开 MySQL执行SELECT * FROM purchase_info;看到新增一条 status 为 0 的购买记录整套系统的主链路就全部打通了。本文还有配套的精品资源点击获取