ThinkPHP 3.2 医院挂号系统部署与高并发实战指南
2026/9/16 2:23:47 网站建设 项目流程

简介:本资源是一套基于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 层,不依赖复杂中间件。适合年接诊量 5000~30000 人次的基层门诊,也正因如此,「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_data
MySQL5.5.62 或 5.6.51不支持 MySQL 8.0 的caching_sha2_password认证插件,连接会报Client does not support authentication protocol
Web 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.phpparseUrl方法硬编码了$_SERVER['PATH_INFO']解析逻辑,PHP 8 的$_SERVER键名规范变更导致路由完全失效。

2.2 数据库初始化:从hospital.sql提取关键结构并规避字符集陷阱

解压后根目录下的hospital.sql是完整数据脚本,但直接source hospital.sql会失败。原因有三:

  • 表引擎混用(user表用 MyISAM,order表用 InnoDB),MySQL 5.6 默认禁用 MyISAM;
  • 字符集声明为utf8mb4,但表定义里CREATE TABLE缺少DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  • admin_user表的password字段为char(32),实际存储的是 MD5 加密值,而非 bcrypt。

正确做法是分步执行:

-- 步骤 1:创建数据库并显式指定字符集 CREATE DATABASE IF NOT EXISTS `hospital_db` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 步骤 2:导入前手动替换 hospital.sql 中所有 ENGINE=MyISAM 为 ENGINE=InnoDB -- 步骤 3:在每张 CREATE TABLE 语句末尾追加 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
2.2.1 关键字段含义速查表(避免业务逻辑错位)
表名字段类型业务含义注意事项
doctor_schedulework_datedate医生出诊日期必须为YYYY-MM-DD格式,不能是时间戳
register_orderstatustinyint(1)挂号状态0=待支付,1=已支付,2=已就诊,3=已取消;无“爽约”状态,需自行扩展
departmentsortint(11)科室排序权重数值越小越靠前,前端科室列表按此字段ORDER BY sort ASC
admin_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 关联删除:如何安全清理挂号订单及其关联记录?

当患者取消挂号(status=3)时,需同步删除其检查预约、缴费记录、病历草稿。ThinkPHP 3.2 不支持 Laravel 式的onDelete('cascade'),必须手动实现关联删除。核心在Common/Model/RegisterOrderModel.class.phpdeleteWithRelations()方法:

// 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)用status=0/1表示启用/禁用,但register_orderstatus=3表示取消 ——不能统一用status字段做逻辑删除严格区分:订单用物理删除(deleteWithRelations),用户用逻辑禁用(update status=0

4. 生产环境加固:ThinkPHP 开启二级域名设置与漏洞防护实操

4.1 ThinkPHP 开启二级域名设置:让 admin.hospital.com 独立承载后台

该系统默认所有模块共用同一域名(如hospital.com/Home/Loginhospital.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),攻击者可通过构造?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[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', // 修改默认模块变量名,避免 ?m=Admin 被猜解 'VAR_ACTION' => 'a', // 同上,防止 ?a=index 被枚举 '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 2>&1

注意:/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; filename="dept_stats_'.date('Ymd').'.csv"'); exit; }

此方法绕过 ThinkPHP 查询构建器限制,直接利用 MySQL 5.6 的TIMESTAMPDIFF函数计算候诊时长,且LEFT JOIN确保无挂号记录的科室仍出现在报表中(COUNT(o.id)为 0),避免运营漏看冷门科室。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询