PHP+MySQL医院管理系统实战:从部署到性能优化

发布时间:2026/10/14 8:38:16
PHP+MySQL医院管理系统实战:从部署到性能优化 简介这是一套面向计算机专业学生、课程设计开发者及中小型医疗机构信息化入门者的医院管理系统源码采用PHPMySQLApache经典组合前端基于Bootstrap构建响应式界面适合用作毕业设计、实训项目或二次开发基础。压缩包共178个文件约4.53MB其中58个PHP文件承载核心业务逻辑44个JavaScript与31个CSS文件负责交互与页面样式另有jpg、png、svg等图片及woff2、ttf、eot等字体资源整体结构完整、便于部署。系统覆盖医院信息、患者、医生、科室、药品、病历六大管理模块可实现患者档案、医生排班、科室排班、药品库存、检查报告与诊断记录的统一录入、查询和维护帮助使用者理解医疗信息系统的数据组织方式与权限划分思路。目前已有119人学习下载适合希望快速搭建可运行项目、对照源码梳理功能模块与数据库设计的学习者参考。1. 从一张排班表说起这套 PHP 医院管理系统到底能跑多快上周帮一个做医疗信息化的朋友看他们内部用的排班工具打开页面转了七八秒才出表格后台一查是每次刷新都在全表扫 MySQL 的排班记录Apache 的 access log 里同一秒堆了四十多条请求。他问我有没有轻量点的参考实现我就把手里这套 PHP MySQL Apache Bootstrap 的医院管理系统翻出来重新跑了一遍。这套东西不是给三甲医院用的它的定位很明确中小型诊所、社区门诊、校医院这类场景需要挂号、患者档案、医生排班、药品库存、收费记录这几块基础功能又不想上 Spring Cloud 那一整套重家伙。技术栈就是最经典的 LAMP 变体PHP 做业务层MySQL 存数据Apache 当 Web 服务器前端用 Bootstrap 把表格和表单撑起来。适合谁适合刚接触 Web 全栈、想拿一个业务闭环完整的项目练手的人也适合小团队要快速搭一个能用的内部系统。下面我按实际部署和二次开发的顺序把这份资源拆开讲。2. 环境搭起来Apache PHP MySQL 的版本咬合与目录规划2.1 为什么这套组合还在被用很多人一听 PHP MySQL 就觉得老但在医院管理这类场景里它的优势恰恰是「不折腾」。Apache 的 mod_php 模式把 PHP 解释器直接嵌进 Web 服务器进程省掉了一层 FastCGI 通信对于并发量在几十到几百 QPS 的小系统来说响应延迟反而比 Nginx PHP-FPM 更可控。MySQL 这边医院系统的数据关系非常规整——患者、医生、科室、药品、挂号记录全是典型的关系型结构用 InnoDB 做外键约束和事务比 MongoDB 那种文档模型省心得多。Bootstrap 负责前端好处是表格、模态框、表单验证样式都是现成的不用自己写 CSS 就能让界面看起来像个正经系统。这套资源的技术选型不是拍脑袋它对应的是一个真实的约束开发人手少、服务器配置低、维护周期长。你如果用 Laravel 或 ThinkPHP 重写功能一样但部署复杂度上去了对小团队来说不划算。2.2 版本选择与安装步骤我一般会锁定 PHP 7.4 或 8.0MySQL 5.7 或 8.0Apache 2.4。PHP 8.1 以上对某些老扩展的兼容性会出问题尤其是mysql_*系列函数已经被移除如果源码里还有旧写法跑起来会直接报 fatal error。下面是在 Ubuntu 22.04 上的安装流程Windows 下用 XAMPP 或 phpstudy 集成包更省事但生产环境不建议。# 更新包索引并安装 Apache、MySQL、PHP 及常用扩展 sudo apt update sudo apt install -y apache2 mysql-server php libapache2-mod-php php-mysql php-mbstring php-gd php-curl php-json # 启动服务并设置开机自启 sudo systemctl enable apache2 mysql sudo systemctl start apache2 mysql # 验证 PHP 是否被 Apache 正确解析 echo ?php phpinfo(); ? | sudo tee /var/www/html/info.php # 浏览器访问 http://你的服务器IP/info.php看到 PHP 信息页即成功这段命令的逻辑是libapache2-mod-php把 PHP 作为 Apache 模块加载php-mysql提供 MySQL 连接驱动php-mbstring处理中文患者姓名php-gd用于生成报表图片。装完后一定要访问info.php确认因为有时候 Apache 和 PHP 的版本对不上模块加载失败但服务不报错页面会直接输出 PHP 源码。提示验证完记得删掉info.php这个文件会暴露 PHP 版本和已加载模块属于信息泄露。2.3 目录结构与虚拟主机配置源码包解压后我习惯把 Web 根目录指向public/而不是项目根这样config/、vendor/这些敏感目录不会暴露在 URL 里。Apache 的虚拟主机配置如下VirtualHost *:80 ServerName hospital.local DocumentRoot /var/www/hospital/public Directory /var/www/hospital/public Options -Indexes FollowSymLinks AllowOverride All Require all granted /Directory ErrorLog ${APACHE_LOG_DIR}/hospital_error.log CustomLog ${APACHE_LOG_DIR}/hospital_access.log combined /VirtualHostOptions -Indexes禁止目录列表AllowOverride All允许.htaccess做 URL 重写这对后面做路由美化很关键。ErrorLog单独指定方便排查 PHP 报错。配置完执行sudo a2ensite hospital.conf sudo systemctl reload apache2生效。3. 数据库设计从挂号表到库存表的字段取舍3.1 核心表结构与索引策略医院管理系统的数据库设计有几个容易翻车的地方患者身份证号用VARCHAR(18)还是CHAR(18)、挂号记录的时间字段用DATETIME还是TIMESTAMP、药品库存的扣减怎么保证并发安全。我拿到的这份源码里核心表大概有七八张下面挑三张最关键的说明。-- 患者基本信息表 CREATE TABLE patients ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 患者姓名, id_card CHAR(18) NOT NULL COMMENT 身份证号定长查询更快, phone VARCHAR(15) DEFAULT NULL, gender TINYINT(1) DEFAULT 0 COMMENT 0未知 1男 2女, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_id_card (id_card), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 挂号记录表 CREATE TABLE registrations ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, patient_id INT UNSIGNED NOT NULL, doctor_id INT UNSIGNED NOT NULL, dept_id INT UNSIGNED NOT NULL, reg_time DATETIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待诊 1就诊中 2已完成 3已取消, PRIMARY KEY (id), KEY idx_patient (patient_id), KEY idx_doctor_time (doctor_id, reg_time), CONSTRAINT fk_reg_patient FOREIGN KEY (patient_id) REFERENCES patients (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;id_card用CHAR(18)而不是VARCHAR因为身份证号长度固定CHAR在 InnoDB 里查询时不需要额外读取长度字节索引效率略高。idx_doctor_time这个联合索引是为了支持「查某医生某天的挂号列表」这个高频查询如果只建doctor_id单列索引MySQL 在过滤时间范围时还是要回表扫。utf8mb4字符集必须指定否则患者姓名里的生僻字会变成问号。3.2 库存扣减的并发处理药品库存表是另一个坑点。如果两个收费窗口同时给同一个患者开同一种药简单的UPDATE stock stock - 1在 InnoDB 行锁下是安全的但前提是你要在事务里先SELECT ... FOR UPDATE锁住那一行。源码里有些地方直接用了UPDATE不带事务并发一高就会出现超卖。// 正确的库存扣减写法 $pdo-beginTransaction(); try { // 锁定药品行防止其他事务同时修改 $stmt $pdo-prepare(SELECT stock FROM medicines WHERE id ? FOR UPDATE); $stmt-execute([$medicineId]); $row $stmt-fetch(); if ($row[stock] $quantity) { throw new Exception(库存不足); } $pdo-prepare(UPDATE medicines SET stock stock - ? WHERE id ?) -execute([$quantity, $medicineId]); $pdo-commit(); } catch (Exception $e) { $pdo-rollBack(); // 记录日志并返回错误给前端 }FOR UPDATE会在读取时加排他锁直到事务提交才释放。这段代码的关键是「先锁后判再扣」顺序不能乱。如果先SELECT不加锁判断完库存再UPDATE中间的时间窗口足够另一个请求把库存扣光。注意FOR UPDATE必须在事务内使用否则锁会立即释放等于没加。4. 业务模块实现挂号、排班与权限控制的代码落地4.1 挂号流程的完整链路挂号这个动作看起来简单实际上涉及患者建档、医生排班校验、号源扣减、费用生成四个步骤。源码里的实现是把它们放在一个控制器方法里顺序执行我把它拆开讲清楚每一步在干什么。// 挂号控制器核心逻辑简化版 public function register($request) { // 1. 患者不存在则先建档 $patient $this-patientModel-findByIdCard($request[id_card]); if (!$patient) { $patientId $this-patientModel-create([ name $request[name], id_card $request[id_card], phone $request[phone], ]); } else { $patientId $patient[id]; } // 2. 校验医生当天是否出诊、号源是否还有余量 $schedule $this-scheduleModel-getByDoctorAndDate( $request[doctor_id], date(Y-m-d) ); if (!$schedule || $schedule[remain] 0) { return [code 400, msg 该医生今日号源已满]; } // 3. 扣减号源并写入挂号记录事务包裹 $this-db-beginTransaction(); try { $this-scheduleModel-decreaseRemain($schedule[id]); $regId $this-registrationModel-create([ patient_id $patientId, doctor_id $request[doctor_id], dept_id $schedule[dept_id], reg_time date(Y-m-d H:i:s), status 0, ]); $this-db-commit(); } catch (Exception $e) { $this-db-rollBack(); return [code 500, msg 挂号失败请重试]; } return [code 200, data [reg_id $regId]]; }第一步的findByIdCard走的是uk_id_card唯一索引查询很快。第二步的排班校验是防止医生停诊但前端没刷新导致误挂号。第三步把扣号源和写记录放在同一个事务里保证不会出现「号扣了但记录没写」的情况。参数方面doctor_id和id_card是必传的phone可选但建议填方便后续短信通知。4.2 医生排班的批量生成排班表如果靠人工一条条录一周七个医生就是四十九条记录很容易漏。源码里提供了一个批量生成的功能按科室和医生列表循环插入。// 批量生成一周排班 public function batchGenerate($deptId, $startDate) { $doctors $this-doctorModel-getByDept($deptId); $shifts [上午, 下午]; $rows []; for ($i 0; $i 7; $i) { $date date(Y-m-d, strtotime($startDate $i day)); foreach ($doctors as $doc) { foreach ($shifts as $shift) { $rows[] [ doctor_id $doc[id], dept_id $deptId, work_date $date, shift $shift, total 30, // 每个班次放号30个 remain 30, ]; } } } // 批量插入比逐条 INSERT 快一个数量级 return $this-scheduleModel-insertBatch($rows); }insertBatch内部拼的是INSERT INTO ... VALUES (...), (...), ...这种多值语法一次网络往返就能写入上百条。total和remain初始值设成一样后续挂号只动remain这样能追溯原始号源数量。如果某个医生临时停诊把对应记录的remain置零即可不用删记录。4.3 基于角色的权限控制医院系统里不同角色的可见范围差别很大挂号员只能看挂号模块药房只能看库存医生能看自己的患者列表管理员什么都能看。源码用的是最简单的角色字段判断没有上 RBAC 那套复杂的权限表。// 权限检查中间件 public function checkAuth($requiredRole) { $user $_SESSION[user] ?? null; if (!$user) { header(Location: /login.php); exit; } // 角色等级1管理员 2医生 3挂号员 4药房 $roleLevel [1 99, 2 50, 3 30, 4 20]; if ($roleLevel[$user[role]] $roleLevel[$requiredRole]) { http_response_code(403); exit(无权访问); } }这种写法的好处是简单直接缺点是角色和权限硬编码在代码里加一个新角色要改多处。如果后续要扩展建议把$roleLevel抽到配置文件或者换成「角色-权限」二维表。对于当前这个规模硬编码够用了。5. 避坑与排查部署和运行中最容易翻车的五个点5.1 中文乱码从数据库到页面的全链路排查现象患者姓名在列表页显示为????或乱码方块。原因字符集在某一层断了。常见的是 MySQL 建库时用了latin1或者 PHP 连接时没设charsetutf8mb4或者 HTML 的meta标签写的是gbk。解决按「数据库 → 连接 → 页面」三层逐一确认。建库语句用CREATE DATABASE hospital DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;PDO 连接串加charsetutf8mb4HTML 头部写meta charsetutf-8。三层都对了就不会乱。5.2 挂号并发超卖没有事务保护的 UPDATE现象两个窗口同时操作同一个号源被挂了两次。原因扣减号源的 SQL 是UPDATE schedule SET remain remain - 1 WHERE id ? AND remain 0这条语句本身在 InnoDB 下是原子的但如果先SELECT判断再UPDATE中间就有窗口。解决要么用上面说的FOR UPDATE事务要么直接用带条件的UPDATE并检查affected_rows后者更轻量。$affected $pdo-exec(UPDATE schedule SET remain remain - 1 WHERE id {$id} AND remain 0); if ($affected 0) { // 号源已满回滚或提示 }5.3 Apache 500 错误看日志而不是猜现象页面白屏或返回 500PHP 错误不显示。原因php.ini里display_errors关了或者 Apache 的ErrorLog路径没权限写。解决先看/var/log/apache2/hospital_error.log里面会有具体的 PHP fatal error 行号和文件。临时调试可以把display_errors设为On但上线前必须关掉否则会暴露文件路径和 SQL 语句。5.4 会话丢失session 保存路径不可写现象登录后跳转又回到登录页或者购物车挂号车内容消失。原因PHP 的session.save_path指向的目录不存在或权限不对session 文件写不进去。解决php -i | grep session.save_path看当前路径确保该目录对www-data用户可写。也可以改用数据库存 session但小系统没必要。5.5 前端表格加载慢N1 查询拖垮页面现象挂号列表页打开要好几秒数据量其实不大。原因每一行挂号记录都单独查一次患者姓名和医生姓名一百行就是两百次查询。解决用JOIN一次性把关联数据取出来或者在 PHP 层先收集所有patient_id和doctor_id再用WHERE id IN (...)批量查一次然后在内存里映射。// 批量查询替代 N1 $patientIds array_column($registrations, patient_id); $patients $this-patientModel-getByIds($patientIds); // 一次查询 $patientMap array_column($patients, name, id); foreach ($registrations as $reg) { $reg[patient_name] $patientMap[$reg[patient_id]] ?? 未知; }6. 二次开发与性能验证用 EXPLAIN 和慢查询日志把系统摸透拿到一套源码跑起来只是第一步真正要用于实际场景得知道它的性能边界在哪。我一般会做两件事打开 MySQL 慢查询日志然后用EXPLAIN看关键 SQL 的执行计划。先改 MySQL 配置把慢查询阈值设成 1 秒记录到文件[mysqld] slow_query_log 1 slow_query_log_file /var/log/mysql/slow.log long_query_time 1重启 MySQL 后跑一遍系统的核心流程——挂号、查患者、开药、收费——然后看慢日志里有没有记录。如果某条 SQL 频繁出现就用EXPLAIN分析EXPLAIN SELECT r.id, p.name, d.name AS doctor_name FROM registrations r JOIN patients p ON r.patient_id p.id JOIN doctors d ON r.doctor_id d.id WHERE r.reg_time BETWEEN 2025-01-01 AND 2025-01-31 ORDER BY r.reg_time DESC;重点看type列如果是ALL说明全表扫需要加索引rows列是预估扫描行数越小越好Extra列出现Using filesort说明排序没走索引数据量大时会很慢。针对上面这条查询应该在registrations.reg_time上建索引并且把ORDER BY的字段纳入索引顺序。另一个验证点是并发。用ab或wrk压一下挂号接口看响应时间和错误率# 用 ab 模拟 50 并发、总共 500 次请求 ab -n 500 -c 50 -p post_data.txt -T application/x-www-form-urlencoded http://hospital.local/api/register.phppost_data.txt里放挂号所需的表单参数。如果错误率超过 1%或者响应时间中位数超过 500ms就要回头查是数据库锁竞争还是 PHP 进程数不够。Apache 的MaxRequestWorkers默认是 150小机器上可能开不了这么多需要根据内存调整。我自己的习惯是每次改完数据库结构或新增一个查询接口都强制走一遍「慢查询日志 → EXPLAIN → 压测」这三步。有一次偷懒没做上线后患者列表页在数据量到两万条时直接超时回头补索引花了半小时但停机影响了几十个挂号窗口。从那以后我每次部署前都强制走一遍这个流程宁可多花二十分钟也不想再被叫去救火。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询