
简介这是一套以 Java 技术为核心的病人挂号系统网站教学资源适合 Java Web 初学者或需要课程设计、毕业设计参考的开发者。项目覆盖从后端框架、数据库设计到前端交互的完整流程可帮助理解 Spring Boot/Spring MVC、MySQL 数据访问以及用户注册登录、预约挂号等核心业务模块。压缩包共 5 个文件含需求文档、项目源码、数据库脚本和演示视频大小约 21.41MB文档说明系统目标和功能设计源码便于逐行分析SQL 脚本可直接导入数据库视频演示操作流程。该资源已有 151 人学习内容组织较为完整。通过研读需求文档和源码可以掌握 MVC 分层思想、ORM 数据操作、前后端交互等实践技能演示视频也可作为初学者上手操作的直观指南整体上是一份实用的 Java Web 项目学习与参考资料。1. 病人挂号系统源码、需求文档与演示视频三件套怎么读拿到一个「病人挂号系统网站」的源码包常见的打开方式是把代码导进 IDEA 跑起来再对着需求文档看界面。这个思路没错但对一个还需要交演示视频的项目来说只追求“能跑”会漏掉最值钱的部分业务闭环的完整性。病人挂号系统的核心不是登录注册而是从“选科室 - 看排班 - 挂号下单 - 分诊叫号 - 医生接诊 - 收费完成”这条链路的每一个状态转换。需求文档负责告诉你这些转换规则源码负责展示这些规则有没有真正落地演示视频则暴露了边界场景有没有兜住。适合读这类项目的人是想用最短时间摸清 Java Web 业务项目套路的在校生以及打算把它改造成个人简历项目的初级开发。2. 病人挂号系统的模块边界与数据库设计从业务闭环到 ER 图落地2.1 先理业务边界一个挂号周期里有多少角色和动作在写任何表结构之前先把需求文档里的“功能需求”翻译成角色和动作。病人端需要注册登录、浏览科室、查医生排班、挂号和退号医生端要看当日排队列表、叫号、写诊断、开处方护士或前台负责叫号、改状态管理员则负责科室维护、医生资料维护、排班生成和号源调整。这里最容易被课设项目做错的是把医生和护士都塞进一个“用户”表里靠一个 role 字段硬撑。常见做法是拆成user、doctor_profile、patient_profile三张表用户表只存登录凭据profile 表存各自的业务属性。这样做的好处是医生端挂排班、病人端挂挂号记录时外键关系不会被账号体系干扰。角色权限边界可以用一张矩阵直接说清角色可操作核心业务明确禁止的动作病人挂号、取消未就诊的号、查询记录修改排班、改动挂号状态医生查看当日待就诊列表、写病历开处方不能给自己挂靠排班护士/分诊叫号、调整就诊顺序不能改处方金额管理员科室与医生排班维护不得直接篡改业务单据状态2.2 ER 关系拆解科室、医生、排班、挂号单之间的关系整个系统里最关键的关系是三段科室和医生是一对多医生和排班是一对多排班和挂号单是一对多。需求文档里如果还有“出诊时间”概念就需要在排班表上继续扩展把上午/下午/晚间拆成独立的排班记录而不是在一个字段里存 “8:30-11:30,14:00-17:00” 这种带格式的字符串。实体关系落到schedule表上时我一般会这样建CREATE TABLE schedule ( id bigint NOT NULL AUTO_INCREMENT COMMENT 排班ID, doctor_id bigint NOT NULL COMMENT 医生ID, dept_id bigint NOT NULL COMMENT 科室ID, work_date date NOT NULL COMMENT 出诊日期, shift_type tinyint NOT NULL COMMENT 1上午 2下午 3晚间, total_slots int NOT NULL DEFAULT 20 COMMENT 号源总数, used_slots int NOT NULL DEFAULT 0 COMMENT 已占用号源数, line_version int NOT NULL DEFAULT 0 COMMENT 行版本号乐观锁, PRIMARY KEY (id), UNIQUE KEY uk_doctor_date_shift (doctor_id, work_date, shift_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班表;doctor_id work_date shift_type的三列唯一键是这张表的底线它直接防止同一个医生在同一天同一时段被生成两条排班。total_slots和used_slots的差就是前端要展示的“剩余号”line_version在并发挂号场景用来做乐观锁。需求文档里通常不会写这个字段源码里没有它高峰挂号时就会出现超卖。2.3 挂号记录表的状态设计四个状态足够不要加无效字段挂号记录表的核心字段是病人ID、排班ID、费用、状态、创建时间、取消时间、完成时间。status字段推荐用tinyint四个枚举值分别是 0 待就诊、1 就诊中、2 已完成、3 已取消。用字符串存储 “待就诊” 虽然在界面显示时省了一次转换但在统计查询里会出现同一个含义多种写法“待就诊”“已挂”“未看”的数据质量问题。挂号记录一旦创建不要物理删除业务上取消号走“状态置为 3”而不是 delete这样月末统计“取消率”“爽约率”时才有原始数据。状态展示时做一层枚举到文案的翻译即可比如在枚举类里维护status.getLabel()前端只认数字编码这样 ER 图和 Java 代码之间的对应关系也干净。3. 病人挂号系统的核心 Java 代码状态机更新、并发扣号与事务边界3.1 状态机更新用一行 UPDATE 挡住状态回跳挂号状态的流转顺序是 0 待就诊 - 1 就诊中 - 2 已完成以及 0 - 3 已取消。在界面里这只是几个按钮在代码里容易踩的坑是并发导致状态回跳。医生端点击“开始就诊”的同时病人端正好点了“取消挂号”如果两段代码都先查一遍状态再更新后提交的一方就可能把状态覆盖成错误值。我用的是条件更新public boolean changeStatus(Long regId, Integer fromStatus, Integer toStatus) { return registrationMapper.compareAndSetStatus(regId, fromStatus, toStatus) 1; }写进 Mapper XML 后是这个 SQLupdate idcompareAndSetStatus update registration set status #{toStatus}, update_time now() where id #{regId} and status #{fromStatus} /update核心逻辑是把旧状态放进 where 条件更新的行数为 0 时就说明状态已经被其他操作修改过业务层可以直接抛异常“当前状态已变更请刷新页面”。相比在内存里加锁这种方式不会阻塞无关的挂号请求对课设项目和中小并发业务都够用。3.2 并发扣号排班表上的两条更新语句配合病人点击挂号时需要同时做两件事查schedule判断剩余号量再插入一条registration。这里最直接的错误是“先 select 判断再用空闲数 update”两个请求同时读到剩余号还剩 1就会产生两条挂号记录。常见做法是给排班表加悲观锁把整个挂号动作包在一个事务里Transactional(rollbackFor Exception.class) public Long register(RegisterRequest request) { Schedule schedule scheduleMapper.selectByIdForUpdate(request.getScheduleId()); if (schedule null) { throw new BizException(排班不存在); } if (schedule.getUsedSlots() schedule.getTotalSlots()) { throw new BizException(该时段号源已挂完); } Registration reg buildRegistration(request, schedule); registrationMapper.insert(reg); int rows scheduleMapper.incrUsedSlots(request.getScheduleId()); if (rows 0) { throw new BizException(号源被抢光请重新选择); } return reg.getId(); }selectByIdForUpdate拿到行锁之后其他事务的同一个排班更新会被阻塞号量判断和incrUsedSlots之间的间隙就不会再插入别的注册单。incrUsedSlots的 SQL 是update schedule set used_slots used_slots 1, line_version line_version 1 where id #{id}不要用set used_slots #{used_slots}这种先查后写的方式否则在锁外执行时会互相覆盖。BizException是自定义运行时异常由全局异常处理器统一转成页面提示不会把堆栈直接抛给用户。3.3 事务边界哪些调用必须「包」在一起事务范围太大会拖慢并发放得太小又会出现数据不一致。一般我会把“扣号源 插挂号记录”放进同一个事务方法也就是上面register的整体。退号操作则需要同时更新挂号单状态、把号源减一同样放进同一个Transactional但要注意先改状态再减号源避免业务成功却把号源方向扣错。分诊叫号、写诊断这类操作不涉及跨表一致性不需要额外加大事务粒度单表 UPDATE 即可。另一个常见问题是 MyBatis 实体类和 XML 映射字段对不上运行时报Invalid bound statement这个是路径或 namespace 的配置问题先检查mybatis.mapper-locations是否指向了 XML 所在的 classpath 路径。4. 本地跑通病人挂号系统环境变量配置、数据库授权与 Tomcat 部署排错4.1 开始前先花两分钟确认 Java 与 Maven 环境无论源码是 Spring Boot 还是传统 SSM第一步都是确认本机的 java 环境变量配置。终端里敲java -version能看到 1.8 或 11 的输出再敲mvn -v确认 Maven 可用。JDK 版本和源码的编译目标不一致时最常见的是UnsupportedClassVersionError优先去pom.xml里看maven.compiler.source和target的配置。如果源码压缩包里有sql目录或doc目录通常就包含建库脚本先把脚本挨个看一遍。找脚本里的CREATE DATABASE语句确认里面指定的字符集是utf8mb4不是utf8否则病人姓名这类生僻字入库后会变成问号。4.2 数据库连接配置连接串写好这四个参数能省一半排错时间启动后连接数据库报错是最常见的失败点。不管是application.yml还是jdbc.properties连接串里至少要确认下面四个参数配置项推荐值不写的后果useUnicodetrue中文字段写入数据库后乱码characterEncodingutf8表格和页面乱码useSSLfalseMySQL 8 报 SSL 告警serverTimezoneAsia/Shanghai日期字段差 8 小时报无法识别时区下面是一份 Spring Boot 常见的连接配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital_register?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456MySQL 5.7 用com.mysql.jdbc.Driver也能跑MySQL 8 之后强制要com.mysql.cj.jdbc.Driver换驱动后需要同步重启应用。如果登录账号没有目标库的权限会直接报 Access denied。用 root 登录后执行一行授权可以省掉不少排查时间GRANT ALL PRIVILEGES ON hospital_register.* TO rootlocalhost; FLUSH PRIVILEGES;4.3 常见报错对照启动阶段能遇到的基本都在这里部署阶段的报错大多是环境问题而不是业务代码问题。拿一张启动排查表逐个对照会高效很多启动现象最可能原因处理动作ClassNotFoundException: com.mysql.cj.jdbc.Driver依赖缺失或版本不对检查pom.xml中mysql-connector-j坐标清理后重新加载Communications link failure连接地址、端口或账号异常先telnet localhost 3306测端口再确认密码Access denied for user rootlocalhost账号密码错误或未授权重置密码或执行上一步的授权语句Invalid bound statement (not found)Mapper XML 路径没被扫描配置mybatis.mapper-locations: classpath:mapper/*.xml页面能打开但样式图片全丢部署路径带项目名检查静态资源引用是否有${pageContext.request.contextPath}Tomcat 启动后访问返回 404请求路径和 controller 映射对不上对照需求文档里的访问路径优先看路由前缀4.4 Tomcat 部署路径固定三步检查如果是 Spring Boot 的 war 包方式部署到 Tomcat 前要确定pom.xml的packaging是war启动类继承SpringBootServletInitializer并重写configure方法。把 war 扔进 webapps 后访问路径默认带上了 war 包文件名比如http://localhost:8080/hospital_register_war_exploded/。演示视频里的访问路径是http://localhost:8080/patient/时需要把 war 包直接改名为ROOT.war或者删掉 webapps 下自带的 ROOT 目录再放置。这个操作对正式部署不一定常用但在交演示视频的机器上几乎必踩。5. 病人挂号系统演示前的一小时预置数据、演示路径和边界场景5.1 用脚本一次铺满演示数据避免现场“空得可怕”源码包自带的开发库通常只有测试账号没有足够的排班和挂号记录直接开演示画面会很空。演示前建议用一条 SQL 把基础数据铺满建好 3 个科室、10 个医生、未来 7 天每天的出诊排班再给每张排班表插入 3 到 5 条不同状态的挂号单。针对演示准备的造数脚本最好单独存成一个文件例如demo-data.sql可以反复执行。每个演示账号固定好对应关系管理员admin/admin123医生账号doctor01/123456病人账号patient01/123456。把账号写进演示文稿的固定位置比现场临时输入效率高很多。5.2 演示路径要闭环不要在每个页面来回横跳演示流程建议固定为一次完整的业务链路病人登录 - 科室列表 - 选择医生排班 - 确认挂号 - 查看挂号成功页 - 退出 - 医生登录 - 今日待就诊列表 - 开始就诊 - 写诊断 - 提交完成 - 管理员查看统计报表。每完成一步在演示视频里留一个界面停留时间方便录屏软件捕捉到状态字段的变化。注意挂号日期一定选未来三天内且未满号的排班不要选当天的否则数据已满就没有后续动作。如果演示当天恰好所有号源都已挂满直接改schedule.work_date为明天再展示避免现场重新维护一张表。5.3 按下录制键前要预演的两个边界场景完整链路走完之后很多项目的演示视频就停在了“功能正常”这一层碰到老师追问“挂号失败怎么办”就答不上来。提前准备两个边界场景是低成本高收益的做法一个是选一个已用号源等于总号源的排班演示“号源已挂完”的提示另一个是对已取消的挂号单再次点“开始就诊”观察状态机是否正确拦截。line_version字段本来就是为边界准备的演示时只要把某张排班表的used_slots手工改成total_slots就能自然触发满号逻辑不用改 Java 代码。录制前一小时把这两个场景走一遍既能验证事务边界有没有兜住也能让演示视频看起来比同批项目多一层细节处理。本文还有配套的精品资源点击获取