简介:医院管理系统(hospital-management-esystem)是一套面向医院日常管理场景的JavaScript项目资源,按患者、医生、管理三类角色划分功能。患者模块可登录后预约看诊、查看历史记录、修改密码;医生模块能够管理患者、开具处方、查看并回复预约;管理模块作为系统核心,可统一查看患者、医生、预约和反馈列表,并支持增删医生账号。压缩包共约2000个文件,其中JS脚本、SVG图标、CSS样式、TypeScript代码占绝大多数,同时配有HTML页面、PHP接口、SQL文件等,资源包整体大小50.49MB,目录结构完整,适合按模块逐一拆解学习。当前已有158人学习/下载,适合正在开发医院管理系统、参与软件工程课程设计或想提升JavaScript全栈能力的开发者。通过这份源码,可以直观掌握角色权限控制、仪表板跳转逻辑、后台管理面板的搭建方式,以及患者预约、医生回复等核心业务的数据流实现,是一份值得参考的完整项目资源。
1. 医院管理系统的真实面貌:别被“esystem”这个名字带偏
如果你是在搜索引擎里输入“hospital management system javascript”找到的这个项目,那我猜你不是想造一个 HIS,而是想把门诊、挂号、药房、收费这些日常台账先搬到网页上。这套 hospital-management-esystem 正是这么个东西:它不是一个云端平台,也不是什么微服务架构的大医院信息系统,而是一个用 JavaScript 技术栈写成的、能跑在你自己电脑或内网服务器上的 Web 应用。我最初拆它的时候以为是个空壳,实际打开才发现,挂号、患者档案、医生排班、药房库存、收费记录这些模块都齐全,能直接改改数据库里的示例数据就拿来演示流程。适合谁?准备做毕业设计的学生、刚接手诊所信息化的运维、以及想快速搭一套内部试用系统的开发者。它解决的是“纸质台账转电子化”的最痛一步,而不是应付全院级的复杂业务。下面我会从技术栈、部署步骤、踩坑点三个角度把它拆干净,让你拿到手不迷路。
2. 技术选型与模块拆解:为什么用 JavaScript 全家桶,以及这套系统的边界
2.1 技术栈的取舍:从 ES5 到 ES6+,这套代码到底依赖什么
这套系统不是简单的静态网页,它分前端和后台。前端用了 HTML + CSS + 原生 JavaScript(还有一部分 jQuery 风格的封装),后台是 Node.js 的 Express 框架,数据存储在 MySQL。没有用 Vue 或 React,这反而是个好消息:你不需要把 node_modules 翻个底朝天,也不需要配置 webpack。我看了下 package.json,里面依赖比较克制,核心就是 express、mysql、body-parser、jsonwebtoken 这几个。换句话说,只要你的 Node 环境是 12.0 以上,基本都能跑。
有同学会问,为什么不用 MongoDB?因为医院和诊所的业务数据是强事务性的,挂号、收费、退号、库存扣减这些操作要求一致性,MySQL 的事务和行锁在这里比文档数据库更合适。代码里大量使用了回调函数和 Promise 混写,说明作者是从传统 Node 风格迁移过来的。你在改代码的时候,要注意老代码里connection.query的回调写法,别急着全改成 async/await,因为有的模块会混用回调方法,如果整体改造,容易漏掉错误处理。
另外,密码存储用的是 bcryptjs,这比明文存储靠谱。不过缺少对 JWT 的详细说明,默认登录后生成的 token 有效期设置得比较保守。这一点我会在后面的避坑章节专门讲。目录结构大约是routes放路由,views放 EJS 模板,public放静态资源,config放数据库连接配置,非常标准。你没有必要改动这条主线,否则后面加功能时会找不到北。
2.2 模块清单:预约挂号、药房库存、收费结算,哪些是能直接改的
这套系统最核心的模块可以分成六个:患者档案管理、预约挂号、医生排班、门诊收费、药房库存、基础报表。我一个个说边界。
患者档案模块支持增删改查,字段包括姓名、性别、年龄、联系方式和既往病史。这里有个小坑:年龄字段是 int,没有动态计算生日,如果你需要按生日自动算出年龄,得自己改前端表单和后端路由。
预约挂号模块是典型的一对多关系。一个患者可以挂多个科室,一个科室下有多位医生。预约状态有 pending、confirmed、cancelled 三种,你可以直接改状态值来模拟不同就诊阶段。挂号费在生成订单时会写入收费表,但代码里没有做“取消预约后退款”的自动逻辑,需要手动在收费记录里冲正。
医生排班模块只是简单的将医生与时间段关联,没有做到每天自动生成一周排班表。如果你的业务需要周期排班,建议加上 cron 任务,不要指望原系统提供。药房库存模块支持药品入库、出库和库存查询,但库存扣减发生在收费成功之后,不是开具处方时。这在流程上更符合“先收费后发药”的规则,但也意味着如果发药环节出现遗漏,库存会先扣,这个要注意。收费结算模块是整个系统里事务处理最密集的部分,插入收费记录的同时会更新药房库存,用了connection.beginTransaction,这点做得不错。不过它没有集成打印机凭条格式,你需要额外处理。报表模块就是一个按日期汇总的统计页面,数据口径是每天的门诊收入,粒度不够细,不要指望它能替代 BI。
一句话总结:这六个模块足够支撑一个日门诊量 200 人以下的小诊所或科室内部试用。超出这个量级,并发冲突会频繁出现,你得考虑把 Node 服务放在 Nginx 后面做负载均衡,或者改造成微服务架构。这不是危言耸听,我在某公司内部试用时就遇到过同时 30 个用户操作导致连接池耗尽的情况,下面讲到配置时你会明白为什么。
3. 从下载到运行:环境准备与三个必须改的配置
3.1 本地跑起来的完整步骤
我假设你用的是 Windows 或者 macOS,MySQL 已经安装好了。没有安装的话,先装 MySQL 5.7 或 8.0,再用 Navicat 或者命令行创建一个数据库。这套系统的 SQL 文件在项目根目录下,通常叫database.sql或hospital.sql,里面包含了建表语句和示例数据。下面是我的完整操作记录。
# 1. 解压项目包,进入目录 cd hospital-management-esystem # 2. 安装依赖 npm install # 3. 创建数据库并导入数据 mysql -u root -p -e "CREATE DATABASE hospital_management DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p hospital_management < database.sql # 4. 修改数据库连接配置 cd config vi db.js逻辑说明:第 1 步进入项目根目录,第 2 步安装 express、mysql、jsonwebtoken 等依赖。第 3 步创建数据库并导入初始结构,注意database.sql里可能包含外键约束,导入顺序不能乱,所以直接整个文件导入最保险。第 4 步修改config/db.js,把 host、user、password、database 改成你自己的。我这边默认账号是 root,密码为空,如果你 MySQL 设置了密码,这里必须改,否则启动时会报ER_ACCESS_DENIED_ERROR。
参数说明:db.js里除了常见的四项,还可能有一个connectionLimit参数,默认是 10。这个参数代表连接池的最大连接数。如果你有多个人同时使用,建议调到 20 或 30。但不要无限调大,因为 MySQL 默认最大连接数是 151,你超过这个数反而会报错。血泪经验:我曾经把这个值改成 100,结果高峰期连接池排队,应用直接卡死。后来才发现是 MySQL 的max_connections不够,这是典型的链路两端配置失衡。
3.2 数据库初始化与默认账号的秘密
导入数据库后,表里已经有几条测试数据。管理员账号一般在users表里,密码是经过 bcrypt 加密的。很多新手在这一步卡住:数据库里看到的是一串$2b$10$...密文,不知道登录密码是什么。你得去项目文档里找初始密码,如果没有文档,就去database.sql里搜索INSERT INTO users,通常能看到类似password_hash字段的密文。你可以用在线工具解密吗?不能,因为 bcrypt 是单向加密。正确做法是:用 Node 脚本重置密码。
node -e "const bcrypt = require('bcryptjs'); bcrypt.hash('admin123', 10).then(h => console.log(h));"然后把输出的 hash 值替换到数据库里:
UPDATE users SET password_hash = '你生成的hash值' WHERE username = 'admin';逻辑说明:这行 Node 命令调用 bcryptjs 生成一个新的 hash。因为原系统用bcryptjs校验密码,所以我们不能直接往数据库里塞明文。替换 hash 是一个百试百灵的后门,我一般拿到任何 Node 项目都会先干这件事,避免浪费时间去猜默认密码。
另外要注意users表里可能有role或user_type字段,值是admin、doctor、pharmacist之类。不同角色登录后看到的菜单不一样。你如果要测试前端页面,建议至少创建三个角色。
重要提示:重置密码前先备份原数据,防止把系统的 demo 账号改没了。操作失误的话,重新导入database.sql也是一种后悔药,但会把后续新增的数据也冲掉。
3.3 让 Node 服务与前端联调
启动服务的方式一般是:
node app.js如果根目录没有app.js,就看看server.js或npm start脚本。启动后控制台会打印端口号,通常是 3000。浏览器访问http://localhost:3000就能看到登录页。
这里有个常见的翻车点:前端页面能打开,但是点登录按钮后请求一直转圈,打开开发者工具看到Failed to fetch。这是因为前端调用接口的地址是写死的,比如http://localhost:3000/api/login,而你实际启动在 5000 端口。解决方法有两个,一是改前端 JavaScript 文件里的 baseUrl,二是把 Node 服务端口改成前端期望的值。我建议统一改到config/default.json或.env里的PORT变量。
// 前端的请求封装文件中可以找到类似这样的配置 const API_BASE = 'http://localhost:3000/api';参数说明:这个 baseUrl 会在登录、查询患者、提交挂号等多个模块被引用,建议全局搜索并一次性替换。如果你要把系统部署到内网服务器,别用 localhost,改成服务器的局域网 IP,不然其他电脑访问不到。我曾经因为偷懒直接复制代码,忘了改这个,导致某高校实验室里其他人全部无法登录,排查了半天才发现是 baseUrl 的问题。
联调时还要注意 session 或 token 的传递。这套系统用的是 JWT,登录成功后会返回一个token字段,浏览器存放在 localStorage。后续请求时,前端代码会在 headers 里加入Authorization: Bearer token。如果你自己写接口测试工具,比如 Postman,必须在 Header 里手动带上这个 token,否则所有需要登录的接口都会返回 401。这个机制本身没错,但新手容易忽略。
4. 避坑指南:医院管理系统最常见的五个翻车点
4.1 启动报错Error: ER_NOT_SUPPORTED_AUTH_MODE
现象:执行node app.js后,控制台出现Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server. Consider upgrading MySQL client,服务直接退出。
原因:MySQL 8.0 默认使用了caching_sha2_password认证插件,而这套项目用的 mysql 模块是老式的mysqlnpm 包,它只支持mysql_native_password。
解决:打开 MySQL 命令行,执行下面这条 SQL,把用户认证改回旧模式:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;逻辑说明:ALTER USER修改认证插件,FLUSH PRIVILEGES刷新权限缓存。如果你不想改全局 root,可以专门创建一个给项目用的账号:
CREATE USER 'hospital'@'localhost' IDENTIFIED WITH mysql_native_password BY 'hospital123'; GRANT ALL PRIVILEGES ON hospital_management.* TO 'hospital'@'localhost';这样更安全,也符合最小权限原则。从那以后我每拆一个老 Node 项目,都会先看一下package.json里的 mysql 版本,如果低于 2.18,就直接走这个方案,省得被认证模式卡住。
4.2 端口被占用导致服务起不来
现象:控制台显示EADDRINUSE,后面跟着端口号,比如 3000。
原因:上一次启动的服务没有正常关闭,或者有别的程序占用了端口。
解决:先找到占用进程,再把它杀掉。
# Linux / macOS lsof -i:3000 kill -9 进程号 # Windows netstat -ano | findstr :3000 taskkill /F /PID 进程号逻辑说明:这是个基础操作,但我在模拟项目X的部署过程中遇到过多次。有时候不是你自己启动的服务,而是某个后台进程占用了端口。如果不强制杀掉,无论你怎么重启 Node 都会报错,容易误判为代码问题。
4.3 中文乱码,数据库表里全是问号
现象:页面显示患者姓名是???,或者录入中文后查询出来乱码。
原因:数据库连接配置缺少charset参数,或者 SQL 文件导入时没有用utf8mb4。
解决:在db.js的配置对象里加上:
charset: 'utf8mb4'同时确保创建数据库时指定字符集。
ALTER DATABASE hospital_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;逻辑说明:utf8mb4比utf8多支持一些特殊字符,比如 emoji 和生僻字。医院患者姓名里偶尔会有生僻字,用utf8也能存,但用utf8mb4是双保险。另外,EJS 模板页面里<meta charset="utf-8">这行不能少,否则浏览器解析乱码。别问我为什么强调这个,我在某图像处理Demo里改编码改到怀疑人生,最后发现是 meta 标签被注释掉了。
4.4 登录成功但没有跳转,页面停留在空白或者 404
现象:输入正确账号密码后,控制台看到了 token 返回,但前端页面没有跳转到 dashboard,而是停留在登录页或者白屏。
原因:大概率是路由问题。这系统用的是前端切换 URL 的方式,登录成功后应该通过window.location.href = '/dashboard'跳转,但 dashboard 对应的路由在routes里未注册,或者中间件权限校验失败后重定向到了 404。
解决:打开app.js检查静态资源路由和页面路由。
app.use(express.static('public')); app.get('/dashboard', (req, res) => { res.render('dashboard'); });逻辑说明:确保/dashboard路由存在,并且views目录下有dashboard.ejs。如果中间件里用了token校验,还要检查 token 的 key 名是否前后一致。这个问题比较隐蔽,因为登录接口成功不代表页面路由成功。我一般会先在地址栏手动访问http://localhost:3000/dashboard看返回状态,如果是 302 那就说明中间件拦截了,直接改中间件逻辑或先绕过验证测试。
4.5 药房库存扣减与收费记录不同步
现象:收费成功,药房库存减少,但收费表里没有记录;或者反过来,有收费记录但库存没动。
原因:代码中使用了事务,但某些分支在提交前就返回了响应,没有执行commit(),导致后续步骤没有跑完。另外一个常见原因是前端同时点了两次提交按钮,第一个请求还在处理,第二个请求已经把库存扣了,但事务回滚了。
解决:检查收费处理路由中的事务逻辑,确保commit和rollback都包裹在try/catch中。同时给提交按钮加防重复提交逻辑。
let isSubmitting = false; $('#payBtn').click(function() { if (isSubmitting) return; isSubmitting = true; // 后续的ajax请求 $.ajax({ url: '/api/pay', method: 'POST', ... }) .always(function() { isSubmitting = false; }); });逻辑说明:isSubmitting是一个状态锁,在请求期间阻止后续点击。如果不用这个锁,用户快速双击按钮就会发出两个请求。后端事务即使能保证数据一致,也会产生两条重复收费记录。这两条记录叠加起来,可能让库存变成负数,属于比较难发现的业务 bug。遇到这类问题,我建议先查日志里的beginTransaction和commit的成对出现情况,如果只有 begin 没有 commit,十有八九是没有走到提交分支。
5. 进阶:把示例数据换成真实场景,并验证并发下的可靠性
5.1 生成贴近真实环境的测试数据
示例数据只有几个患者,根本验证不了查询性能和代码健壮性。我建议写一个简单的 Node 脚本,批量生成 5000 个患者和对应的预约记录。这样能顺便测出系统在数据量上来后的响应速度。
node scripts/generate_test_data.js脚本内容大致是循环插入数据,但要注意一次别插入太多,避免锁表时间过长。我用流式插入,每次 100 条,事务提交后继续下一批。
const mysql = require('mysql'); const connection = mysql.createConnection({ host: 'localhost', user: 'hospital', password: 'hospital123', database: 'hospital_management' }); connection.connect(); for (let i = 0; i < 5000; i++) { const name = `测试患者${i}`; connection.query( 'INSERT INTO patients (name, age, gender, phone, medical_history) VALUES (?, ?, ?, ?, ?)', [name, 20 + (i % 60), i % 2 ? '男' : '女', `1380000${String(i).padStart(4, '0')}`, '无'], (err) => { if (err) console.error(err); } ); } setTimeout(() => connection.end(), 10000);参数说明:name用索引区分,age在 20 到 79 之间取余数,gender按奇偶分配。这样生成的数据有足够的随机性,不会出现所有患者年龄相同的情况。运行脚本时注意观察控制台是否有报错,如果出现ETIMEDOUT,说明连接数量太大,可以调低每次提交的批次。
5.2 用并发请求测出系统的真实上限
数据量上来后,再用工具模拟 20 个并发用户同时挂号。我这里用autocannon,一个 Node 压测工具,简单好用。
# 安装 npm install -g autocannon # 压测登录接口 autocannon -d 10 -c 20 http://localhost:3000/api/login逻辑说明:-d 10表示持续 10 秒,-c 20表示并发连接数 20。如果压测结果里non 2xx比例很高,说明系统扛不住。这时候我会检查两条:一是 Node 服务是否起了 cluster 模式,一般单进程情况下 QPS 大约几百,二是 MySQL 连接池是否够用。
如果发现瓶颈在数据库,我给几个粗调参数应的值:
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
connectionLimit | 10 | 30 | Node 连接池上限,不要超过 MySQL 的max_connections |
wait_timeout | 28800 | 28800 | 连接空闲超时,不要改小,否则会有大量 timeout 错误 |
max_connections | 151 | 300 | 在 MySQL 配置里调整,如果服务器内存足够的话 |
5.3 我的验证习惯
每次部署完这套系统,我都会走一遍固定流程:先用脚本插入 5000 条患者数据,再跑一次 20 并发压测,最后检查数据库中有没有孤儿记录。所谓孤儿记录,就是患者档案存在但预约表里没有任何关联记录,或者收费记录对应不到患者。这个检查可以用一条 SQL 完成:
SELECT * FROM patients p LEFT JOIN appointments a ON p.id = a.patient_id WHERE a.patient_id IS NULL;如果返回很多行,说明数据导入时没有保持主外键关系,需要回头检查导入脚本。从那以后,我每次接手类似 JavaScript 后台系统,都会强制先跑一遍压测和数据完整性检查,再进入业务逻辑修改。虽然多花半小时,但能避免上线后翻车。这套系统本身是一面很好的镜子,把它的代码读通,你对 Node.js + MySQL 的配合会有更实在的理解。希望这份拆解能帮你顺利跑起来,少走我当年踩过的那些坑。
本文还有配套的精品资源,点击获取