ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

互联网医院在线问诊系统源码:PHP/HTML/JavaScript/CSS/Shell 全栈实现与状态机设计

互联网医院在线问诊系统源码:PHP/HTML/JavaScript/CSS/Shell 全栈实现与状态机设计 简介这份源码面向医疗信息化开发者、创业团队及计算机专业学生提供一套基于PHP、HTML、JavaScript、CSS与Shell的互联网医院在线问诊系统实现可用于搭建在线医疗服务平台或作为课程设计、毕业设计参考。压缩包共2000个文件约160.15MB以1224个HTML页面、409个JavaScript脚本、162个CSS样式表及124个properties配置文件为主另有json、xml、pdf等辅助文件覆盖前端页面、交互逻辑与后端配置等层次。系统整合互联网医院、智慧医院、在线问诊、在线开方、在线药房、随访、慢病管理、家庭医生等模块并涉及互联网医院牌照申请相关功能目录结构完整便于按业务模块检索与二次开发。目前已有118人学习下载适合希望快速理解在线问诊业务闭环、复用页面与配置资源、缩短项目搭建周期的读者参考。1. 互联网医院在线问诊系统一套 PHP/HTML/JavaScript/CSS/Shell 源码到底解决了什么很多团队第一次做互联网医院在线问诊都会低估一件事这不是一个「表单提交 后台列表」的普通网站而是要在浏览器、服务端、数据库、部署脚本四层之间同时保证状态一致。患者从选科室、选医生、填病情、上传报告到医生接诊、开方、结束会话中间任何一步丢状态用户就会看到「排队中」卡死或者「会话已结束」却还能发消息。这套基于 PHP/HTML/JavaScript/CSS/Shell 的在线问诊设计源码核心价值就在于把这条链路用最朴素的技术栈串起来PHP 负责会话与业务状态HTML/CSS 负责多端可用的问诊界面JavaScript 负责轮询与表单校验Shell 负责一键部署和日志切割。它适合两类人一是想快速搭一个可演示、可二次开发的问诊原型的开发者二是需要理解问诊系统状态机边界、再决定要不要上框架的后端工程师。下面按「先立住模型、再动手复现、最后讲坑」的顺序拆开讲。2. 问诊状态机与数据表PHP 侧先把「会话」这件事定义清楚2.1 为什么问诊系统必须先定状态机而不是先写页面在线问诊和普通 IM 最大的区别是消息不是平等的它带业务语义。一条「医生已接诊」和一条「患者发了一张血常规」在数据库里如果都只是 message 表的一行后面做超时未接诊退款、做问诊记录归档时就会彻底失控。常见做法是把问诊会话抽象成一条consultation记录用状态字段驱动整个流程消息表只挂会话 ID。我一般会把状态收敛成这几个pending待接诊、accepted医生已接诊、chatting问诊中、closed已结束、timeout超时未接诊。状态迁移只允许单向或有限回退比如pending → accepted → chatting → closedpending → timeout。任何前端按钮能不能点都由服务端返回的当前状态决定而不是前端自己判断。这一点是后面所有坑的根源前端一旦自己维护状态多标签页、刷新、弱网重试就会打架。数据表最小集合是四张patient、doctor、consultation、message。consultation里放patient_id、doctor_id、status、created_at、accepted_at、closed_at。message里放consultation_id、sender_role、content、created_at。不要急着加已读未读先跑通主流程。2.2 建表 SQL 与状态字段的取值约束-- 问诊会话表状态字段是整个系统的核心 CREATE TABLE consultation ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, patient_id INT UNSIGNED NOT NULL, doctor_id INT UNSIGNED NOT NULL, status ENUM(pending,accepted,chatting,closed,timeout) NOT NULL DEFAULT pending, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, accepted_at DATETIME DEFAULT NULL, closed_at DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY idx_doctor_status (doctor_id,status), KEY idx_patient (patient_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 消息表只挂会话不重复存业务状态 CREATE TABLE message ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, consultation_id BIGINT UNSIGNED NOT NULL, sender_role ENUM(patient,doctor) NOT NULL, content TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_consult (consultation_id,id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明status用 ENUM 而不是 VARCHAR是为了让数据库层直接拒绝非法状态避免应用层写错字符串。idx_doctor_status这个联合索引是给医生端「我的待接诊列表」用的查询条件固定是doctor_id ? AND status pending走这个索引能避免全表扫描。message表用consultation_id id做联合索引是因为拉取历史消息永远是「按会话取、按时间正序」这个索引能直接覆盖排序。参数说明字符集统一utf8mb4因为患者可能输入 emoji 或生僻字utf8会在写入时报错。content用 TEXT 而不是 VARCHAR(255)问诊描述经常超过 255 字。时间字段用 DATETIME 而不是 TIMESTAMP避免 2038 问题和时区隐式转换带来的玄学偏差。2.3 PHP 侧状态迁移函数把「谁能改状态」收口到一个地方?php // 状态迁移白名单只允许这些跳转 function can_transition(string $from, string $to): bool { $map [ pending [accepted, timeout], accepted [chatting, closed], chatting [closed], closed [], timeout [], ]; return in_array($to, $map[$from] ?? [], true); } // 医生接诊带乐观锁防止两个医生同时抢单 function accept_consultation(PDO $pdo, int $consultId, int $doctorId): bool { $stmt $pdo-prepare( UPDATE consultation SET statusaccepted, accepted_atNOW() WHERE id? AND doctor_id? AND statuspending ); $stmt-execute([$consultId, $doctorId]); // rowCount 为 0 说明状态已被别人改过或医生不匹配 return $stmt-rowCount() 1; }逻辑说明can_transition把合法迁移写成一张表任何改状态的地方都先过这个函数避免出现「已结束的会话又被接诊」这种脏数据。accept_consultation用WHERE statuspending做条件更新这是最轻量的乐观锁——两个请求同时进来只有一个rowCount会是 1另一个自然失败不需要额外加锁。参数说明$consultId和$doctorId都必须来自服务端会话不能信前端传的医生 ID否则越权接诊。rowCount()在 MySQL 里对 UPDATE 返回的是「实际改变的行数」如果状态本来就是 accepted即使 WHERE 命中也不会算改变所以这里判断 1是安全的。3. 前端问诊界面HTML/CSS/JavaScript 怎么把轮询和表单校验做稳3.1 问诊页面的 HTML 骨架与 CSS 布局要点问诊界面在手机上占绝大多数流量所以布局优先按移动端来。常见做法是顶部固定医生信息条中间消息滚动区底部固定输入框。CSS 上最容易翻车的是「输入框被软键盘顶飞」和「消息区不滚动」这两个问题基本都出在高度计算上。!DOCTYPE html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1, viewport-fitcover title在线问诊/title link relstylesheet href/static/consult.css /head body header classdoctor-bar iddoctorBar加载中…/header main classmsg-list idmsgList/main footer classinput-bar textarea idmsgInput placeholder描述你的症状…/textarea button idsendBtn disabled发送/button /footer script src/static/consult.js/script /body /html逻辑说明viewport-fitcover是为了适配刘海屏配合 CSS 里的env(safe-area-inset-bottom)让输入框不被底部横条挡住。消息区用main独立滚动而不是整页滚动这样顶部医生条和底部输入框才能固定。参数说明meta charsetutf-8必须放在 head 最前面否则中文可能乱码。langzh-cn影响部分浏览器对中文断行的处理。CSS 里消息区建议写flex:1; overflow-y:auto; -webkit-overflow-scrolling:touch;最后一条是为了 iOS 上的惯性滚动。3.2 JavaScript 轮询拉消息为什么不用 WebSocket以及怎么防重复小规模问诊系统我一般先用轮询而不是 WebSocket。原因很实际PHP 常驻进程模型对 WebSocket 不友好要额外上 Swoole 或独立网关部署复杂度陡增。轮询只要控制好频率和去重几十到几百并发完全够用。// 轮询拉取新消息lastId 做增量游标 let lastId 0; let polling false; async function fetchMessages(consultId) { if (polling) return; // 防止上一次没回来又发一次 polling true; try { const res await fetch(/api/messages.php?consult_id${consultId}after${lastId}); const data await res.json(); if (data.code 0 data.list.length) { data.list.forEach(renderMessage); lastId data.list[data.list.length - 1].id; // 游标前移 } } catch (e) { console.warn(poll failed, e); // 弱网失败不弹窗下轮重试 } finally { polling false; } } setInterval(() fetchMessages(window.CONSULT_ID), 3000);逻辑说明polling标志位是关键弱网下 fetch 可能 5 秒才超时如果不定这个锁3 秒一次的定时器会堆叠出一串请求服务端压力翻倍。afterlastId做增量拉取避免每次全量返回历史消息。失败只console.warn不打断用户因为轮询本身会自愈。参数说明轮询间隔 3000ms 是经验值低于 2000ms 服务端 QPS 会明显上升高于 5000ms 用户会觉得「对方回了我没提示」。lastId必须用服务端返回的真实消息 ID不能用数组下标否则删除消息后会错位。3.3 表单校验与发送按钮的启用逻辑发送按钮的禁用状态要和输入内容联动同时防止空消息和超长消息。这里有个细节中文输入法在拼字过程中会触发input事件如果直接按长度判断拼音阶段就会误判。const input document.getElementById(msgInput); const sendBtn document.getElementById(sendBtn); input.addEventListener(compositionstart, () { input.dataset.composing 1; }); input.addEventListener(compositionend, () { delete input.dataset.composing; toggleSend(); }); input.addEventListener(input, toggleSend); function toggleSend() { if (input.dataset.composing) return; // 拼字中不判断 const len input.value.trim().length; sendBtn.disabled len 0 || len 500; }逻辑说明compositionstart/compositionend是处理中文输入法的标准做法拼字期间跳过校验避免用户打「你好」时按钮一直闪。长度上限 500 是配合后端 TEXT 字段和业务合理性定的。参数说明trim()后再判断防止用户只打空格就能发送。上限值要和后端校验保持一致前端只是体验优化后端必须再校验一次否则绕过前端就能塞超长内容。4. 部署与运维Shell 脚本怎么把上线和日志这两件事自动化4.1 一键部署脚本从拉代码到重启服务的完整链路问诊系统上线频率不高但每次手动传文件、改配置很容易漏。我一般写一个部署脚本把「备份 → 同步 → 迁移 → 重启」串起来。注意 Shell 里最常见的坑是变量没加引号、路径带空格就崩。#!/usr/bin/env bash set -euo pipefail # 任一命令失败即退出未定义变量报错 APP_DIR/var/www/consult BACKUP_DIR/var/backups/consult STAMP$(date %Y%m%d_%H%M%S) # 1. 备份当前代码出问题能回滚 mkdir -p $BACKUP_DIR tar -czf $BACKUP_DIR/app_$STAMP.tar.gz -C $APP_DIR . || true # 2. 同步新代码假设已解压到 /tmp/release rsync -a --delete /tmp/release/ $APP_DIR/ # 3. 执行数据库迁移 php $APP_DIR/bin/migrate.php || { echo migrate failed; exit 1; } # 4. 重载 PHP-FPM不中断现有连接 systemctl reload php-fpm echo deploy done: $STAMP逻辑说明set -euo pipefail是 Shell 脚本的后悔药任何一步失败立刻停避免带着半成品继续跑。备份用tar打时间戳包回滚时直接解压覆盖。rsync --delete保证删除的文件也同步避免残留旧文件被意外访问。参数说明$APP_DIR等变量全部加双引号防止路径含空格时被拆成多个参数。systemctl reload而不是restartreload 是平滑重载正在处理的问诊请求不会被打断。迁移脚本失败时显式exit 1让部署流程中断而不是继续。4.2 日志切割脚本别让问诊日志把磁盘写满PHP 应用日志如果不切割几个月就能把磁盘撑爆尤其是问诊系统会记录大量请求参数。用 Shell 配合 logrotate 或者自己写一个按天切割的脚本都行。#!/usr/bin/env bash set -euo pipefail LOG_DIR/var/log/consult KEEP_DAYS14 # 把当天日志归档文件名带日期 if [ -f $LOG_DIR/app.log ]; then mv $LOG_DIR/app.log $LOG_DIR/app_$(date %Y%m%d).log # 通知 PHP 重新打开日志句柄如果用了长连接 kill -USR1 $(cat /var/run/php-fpm.pid) 2/dev/null || true fi # 删除超过保留期的日志 find $LOG_DIR -name app_*.log -mtime $KEEP_DAYS -delete逻辑说明先mv再让进程重开句柄这是切割日志的标准顺序直接删正在写的文件会导致句柄悬空、日志丢失。kill -USR1是 PHP-FPM 重开日志的信号失败也不影响主流程所以加了|| true。参数说明KEEP_DAYS14按合规和排查需求定问诊日志涉及患者信息保留期不宜过长。find -mtime 14表示修改时间超过 14 天的文件注意是「天」不是「秒」。5. 避坑与排查问诊系统上线后最容易翻车的 5 个点5.1 现象患者刷新页面后消息重复出现原因前端lastId存在内存变量里刷新后归零轮询又从第一条开始拉。解决把lastId存到sessionStorage初始化时读取或者服务端返回时带上会话的last_message_id前端首次加载直接用它做游标。5.2 现象两个医生同时点「接诊」都提示成功原因接诊接口只判断了statuspending但没带doctor_id条件或者用了先查后改的两步操作。解决改成单条条件 UPDATE用rowCount()判断是否真的抢到参考 2.3 的写法。两步操作在并发下必然出问题。5.3 现象中文消息在数据库里变成问号原因连接字符集不是utf8mb4或者建表时用了utf8。解决PDO 连接串加charsetutf8mb4表也统一utf8mb4。注意utf8在 MySQL 里是「最多 3 字节」的残缺实现emoji 一定存不进去。5.4 现象Shell 部署脚本在本地能跑服务器上报「command not found」原因本地 PATH 和服务器不同或者脚本用了 CRLF 换行Windows 编辑过。解决脚本里用绝对路径调用命令或者开头显式设置 PATH用file deploy.sh检查换行符必要时dos2unix转成 LF。5.5 现象轮询把服务器 CPU 打满原因轮询接口每次都全量查消息表或者没加索引导致全表扫描。解决确认message表走了consultation_id id索引接口只返回after之后的数据同时把轮询间隔从 1 秒放宽到 3 秒并在页面隐藏时暂停轮询document.hidden判断。6. 进阶技巧用 Shell 做问诊会话的健康巡检主流程跑通后真正让系统稳下来的是「主动发现问题」。我习惯写一个巡检脚本定时检查几类异常长时间停留在pending的会话、chatting但超过 24 小时没消息的会话、以及消息表里sender_role为空的脏数据。这个脚本不写业务逻辑只做统计和告警跑在 crontab 里。#!/usr/bin/env bash set -euo pipefail DBconsult USERconsult_ro # 只读账号巡检不需要写权限 # 1. 超过 30 分钟未接诊的会话 mysql -u$USER -p$DB -N -e SELECT COUNT(*) FROM consultation WHERE statuspending AND created_at NOW() - INTERVAL 30 MINUTE; # 2. 超过 24 小时无消息的进行中会话 mysql -u$USER -p$DB -N -e SELECT c.id FROM consultation c LEFT JOIN message m ON m.consultation_id c.id WHERE c.statuschatting GROUP BY c.id HAVING MAX(m.created_at) NOW() - INTERVAL 24 HOUR;逻辑说明巡检用只读账号避免脚本本身成为风险源。第一条查「卡在待接诊」的会话这类往往是医生端没收到通知需要人工介入。第二条用LEFT JOIN HAVING找出「进行中但没人说话」的会话这类要么是双方都忘了要么是消息写入失败值得排查。参数说明-N去掉列名方便脚本直接解析数字。时间阈值 30 分钟和 24 小时按业务定问诊场景下 30 分钟未接诊已经算异常。巡检结果建议输出到日志再配合告警不要直接在脚本里发短信否则阈值调错会半夜炸醒人。我自己的习惯是任何问诊系统上线前先把状态迁移函数和巡检脚本写好再写页面。页面可以慢慢调状态一旦乱了用户投诉和退款会一起涌上来那时候再补就是血泪经验了。这套 PHP/HTML/JavaScript/CSS/Shell 的组合不新但胜在每一层都看得见、改得动适合作为问诊系统的起点。希望帮到你。本文还有配套的精品资源点击获取
返回列表