SpringBoot电子病历共享系统:权限模型与数据授权设计实战 当你准备把“SpringBoot电子病历共享系统”作为毕业设计题目时恭喜你你选了一个非常“能打”的方向。它既有医疗信息化真实业务场景又把权限管理、数据共享、文件存储、日志审计这些后端高频考点全部串在了一起。我陪跑过不少毕业设计项目发现真正让评委眼前一亮的往往不是那种千篇一律的商城系统而是这种“业务有深度、技术有落点”的平台型项目。这篇内容不是给你丢一坨源码让你自己拼而是把我实际搭建和调试这个系统时踩过的坑、反复权衡过的方案、以及可以直接复制过去改改用的代码片段全部捋一遍。不管你是刚把SpringBoot跑通的小白还是准备拿它去答辩的准毕业生按着这条思路走下来至少能少折腾一个月。先记住一句话这个系统的灵魂不是“写病历”而是“共享”两个字。病历管理只是基础能力跨科室、跨机构、甚至跨医院的数据授权和访问才是你区别于普通增删改查项目的核心亮点。1. 项目整体设计与思路拆解1.1 先想清楚这个系统到底要解决什么问题去医院看病的人都有体会在这家医院做的检查换一家医院就不认了要么重新排队检查要么把纸质报告翻出来拍照发给医生。根源在于医院系统各自独立患者病历被锁在单个机构内部形成数据孤岛。电子病历共享系统的目标就是把病历的“所有权”还给患者把“查看权”交给患者信任的医生。放到毕业设计的语境里这个项目背后的价值可以归纳成三点一是业务链条完整。系统覆盖患者注册、医生接诊、病历录入、共享申请、授权审批、病历查看、操作追踪闭环很长能展示你全栈设计能力。二是权限需求多样。同一个系统里患者、普通医生、科室负责人、医院管理员、系统管理员看到的页面和能操作的功能完全不同这正是RBAC权限模型最天然的落地场景。三是数据关系复杂。病历和患者、医生、医院、共享申请、操作日志之间都有明确的依赖关系数据库设计难度适中既不会简单到没东西写也不至于复杂到做不完。1.2 技术栈选型为什么最终选择了SpringBoot MyBatis-Plus后端框架可选很多但SpringBoot几乎是当前毕业设计场景的“标准答案”。理由很朴素官方文档多、社区教程多、第三方生态完善遇到问题随手能查到解决方案这一点在赶论文的时候比什么都重要。持久层我推荐MyBatis-Plus而不是原生MyBatis或者Spring Data JPA。很多人纠结选型我给出的理由非常实际MyBatis-Plus在MyBatis基础上封装了通用CRUD方法单表增删改查不需要手写SQL节省大量时间。它支持LambdaQueryWrapper做条件查询、分页查询极其方便比如“根据患者姓名模糊搜索”这种需求链式调用几行代码写完。国内面试和实际工作中碰到的技术栈MyBatis系的比例明显偏高写在简历上比JPA更有辨识度。数据库选MySQL是绝大多数人的选择原因不用多说。我建议额外引入Redis哪怕只做验证码缓存和Token黑名单也能让设计文档里的“高可用”“分布式会话”词汇变得有据可依。1.3 角色与权限模型设计我见过很多同学把权限写死在if else里比如“当前用户是患者就跳转A页面”一旦角色增多代码直接变成一坨。这里推荐用经典的RBAC基于角色的访问控制模型三张核心表就能说清楚用户表存储账号、密码、昵称、角色ID角色表定义医生、护士、患者、管理员等权限表定义具体操作比如“病历_查看”“病历_导出”。中间加用户角色关联表和角色权限关联表就是完整的RBAC。实际开发时不需要把五张表全部做出来毕业设计更看重逻辑清晰。我的做法是简化成用户表带角色字段然后把“权限判断”集中在接口层拦截器里处理。举个典型场景医生A要查看患者B的病历系统先验证医生A的登录Token再验证医生A与患者B是否存在有效的共享授权记录两个条件同时满足才放行。这种“用户身份校验 业务数据授权校验”的双层校验机制是答辩时能不能把项目讲出深度的分水岭。2. 数据库设计表结构决定系统上限2.1 核心数据表一览数据库设计是评审最先关注的部分表字段是否合理能直接反映你有没有做过真实项目。我整理了一份常用核心表的字段设计你可以直接作为参考起点表名核心字段设计要点userid, username, password, role, status, create_time密码存MD5加盐或BCrypt密文不要存明文patientid, user_id, real_name, id_card, phone, address关联用户表一患者一用户doctorid, user_id, real_name, hospital_id, dept_id, title医生与医院、科室关联medical_recordid, patient_id, doctor_id, record_type, diagnosis, content, create_time核心业务表后面单独讲sharing_recordid, patient_user_id, doctor_user_id, record_id, status, expire_time共享授权记录表关键设计见2.3operation_logid, user_id, action, target_type, target_id, detail, create_time审计日志答辩加分项主键一律用自增id还是雪花ID建议直接使用MyBatis-Plus自带的ASSIGN_ID雪花算法生成避免后期分表时主键冲突这一点在你写论文时可以展开讲。关联字段比如patient_id、doctor_id、hospital_id我建议不建物理外键而是通过普通索引和业务级校验维护一致性。理由是外键会让插入、更新、删除操作产生额外锁真实项目里也很少用物理外键用逻辑关联更灵活。2.2 病历表设计里那些容易被忽视的坑medical_record表是整个系统的数据核心。不少同学一上来就设计成“一条记录代表一次就诊”既存主诉、诊断又存医嘱、检查结果最后所有内容堆在几个大文本字段里看起来能跑实际上把关系型数据库用成了文档库。我的建议是拆开设计至少分成主表和明细表两部分。主表存一次就诊的概要信息患者ID、接诊医生ID、就诊类型、就诊时间、主诉、初步诊断。明细表存具体的检查项目、药品处方、检验结果等主从表之间通过medical_record_id关联。这样一来列表页查概要只需要扫主表详情页再根据ID带出明细性能比全表TEXT字段好得多。还要考虑病历的“版本”问题。医生录入病历后患者在其他医院就诊时原医院又补充了诊断结果如果直接覆盖之前的记录就丢了。我的处理方式是增加一个version字段每次修改生成新记录或者额外存储修改历史前端查看时默认加载最新版本但可以展开历史版本。这个设计思路虽然简单但能有效拉开你和其他“CRUD选手”的差距。2.3 共享授权表整个系统的“灵魂”所在很多人把共享做成了“患者点一下分享生成一个链接医生打开链接看”。这完全不符合医疗场景的合规要求。共享必须具备明确的授权对象、授权时间、授权范围和状态流转所以必须单独设计一张共享记录表。我的共享授权表字段如下id主键patient_user_id患者用户ID表示“谁来授权”doctor_user_id被授权的医生用户ID表示“授权给谁”record_id授权指向的病历ID为空则代表该患者全部病历可查看status0待患者确认1已通过2已拒绝3已撤销expire_time授权截止时间超过后自动失效create_time、audit_time申请时间和处理时间表设计好之后共享的完整流程就非常清晰了医生发起共享申请插入一条status为0的记录。患者在“共享申请列表”中看到该申请点击同意后status变为1。医生查看病历时系统校验该医生与患者之间存在status1且未过期的共享记录校验通过才返回数据。患者可以在系统中随时撤销授权status变为3。这套状态机模型解决了“谁允许谁看什么看到什么时候”的问题也是你项目答辩时最值得深挖的业务亮点。3. 后端实战从搭骨架到核心接口实现3.1 项目初始化和配置文件创建SpringBoot项目时我习惯用Maven管理依赖。下面是我整理的核心pom依赖你可以对照自己的版本调整dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependencyapplication.yml里最需要注意的是数据库时区、编码和MyBatis-Plus配置spring: datasource: url: jdbc:mysql://localhost:3306/emr_share?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: assign_id logic-delete-field: deleted为什么url里要加serverTimezoneAsia/Shanghai因为新版MySQL驱动默认时区是UTC不加这个配置本地连数据库时经常会报server timezone异常浪费大量排查时间。3.2 登录认证用JWT代替Spring Security我们常用到Spring Security但对毕业设计来说Spring Security的过滤器链路、配置方式、密码加密方案都过于繁琐答辩时很难在三分钟内讲清楚。我推荐改用JWT 拦截器能更直观地展示你掌握认证原理代码量也少得多。登录接口的逻辑很简单用户名密码校验成功生成一个包含用户ID和角色的JWT字符串返回给前端。前端请求需要认证的接口时在HTTP头里携带Authorization: Bearer 。后端写一个HandlerInterceptor在preHandle方法里解析JWT并校验有效期校验通过就把userId放进ThreadLocal或请求头方便Controller直接取用。核心拦截器逻辑可以这样写Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); if (JwtUtils.verify(token)) { String userId JwtUtils.getUserId(token); request.setAttribute(currentUserId, userId); return true; } } response.setStatus(401); return false; } }这里有个很常见的坑拦截器放行路径没配置好。登录、注册、验证码获取这些接口不能被拦截否则你永远登录不进去。建议在注册拦截器的配置类里先放行/login、/register、/captcha以及静态资源路径再拦截其他路径。3.3 病历查询与CRUD接口实现MyBatis-Plus最香的地方就是写单表CRUD时几乎不需要SQL映射。定义一个实体类再用BaseMapper继承一下Data TableName(medical_record) public class MedicalRecord { TableId(type IdType.ASSIGN_ID) private Long id; private Long patientId; private Long doctorId; private String recordType; private String diagnosis; private String content; private LocalDateTime createTime; }Controller里查询列表时用LambdaQueryWrapper动态拼接条件GetMapping(/list) public Result list(RequestParam Long patientId, RequestParam(required false) String keyword, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { LambdaQueryWrapperMedicalRecord wrapper Wrappers.lambdaQuery(); wrapper.eq(MedicalRecord::getPatientId, patientId); if (StrUtil.isNotBlank(keyword)) { wrapper.like(MedicalRecord::getDiagnosis, keyword) .or().like(MedicalRecord::getContent, keyword); } PageMedicalRecord pageResult medicalRecordMapper.selectPage(new Page(page, size), wrapper); return Result.ok(pageResult); }强烈建议统一返回Result对象包含code、message、data三个字段。这样前端统一处理成功失败逻辑后端项目显得更规范。我见过不少同学的Controller里到处返回Map最后前端对接时命名对不上极其痛苦。3.4 共享申请和授权校验的核心流程共享申请流程需要两张接口相互配合。医生端调用申请接口插入一条共享记录状态为0PostMapping(/apply) public Result apply(RequestBody ShareApplyRequest req) { SharingRecord rec new SharingRecord(); rec.setPatientUserId(req.getPatientUserId()); rec.setDoctorUserId(CurrentUser.getId()); rec.setRecordId(req.getRecordId()); rec.setStatus(0); rec.setExpireTime(LocalDateTime.now().plusDays(7)); sharingRecordMapper.insert(rec); return Result.ok(); }患者端查看待处理申请点击同意后更新状态为1。这部分代码不多但务必加上逻辑校验只有申请记录的patientUserId等于当前登录用户ID才允许操作否则存在严重越权漏洞。医生查看病历时除了走拦截器校验登录状态还要在业务层再查一次共享记录boolean authorized sharingRecordMapper .exists(new LambdaQueryWrapperSharingRecord() .eq(SharingRecord::getPatientUserId, patientId) .eq(SharingRecord::getDoctorUserId, currentDoctorId) .eq(SharingRecord::getStatus, 1) .gt(SharingRecord::getExpireTime, LocalDateTime.now())); if (!authorized) { return Result.fail(无访问权限); }这层校验一定要放在Service里只依赖拦截器是不够的。拦截器只能证明“你是合法登录用户”不能证明“你有权查看这条病历数据”。3.5 用Redis缓存验证码和热点数据毕业设计里加Redis不需要做太复杂的分布式缓存只需要两个亮点第一登录时用图形验证码或短信验证码验证码存到Redis里设置5分钟过期。比存session更规范也更符合分布式应用场景。第二把系统里查询频次高、更新频率低的配置数据放进缓存。比如常见的就诊类型字典、科室列表直接用Spring自带的Cacheable注解即可。Cacheable(value dict, key #type) public ListDictItem getDictByType(String type) { return dictMapper.selectList(...); }为了防止缓存穿透空值也要存储或者设置短过期时间。这个细节一讲答辩评委就会知道你真正理解缓存而不是只会在项目简介里堆名词。4. 从“能跑”到“能过答辩”的进阶细节4.1 跨医院共享怎么做数据隔离很多同学的共享只做了“同一个系统里不同用户”的共享还没有真正体现“跨医院”。我的做法是用户表上增加hospital_id字段所有业务数据都通过这个字段进行逻辑隔离。比如医生A属于医院X患者B的病历来自医院Y当医院X的医生请求查看医院Y的病历时共享记录只需要明确记录请求方医生ID、归属方患者ID即可。查看病历时后端返回的数据里并不需要暴露其他与该病历无关的患者信息。这样设计既简单又安全答辩时可以配合流程图讲解“数据不落库只做授权映射”的思路。4.2 检查报告文件上传与附件管理病历系统中经常要上传检验报告、影像图片。Spring Boot接收MultipartFile存到服务器本地目录或者对象存储再把文件访问路径写入attach表。附件表字段一般是id、record_id、file_name、file_url、file_size、upload_time。文件上传有几个安全细节需要注意限制扩展名只允许jpg、png、pdf、docx等白名单类型。限制文件大小Spring Boot在application.yml里用max-file-size和max-request-size配置。存储路径不要包含用户上传时的原始路径统一用UUID重命名文件避免路径穿越攻击。4.3 操作日志与审计病历属于敏感数据任何查看行为都应当被记录。用Spring AOP做个切面拦截标注了LogAction注解的Controller方法在方法执行后记录操作人、操作类型、目标对象和操作时间。日志表字段比较简单核心是user_id、action、target_type、target_id、detail。这里多说一句不要在日志表里存明文密码也不要在detail里存完整的病历文本只存摘要或业务ID否则日志表越来越大本身也变成了隐私风险点。4.4 前端页面的快速搭建建议如果前端不想从零写可以直接用Vue ElementUI做后台管理系统套壳。不需要复杂到微前端按角色拆成患者端、医生端、管理端三个页面组即可。登录页面埋一套Token管理逻辑患者端重点展示“我的病历”“共享申请”“授权记录”医生端重点展示“患者查询”“发起共享”“病历查看”“共享中列表”。前端调用后端时统一在axios请求拦截器里加Authorization头在响应拦截器里处理401跳转登录页。这些代码网上模板很多但想拿高分我建议至少手写一遍请求和响应拦截器面试和答辩被问到的概率极高。5. 常见问题与排查技巧实录5.1 数据库连接报timezone异常这个问题几乎每隔几届都会有人踩。解决办法有三个我推荐第二个数据库连接url加serverTimezoneAsia/Shanghai使用MySQL Connector/J 8.0以上并把驱动类换成com.mysql.cj.jdbc.Driver在数据库端执行set global time_zone 8:00排查方法很简单启动项目看报错第一行只要出现serverTimezone关键词直接去改url参数就能解决。5.2 跨域问题导致前后端联调失败前后端分离开发时前端地址是localhost:5173后端地址是localhost:8080端口不同就会触发跨域。表现形式是浏览器里POST请求发送后报CORS error或者axios拿不到响应。直接的解法是配置CorsFilterConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意如果你同时使用了拦截器或Spring Security跨域配置必须放在最外层否则预检请求OPTIONS会被拦截器拦截直接返回401这种坑排查起来很容易心态爆炸。5.3 MyBatis-Plus分页不生效很多人配置了PaginationInnerInterceptor之后分页查询始终返回全量数据。多半是在高版本中没配new PaginationInnerInterceptor(DbType.MYSQL)或者没有把插件放进MybatisPlusInterceptor里。正确写法Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }5.4 时间字段格式化不对齐后端返回LocalDateTime格式是“2024-12-20T10:30:00”前端拿到带T的字符串显示很丑。在字段上加JsonFormat注解解决。JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;批量处理的话也可以统一在application.yml里配置spring.jackson.date-format和time-zone让全局生效。5.5 逻辑删除字段导致查询数据莫名丢失使用TableLogic注解配置逻辑删除之后MyBatis-Plus会自动在查询条件里拼接deleted0。如果你在数据库表里没有手动加deleted字段插一条数据时这个字段值为NULL查询时NULL和0不相等数据就“消失”了。所以逻辑删除字段一定要设置默认值为0或者插入时显式赋值。6. 写在最后的几点实操体会这个项目我实际跟做过好几轮最大的感受是技术难点并不在某个框架的API调用上而在于把“共享授权”这条业务线想通透。只要你愿意把共享状态机画清楚把权限校验写完整把日志审计加上这个系统的完成度会明显超出普通毕业设计一大截。答辩前我建议你把这几类用例准备好患者注册并上传基本信息、医生创建病历、医生申请跨院共享、患者同意授权、医生读取共享病历、患者撤销授权、管理员查看操作日志。演示的时候按这条主线走评委能非常直观地看到整个系统的业务闭环。再提醒一句项目文档里一定要写清楚“共享记录表”和“授权状态机”的设计理由。很多同学答辩被问倒不是代码运行失败了而是说不出“为什么这张表要加状态字段”“为什么病历和共享记录不合并”。回答的时候抓住“可追溯、可控制、可撤销”这三点基本不会出差错。最后给你一个小建议把项目的SQL脚本、初始化数据写到项目的resources目录里再写一条清晰的README说明如何建库、改配置、启动。技术能力到答辩阶段大家的差距不会特别夸张谁的项目文档干净、演示流畅、边界问题想得全谁就能拿到不错的评价。这个习惯也会让你以后进团队协作时少挨很多骂。