简介面向Java毕业设计的医疗服务系统项目后端采用Spring Boot框架环境搭配MySQL 5.7、Tomcat 7、Maven 3.3.9覆盖管理员、乡村医生、普通村民三类角色权限划分清晰。管理端包含首页、个人中心、普通村民管理、乡村医生管理、公告信息管理、乡村诊室管理、健康档案管理、学习培训管理、考核信息管理、医疗地图管理、医疗药品管理、类型信息管理、购买信息管理、留言板管理等功能乡村医生可维护诊室、健康档案、紧急事故、学习培训、考核信息与购买信息普通村民则支持健康档案查看、紧急事故上报和留言板反馈。前台首页还提供公告信息、医疗地图、医疗药品、留言反馈、个人中心、后台管理及医生咨询入口完整呈现从后台管理到前台操作的业务闭环。资源为ZIP压缩包大小约71.74MB内含Spring Boot完整源码、LW文档、PPT答辩幻灯片及操作演示视频源码按角色与业务模块分层组织目录清晰便于二次开发与答辩准备。目前已有166人学习下载适合计算机专业学生作为毕业设计或课程设计与答辩参考。1. Spring Boot医疗服务系统先想清楚它可以被复用的边界在java方向的毕业设计里“Spring Boot源码LWPPT视频”几乎是医疗服务系统最常见的交付形态。很多人的做法是把管理系统里的用户、订单换成患者、科室和挂号记录然后直接堆CRUD结果答辩时被问到“号源扣减怎么保证不超卖”“为什么选JPA不用MyBatis”就卡住了。这套组合本身没有问题Spring Boot负责把REST API快速跑通LW负责把设计过程讲清楚PPT和视频负责把成果演示出来。真正决定项目有没有信息量的是你在写代码之前有没有把领域模型和核心业务流程想清楚。我会按自己做这类系统时的顺序来写先定数据模型和表结构再搭工程骨架然后实现登录、挂号和排班最后把源码往答辩材料方向收口。2. 医疗服务系统的数据模型与模块拆分先从ER图走向建表SQL医疗服务系统的用户角色天然分成患者、医生、管理员三类。在代码里面我习惯先定包结构边界再写表结构。因为表结构一旦定错后面所有Service和Controller都要跟着返工。下面这一章会把“有哪些表、表之间怎么关联、字段怎么取舍”一次说清顺带把JPA和MyBatis的选型理由讲透方便你在LW里直接引用。2.1 用包结构界定患者端、医生端和管理端的三段边界后端规模通常在10张表以内时不需要拆分微服务甚至不需要拆多模块。常见做法是一个单工程下按业务拆包controller/patient、controller/doctor、controller/adminService和Repository也按同样方式分组。这样做的好处很直接答辩演示时能说清楚“患者端走哪些接口、医生端走哪些接口”权限配置也能按照包路径去写拦截规则。用户表需要同时服务三种角色登录时用的账号密码个人资料里的姓名和手机号以及判断接口权限的角色字段。我通常把账号信息放在sys_user表把患者的扩展信息放在patient表通过user_id关联。医生和管理员直接复用sys_user不需要建三张结构相同的账号表。这样LW里的ER图也清爽很多。2.2 从ER图到建表SQL核心表与关联字段怎么定2.2.1 核心表的字段边界与逻辑删除表名核心字段与上游的关系departmentid、name、parent_id、sort、deleted科室支持两级分类doctorid、user_id、department_id、title、intro医生挂在科室下scheduleid、doctor_id、department_id、work_date、shift、total、remaining、version排班表一个医生一天可有多个班次registrationid、registration_no、patient_id、schedule_id、status、create_time挂号记录状态流转驱动业务排班表schedule要把“号源总数”和“剩余号源”同时存下来不要在查询时通过count(registration)去反推否则列表页会随着数据量变大越查越慢还要处理已取消记录的过滤逻辑。字段名统一用下划线命名Java实体类通过Column(name work_date)映射到驼峰属性。所有核心表都加deleted逻辑删除字段用0和1表示有效与删除这样在LW的数据库设计章节里能多写一段“基于软删除的数据保留策略”。2.2.2 建表SQL示例排班表和挂号表-- 排班表一个医生 一个日期 一个班次 对应一条记录 CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT 医生ID, department_id BIGINT NOT NULL COMMENT 科室ID冗余便于按科室查询, work_date DATE NOT NULL COMMENT 出诊日期, shift TINYINT NOT NULL COMMENT 班次1上午 2下午, total INT NOT NULL DEFAULT 20 COMMENT 号源总数, remaining INT NOT NULL DEFAULT 20 COMMENT 剩余号源, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0有效 1已删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_doctor_date_shift (doctor_id, work_date, shift, deleted) ) COMMENT 医生排班表;注意uk_doctor_date_shift这个唯一键同一个医生在同一天的同一个班次只能有一条有效记录这是防止重复排班的第一道防线。department_id是冗余字段因为按科室查排班时如果每次都去doctor表关联一次查询语句会多一层join列表性能不划算。version字段为第四章的乐观锁扣减做准备。CREATE TABLE registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, registration_no VARCHAR(32) NOT NULL COMMENT 挂号单号业务编号, patient_id BIGINT NOT NULL COMMENT 患者ID, schedule_id BIGINT NOT NULL COMMENT 排班ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待就诊 1已完成 2已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_reg_no (registration_no) ) COMMENT 挂号记录表;registration_no建唯一键而不只靠自增id是因为医院业务里需要把挂号单号打印出来或者拿它做条码。生成规则可以是yyyyMMddHHmmss 四位随机数在Java代码里本地生成后插入避免依赖数据库序列。2.3 JPA还是MyBatis不解释清楚就答辩必踩坑这个问题几乎每次答辩都会被问到。如果对Java的对象关系映射比较熟建议用Spring Data JPA实体类和表结构的对应关系直接Service层可以少写大量SQL。如果更习惯手写SQL或者说不清ManyToOne(fetch FetchType.LAZY)的含义就老实选MyBatis。毕业设计最忌讳的是两个都引入把spring-boot-starter-data-jpa和mybatis-spring-boot-starter同时加到pom里最后被两套会话工厂来回切换折腾到崩溃。用JPA时要特别留意懒加载导致的N1查询。比如查询医生列表并展示排班信息如果直接通过关系属性get控制台会打印出几十条select。应对方式是用EntityGraph(attributePaths {department})或者写JPQL里的join fetch。LW中可以把这写成“基于JPA的查询优化”这是加分点。3. 用Spring Boot搭建医疗服务系统的最小可运行工程Spring Boot能省事靠的是把数据库、Web、安全相关的初始化工作收敛到几个starter里。医疗服务系统用到的组件并不特殊标准Web工程加MySQL、JPA、Security就足够支撑完整源码交付。这一章给出从创建工程到启动自测的可复现步骤。3.1 创建工程和pom.xml的最小依赖集合直接在 start.spring.io 生成基础工程Java选8或17都可以。依赖选择 Spring Web、Spring Data JPA、Spring Security、Validation、MySQL Driver后面写JWT时再手动补三个jjwt的artifact。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies版本统一交给spring-boot-starter-parent管理MySQL驱动用runtime scope打包时不会混进编译路径。需要留意的坑是Spring Boot 2.7与3.x的包名差异——2.7用javax.*3.x用jakarta.*。现在很多源码模板还停留在2.7如果决定用3.x写代码时所有javax.persistence.*和javax.servlet.*的导入都要换成jakarta.*。3.2 application.yml里的3个必调参数spring: datasource: url: jdbc:mysql://localhost:3306/medical?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true open-in-view: false jackson: time-zone: GMT8 date-format: yyyy-MM-dd HH:mm:ss server: port: 8080serverTimezoneAsia/Shanghai必加MySQL驱动8.x之后URL里不带时区会直接报The server time zone value异常加了之后Java侧LocalDateTime的读写才不会偏移。ddl-auto: update在开发阶段能省掉手动建表步骤但到提交源码阶段我一般会改成none并把建表SQL放到src/main/resources/sql目录下。open-in-view: false是为了避免Controller层渲染时还开着数据库会话关掉之后强迫Service层把数据查好减少之后做前后端分离时遇到LazyInitializationException的概率。提示把ddl-auto从 update 改成 none 时记得先导出一份完整的建表脚本放进项目不然换一台Windows或Linux机器运行时应用会直接报表不存在。3.3 从启动到自测验证服务已连上数据库mvn spring-boot:run # 另开一个终端 curl http://localhost:8080/api/health启动时看日志关键字出现Started MedicalApplication in X.XXX seconds表示常规启动成功出现Access denied for user说明账号或密码配置有误出现Unknown database需要先到MySQL里执行create database medical default character set utf8mb4;。健康检查接口可以直接在Controller里返回固定字符串也可以引入spring-boot-starter-actuator用/actuator/health。后者在LW里可以写成“基于Actuator的服务健康检查”更专业。4. 让源码能答辩登录、挂号和医生排班的核心实现工程骨架搭好后核心工作量集中在三个业务点登录认证、挂号扣减、条件查询分页。这三个点也正好是答辩时最容易被追问的地方。下面的写法是让我自己至少能自圆其说的版本。4.1 用JWT代替Session落到Security配置里毕业设计八股文里必问的“JWT和Session的区别”在代码里的落点其实很简单Session靠服务端内存保存状态重启后用户登录态就丢了JWT把用户标识和过期时间签进token服务端不需要存会话更适合后面把管理端和患者端拆成独立前端项目的场景。4.1.1 JwtUtil的生成与校验public class JwtUtil { private static final SecretKey KEY Keys.hmacShaKeyFor( medical-system-jwt-secret-key-2025.getBytes(StandardCharsets.UTF_8)); public static String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .setExpiration(new Date(System.currentTimeMillis() 1000L * 60 * 120)) .signWith(KEY, SignatureAlgorithm.HS256) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder().setSigningKey(KEY).build() .parseClaimsJws(token).getBody(); } }setSubject应该放用户id而不是用户名因为在鉴权过后的接口里要拿唯一标识去查“当前用户是谁”。过期时间给了120分钟医疗服务系统里医生和患者的使用习惯是“登录一次在数小时内完成操作”如果做成在线问诊类应用建议缩到15分钟并配合刷新令牌。jjwt需要引入jjwt-api、jjwt-impl、jjwt-jackson三个artifact版本以Maven中央仓库当前稳定版为准缺一个启动时会报ClassNotFoundException。4.1.2 认证过滤器与放行规则Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header request.getHeader(Authorization); if (StringUtils.hasText(header) header.startsWith(Bearer )) { try { Claims claims JwtUtil.parseToken(header.substring(7)); UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(claims.getSubject(), null, List.of()); auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(auth); } catch (Exception e) { SecurityContextHolder.clearContext(); } } chain.doFilter(request, response); } }这个过滤器的逻辑是在请求进入Controller之前从Authorization头里取出token并解析。解析成功就放入SecurityContextHolder后续Controller可以通过AuthenticationPrincipal拿到当前用户id解析失败则清空上下文最终由安全框架统一返回401。登录接口、科室列表这类公开内容要通过SecurityConfig里的requestMatchers().permitAll()放行否则会拦截登录请求形成死循环。4.2 挂号业务号源扣减的乐观锁写法挂号不是简单insert它包含“检查排班是否有号”和“扣减剩余号源”两步。如果两个患者同时点到最后1个号先检查再扣减就会超卖。不依赖synchronized因为单机锁在将来部署多实例时失效。更稳的写法是把扣减条件放进SQLModifying Query(update Schedule s set s.remaining s.remaining - 1, s.version s.version 1 where s.id :scheduleId and s.remaining 0 and s.deleted 0) int deductRemaining(Param(scheduleId) Long scheduleId);这段JPQL返回的是受影响行数等于1说明扣减成功等于0说明号源已满Service层直接抛出“该时段号源已满”业务异常。很多模板源码是先在Service里查一遍remaining再做if判断演示时没问题但答辩老师一问“并发场景下两个事务都读到 remaining1 怎么办”如果源码里没有这个update条件就很难圆场。挂号事务的边界要注意扣减号源和创建挂号记录要放在同一个事务里。Service方法上加Transactional让deductRemaining先执行再saveRegistration如果第二步失败整个事务回滚号源扣减一并回滚数据保持一致。4.3 排班查询Specification条件组合与分页参数public PageSchedule searchSchedule(Long departmentId, String doctorName, LocalDate workDate, int page, int size) { SpecificationSchedule spec (root, query, cb) - { ListPredicate predicates new ArrayList(); predicates.add(cb.equal(root.get(deleted), 0)); if (departmentId ! null) { predicates.add(cb.equal(root.get(departmentId), departmentId)); } if (StringUtils.hasText(doctorName)) { JoinSchedule, Doctor doctor root.join(doctor); predicates.add(cb.like(doctor.get(name), % doctorName %)); } if (workDate ! null) { predicates.add(cb.equal(root.get(workDate), workDate)); } return cb.and(predicates.toArray(new Predicate[0])); }; Pageable pageable PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, workDate, shift)); return scheduleRepository.findAll(spec, pageable); }这段代码解决科室、医生姓名、出诊日期三个可选条件的组合查询。Specification转成SQL时使用预编译参数不会出现字符串拼接注入的问题。分页参数page在Spring Data里从0开始如果前端从1开始传Controller层要做page - 1转换否则第一页数据会被跳过。排序规则里workDate和shift都是升序这样同一天上午下午的排班会按顺序展示。如果后续需求要支持“只看有号”的排班只需再加一个cb.gt(root.get(remaining), 0)条件。5. 把源码变成可交付的毕业设计验证方法与3个排错点5.1 在源码里留下可答辩的“工程痕迹”评审老师看源码最关注三件事异常有没有统一处理、核心业务有没有注释、有没有测试。建议在项目里加一个RestControllerAdvice全局异常处理器把业务异常、参数校验异常和兜底Exception分开返回返回体统一为{code, message, data}。在挂号Service里给号源扣减方法写清楚“为什么不用先查再改”的注释。这两处是LW里写“系统可靠性与容错设计”时直接对照的代码素材。另外给RegistrationService写一个简单的SpringBootTest用例验证“号源扣减后剩余数减1”哪怕只有一个核心测试也比完全没有测试可讲要好。5.2 3个高频启动报错的定位方法java.sql.SQLSyntaxErrorException优先检查表名或字段名是不是MySQL保留字。schedule、order这类词在部分MySQL版本里需要反引号包裹或者把表名改成doctor_schedule。Cannot determine embedded database driver class没有引入MySQL依赖或者spring.datasource配置被注释掉。检查pom里是否有mysql-connector-j。Field id doesnt have a default value手动导入SQL时表结构里主键没有自增。确认建表语句里是id BIGINT PRIMARY KEY AUTO_INCREMENT而不是只写了PRIMARY KEY。5.3 用curl验证完整的挂号链路# 登录获取JWT TOKEN$(curl -s -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:patient01,password:123456} | jq -r .data.token) # 查询排班第1页每页10条 curl -s http://localhost:8080/api/schedule?departmentId1page1size10 \ -H Authorization: Bearer $TOKEN # 提交挂号 curl -s -X POST http://localhost:8080/api/registration \ -H Authorization: Bearer $TOKEN -H Content-Type: application/json \ -d {scheduleId:1}把这三条命令跑通说明登录、权限、列表查询、写库已经串成一条完整链路。之后再拿这条流程去录PPT里的演示视频并把页面上展示的字段和LW中的表结构对着讲一遍整个医疗服务系统的源码交付才算收口。本文还有配套的精品资源点击获取