☰
ThinkPHP6医疗挂号系统:解决号源超卖、支付对账与实名核验三大难题
2026/10/27 17:36:10 网站建设 项目流程

简介:这是一套基于ThinkPHP框架开发的医疗门诊病人挂号管理与在线付款系统源码,面向PHP中级开发者及医疗信息化系统学习者,适用于中小型诊所或社区医院的业务流程数字化改造需求。资源包共2000个文件,主体为1236个PHP业务逻辑文件、360个DAT配置与缓存数据文件、76个HTML前端页面及配套的JS、CSS、SQL等资源,整体压缩后仅32.22MB,结构清晰、模块解耦,涵盖科室管理、医生排班、挂号收费、处方附加费、检查项目计费、药品进销存及就诊全流程统计分析功能。已有37人下载学习,可直接部署调试,快速掌握医疗类业务系统的核心设计模式——如多角色权限控制、支付状态机、药品库存事务处理及可视化数据报表生成逻辑。源码中包含完整数据库脚本、TCPDF生成PDF处方示例、XXTEA加密配置及典型部署脚本(如merge.bat、install等),便于理解安全加固与生产环境适配要点。

1. 这不是又一个“PHP+MySQL”模板站:ThinkPHP医疗门诊挂号系统到底在解决什么真实问题?

你打开这个ThinkPHP医疗门诊病人挂号管理在线付款系统源码下载.zip,第一眼可能觉得——又是套CRUD后台?但真把它跑起来、填几条模拟数据、走一遍挂号→分诊→支付→退号流程,就会发现它卡在三个医疗信息化落地的硬骨头缝里:身份核验不闭环、支付状态难对账、号源并发超卖。这不是给程序员练手的“增删改查大全”,而是门诊场景下,用ThinkPHP 6.x(非5.1那种老古董)把「实名制挂号」和「微信/支付宝即时到账」拧进同一套事务流里的实战方案。它面向的是中小型私立诊所、社区卫生服务中心或医联体基层站点——没有HIS厂商驻场支持,IT人员常是兼职运维,但医保对接、电子病历归档、财务日结这些事一样不能少。源码里没写“支持医保接口”,但预留了/api/insurance/路由和InsuranceService抽象类;没提“对接电子健康卡”,但用户表字段id_card和health_card_no并存,且登录态校验逻辑明确区分了身份证与电子健康卡两种凭证。这意味着:它不是Demo,是能上线、要过审、得扛住每天300+挂号请求的真实业务脚手架。


2. 从解压到可运行:ThinkPHP 6.1+环境搭建与核心配置落地

2.1 环境检查清单:别让PHP版本成为第一个拦路虎

这套系统基于ThinkPHP 6.1 LTS(长期支持版),强制要求PHP 7.3+,且必须启用pdo_mysql、openssl、curl、json、mbstring扩展。很多新手直接拿宝塔面板默认的PHP 8.0装,结果首页白屏报错Class 'think\facade\Cache' not found——这是TP6.1尚未完全兼容PHP 8.0的已知问题。我一般会先执行:

# 检查PHP版本与关键扩展 php -v php -m | grep -E "(pdo_mysql|openssl|curl|json|mbstring)" # 验证Composer是否可用(TP6依赖Composer自动加载) composer --version

提示:若PHP版本过高(如8.1+),请降级至7.4。CentOS 7用户注意:系统自带PHP 5.4已彻底淘汰,必须用yum install php74-php-pdo等命令安装SCL源的PHP 7.4包,而非简单yum install php。

2.2 解压与目录结构认知:哪些文件夹动不得,哪些必须重命名

解压后你会看到典型TP6结构:

├── app/ # 应用核心:控制器、模型、中间件全在这里 ├── config/ # 关键!database.php、wechat.php、alipay.php在此 ├── public/ # Web入口:index.php和静态资源(CSS/JS/IMG) ├── runtime/ # 运行时缓存(首次访问自动生成,勿提交Git) ├── think # 命令行工具(用于生成模块、清除缓存) └── vendor/ # Composer依赖(含thinkphp/framework及支付SDK)

必须修改的三处路径:

  • 将整个项目目录移动到Web服务器根目录(如Nginx的/var/www/html/clinic/),不要把public/目录单独放进去;
  • 修改Nginx配置,将root指向/var/www/html/clinic/public(不是项目根目录!);
  • Apache用户需确保.htaccess在public/下生效,且AllowOverride All已开启。

2.3 数据库初始化:建库、导入SQL、配置连接三步闭环

系统附带database.sql(通常在/data/或根目录),包含6张核心表:user(患者)、doctor(医生)、schedule(排班)、order(挂号订单)、payment(支付记录)、admin(后台账号)。执行前务必确认:

-- 1. 创建数据库(推荐UTF8MB4编码) CREATE DATABASE `clinic_db` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 导入SQL(注意路径替换) mysql -u root -p clinic_db < /path/to/database.sql -- 3. 验证关键数据(初始管理员账号密码为 admin/123456) SELECT username, password FROM admin WHERE username = 'admin';

然后编辑config/database.php,填入你的实际数据库参数:

// config/database.php 片段 'hostname' => '127.0.0.1', // 数据库IP,生产环境禁用localhost(DNS解析慢) 'database' => 'clinic_db', // 数据库名,必须与上面创建的一致 'username' => 'clinic_user', // 推荐新建专用账号,非root 'password' => 'StrongPass2024!', // 密码强度必须含大小写字母+数字+符号 'hostport' => '3306', // MySQL端口,阿里云RDS可能为33060 'charset' => 'utf8mb4', // 强制设为utf8mb4,否则微信昵称emoji存不进 'prefix' => 'tp_', // 表前缀,SQL文件中已按此生成,勿改!

注意:prefix值必须与database.sql中建表语句的前缀完全一致(如CREATE TABLE tp_user (...)),否则所有查询都会报Table 'clinic_db.tp_user' doesn't exist。

2.4 支付通道配置:微信与支付宝的密钥不是“填上就行”

系统支持微信公众号支付(JSAPI)和支付宝手机网站支付(WAP),二者配置逻辑不同,但都遵循“公钥验签、私钥签名”原则。配置文件位于config/wechat.php和config/alipay.php。

微信配置要点:

  • app_id:公众号后台“开发 > 基本配置”里的AppID(不是小程序ID!);
  • mch_id:微信商户平台“账户中心 > 商户信息”中的商户号;
  • key:APIv3密钥(32位随机字符串,在商户平台“API安全 > APIv3密钥”设置);
  • cert_path与key_path:必须是绝对路径,且PHP进程有读取权限(chmod 600);
  • notify_url:必须是公网可访问的URL,如https://yourdomain.com/api/payment/wechat/notify。

支付宝配置要点:

  • app_id:支付宝开放平台“我的应用”中创建的应用APPID;
  • private_key:应用私钥(PKCS#1格式,非PKCS#8!用OpenSSL转换:openssl rsa -in app_private_key.pem -out app_private_key_pkcs1.pem);
  • public_key:支付宝公钥(从开放平台“开发者中心 > 应用公钥”获取);
  • notify_url:同微信,必须公网可达且HTTPS。

验证配置是否生效:访问/api/test/payment(需登录管理员),返回{"code":200,"msg":"支付SDK初始化成功"}即通过。


3. 核心业务链路拆解:挂号、分诊、支付、退号如何用TP6事务保障一致性

3.1 挂号下单:为什么ScheduleModel要加数据库锁?

挂号本质是“抢号”,高并发下极易超卖。系统在app/controller/Order.php的create()方法中,对号源表tp_schedule执行行级写锁:

// app/model/ScheduleModel.php public function lockForUpdate($id) { return $this->where('id', $id)->lock(true)->find(); // true=FOR UPDATE }

调用逻辑如下:

// app/controller/Order.php public function create() { $scheduleId = input('post.schedule_id'); // 1. 查询并锁定该排班记录(阻塞其他请求直到本事务结束) $schedule = (new ScheduleModel())->lockForUpdate($scheduleId); if (!$schedule || $schedule['remaining'] <= 0) { return json(['code' => 400, 'msg' => '号源已满']); } // 2. 扣减剩余号源(原子操作) $result = $schedule->save(['remaining' => $schedule['remaining'] - 1]); // 3. 创建挂号订单(关联schedule_id) $orderData = [ 'user_id' => session('user.id'), 'schedule_id' => $scheduleId, 'status' => 'pending', // 待支付 'created_at' => date('Y-m-d H:i:s') ]; $orderId = Db::name('order')->insertGetId($orderData); // 4. 事务提交,释放锁 return json(['code' => 200, 'data' => ['order_id' => $orderId]]); }

关键点:lock(true)生成SELECT ... FOR UPDATE语句,配合InnoDB引擎实现行锁。若不加锁,10个并发请求同时读到remaining=1,全部扣减后变成-9——这就是典型的超卖。

3.2 在线支付:JSAPI与WAP支付的前端调用差异

系统未使用TP6的think-pay扩展,而是封装了原生SDK调用。支付发起入口统一为/api/payment/init,但返回参数因渠道而异:

字段微信JSAPI支付宝WAP说明
pay_paramsJSON字符串(含appId,timeStamp,nonceStr,package,signType,paySign)URL编码后的查询字符串(含app_id,method,format,charset,sign_type,sign,timestamp,version,notify_url,return_url,biz_content)前端需按渠道分别解析
redirect_url无有(支付宝收银台跳转地址)WAP支付必须重定向,JSAPI在微信内唤起支付弹窗

前端调用示例(Vue):

// 微信JSAPI this.$http.post('/api/payment/init', { order_id: this.orderId, channel: 'wechat' }) .then(res => { const params = JSON.parse(res.data.pay_params); WeixinJSBridge.invoke('getBrandWCPayRequest', params, ...); // 微信内置JS }); // 支付宝WAP this.$http.post('/api/payment/init', { order_id: this.orderId, channel: 'alipay' }) .then(res => { // 直接跳转到支付宝收银台 window.location.href = res.data.redirect_url; });

3.3 支付回调与状态同步:为什么notify接口必须幂等?

微信/支付宝回调是异步的,网络抖动可能导致重复通知。系统在app/controller/Payment.php中强制实现幂等:

// app/controller/Payment.php public function wechatNotify() { $xml = file_get_contents('php://input'); $data = xml_to_array($xml); // 自定义函数,将XML转数组 // 1. 验签(必须!否则黑客可伪造支付成功) if (!WechatPay::verify($data)) { return 'fail'; // 返回fail,微信会重试 } // 2. 查询订单是否存在且状态为pending $order = Db::name('order')->where('order_no', $data['out_trade_no'])->find(); if (!$order || $order['status'] !== 'pending') { return 'success'; // 已处理过,直接返回success } // 3. 更新订单状态 + 记录支付流水 Db::startTrans(); try { Db::name('order')->where('id', $order['id'])->update(['status' => 'paid', 'paid_at' => date('Y-m-d H:i:s')]); Db::name('payment')->insert([ 'order_id' => $order['id'], 'channel' => 'wechat', 'amount' => $data['total_fee'] / 100, // 微信单位为分 'trade_no' => $data['transaction_id'] ]); Db::commit(); } catch (\Exception $e) { Db::rollback(); return 'fail'; } return 'success'; // 只有全部成功才返回success }

血泪经验:曾因忘记在Db::name('order')前加Db::startTrans(),导致支付成功但订单状态未更新,财务对账时发现“钱到了,单没了”。TP6的事务必须显式开启,不会自动包裹。

3.4 退号退款:如何保证“退号成功=退款成功”?

退号不是简单把status改成refunded,必须联动支付渠道发起原路退款。系统在app/controller/Refund.php中封装了双通道退款:

public function apply() { $orderId = input('post.order_id'); $order = Db::name('order')->where('id', $orderId)->find(); if ($order['status'] !== 'paid') { return json(['code' => 400, 'msg' => '仅支持已支付订单退款']); } // 1. 查询支付流水 $payment = Db::name('payment')->where('order_id', $orderId)->find(); // 2. 调用对应渠道退款接口 if ($payment['channel'] === 'wechat') { $result = WechatPay::refund([ 'out_trade_no' => $order['order_no'], 'out_refund_no' => 'REFUND_' . date('YmdHis') . rand(1000, 9999), 'total_fee' => $payment['amount'] * 100, 'refund_fee' => $payment['amount'] * 100 ]); } else { $result = Alipay::refund([ 'out_trade_no' => $order['order_no'], 'refund_amount' => $payment['amount'] ]); } // 3. 无论成功失败,都记录退款单(状态由回调更新) Db::name('refund')->insert([ 'order_id' => $orderId, 'channel' => $payment['channel'], 'amount' => $payment['amount'], 'status' => $result['success'] ? 'success' : 'failed', 'raw_data' => json_encode($result) ]); return json(['code' => 200, 'msg' => '退款申请已提交']); }

玄学提示:微信退款必须用out_trade_no(商户订单号)而非transaction_id(微信订单号),且out_refund_no必须全局唯一。支付宝则要求out_trade_no与支付时一致。记混会导致“退款失败但状态变已退”,患者投诉率飙升。


4. 避坑指南:上线前必须排查的5个高频翻车点

4.1 现象:首页空白,浏览器控制台报Uncaught SyntaxError: Unexpected token '<'

原因:Nginx/Apache未正确指向public/目录,导致JS/CSS请求被重写到index.php,返回HTML而非资源文件。
解决:检查Web服务器配置,确认root指令指向/path/to/project/public,且location ~ \.php$块中fastcgi_param SCRIPT_FILENAME值为$document_root$fastcgi_script_name(非$realpath_root)。

4.2 现象:挂号成功但支付页提示“支付参数错误”,微信返回INVALID_REQUEST

原因:config/wechat.php中notify_url配置了HTTP协议,但微信强制要求HTTPS回调。
解决:将notify_url改为https://yourdomain.com/api/payment/wechat/notify,并确保SSL证书有效(Let's Encrypt免费证书即可)。

4.3 现象:支付宝支付成功,但后台订单状态仍为pending,payment表无记录

原因:支付宝回调URL被防火墙拦截,或服务器安全组未开放443端口,导致notify_url无法被支付宝服务器访问。
解决:在服务器执行curl -I https://yourdomain.com/api/payment/alipay/notify,确认返回HTTP/2 200;检查云服务商安全组规则,放行443端口入方向。

4.4 现象:患者用身份证挂号后,再用微信扫码登录,系统提示“用户不存在”

原因:系统将微信OpenID与身份证号视为独立用户,未做账号打通。app/model/UserModel.php中缺少bindWechat()方法。
解决:在用户微信授权登录逻辑中,增加绑定判断:

// 若手机号相同,则合并账号 $user = UserModel::where('phone', $wechatUserInfo['phone'])->find(); if ($user) { $user->wechat_openid = $wechatUserInfo['openid']; $user->save(); }

4.5 现象:导出Excel报表时中文乱码,显示为??

原因:TP6的thinkphp/library/think/db/Query.php中exportExcel()方法未设置UTF-8 BOM头。
解决:在app/common/function.php中重写导出函数:

function exportExcel($data, $header = []) { header('Content-Type: application/vnd.ms-excel'); header('Content-Disposition: attachment;filename="export_' . date('YmdHis') . '.xls"'); header('Cache-Control: max-age=0'); $fp = fopen('php://output', 'a'); // 写入BOM头,解决Excel中文乱码 fwrite($fp, "\xEF\xBB\xBF"); if (!empty($header)) { fputcsv($fp, $header); } foreach ($data as $row) { fputcsv($fp, $row); } fclose($fp); }

5. 生产就绪加固:从日志审计、SQL防注入到支付对账自动化

5.1 日志分级与敏感信息脱敏:别让runtime/log/成为攻击者的地图

TP6默认日志写入runtime/log/,但DEBUG级别会记录完整SQL(含密码、身份证号)。生产环境必须关闭调试并过滤敏感字段。编辑config/app.php:

// config/app.php 'app_debug' => false, // 强制关闭调试模式 'log' => [ 'type' => 'File', 'level' => ['error', 'notice'], // 仅记录error和notice,屏蔽sql、debug 'file_size' => 2097152, // 单文件2MB,自动轮转 ],

更进一步,在app/middleware/LogMiddleware.php中对请求参数脱敏:

// app/middleware/LogMiddleware.php public function handle($request, \Closure $next) { $param = $request->param(); // 对身份证、手机号、银行卡号做掩码 if (isset($param['id_card'])) { $param['id_card'] = substr($param['id_card'], 0, 4) . '****' . substr($param['id_card'], -4); } if (isset($param['phone'])) { $param['phone'] = substr($param['phone'], 0, 3) . '****' . substr($param['phone'], -4); } Log::record('Request param: ' . json_encode($param), 'info'); return $next($request); }

5.2 SQL注入防御:TP6的where()不是万能护身符

TP6的where('id', input('id'))看似安全,但若input('id')来自$_GET且未校验类型,攻击者传入id[0]=exp&id[1]=1 and updatexml(1,concat(0x7e,(select user()),0x7e),1)仍可触发报错注入。必须强制类型转换:

// ❌ 危险写法 $id = input('id'); Db::name('user')->where('id', $id)->find(); // ✅ 安全写法(整型必须用intval) $id = intval(input('id')); if ($id <= 0) { $this->error('非法ID'); } Db::name('user')->where('id', $id)->find(); // ✅ 安全写法(字符串必须用trim+htmlspecialchars) $name = trim(htmlspecialchars(input('name'))); Db::name('user')->where('name', 'like', "%{$name}%")->select();

5.3 支付对账自动化:每天凌晨比对微信/支付宝流水与本地订单

人工对账是运维噩梦。系统提供command/ReconcileCommand.php,可添加到Linux定时任务:

// app/command/ReconcileCommand.php protected function configure() { $this->setName('reconcile:today') ->setDescription('对账:比对今日微信/支付宝流水与本地订单'); } protected function execute(Input $input, Output $output) { $date = date('Y-m-d'); // 1. 从微信商户平台下载当日账单(需提前在后台开通账单下载权限) $wechatBill = WechatPay::downloadBill(['bill_date' => $date, 'bill_type' => 'ALL']); // 2. 从支付宝开放平台下载账单(需配置支付宝公钥) $alipayBill = Alipay::downloadBill(['bill_date' => $date, 'bill_type' => 'trade']); // 3. 读取本地订单(status=paid且created_at在当日) $localOrders = Db::name('order') ->where('status', 'paid') ->whereTime('created_at', 'today') ->select(); // 4. 三者比对,生成差异报告 $diff = $this->compareBills($wechatBill, $alipayBill, $localOrders); // 5. 发送邮件告警(需配置SMTP) if (!empty($diff)) { $this->sendAlertEmail($diff); } }

添加到crontab(每天凌晨2点执行):

0 2 * * * /usr/bin/php /var/www/html/clinic/think reconcile:today >> /var/log/clinic-reconcile.log 2>&1

5.4 HTTPS强制跳转:避免混合内容(Mixed Content)导致支付JS失效

Chrome对HTTP页面中加载HTTPS资源(如微信JS-SDK)会拦截。必须在Web服务器层强制跳转:

Nginx配置:

server { listen 80; server_name yourdomain.com; return 301 https://$server_name$request_uri; }

Apache配置(.htaccess):

RewriteEngine On RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

5.5 数据库备份策略:mysqldump+rsync+ 七天滚动

runtime/目录不可靠,数据库备份必须独立。编写backup.sh脚本:

#!/bin/bash DATE=$(date +%Y%m%d) BACKUP_DIR="/backup/mysql" DB_NAME="clinic_db" DB_USER="clinic_user" DB_PASS="StrongPass2024!" # 导出SQL mysqldump -u${DB_USER} -p${DB_PASS} ${DB_NAME} --single-transaction > ${BACKUP_DIR}/${DB_NAME}_${DATE}.sql # 压缩 gzip ${BACKUP_DIR}/${DB_NAME}_${DATE}.sql # 删除7天前备份 find ${BACKUP_DIR} -name "${DB_NAME}_*.sql.gz" -mtime +7 -delete # 同步到远程NAS(假设NAS挂载在/mnt/nas) rsync -avz --delete ${BACKUP_DIR}/ user@nas-ip:/backup/clinic/

加入crontab(每天凌晨1点):

0 1 * * * /bin/bash /root/scripts/backup.sh

我坚持一个习惯:每次上线新功能前,必用tcpdump抓包验证支付回调是否真实到达服务器(sudo tcpdump -i any port 443 -w payment.pcap),再用Wireshark过滤http.host contains "yourdomain"。这招帮我揪出过三次“以为回调成功,实则被CDN缓存拦截”的玄学问题。希望帮到你。

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

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

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

立即咨询