Spring Boot+MyBatis-Plus社区医疗预约挂号系统设计与实现
2026/8/27 7:12:14 网站建设 项目流程

每年到了毕业设计选题季,后台总会有同学来问类似的问题:“老师要求做一个 Web 系统,自己 Java 基础一般,不想选烂大街的商城和博客系统,有没有功能完整、好写、答辩又容易讲清楚的选题?”如果你也在纠结这个问题,社区医疗预约挂号平台是一个很合适的切入点。

原因很简单:这个选题的业务场景真实,功能边界清晰,既包含用户注册登录、医生排班、预约挂号这样常规的业务模块,又包含“号源锁定”“防重复提交”“分时段放号”等能拿出来当亮点的核心逻辑。用 Spring Boot + MyBatis-Plus + MySQL 这套主流技术栈就能完成,非常适合 Java 方向的同学作为计算机毕设选题。

这篇文章会从选题价值分析开始,给出系统架构设计、数据库表结构、核心功能代码、部署测试流程、答辩常被问到的问题和项目扩展思路,目的是让新手能照着搭,让有一定基础的同学能直接改成自己的完整项目。

文章内容适合以下几类读者:

  • 正在做计算机毕设选题、需要快速确定系统方案的在校生;
  • 想用 Spring Boot 做一个完整项目练手的 Java 初学者;
  • 准备在简历上写“预约挂号系统”项目经验、但还没理清实现方案的求职者。

1. 为什么社区医疗预约挂号平台适合做毕设

1.1 这个选题解决什么问题

在社区医疗场景中,患者挂号通常是线下排队或者电话预约,存在几个明显问题:

  • 患者不知道某个科室当前有哪些医生出诊;
  • 到了医院才发现号源已满,白跑一趟;
  • 医生排班信息靠人工管理,变更后消息同步不及时;
  • 缺少预约记录,患者无法查看自己之前的挂号历史。

社区医疗预约挂号平台要解决的就是“患者能在线查看科室和医生,按时间选择号源,预约后能查询记录,管理员能管理医生排班和号源”这样一个完整的业务闭环。

1.2 这个选题在毕设中的优势

第一,功能规模适中。不需要做复杂算法,也不涉及高并发高可用,但该有的开发流程都有:需求分析、数据库设计、接口开发、前后端联调、部署运行。

第二,技术栈主流。Spring Boot + MySQL 的组合在简历上很常见,项目所用的技术点不会和工作岗位要求脱节。

第三,容易扩展。等你完成基础版本之后,还可以加 Redis 缓存、定时任务、消息队列、微信小程序端等,从毕设变成求职项目基本是顺畅的。

第四,答辩阶段好讲。预约挂号平台的业务逻辑直观,评审老师不需要你解释“这个项目为什么要存在”,所有模块都是为了解决实际挂号问题,逻辑说服力很强。

2. 系统功能模块与整体技术方案

2.1 用户角色划分

系统按角色可以分成三类:

  • 患者(普通用户):注册登录、查看科室和医生、在线预约、取消预约、查看个人预约记录。
  • 医生:查看自己的排班信息、查看被预约的患者列表。
  • 管理员(系统管理员):维护用户信息、管理科室、管理医生排班、设置号源数量、查看预约统计。

2.2 功能模块拆解

一个能正常演示的毕设版本至少需要包含这些模块:

模块功能说明
用户模块注册、登录、退出、个人信息修改
科室管理科室列表、科室详情、支持多级科室或单级科室
医生管理医生列表、医生所在科室、医生擅长领域、医生排班状态
排班管理管理员发布排班,设置日期、时段、总号源、剩余号源
预约挂号选择科室 -> 选择医生 -> 选择日期时段 -> 确认预约
预约记录患者查看历史预约、当前预约、取消预约
医生端视图医生查看本人排班和对应预约患者信息
数据统计管理员查看每日预约量、科室预约量排行

2.3 技术栈选择

项目以 Java 为主,推荐如下组合:

  • JDK:1.8 或 11(根据本机环境选择,新版 Spring Boot 3.x 需要 JDK 17,这里以常规 JDK 1.8 为例)
  • 框架:Spring Boot 2.7.x
  • ORM:MyBatis-Plus(简化单表 CRUD,减少代码量)
  • 数据库:MySQL 5.7 或 8.0
  • 权限认证:Spring Security + JWT(也可以先用拦截器+Hutool JWT 简化处理)
  • API 文档:Swagger / Knife4j
  • 项目构建:Maven
  • 前端:可以用 Vue + Element UI,也可以直接用 Thymeleaf 写页面,根据个人基础选择

版本需要根据你的项目实际情况调整,这里给出的是常见毕设组合,重点演示后端接口与配置思路,前端不是核心。

2.4 为什么选择 Spring Boot + MyBatis-Plus

Spring Boot 解决了配置繁琐的问题,内嵌 Tomcat,最终打包成 jar 直接运行。MyBatis-Plus 在 MyBatis 基础上提供了 BaseMapper、Wrapper 查询、分页插件等能力,写单表 CRUD 基本不需要手写 SQL,这对毕设开发效率提升非常明显。

3. 数据库设计与核心表结构

数据库设计是答辩时几乎一定会被问到的问题。建议表不要设计得太多,过度拆分反而难维护。这里我给出一个稳定的核心方案。

3.1 核心数据表清单

  • user:用户表(患者或后台管理员都放在这里,用 role 字段区分)
  • department:科室表
  • doctor:医生表
  • schedule:医生排班表(一个排班对应一个日期时段,包含总号源和剩余号源)
  • appointment:预约记录表

3.2 建表 SQL 示例

以下 SQL 基于 MySQL 8.0,字符集使用 utf8mb4。

CREATE DATABASE IF NOT EXISTS community_hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE community_hospital; -- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', password VARCHAR(100) NOT NULL COMMENT '加密后的密码', real_name VARCHAR(50) COMMENT '真实姓名', phone VARCHAR(20) COMMENT '手机号', role TINYINT NOT NULL DEFAULT 1 COMMENT '角色:1-患者,2-医生,3-管理员', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-正常,0-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) COMMENT '系统用户表'; -- 科室表 CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(100) NOT NULL COMMENT '科室名称', dept_desc VARCHAR(500) COMMENT '科室介绍', sort_order INT DEFAULT 0 COMMENT '排序', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '科室表'; -- 医生表 CREATE TABLE doctor ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '关联sys_user.id', dept_id BIGINT NOT NULL COMMENT '关联department.id', title VARCHAR(50) COMMENT '职称,如主任医师', specialty VARCHAR(500) COMMENT '擅长领域', introduction TEXT COMMENT '医生简介', doctor_photo VARCHAR(255) COMMENT '头像URL', status TINYINT DEFAULT 1 COMMENT '状态:1-出诊,0-停诊', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '医生信息表'; -- 排班表 CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT '医生ID', dept_id BIGINT NOT NULL COMMENT '科室ID', schedule_date DATE NOT NULL COMMENT '出诊日期', time_slot TINYINT NOT NULL COMMENT '时段:1-上午,2-下午', total_count INT NOT NULL DEFAULT 20 COMMENT '总号源', remain_count INT NOT NULL DEFAULT 20 COMMENT '剩余号源', status TINYINT DEFAULT 1 COMMENT '状态:1-可预约,0-已停诊', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_doctor_date_slot (doctor_id, schedule_date, time_slot) ) COMMENT '医生排班表'; -- 预约记录表 CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '预约患者ID', doctor_id BIGINT NOT NULL COMMENT '医生ID', dept_id BIGINT NOT NULL COMMENT '科室ID', schedule_id BIGINT NOT NULL COMMENT '关联排班ID', appointment_date DATE NOT NULL COMMENT '预约日期', time_slot TINYINT NOT NULL COMMENT '1-上午,2-下午', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待就诊,1-已完成,2-已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_schedule_id (schedule_id) ) COMMENT '预约记录表';

3.3 表设计中的关键点

这里要重点理解一个关系:排班表和预约记录表要分开设计。排班表维护的是“某个医生在某天、某个时段有多少号源”,预约记录表维护的是“哪个用户预约了哪个排班”。这样当号源变动时,只需要同步排班表的 remainder 字段,不会破坏用户预约历史。

为了防止同一患者重复预约同一个排班,可以在预约表加唯一索引(user_id, schedule_id),不过只加索引还不够,具体实现要配合业务层校验,后面代码部分会讲。

4. 项目环境搭建与基础结构

4.1 开发环境

  • 操作系统:Windows / macOS / Linux 均可
  • IDE:IDEA(社区版或旗舰版都行)
  • JDK:1.8
  • Maven:3.6 或以上
  • MySQL:5.7 / 8.0
  • 前端:Vue 或 Thymeleaf,后端接口必测

4.2 创建 Spring Boot 项目

在 IDEA 中通过 Spring Initializr 创建项目,坐标参考:

  • GroupId:com.example
  • ArtifactId:community-hospital
  • Java 版本:8
  • 打包方式:jar

依赖建议加入:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>

这里说明一下:MyBatis-Plus 3.5.x 自带分页插件,不需要单独引入分页依赖。Knife4j 是为了生成接口文档,方便测试接口,也方便答辩时演示。

4.3 配置文件 application.yml

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/community_hospital?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

4.4 统一返回结果类

为了让前端能统一处理接口返回结构,项目里定义一个 Result 类,这是项目开发中很常见的做法,也是答辩时可以提到的代码规范。

// 文件路径:src/main/java/com/example/hospital/common/Result.java package com.example.hospital.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> success() { return success(null); } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

4.5 全局异常处理

在实际项目开发中,如果每个 Controller 都用 try-catch,代码会非常冗余。更推荐的做法是定义一个全局异常处理器,将业务异常和系统异常统一处理。

// 文件路径:src/main/java/com/example/hospital/exception/GlobalExceptionHandler.java package com.example.hospital.exception; import com.example.hospital.common.Result; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result<Void> handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e) { e.printStackTrace(); return Result.error(500, "系统异常,请稍后重试"); } }

BusinessException 就是自定义业务异常,里面包含 code 和 message 两个字段,代码量很少,这里不再展开。

5. 核心功能代码实现

下面的代码均以“后端接口”为主线,文件路径按 Maven 标准结构写清楚。

5.1 用户注册与登录

登录逻辑本身不复杂,关键在于密码不能明文存储,登录成功后需要返回一个 token 给前端,后续请求通过 token 识别用户身份。

这里使用 Hutool 工具包生成一个简易 JWT,避免引入额外的 Spring Security 依赖,好处是代码简洁、便于讲解。

// 文件路径:src/main/java/com/example/hospital/controller/AuthController.java package com.example.hospital.controller; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.jwt.JWTUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.hospital.common.Result; import com.example.hospital.entity.SysUser; import com.example.hospital.mapper.SysUserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private SysUserMapper sysUserMapper; // 注册接口 @PostMapping("/register") public Result<Void> register(@RequestBody SysUser user) { Long count = sysUserMapper.selectCount( new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, user.getUsername())); if (count > 0) { return Result.error(400, "用户名已存在"); } // MD5 加盐加密,简单演示,实际项目推荐使用 BCrypt user.setPassword(DigestUtil.md5Hex(user.getPassword() + "hospital_salt")); user.setRole(1); sysUserMapper.insert(user); return Result.success(); } // 登录接口 @PostMapping("/login") public Result<Map<String, Object>> login(@RequestBody SysUser loginUser) { SysUser user = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, loginUser.getUsername())); if (user == null) { return Result.error(400, "用户不存在"); } String encrypted = DigestUtil.md5Hex(loginUser.getPassword() + "hospital_salt"); if (!encrypted.equals(user.getPassword())) { return Result.error(400, "密码错误"); } if (user.getStatus() == 0) { return Result.error(403, "账号已被禁用"); } // 生成 token,这里只放入 userId 和 username Map<String, Object> payload = new HashMap<>(); payload.put("userId", user.getId()); payload.put("username", user.getUsername()); String token = JWTUtil.createToken(payload, "community-hospital-secret".getBytes()); Map<String, Object> resultMap = new HashMap<>(); resultMap.put("token", token); resultMap.put("userId", user.getId()); resultMap.put("role", user.getRole()); resultMap.put("realName", user.getRealName()); return Result.success(resultMap); } }

实际项目登录密码建议使用 BCrypt,但毕设为了代码不过度膨胀,MD5 加盐也可以解释得通。答辩时如果老师问“密码这样安全吗”,你可以回答“生产环境推荐使用 BCrypt,本系统为演示方便使用 MD5 加盐,并将加密插件留出了替换接口”。

5.2 科室与医生查询

科室查询和医生查询都是典型的分页列表接口,使用 MyBatis-Plus 的 Page 对象非常方便。

// 文件路径:src/main/java/com/example/hospital/controller/DoctorController.java package com.example.hospital.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.hospital.common.Result; import com.example.hospital.entity.Doctor; import com.example.hospital.mapper.DoctorMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/doctor") public class DoctorController { @Autowired private DoctorMapper doctorMapper; // 分页查询医生列表,支持按科室ID筛选 @GetMapping("/page") public Result<Page<Doctor>> page( @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Long deptId) { LambdaQueryWrapper<Doctor> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(deptId != null, Doctor::getDeptId, deptId) .eq(Doctor::getStatus, 1) .orderByDesc(Doctor::getId); Page<Doctor> page = doctorMapper.selectPage(new Page<>(current, size), wrapper); return Result.success(page); } // 查看医生详情 @GetMapping("/{id}") public Result<Doctor> detail(@PathVariable Long id) { Doctor doctor = doctorMapper.selectById(id); return Result.success(doctor); } }

这里要注意一个点:当你分页查询医生列表时,前端通常需要展示“科室名称”,而 doctor 表里只有 deptId。有两种处理方式:第一种是连表查询,第二种是在返回结果中额外补充字段。为了降低复杂度,建议在 Doctor 实体中增加一个deptName的 @TableField(exist = false) 字段,然后通过业务层填充。

5.3 排班管理接口

排班是预约挂号系统的核心数据源,只有管理员能新增和修改,患者端只读。

// 文件路径:src/main/java/com/example/hospital/service/ScheduleService.java package com.example.hospital.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.hospital.entity.Schedule; import com.example.hospital.exception.BusinessException; import com.example.hospital.mapper.ScheduleMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class ScheduleService { @Autowired private ScheduleMapper scheduleMapper; // 获取某个医生某天某时段的排班 public Schedule getSchedule(Long doctorId, String scheduleDate, Integer timeSlot) { return scheduleMapper.selectOne(new LambdaQueryWrapper<Schedule>() .eq(Schedule::getDoctorId, doctorId) .eq(Schedule::getScheduleDate, scheduleDate) .eq(Schedule::getTimeSlot, timeSlot)); } // 获取医生某天的排班列表 public List<Schedule> listByDoctorAndDate(Long doctorId, String date) { return scheduleMapper.selectList(new LambdaQueryWrapper<Schedule>() .eq(Schedule::getDoctorId, doctorId) .eq(Schedule::getScheduleDate, date) .orderByAsc(Schedule::getTimeSlot)); } }

新增排班时,建议判断同一医生同一天同一时段是否已存在排班。数据库层面已经加了联合唯一索引,但业务层提前判断可以给用户更友好的提示。

5.4 预约挂号核心实现

预约模块是整个项目中业务复杂度最高的部分,也是面试、答辩最值得展开讲的部分。

核心逻辑如下:

  1. 校验用户是否存在,且不是管理员角色;
  2. 查询排班,校验排班存在且状态正常;
  3. 判断剩余号源是否大于 0;
  4. 校验用户是否已经预约过该排班(防止重复预约);
  5. 防止并发问题,使用乐观锁更新号源;
  6. 插入预约记录,返回预约结果。
// 文件路径:src/main/java/com/example/hospital/service/AppointmentService.java package com.example.hospital.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.hospital.entity.Appointment; import com.example.hospital.entity.Schedule; import com.example.hospital.exception.BusinessException; import com.example.hospital.mapper.AppointmentMapper; import com.example.hospital.mapper.ScheduleMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @Service public class AppointmentService { @Autowired private AppointmentMapper appointmentMapper; @Autowired private ScheduleMapper scheduleMapper; /** * 预约挂号 */ @Transactional(rollbackFor = Exception.class) public Appointment createAppointment(Long userId, Long scheduleId) { Schedule schedule = scheduleMapper.selectById(scheduleId); if (schedule == null) { throw new BusinessException(400, "排班不存在"); } if (schedule.getStatus() == 0) { throw new BusinessException(400, "该排班已停诊"); } if (schedule.getRemainCount() <= 0) { throw new BusinessException(400, "号源已约满"); } // 校验是否重复预约 Long existCount = appointmentMapper.selectCount(new LambdaQueryWrapper<Appointment>() .eq(Appointment::getUserId, userId) .eq(Appointment::getScheduleId, scheduleId) .ne(Appointment::getStatus, 2)); // 状态2表示已取消,取消后可重新预约 if (existCount > 0) { throw new BusinessException(400, "您已预约过该号源,请勿重复预约"); } // 乐观锁更新剩余号源,这里用更新行数判断是否更新成功 int updateCount = scheduleMapper.updateRemainCount(scheduleId); if (updateCount == 0) { throw new BusinessException(400, "预约失败,号源刚被抢完"); } // 插入预约记录 Appointment appointment = new Appointment(); appointment.setUserId(userId); appointment.setDoctorId(schedule.getDoctorId()); appointment.setDeptId(schedule.getDeptId()); appointment.setScheduleId(schedule.getId()); appointment.setAppointmentDate(schedule.getScheduleDate()); appointment.setTimeSlot(schedule.getTimeSlot()); appointment.setStatus(0); appointmentMapper.insert(appointment); return appointment; } /** * 取消预约 */ @Transactional(rollbackFor = Exception.class) public void cancelAppointment(Long appointmentId, Long userId) { Appointment appointment = appointmentMapper.selectOne( new LambdaQueryWrapper<Appointment>() .eq(Appointment::getId, appointmentId) .eq(Appointment::getUserId, userId)); if (appointment == null) { throw new BusinessException(400, "预约记录不存在"); } if (appointment.getStatus() == 2) { throw new BusinessException(400, "该预约已取消,请勿重复操作"); } if (appointment.getStatus() == 1) { throw new BusinessException(400, "该预约已完成,无法取消"); } // 恢复号源数量 Schedule schedule = scheduleMapper.selectById(appointment.getScheduleId()); schedule.setRemainCount(schedule.getRemainCount() + 1); scheduleMapper.updateById(schedule); appointment.setStatus(2); appointmentMapper.updateById(appointment); } }

这里的核心方法是updateRemainCount,它本质是一条带条件的 SQL:只有当剩余号源大于 0 时才把剩余号源减 1。这样能避免两个线程同时读到 remainCount=1,从而造成超卖。

// 文件路径:src/main/java/com/example/hospital/mapper/ScheduleMapper.java package com.example.hospital.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.hospital.entity.Schedule; import org.apache.ibatis.annotations.Update; public interface ScheduleMapper extends BaseMapper<Schedule> { @Update("UPDATE schedule SET remain_count = remain_count - 1 WHERE id = #{scheduleId} AND remain_count > 0") int updateRemainCount(Long scheduleId); }

取消预约时有一个细节:恢复号源之前最好先校验排班状态,如果排班已经停诊,再恢复号源就没有意义了。你可以根据实际需求补充这一层逻辑。

5.5 管理员端统计接口

管理员端用一个简单的统计接口展示系统运营情况,包括科室预约量排行、每日预约量。

// 文件路径:src/main/java/com/example/hospital/controller/AdminStatsController.java package com.example.hospital.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.hospital.common.Result; import com.example.hospital.entity.Appointment; import com.example.hospital.entity.Department; import com.example.hospital.mapper.AppointmentMapper; import com.example.hospital.mapper.DepartmentMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/admin/stats") public class AdminStatsController { @Autowired private AppointmentMapper appointmentMapper; @Autowired private DepartmentMapper departmentMapper; // 科室预约量排行 @GetMapping("/deptRank") public Result<List<Map<String, Object>>> deptRank() { List<Department> departments = departmentMapper.selectList(null); List<Map<String, Object>> result = new ArrayList<>(); for (Department dept : departments) { Long count = appointmentMapper.selectCount( new LambdaQueryWrapper<Appointment>().eq(Appointment::getDeptId, dept.getId())); Map<String, Object> item = new HashMap<>(); item.put("deptName", dept.getDeptName()); item.put("count", count); result.add(item); } return Result.success(result); } }

6. 运行部署与接口测试

6.1 初始化数据

在 application.yml 里配置好数据库连接之后,执行前面的建表 SQL,再插入几条基础数据。

-- 管理员账号 admin / 123456(MD5加盐后) INSERT INTO sys_user (username, password, real_name, role) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员', 3); INSERT INTO sys_user (username, password, real_name, role) VALUES ('zhangsan', 'e10adc3949ba59abbe56e057f20f883e', '张三', 2); INSERT INTO sys_user (username, password, real_name, role) VALUES ('lisi', 'e10adc3949ba59abbe56e057f20f883e', '李四', 1); INSERT INTO department (dept_name, dept_desc) VALUES ('内科', '社区内科常见病诊治'); INSERT INTO department (dept_name, dept_desc) VALUES ('外科', '社区外科门诊'); INSERT INTO doctor (user_id, dept_id, title, specialty) VALUES (2, 1, '主治医师', '呼吸道疾病、高血压、糖尿病等慢病管理'); -- 明天的排班,假设今天是2025-01-01 INSERT INTO schedule (doctor_id, dept_id, schedule_date, time_slot, total_count, remain_count) VALUES (1, 1, '2025-01-02', 1, 20, 20);

这里要特别提醒:上面INSERT INTO sys_user中的 MD5 值是123456加盐后加密的结果,我为了表格简洁没有写出加盐字符串,实际项目请用代码中一致的工具类生成,不要直接复制这条 SQL。密码字段必须以代码逻辑生成的值才能正常登录。

6.2 启动项目

直接运行 Spring Boot 启动类:

mvn clean package -DskipTests java -jar target/community-hospital-0.0.1-SNAPSHOT.jar

也可以直接在 IDEA 中右键启动类运行。控制台出现类似下面的日志说明启动成功:

Tomcat started on port(s): 8080 (http) Started CommunityHospitalApplication in 3.2 seconds

6.3 使用 Knife4j 测试接口

启动后访问:

http://localhost:8080/doc.html

这个页面会列出所有接口,登录接口可以直接测试。登录时输入:

{ "username": "lisi", "password": "123456" }

拿到 token 之后,在 Knife4j 页面右上角配置全局 token,后面的预约接口就能正常访问。

6.4 前端联调说明

如果前端使用 Vue,需要在请求拦截器中把 token 放到请求头里,后端接口通过拦截器统一校验:

// 文件路径:src/main/java/com/example/hospital/interceptor/JwtInterceptor.java package com.example.hospital.interceptor; import cn.hutool.jwt.JWTUtil; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录和注册接口 if (request.getRequestURI().contains("/api/auth/")) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); } boolean valid = JWTUtil.verify(token, "community-hospital-secret".getBytes()); if (!valid) { response.setStatus(401); response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录或token已过期\"}"); return false; } return true; } }

注意:这个拦截器必须要在 WebMvcConfigurer 中注册:

// 文件路径:src/main/java/com/example/hospital/config/WebConfig.java package com.example.hospital.config; import com.example.hospital.interceptor.JwtInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register", "/doc.html"); } }

7. 常见问题与排查思路

7.1 高频问题速查表

问题现象常见原因解决思路
启动时报数据库连接失败数据库地址、账号密码不正确检查 MySQL 是否启动,检查 application.yml 连接信息
数据库时区报错URL 缺少 serverTimezone在连接 URL 后加上serverTimezone=Asia/Shanghai
接口返回 401token 缺失或过期前端检查 Authorization 请求头是否正确传递
中文乱码数据库字符集不是 utf8mb4建库时使用 utf8mb4,连接 URL 加 useUnicode=true
MyBatis-Plus 分页失效没有配置分页插件需要注册 MybatisPlusInterceptor 并添加 PaginationInnerInterceptor
端口被占用本地 8080 被其他程序占用修改 server.port,或者找出占用进程
横向越权:用户A看到用户B的预约查询条件缺少 userId 校验登录后从 token 中解析 userId,查询时强制拼接该条件

7.2 预约并发优化的讲解思路

如果你在答辩时说“系统用了乐观锁控制号源”,评审老师很可能会追问“乐观锁和悲观锁有什么区别”。建议准备这样的回答:

乐观锁假设并发冲突较少,更新数据时在 SQL 条件中带上期望值,例如本文的WHERE id=? AND remain_count > 0,如果更新行数为 0,说明冲突发生,提示用户重试。悲观锁则是用SELECT ... FOR UPDATE把数据行锁住,直到事务提交才释放,适合高频冲突场景。社区医疗系统并发量不大,乐观锁方案已经足够,而且实现成本低。

7.3 Lombok 编译问题

如果你在 IDEA 中遇到 Lombok 生成的 getter/setter 找不到,通常是没装 Lombok 插件或者没有开启注解处理。在 pom 中确认依赖存在后,打开 IDEA 的Settings -> Build -> Compiler -> Annotation Processors,勾选Enable annotation processing

8. 答辩要点与项目扩展方向

8.1 答辩时可能被问到的问题

  • 为什么选择 Spring Boot?相比传统 SSM 有哪些优势?
  • 用户密码是怎么存储的?安全性如何保障?
  • 如何防止同一个用户重复预约?
  • 多个用户同时抢同一个号源,怎么避免超卖?
  • 医生停诊后,已经预约的患者怎么处理?
  • 排班表为什么单独设计,而不直接在医生表里维护号源?

这些问题的答案,基本都能从本文前面的代码和讲解中找到线索。建议你在答辩前逐条口述一遍,能讲清楚“为什么这么设计”比背代码重要得多。

8.2 可以加分的扩展方向

  • 预约成功后发送邮件或短信通知(接入阿里云短信或 Spring Mail);
  • 使用 Redis 缓存科室列表、医生列表,降低数据库压力;
  • 增加定时任务,在预约日期前一天自动提醒;
  • 将密码加密改为 BCrypt;
  • 使用 Spring Security 替换手写 JWT 拦截器;
  • 增加患者就诊记录,与预约记录联动;
  • 开发一个基于 Vue 的 H5 页面,方便真实场景使用。

这些扩展不需要全部实现,能选一到两个做出来,并且能在项目文档中解释清楚原理,项目质量就能提升一个档次。

8.3 项目亮点如何写到简历上

如果你打算把这个项目写进简历,不要只写“开发了一个预约挂号平台”,尽量具体:

  • 使用 Spring Boot + MyBatis-Plus + MySQL 开发社区医疗预约挂号系统,包含用户、科室、医生、排班、预约记录、统计报表等模块;
  • 基于 MyBatis-Plus 乐观锁机制设计号源扣减方案,通过UPDATE ... WHERE remain_count > 0解决并发抢号场景下的超卖问题;
  • 使用 JWT 实现无状态登录认证,通过拦截器统一校验接口访问权限;
  • 通过 Knife4j 自动化生成接口文档,提升前后端联调效率。

9. 项目源码结构参考

最终项目的核心源码结构如下,方便你写项目文档时参考:

community-hospital ├── src/main/java/com/example/hospital │ ├── common // Result 统一返回 │ ├── config // Web 配置、MyBatis-Plus 配置 │ ├── controller // 接口层 │ ├── entity // 实体类 │ ├── exception // 自定义异常与全局处理 │ ├── interceptor // JWT 拦截器 │ ├── mapper // MyBatis-Plus Mapper │ ├── service // 业务逻辑层 │ ├── utils // 工具类 │ └── CommunityHospitalApplication.java ├── src/main/resources │ ├── application.yml │ └── sql/init.sql └── pom.xml

建议在写论文时按照“需求分析 -> 系统设计 -> 数据库设计 -> 详细设计 -> 系统实现 -> 系统测试”的结构组织,和本文的章节顺序基本一致。

10. 总结与下一步学习建议

完成这个项目,你已经基本掌握了 Spring Boot 开发一个完整业务系统的常见套路:统一返回结构、全局异常处理、JWT 登录认证、MyBatis-Plus 数据操作、乐观锁解决并发扣减、接口文档生成。这些能力不仅在毕设中能用,以后进入企业做后端开发也会反复用到。

下一步的学习方向,可以先把 Spring Security 和 JWT 的完整集成过一遍,再用 Redis 把科室和医生列表缓存起来,看看接口响应速度有什么变化。之后可以接触更贴近生产的项目:多模块工程、数据库读写分离、分布式事务、容器化部署。每往前走一步,你对“一个真实项目是如何运转的”理解就会更深一些。

项目里面用到的大多数代码,你都可以在本地手动敲一遍,不要直接复制粘贴。自己写过一遍之后,有问题再对照文章改,这样到了答辩的时候,你对系统的每一个接口、每一行关键逻辑都会非常熟悉。如果这篇文章对你有帮助,建议收藏备用,后续写论文、做项目、面试复盘时都能拿出来参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询