简介:一套基于Java实现的智慧医院门诊管理系统项目,面向高校Java学习者、课程设计与毕业设计人员,也可供初入医院信息系统开发的实习生作为工程范例参考。项目源码配套设计文档、实验报告、详细资料与数据库SQL文件,业务涵盖订单管理、检查检验服务、支付宝支付、用户与医生管理、Token鉴权等典型模块,适合用作Java Web综合实训或毕设选题的完整样例。压缩包共315个文件,其中Java源码65个、Class文件65个、XML配置144个,XML多为MyBatis映射或Spring相关配置,另含SQL建库脚本、Word/Excel文档、Jar依赖与项目管理文件,整体大小约48.14MB,目录结构清晰便于按模块查阅。内容预览中包含订单、支付、检验、用户登录等业务场景对应实现类,便于直接定位关键逻辑;目前已有101人浏览学习,适合需要一套可直接导入运行并对照实现的医院门诊系统开发者。
1. 基于java实现的智慧医院门诊管理系统到底解决什么问题
如果你在找基于java的智慧医院门诊管理系统项目源码,多半不是为了炫技,而是被门诊流程的复杂度缠住了:号源怎么放、挂号怎么不超卖、医生怎么接诊、收费怎么闭环。这套源码压缩包里同时带着设计文档、实验报告和数据库sql文件,恰好把这一串问题串起来。它的“智慧”不在于用了多新的技术,而在于把整个门诊链路拆成了可管理的状态流转。医院早高峰的号源超卖、候诊叫号乱序、医生开单后患者不知道下一站去哪,都是这套系统要解决的真实痛点。适合毕业设计、课程设计和中小门诊信息化练手。有了它,你可以从跑通流程开始,再一步步改进并发和数据一致性,这是很多Java新人最顺的学习路线。
2. 跑通项目最小闭环:JDK/MySQL/Maven环境与导入启动步骤
2.1 项目包里都有什么:先按资料结构定启动顺序
拿到zip先别急着双击运行。根据这类Java后端项目的常见打包习惯,压缩包里通常会有后端工程目录(src/main/java、src/main/resources)、数据库脚本目录(sql或db)、文档目录(设计文档、实验报告、答辩PPT)。这个zip的名字里有“源码+设计文档+实验报告+详细资料+数据库sql文件”,意味着它不像GitHub上很多裸仓库那样只有代码,可以按下面顺序用。
| 包内常见部分 | 主要内容 | 上手优先级 |
|---|---|---|
| sql/ 或 database/ | 建库建表脚本、初始数据 | 最高,先导库 |
| src/main/resources | application.yml、mapper映射、静态资源 | 高,改完才能启动 |
| src/main/java | controller/service/mapper/entity | 中,跑通后再读 |
| docs/ 或资料文档 | 设计文档、实验报告、数据库说明 | 中低,写论文答辩时用 |
读代码的顺序我一般建议:先看数据库SQL把业务模型建起来,再看application.yml确认连接参数,然后启动项目跑一遍页面,最后再回头读Controller和Service。反过来从源码开始容易陷入细节,一上午都看不完一个模块还找不到入口。这个包里的设计文档和实验报告是辅助理解业务的好材料,但注意材料里的表名、字段和实际SQL文件可能对不上——后面会专门讲怎么用这些资料。
2.2 把项目跑起来:环境变量、数据库初始化和启动参数
跑Java Web项目最基础的三件套是JDK、MySQL和Maven。这个项目如果是Spring Boot单体,JDK一般用1.8或11;如果pom.xml里用了Jakarta命名空间,那就是Spring Boot 3,需要JDK 17及以上。打开压缩包后先看一眼pom.xml里的<java.version>,再决定装哪个JDK,别凭感觉装。先做环境检查:
java -version mvn -v mysql --versionjava -version确认JDK版本,mvn -v会同时打印Maven版本和它使用的JAVA_HOME,mysql --version确认客户端可用。这三行任何一个报“command not found”,就要去配环境变量。Java环境变量配置详细教程在网上很容易找到,核心就两步:设JAVA_HOME指向JDK安装目录,把%JAVA_HOME%\bin追加到Path。Maven同理要设MAVEN_HOME。我见过很多第一次跑项目的同学卡在这一步,其实是Path里旧版本JDK排在前面,java -version看到的是老版本。
环境就绪后,用IDEA打开项目:File → Open,选择zip解压后的目录,找到pom.xml确认它是Maven项目。等右下角依赖下载完成后,打开src/main/resources/application.yml,这是整个项目启动成败的关键。典型配置长这样:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driverurl里的四个参数不要乱删:useUnicode=true&characterEncoding=utf8保证中文不乱码,serverTimezone=Asia/Shanghai解决MySQL 8.0的时区报错,allowPublicKeyRetrieval=true解决新版MySQL驱动连接时的公钥校验问题,driver-class-name必须是com.mysql.cj.jdbc.Driver而不是旧版的com.mysql.jdbc.Driver。如果包里的SQL文件要求单独库名,jdbc:mysql://localhost:3306/hospital这段最后的hospital要改成SQL脚本里实际的库名。
接着初始化数据库。在项目根目录执行:
mysql -uroot -p < sql/database.sql执行前确认SQL文件的路径。有些项目把脚本放在src/main/resources/db,有些放在根目录database,找一下带CREATE TABLE的那个文件。这条命令会提示输入root密码,然后把建库、建表、插入管理员账号等一次性执行完。执行完可以用Navicat或DBeaver连上去看一眼表是否齐全,比如patient、doctor、schedule、registration这些表在不在。最后启动项目,两种方式任选:
mvn spring-boot:run或者在IDEA中找到XxxApplication类,点击旁边绿色箭头直接Run。启动日志出现Tomcat started on port 8080之后,浏览器访问http://localhost:8080/login。如果没有login页面,就看控制台里打印的欢迎页地址,很多项目把入口写在/或/index。
提示:数据库密码和端口这类配置如果写死在application.yml里,只适合本地开发。真正要交给别人的文档版本里,最好改成通过环境变量读取,避免泄露密码。
2.3 启动失败先看这3个日志:端口占用、数据库连接和依赖报错
跑不起来时先别怀疑代码,百分之八十是环境参数问题。第一类报错是端口占用,日志长这样:
Caused by: java.net.BindException: Address already in use: bind原因很直接:8080被别的进程占了。Windows下用netstat -ano | findstr 8080找到PID,然后taskkill /PID 进程号 /F;Linux/Mac用lsof -i:8080确认进程后kill -9。不确定就干脆换端口,在application.yml里把server.port改成8081,一了百了。
第二类报错在数据库连接,日志里常见两句话:
Access denied for user 'root'@'localhost' (using password: YES) Communications link failure前者是账号或密码错,检查application.yml里的username和password;后者多半是MySQL服务没启动,或者url里的端口、库名写错。还有个隐蔽原因:MySQL 8.0默认用caching_sha2_password插件,旧驱动连不上会报Public Key Retrieval is not allowed,url里没加allowPublicKeyRetrieval=true就会出现。
第三类报错是Maven依赖下载失败,日志里大片Could not transfer artifact from repo.maven.apache.org。这是网络不通或镜像太慢。去Maven安装目录的conf/settings.xml里配置阿里云镜像,没有权限写安装目录就在用户目录的.m2/settings.xml里配:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>配好之后重启IDEA,它会重新拉依赖。这三类日志占了启动失败的大头,剩下的比如Thymeleaf模板解析失败、找不到mapper.xml,多半是路径问题,放到避坑章再展开。跑起来之后,再去研究业务代码会轻松很多。
3. 把门诊核心业务拆开:表设计、挂号事务和号源扣减是怎么配合的
3.1 核心数据表:从user到registration,看懂门诊状态流转
医院门诊系统的业务模型可以用一句“以挂号为主线,从患者建档到医生接诊”概括。Java后端常用的ORM是MyBatis或MyBatis-Plus,表设计的核心不在字段多不多,而在外键关系和状态字段能不能支撑流程。以下是这套系统里最常见的核心表:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | user_id, username, password, role | 登录账号,role区分管理员/医生/挂号员 |
| patient | patient_id, name, gender, id_card, phone, allergy_history | 患者档案,id_card做唯一索引 |
| department | dept_id, dept_name, location, dept_type | 科室,比如内科/外科/检验科 |
| doctor | doctor_id, name, dept_id, title, quota, status | 医生,quota是每天默认号源数 |
| schedule | schedule_id, doctor_id, work_date, time_slot, total, remaining | 排班号源,time_slot一般是上午/下午 |
| registration | reg_no, patient_id, doctor_id, schedule_id, status, fee | 挂号单,status从待就诊到完成/退号 |
| prescription | pres_id, reg_no, drug_name, dosage, quantity, amount | 处方明细 |
| payment | payment_id, reg_no, amount, pay_time, pay_type | 收费记录 |
我一般会要求设计文档里必须画清楚一条患者主数据链路:patient建档 → 查schedule找号源 → 扣号源生成registration → 医生看诊填prescription → 收费生成payment。这个链路里,schedule和registration是承上启下的核心。很多新手会把“号源”和“挂号记录”混在同一张表里,导致一个患者挂号后号源被永久占掉、退号后号源回不来。正确做法是号源表管可挂数量,挂号表管交易记录,退号时再给号源表把remaining加回去,状态才能流转。
表与表之间最好都有逻辑主键关联。比如doctor.dept_id指向department,registration.schedule_id指向schedule。SQL脚本里通常会建外键,但我建议只留普通索引,别真建物理外键——高并发挂号和后续扩展时,物理外键会成为性能瓶颈和改表结构的麻烦。这是很多有经验的Java开发做HIS类系统的共同选择。实体类用注解或XML映射都行,重点是字段命名全部下划线风格,和SQL列一一对应,别让MyBatis做无谓的驼峰猜测。
3.2 号源扣减为什么要用条件UPDATE:高并发挂号的正确写法
门诊系统最容易被问倒的并发问题就是号源超卖。先看一个初版很常见的实现:先查remaining,判断大于0再update减一。代码写出来挺自然,但在并发下是错的:
// 错误示范:先查询再更新,两个线程会同时读到 remaining=1 Schedule schedule = scheduleMapper.selectById(scheduleId); if (schedule.getRemaining() > 0) { schedule.setRemaining(schedule.getRemaining() - 1); scheduleMapper.updateById(schedule); // 然后插入挂号单 }两个请求同时执行selectById,都看到remaining=1,都认为能挂,然后各减一次,数据库里余量变成-1,挂号单却生成了两条。原因在于“检查-然后-更新”不是原子操作。要解决,把检查行为直接合并进UPDATE语句:
UPDATE schedule SET remaining = remaining - 1 WHERE schedule_id = #{scheduleId} AND remaining > 0这条SQL在MySQL InnoDB下会锁定命中的行,并且只有在remaining大于0时才更新成功。MyBatis的Mapper方法写好后,Java调用处判断影响行数:
@Transactional(rollbackFor = Exception.class) public RegistrationResult register(RegisterRequest req) { int affected = scheduleMapper.deduct(req.getScheduleId()); if (affected == 0) { throw new BusinessException("该时段号源已挂满,请选择其他时段"); } // 生成挂号单号并插入,save方法在同一个事务里 Registration reg = new Registration(); reg.setRegNo(regNoGenerator.generate()); reg.setPatientId(req.getPatientId()); reg.setDoctorId(req.getDoctorId()); reg.setScheduleId(req.getScheduleId()); reg.setStatus("待就诊"); registrationMapper.insert(reg); return success(reg); }这里有两个必须说透的参数。第一,@Transactional(rollbackFor = Exception.class)很关键:Spring声明式事务默认只回滚RuntimeException,如果你的业务异常继承的是Exception,不加rollbackFor会提交一半数据,号源扣了但挂号单丢了。第二,deduct方法返回int,MyBatis里这个值就是SQL影响行数,一定拿它判断是否继续。如果affected为0还强行插入,就违背了“先扣号源再出挂号单”的约束。
为什么条件UPDATE在单独执行时不需要显式加锁?InnoDB对UPDATE默认走行锁,带WHERE remaining > 0的条件会让锁等待的第二个事务重新读最新数据。第二个事务的UPDATE同样会执行,只是remaining不再大于0,影响行数是0,自然就不会产生超卖。这套机制在“java怎么保证数据一致性”这类面试题里也是经典答案。如果项目里还挂了Redis,想要更高吞吐可以先用Redis扣减号源,再用Lua脚本保证原子性,但单体MySQL场景下条件UPDATE已经够用,不要为了炫技把简单方案复杂化。
3.3 定时任务与号源刷新:用@Scheduled给每个医生生成当日排班
门诊系统不可能每天手工去创建号源,常见做法是跑一个定时任务,在零点过后为当天的date生成各个医生上午、下午两个时段的schedule记录。Spring自带的@Scheduled在单体应用里就够了,也是这个项目里最常见的Java定时任务框架用法。先开启调度,在主启动类上加注解:
@SpringBootApplication @EnableScheduling public class HospitalApplication { public static void main(String[] args) { SpringApplication.run(HospitalApplication.class, args); } }然后写一个每天执行的任务:
@Component public class ScheduleGenerateJob { private final DoctorMapper doctorMapper; private final ScheduleMapper scheduleMapper; // cron:秒 分 时 日 月 星期,0 0 0 * * ? 表示每天0点0分0秒 @Scheduled(cron = "0 0 0 * * ?") public void generateTodaySchedule() { LocalDate today = LocalDate.now(); // 先清掉当天已有记录,保证任务幂等 scheduleMapper.deleteByWorkDate(today); List<Doctor> doctors = doctorMapper.selectActiveDoctors(); for (Doctor doctor : doctors) { for (String timeSlot : new String[]{"上午", "下午", "晚间"}) { scheduleMapper.insert( doctor.getId(), today, timeSlot, doctor.getQuota(), // 总号 doctor.getQuota() // 剩余号 ); } } } }cron表达式的参数顺序是秒、分、时、日、月、星期,0 0 0 * * ?里的?表示“不指定星期几”。如果希望早上6点放次日的号,可以改成0 0 6 * * ?。写定时任务时最重要的一点是幂等:同一个任务被重复执行不会产生重复数据。所以我在插入前先deleteByWorkDate(today),哪怕上次任务跑了一半挂了,第二天重跑也不会生成两批号源。如果项目部署了多个实例,@Scheduled会在每个实例上各跑一遍,那就得加分布式锁或者把任务收敛到单独模块,这属于后话。另外,注意服务器时区。LocalDate.now()取的是OS时区,如果服务器是UTC而业务在中国,日期会偏一天,运维部署时要把JVM时区参数-Duser.timezone=Asia/Shanghai设上。
我一般会把定时任务和用户手动补号分开:定时任务负责每天自动放号,管理员后台也留一个手动生成排班的接口,应对节假日的特殊排班。定时任务跑完可以看一眼日志或去schedule表查数据,确认每个医生当天有记录,再进入挂号流程。
4. 数据库SQL文件与设计文档怎么用:建库导数据与账号权限体系
4.1 导入SQL的三种方式:命令行、source和图形客户端
很多人拿到数据库sql文件后直接双击,结果用记事本打开满屏中文乱码,或者导到一半报错。SQL文件本质是文本,导入方式主要三种,但字符集必须统一。先说明最常见的命令行方式,它能让你看清错误信息。
mysql -uroot -p --default-character-set=utf8mb4 < sql/database.sql--default-character-set=utf8mb4告诉客户端按UTF-8解释文件内容,避免MySQL客户端默认latin1导致中文变问号。如果数据库还没创建,脚本里通常有CREATE DATABASE hospital;如果脚本只包含建表和插入语句,就要先手动建库再导入。
第二种方式是在已登录的MySQL客户端里执行source命令:
USE hospital; SOURCE /home/user/project/sql/database.sql;注意source后面要写SQL文件在操作系统里的绝对路径或相对当前客户端的路径,不是URL。source适合边导边看输出,出错时能看到具体是哪条语句。
第三种是图形客户端。Navicat/DBeaver里右键数据库 → 运行SQL文件,选文件时同样要留意编码设置。导入完成后老规矩验证:
SHOW CREATE DATABASE hospital; SHOW TABLES; SELECT COUNT(*) FROM user;SHOW CREATE DATABASE能看数据库默认字符集是不是utf8mb4;SHOW TABLES确认表建出来了;查user表行数确认初始管理员数据在不在。很多项目会把一个admin123的管理员账号插在SQL文件里,登录前先拿这条SQL确认一下。如果导入时中文已经乱码,不是重新导入就能解决的,得先ALTER DATABASE hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,再把表和列的字符集统一,然后drop掉表重新导入干净的SQL文件。
4.2 管理员、医生、挂号员三类角色:认证、授权和数据行过滤
智慧医院门诊系统至少有三类使用角色。管理员管科室和医生,挂号员负责患者建档和收费,医生只看自己负责的待诊队列。对应到Java后端,就是认证(你是谁)加授权(你能干什么)。如果项目用了Spring Security或者Shiro,配置文件里会有role字段校验。如果是SSM项目没有安全框架,常见做法是写一个登录拦截器。
一个精简的角色权限示例:
@Component public class RoleInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User currentUser = (User) request.getSession().getAttribute("loginUser"); if (currentUser == null) { response.sendRedirect("/login"); return false; } String uri = request.getRequestURI(); if (uri.startsWith("/doctor/") && !currentUser.getRole().equals("doctor")) { response.sendError(403, "仅医生可访问该路径"); return false; } return true; } }拦截器按URL前缀做粗粒度拦截,能挡住绝大多数越权访问。但真正要防范的不是URL直跳,而是“医生A修改医生B的患者数据”。光靠拦截器不够,需要在Service层做行级数据过滤。常见做法是把当前登录医生ID塞进查询条件:
public List<Registration> findMyQueue(Long doctorId) { return registrationMapper.selectByDoctorIdAndStatus(doctorId, "待就诊"); }MyBatis Mapper里写WHERE doctor_id = #{doctorId},而不是让前端传入任意doctorId。这种行级权限思路在项目源码里如果改了类名,设计文档也要同步更新,否则答辩时被问到“系统怎么防止医生串号”就露馅了。密码字段不要明文存储,至少用MD5加盐或BCrypt。检查一下SQL文件里初始化的admin账号密码是不是明文,是的话建议改掉再演示。
4.3 设计文档和实验报告怎么改写成自己的交付物
压缩包里的设计文档和实验报告对上手很有价值,但直接提交或拿去演示往往会被看出破绽。常见做法是把它当“骨架”而不是“答案”。我拿到这类资料后,会先对照实际SQL文件核对表名、字段类型,然后按下面这个checklist改写:
| 文档章节 | 建议写什么 | 不要写什么 |
|---|---|---|
| 需求分析 | 三张用例图:挂号员、医生、管理员 | 空泛的“智慧医院前景” |
| 数据库设计 | ER图 + 字段类型说明 + 核心外键关系 | 只贴建表SQL不讲业务原因 |
| 系统设计 | 时序图:挂号时号源如何扣减 | 大段介绍Spring Boot是什么 |
| 功能实现 | 每个核心流程配2-3张系统截图 | 贴一整段没注释的代码 |
| 测试 | 正常流程 + 并发挂号的异常测试 | 一句“测试全部通过” |
改的时候把项目里真实的页面截图替换掉旧截图,数据也要换成自己的测试数据,比如患者名字、挂号时段、医生排班。有一个坑特别值得注意:很多下载包里的设计文档写的是“基于SSM框架”,实际源码是Spring Boot,或者SQL文件里的reg_no字段长度和文档里的定义不一致。这种不一致在答辩现场一问一个准。所以我会把文档里所有“系统环境”章节改为与你本机一致的版本,比如JDK 1.8、MySQL 8.0、Maven 3.6。
实验报告一般要求写测试结论。不要写“系统运行稳定”,要写具体数据:使用JMeter模拟100个并发挂号请求,最终成功挂号50个,失败50个,失败请求都提示“号源已满”,数据库号源余量为0。这种结果能证明你真的跑过并发场景。设计文档里如果提到了后续优化方向,比如小票打印、排队呼叫大屏、微信预约,可以在“展望”里保留,但别写得太夸张,能做到的才提。
5. 智慧医院门诊管理系统避坑指南:从环境配置到并发挂号的5个翻车现场
这五条坑分布在不同阶段:前两条让你起不来,后三条让你跑起来却数据不对。每条我都按现象、原因、解决三步来拆,最后也会说一下怎么快速验证自己有没有踩到同类问题。
5.1 MySQL 8.0驱动和时区导致连接失败
现象:项目启动后Spring Boot日志报错,提示Caused by: java.sql.SQLException: The server time zone value '�й���ʱ��' is unrecognized,或者Public Key Retrieval is not allowed。
原因:MySQL 8.0的认证插件和驱动行为与5.7不同。老旧驱动com.mysql.jdbc.Driver已经被移除,url里不指定时区,驱动就无法判断连接怎么处理时间;allowPublicKeyRetrieval默认false,使用caching_sha2_password认证时就会被拒绝。
解决:把application.yml里的driver改成com.mysql.cj.jdbc.Driver,url末尾加serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true。如果还有问题,用SHOW VARIABLES LIKE 'default_authentication_plugin';查MySQL默认插件,必要时把用户改成mysql_native_password。这是最常见的启动坑,改完连接串基本能解决。验证方式很简单:改完重启,看到Tomcat started on port 8080就过了。
5.2 高并发下号源超卖:检查你的扣减SQL是否带条件
现象:用Postman或JMeter对预约挂号接口做100个并发,模拟同一个时段,最终数据库里schedule表的remaining变成负数,registration表多出超过号源总数的记录。
原因:代码里是“先select再update”,两个请求同时读到剩余1个号,各自判断可以挂,最后都插入挂号单。超卖的根因是检查与更新之间存在时间窗。
解决:使用UPDATE schedule SET remaining = remaining - 1 WHERE schedule_id = ? AND remaining > 0,并让Service根据返回影响行数决定是否插入挂号单。事务里还要给这项操作加上@Transactional(rollbackFor = Exception.class)。我很建议在启动前用并发工具压一遍,通过的人再讲“系统支持高并发”才有底气。压测时记得把数据库连接池maxPoolSize调大一点,默认10个连接在100并发下会让部分请求等待连接,导致误判为超卖。
5.3 SQL文件导入后中文乱码:把utf8mb4贯彻到底
现象:导入数据库sql文件后,页面或数据表里患者姓名显示为“???”,或者英文正常但中文变成乱码。
原因:SQL文件是UTF-8编码,但命令行客户端用latin1连接数据库执行,入库时就存成了乱码;还有一种可能是数据库、表、连接三级字符集不一致。
解决:导入时指定--default-character-set=utf8mb4;在SQL脚本开头显式加一句SET NAMES utf8mb4;;建库语句写成CREATE DATABASE hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。如果已经乱码,先调整字符集再重新导入,不要试图在乱码基础上修。用Navicat导入时,连接属性里也要把编码设为UTF-8。验证方法是连上数据库执行SELECT name FROM patient LIMIT 1;,能看到中文就说明字符集通了。
5.4 挂号单号不能只靠日期加自增ID:并发重号和回滚断号的坑
现象:线上挂号时两份挂号单号相同,或者一个事务回滚后自增ID跳过一截,导致流水号不连续,数据库唯一索引报错。
原因:用SELECT MAX(reg_no)加一或者依赖数据库自增ID来拼接业务流水号,在并发下前后两步不是原子操作;自增ID在事务回滚后也不会回收。
解决:使用Redis的INCR按天生成序号:key用reg:20250607,每天从1开始自增,然后拼成202506070001号。如果项目没用Redis,就用雪花算法。不要让普通自增主键和业务号混在一起。如果已经在用唯一索引,插入前要捕获DuplicateKeyException并提示用户稍后重试。这个坑在答辩时很容易被追问“你的挂号单号生成会不会重复”,值得提前准备好。
5.5 定时任务重复生成号源:当日schedule出现两条相同记录
现象:第二天打开门诊排班页面,同一个医生同一天上午出现两行schedule,号源总量翻倍,患者还能挂两次相同的号。
原因:启动类没有做幂等控制,定时任务执行两遍;或者项目在多个端口/多实例部署,每个实例都跑了一次@Scheduled。
解决:在定时任务里先运行deleteByWorkDate(today)再插入,保证任务重入不累加。多实例部署就要加分布式锁,常见做法是引入Redis的setnx或者使用xxl-job把任务收敛到一台机器上。再有就是注意cron表达式别把月份和星期混用了,0 0 0 * * ?星期的?不能改成*,否则每周每天都会触发多次,早上起来数据库全是重复号源。这类问题不会报错,只有后台看数据才发现,属于隐蔽坑。验证方法就是第二天上班前查一下schedule表有没有重复行,或者干脆手动执行一次任务看结果。
6. 从“能跑”到“能演示/能上线”:验证门诊流程的10个用例与性能优化切入点
6.1 让门诊闭环可信:按顺序验证这10个用例
拿到项目后,别只在登录页转圈。按下面这套用例走一遍,走通才算真的能演示。
| 用例 | 操作 | 预期结果 |
|---|---|---|
| 1 | 管理员登录,新增一个科室 | 科室列表多一条记录 |
| 2 | 管理员新增医生并绑定科室 | 医生出现在排班候选名单 |
| 3 | 定时任务生成当天号源 | schedule表有该医生上下午记录 |
| 4 | 挂号员为患者建档 | patient表生成唯一patient_id |
| 5 | 挂号员挂一个上午号 | registration状态待就诊,remaining减1 |
| 6 | 同一时段重复挂号 | 提示号源已满,不生成第二条记录 |
| 7 | 医生登录查看待诊队列 | 只显示该医生本人患者 |
| 8 | 医生接诊并开处方 | 状态变就诊中,prescription有数据 |
| 9 | 收费员缴费 | payment产生记录,挂号状态变已完成 |
| 10 | 退号后再查号源 | remaining加回1,状态变退号 |
这套流程同时覆盖正常和异常路径。我一般会打开两个浏览器窗口模拟并发,再配合JMeter打一下挂号接口,看最终号源数是否对得上。把这些结果写进实验报告,比空写“运行稳定”强很多。
6.2 真想优化并发,先动连接池和索引,别急着上Redis
赶项目时最常见的错误是一上来就加Redis,结果把简单方案搞复杂。先改两个地方:Hikari连接池maximum-pool-size从10调到50,schedule表加一个(work_date, time_slot, doctor_id)联合索引。这两处改动成本低、效果好,演示时能明显感觉到挂号接口响应变快。想再进一步,才考虑用Redis做号源扣减,而且扣减必须用Lua脚本保证原子性,否则反而引入新的不一致。
6.3 一个过来人的收尾
以前拿一份类似源码跑通后就以为稳了,答辩时老师一句“你这个挂号单号怎么生成不重复”就把我问住了。源码包的价值不在能运行,而在你能把设计讲透。基于java的智慧医院门诊管理系统项目源码配数据库sql文件,是绝佳的练手载体,但建议你主动改两个点:把号源扣减改成条件UPDATE,把医生查询加上行级过滤,然后写一个并发测试证明它不超卖。改完再讲需求、表设计、事务边界,整个人的底气完全不同。希望帮到你。
本文还有配套的精品资源,点击获取