
每年到了毕业设计选题季后台总会有同学来问类似的问题“老师要求做一个 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 为主推荐如下组合JDK1.8 或 11根据本机环境选择新版 Spring Boot 3.x 需要 JDK 17这里以常规 JDK 1.8 为例框架Spring Boot 2.7.xORMMyBatis-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-PlusSpring 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 均可IDEIDEA社区版或旗舰版都行JDK1.8Maven3.6 或以上MySQL5.7 / 8.0前端Vue 或 Thymeleaf后端接口必测4.2 创建 Spring Boot 项目在 IDEA 中通过 Spring Initializr 创建项目坐标参考GroupIdcom.exampleArtifactIdcommunity-hospitalJava 版本8打包方式jar依赖建议加入dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi2-spring-boot-starter/artifactId version4.4.0/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.22/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies这里说明一下MyBatis-Plus 3.5.x 自带分页插件不需要单独引入分页依赖。Knife4j 是为了生成接口文档方便测试接口也方便答辩时演示。4.3 配置文件 application.ymlserver: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/community_hospital?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 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: 04.4 统一返回结果类为了让前端能统一处理接口返回结构项目里定义一个 Result 类这是项目开发中很常见的做法也是答辩时可以提到的代码规范。// 文件路径src/main/java/com/example/hospital/common/Result.java package com.example.hospital.common; import lombok.Data; Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT success() { return success(null); } public static T ResultT error(Integer code, String message) { ResultT 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 ResultVoid handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } ExceptionHandler(Exception.class) public ResultVoid 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 ResultVoid register(RequestBody SysUser user) { Long count sysUserMapper.selectCount( new LambdaQueryWrapperSysUser().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 ResultMapString, Object login(RequestBody SysUser loginUser) { SysUser user sysUserMapper.selectOne(new LambdaQueryWrapperSysUser() .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 MapString, Object payload new HashMap(); payload.put(userId, user.getId()); payload.put(username, user.getUsername()); String token JWTUtil.createToken(payload, community-hospital-secret.getBytes()); MapString, 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 ResultPageDoctor page( RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Long deptId) { LambdaQueryWrapperDoctor wrapper new LambdaQueryWrapper(); wrapper.eq(deptId ! null, Doctor::getDeptId, deptId) .eq(Doctor::getStatus, 1) .orderByDesc(Doctor::getId); PageDoctor page doctorMapper.selectPage(new Page(current, size), wrapper); return Result.success(page); } // 查看医生详情 GetMapping(/{id}) public ResultDoctor 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 LambdaQueryWrapperSchedule() .eq(Schedule::getDoctorId, doctorId) .eq(Schedule::getScheduleDate, scheduleDate) .eq(Schedule::getTimeSlot, timeSlot)); } // 获取医生某天的排班列表 public ListSchedule listByDoctorAndDate(Long doctorId, String date) { return scheduleMapper.selectList(new LambdaQueryWrapperSchedule() .eq(Schedule::getDoctorId, doctorId) .eq(Schedule::getScheduleDate, date) .orderByAsc(Schedule::getTimeSlot)); } }新增排班时建议判断同一医生同一天同一时段是否已存在排班。数据库层面已经加了联合唯一索引但业务层提前判断可以给用户更友好的提示。5.4 预约挂号核心实现预约模块是整个项目中业务复杂度最高的部分也是面试、答辩最值得展开讲的部分。核心逻辑如下校验用户是否存在且不是管理员角色查询排班校验排班存在且状态正常判断剩余号源是否大于 0校验用户是否已经预约过该排班防止重复预约防止并发问题使用乐观锁更新号源插入预约记录返回预约结果。// 文件路径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 LambdaQueryWrapperAppointment() .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 LambdaQueryWrapperAppointment() .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。这样能避免两个线程同时读到 remainCount1从而造成超卖。// 文件路径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 BaseMapperSchedule { 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 ResultListMapString, Object deptRank() { ListDepartment departments departmentMapper.selectList(null); ListMapString, Object result new ArrayList(); for (Department dept : departments) { Long count appointmentMapper.selectCount( new LambdaQueryWrapperAppointment().eq(Appointment::getDeptId, dept.getId())); MapString, 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 / 123456MD5加盐后 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 seconds6.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;charsetutf-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 后加上serverTimezoneAsia/Shanghai接口返回 401token 缺失或过期前端检查 Authorization 请求头是否正确传递中文乱码数据库字符集不是 utf8mb4建库时使用 utf8mb4连接 URL 加 useUnicodetrueMyBatis-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 把科室和医生列表缓存起来看看接口响应速度有什么变化。之后可以接触更贴近生产的项目多模块工程、数据库读写分离、分布式事务、容器化部署。每往前走一步你对“一个真实项目是如何运转的”理解就会更深一些。项目里面用到的大多数代码你都可以在本地手动敲一遍不要直接复制粘贴。自己写过一遍之后有问题再对照文章改这样到了答辩的时候你对系统的每一个接口、每一行关键逻辑都会非常熟悉。如果这篇文章对你有帮助建议收藏备用后续写论文、做项目、面试复盘时都能拿出来参考。