ThinkPHP 3.2 医院挂号系统部署与高并发实战指南
简介本资源是一套基于ThinkPHP框架开发的小型医疗门诊挂号系统后台源码面向PHP初学者、Web全栈开发者及中小型医疗机构信息化建设者旨在解决门诊预约管理、患者档案电子化、医生排班协同等实际业务痛点。压缩包共2324个文件主体为1443个PHP业务逻辑与控制器文件、360个数据缓存dat文件、78个HTML前端模板及77个CSS/JS样式与交互脚本辅以配置文件config、yaml、数据库脚本sql及字体、图标等静态资源整体体积30.74MB结构完整、模块清晰涵盖挂号、患者管理、微信登录、第三方支付对接与统计分析等核心功能。目前已有212人学习下载开发者可直接部署运行深入理解ThinkPHP MVC架构在医疗场景中的落地实践掌握微信开放平台集成、安全防护防SQL注入/XSS及前后端协同开发要点并参考其规范的目录组织与代码分层设计提升工程能力。1. 小型医疗门诊挂号系统为什么用 ThinkPHP 内核反而更稳、更易交付很多刚接手社区诊所或私立门诊信息化建设的开发者第一反应是“直接上 Spring Boot 或 Django”结果两周没跑通登录模块数据库字段对不上医生排班逻辑改三次全崩。而这个「小型医疗门诊挂号系统医院管理系统后台 ThinkPHP 内核」恰恰反其道而行——它不拼架构高度专攻「3 天能上线、7 天可交付、1 人能运维」的真实场景。ThinkPHP 3.2.x注意不是 5/6/8 版本作为内核不是技术倒退而是对 PHP 环境兼容性、国产主机面板适配性、低代码扩展能力的精准取舍它天然支持宝塔、WDCP 等主流 Linux 面板一键部署无需 Composer 全局环境MySQL 5.5 即可运行且挂号核心流程号源生成、时段锁定、患者实名校验、诊室分流全部封装在 Model 层不依赖复杂中间件。适合年接诊量 500030000 人次的基层门诊也正因如此「thinkphp 医院管理系统」「thinkphp 挂号系统」长期稳居中小医疗 IT 服务商采购关键词前三。2. 搭建前必做的三件事确认 ThinkPHP 3.2 兼容边界与环境锁死策略2.1 明确版本锚点为什么必须用 ThinkPHP 3.2.3 而非更高或更低版本该源码包基于 ThinkPHP 3.2.3 官方稳定版构建不兼容 ThinkPHP 3.2.4 的钩子机制变更也不支持 PHP 8.0 的严格类型提示。常见误判是看到「thinkphp 3.2 版本兼容 php8」热词就盲目升级 PHP结果__call()方法报错、D()函数失效、addslashes()自动转义冲突。实际生产中我们只接受以下组合组件推荐版本强制说明PHP5.6.40 或 7.2.34二选一PHP 7.3 已移除mysql_*扩展而该系统部分旧挂号日志模块仍调用mysql_connect()PHP 5.6 必须关闭always_populate_raw_post_dataMySQL5.5.62 或 5.6.51不支持 MySQL 8.0 的caching_sha2_password认证插件连接会报Client does not support authentication protocolWeb ServerApache 2.4.46 或 Nginx 1.18.0Nginx 需额外配置try_files $uri $uri/ /index.php?$query_string;否则/Admin/Login/index路由 404提示不要试图用 Docker 拉取最新php:8.0-apache镜像直接跑——这不是版本问题而是 ThinkPHP 3.2 的Dispatcher.class.php中parseUrl方法硬编码了$_SERVER[PATH_INFO]解析逻辑PHP 8 的$_SERVER键名规范变更导致路由完全失效。2.2 数据库初始化从hospital.sql提取关键结构并规避字符集陷阱解压后根目录下的hospital.sql是完整数据脚本但直接source hospital.sql会失败。原因有三表引擎混用user表用 MyISAMorder表用 InnoDBMySQL 5.6 默认禁用 MyISAM字符集声明为utf8mb4但表定义里CREATE TABLE缺少DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ciadmin_user表的password字段为char(32)实际存储的是 MD5 加密值而非 bcrypt。正确做法是分步执行-- 步骤 1创建数据库并显式指定字符集 CREATE DATABASE IF NOT EXISTS hospital_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 步骤 2导入前手动替换 hospital.sql 中所有 ENGINEMyISAM 为 ENGINEInnoDB -- 步骤 3在每张 CREATE TABLE 语句末尾追加 DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci -- 步骤 4执行修正后的脚本以 user 表为例 CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, id_card char(18) DEFAULT NULL, phone char(11) NOT NULL, created_time int(11) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;2.2.1 关键字段含义速查表避免业务逻辑错位表名字段类型业务含义注意事项doctor_schedulework_datedate医生出诊日期必须为YYYY-MM-DD格式不能是时间戳register_orderstatustinyint(1)挂号状态0待支付,1已支付,2已就诊,3已取消无“爽约”状态需自行扩展departmentsortint(11)科室排序权重数值越小越靠前前端科室列表按此字段ORDER BY sort ASCadmin_userrole_idtinyint(1)管理员角色1超级管理员,2门诊主任,3收费员角色权限硬编码在Common/Conf/config.php中3. 后台核心功能落地挂号流程闭环与 ThinkPHP 关联删除实战3.1 挂号主流程从号源生成到订单锁定的 5 个关键 Controller 动作整个挂号链路集中在Home/Controller/RegisterController.class.php其index()、createOrder()、paySuccess()三个方法构成最小闭环。但真正决定并发安全的是createOrder()中的双重校验锁// Home/Controller/RegisterController.class.php 第 87 行起 public function createOrder() { $data I(post.); // Step 1前置校验非原子操作仅防基础错误 if (!$this-checkPatientInfo($data[patient_id])) { $this-error(患者信息异常); } // Step 2数据库级悲观锁关键 $schedule M(doctor_schedule)-lock(true)-where([ id $data[schedule_id], remain_num array(gt, 0) // 必须在此处判断余号 0 ])-find(); if (!$schedule) { $this-error(号源已被抢完请刷新重试); } // Step 3扣减余号并创建订单原子操作 $result M(doctor_schedule)-where([id $schedule[id]]) -setDec(remain_num) // ThinkPHP 3.2 原生方法等价于 UPDATE ... SET remain_num remain_num - 1 M(register_order)-add([ patient_id $data[patient_id], schedule_id $schedule[id], order_no REG.date(ymdHis).rand(100,999), status 0, create_time time() ]); if ($result) { $this-success(挂号成功, U(Register/pay, [order_id $last_insert_id])); } else { $this-error(系统繁忙请稍后再试); } }注意setDec(remain_num)是 ThinkPHP 3.2 提供的原子减法操作比手写SET remain_num remain_num - 1更安全因为它自动包裹在事务中前提是数据库引擎为 InnoDB。若换成 MyISAM则必须用lock(true) 手动UPDATE否则高并发下会出现超挂。3.2 ThinkPHP 关联删除如何安全清理挂号订单及其关联记录当患者取消挂号status3时需同步删除其检查预约、缴费记录、病历草稿。ThinkPHP 3.2 不支持 Laravel 式的onDelete(cascade)必须手动实现关联删除。核心在Common/Model/RegisterOrderModel.class.php的deleteWithRelations()方法// Common/Model/RegisterOrderModel.class.php public function deleteWithRelations($order_id) { // 启动事务 $this-startTrans(); try { // 1. 删除主订单 $this-where([id $order_id])-delete(); // 2. 删除关联的检查预约exam_appointment 表 M(exam_appointment)-where([order_id $order_id])-delete(); // 3. 删除关联的缴费记录payment_log 表 M(payment_log)-where([order_id $order_id])-delete(); // 4. 清空病历草稿medical_record_draft 表仅删除草稿正式病历保留 M(medical_record_draft)-where([order_id $order_id])-delete(); $this-commit(); return true; } catch (Exception $e) { $this-rollback(); return false; } }3.2.1 关联删除的三个避坑点问题现象解决方案未开启事务只删了订单检查预约残留导致后续统计异常必须调用$this-startTrans()和$this-commit()/$this-rollback()外键缺失exam_appointment.order_id未设外键约束DELETE时无法触发数据库级级联在 MySQL 中手动添加ALTER TABLE exam_appointment ADD CONSTRAINT fk_order_id FOREIGN KEY (order_id) REFERENCES register_order(id) ON DELETE CASCADE;软删除混淆源码中部分表如user用status0/1表示启用/禁用但register_order用status3表示取消 ——不能统一用status字段做逻辑删除严格区分订单用物理删除deleteWithRelations用户用逻辑禁用update status04. 生产环境加固ThinkPHP 开启二级域名设置与漏洞防护实操4.1 ThinkPHP 开启二级域名设置让 admin.hospital.com 独立承载后台该系统默认所有模块共用同一域名如hospital.com/Home/Login和hospital.com/Admin/Login但生产环境必须分离前后端域名既提升安全性也便于 CDN 分流。ThinkPHP 3.2 的二级域名支持需三步配置4.1.1 Apache/Nginx 域名解析层配置# Apache 虚拟主机配置httpd-vhosts.conf VirtualHost *:80 ServerName hospital.com DocumentRoot /var/www/hospital/Public Directory /var/www/hospital/Public AllowOverride All Require all granted /Directory /VirtualHost VirtualHost *:80 ServerName admin.hospital.com DocumentRoot /var/www/hospital/Admin # 注意Admin 目录需复制 Public 下的 index.php 并修改入口文件 /VirtualHost# Nginx server 块nginx.conf server { listen 80; server_name hospital.com; root /var/www/hospital/Public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } } server { listen 80; server_name admin.hospital.com; root /var/www/hospital/Admin; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } }4.1.2 ThinkPHP 内核层路由绑定修改ThinkPHP/Conf/convention.php在URL_ROUTER_ON true,下方添加// 支持二级域名路由绑定 URL_DOMAIN_ROOT , URL_ROUTER_ON true, URL_ROUTER_RULES array( admin.hospital.com Admin, // 将 admin.hospital.com 绑定到 Admin 模块 hospital.com Home, // 将 hospital.com 绑定到 Home 模块 ),同时Admin目录下的index.php入口文件需重写为?php // Admin/index.php define(APP_PATH,../Application/); define(BIND_MODULE,Admin); // 强制绑定 Admin 模块 require ../ThinkPHP/ThinkPHP.php;提示BIND_MODULE是 ThinkPHP 3.2 的隐藏特性不写这行即使域名解析正确访问admin.hospital.com仍会进入 Home 模块。4.2 ThinkPHP 漏洞防护针对 CVE-2018-15383 的补丁级修复该源码存在经典 ThinkPHP 3.2 远程代码执行漏洞CVE-2018-15383攻击者可通过构造?sindex/think\app/invokefunctionfunctioncall_user_func_arrayvars[0]phpinfovars[1][]1直接执行任意 PHP 函数。官方补丁已失效必须手动加固4.2.1 两行代码堵住核心入口打开ThinkPHP/Lib/Core/App.class.php定位到exec()方法约第 172 行在if (C(URL_ROUTER_ON)) {前插入// 阻断 think\app\invokefunction 路径 if (strpos($_SERVER[REQUEST_URI], think\\app\\invokefunction) ! false || strpos($_SERVER[REQUEST_URI], think\\app\\run) ! false) { header(HTTP/1.1 403 Forbidden); exit(Access Denied); }4.2.2 关键配置项强制关闭Application/Common/Conf/config.php// 必须关闭的危险配置 DEFAULT_MODULE Home, URL_CASE_INSENSITIVE false, // 防止大小写绕过 URL_MODEL 2, // 强制使用 PATHINFO 模式禁用兼容模式 VAR_MODULE m, // 修改默认模块变量名避免 ?mAdmin 被猜解 VAR_ACTION a, // 同上防止 ?aindex 被枚举 HTML_CACHE_ON false, // 关闭模板缓存防止恶意缓存注入5. 运维提效技巧用一条命令完成挂号数据导出与号源自动重置5.1 每日凌晨自动重置号源用 crontab ThinkPHP CLI 模式门诊系统要求每天凌晨 00:00 将doctor_schedule.remain_num重置为total_num当日总号数但 ThinkPHP 3.2 默认不支持 CLI 模式。需手动启用5.1.1 创建 CLI 入口脚本reset_schedule.php#!/usr/bin/env php ?php // reset_schedule.php define(APP_PATH, ./Application/); define(APP_DEBUG, false); define(BIND_MODULE, Home); require ./ThinkPHP/ThinkPHP.php; // 执行号源重置逻辑 $schedule M(doctor_schedule); $today date(Y-m-d); $result $schedule-where(work_date {$today})-save([ remain_num array(exp, total_num) ]); if ($result ! false) { echo 【.date(Y-m-d H:i:s).】号源重置完成影响 . $result . 条记录\n; } else { echo 【.date(Y-m-d H:i:s).】号源重置失败\n; }5.1.2 添加定时任务Linux# 编辑 crontab crontab -e # 添加以下行假设项目路径为 /var/www/hospital 0 0 * * * /usr/bin/php /var/www/hospital/reset_schedule.php /var/log/hospital_reset.log 21注意/usr/bin/php路径需通过which php确认 /var/log/hospital_reset.log用于追踪每日执行结果避免号源未重置导致次日停诊。5.2 导出挂号统计报表用 ThinkPHP 原生 SQL 构建跨表聚合查询运营人员常需导出「各科室日挂号量 Top10」但 ThinkPHP 3.2 的join()不支持多表嵌套聚合。直接写原生 SQL 更可靠// Application/Home/Controller/ReportController.class.php public function exportDeptStats() { $sql SELECT d.name AS dept_name, COUNT(o.id) AS total_orders, SUM(CASE WHEN o.status 2 THEN 1 ELSE 0 END) AS finished_count, ROUND(AVG(TIMESTAMPDIFF(MINUTE, o.create_time, o.finish_time)), 2) AS avg_wait_min FROM department d LEFT JOIN doctor_schedule ds ON d.id ds.dept_id LEFT JOIN register_order o ON ds.id o.schedule_id WHERE o.create_time UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 7 DAY)) GROUP BY d.id, d.name ORDER BY total_orders DESC LIMIT 10; $data M()-query($sql); // 导出为 CSV简化版生产环境建议用 fputcsv $fp fopen(php://output, w); fputcsv($fp, [科室名称, 挂号总数, 已完成数, 平均候诊分钟]); foreach ($data as $row) { fputcsv($fp, [$row[dept_name], $row[total_orders], $row[finished_count], $row[avg_wait_min]]); } fclose($fp); header(Content-Type: text/csv); header(Content-Disposition: attachment; filenamedept_stats_.date(Ymd)..csv); exit; }此方法绕过 ThinkPHP 查询构建器限制直接利用 MySQL 5.6 的TIMESTAMPDIFF函数计算候诊时长且LEFT JOIN确保无挂号记录的科室仍出现在报表中COUNT(o.id)为 0避免运营漏看冷门科室。本文还有配套的精品资源点击获取