news 2026/10/7 7:27:13

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
互联网医院在线问诊系统源码: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 → closed,pending → 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 消息表:只挂会话,不重复存业务状态 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明: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 status='accepted', accepted_at=NOW() WHERE id=? AND doctor_id=? AND status='pending'" ); $stmt->execute([$consultId, $doctorId]); // rowCount 为 0 说明状态已被别人改过,或医生不匹配 return $stmt->rowCount() === 1; }

逻辑说明:can_transition把合法迁移写成一张表,任何改状态的地方都先过这个函数,避免出现「已结束的会话又被接诊」这种脏数据。accept_consultation用WHERE status='pending'做条件更新,这是最轻量的乐观锁——两个请求同时进来,只有一个rowCount会是 1,另一个自然失败,不需要额外加锁。

参数说明:$consultId和$doctorId都必须来自服务端会话,不能信前端传的医生 ID,否则越权接诊。rowCount()在 MySQL 里对 UPDATE 返回的是「实际改变的行数」,如果状态本来就是 accepted,即使 WHERE 命中也不会算改变,所以这里判断=== 1是安全的。

3. 前端问诊界面:HTML/CSS/JavaScript 怎么把轮询和表单校验做稳

3.1 问诊页面的 HTML 骨架与 CSS 布局要点

问诊界面在手机上占绝大多数流量,所以布局优先按移动端来。常见做法是顶部固定医生信息条,中间消息滚动区,底部固定输入框。CSS 上最容易翻车的是「输入框被软键盘顶飞」和「消息区不滚动」,这两个问题基本都出在高度计算上。

<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <title>在线问诊</title> <link rel="stylesheet" href="/static/consult.css"> </head> <body> <header class="doctor-bar" id="doctorBar">加载中…</header> <main class="msg-list" id="msgList"></main> <footer class="input-bar"> <textarea id="msgInput" placeholder="描述你的症状…"></textarea> <button id="sendBtn" disabled>发送</button> </footer> <script src="/static/consult.js"></script> </body> </html>

逻辑说明:viewport-fit=cover是为了适配刘海屏,配合 CSS 里的env(safe-area-inset-bottom)让输入框不被底部横条挡住。消息区用<main>独立滚动,而不是整页滚动,这样顶部医生条和底部输入框才能固定。

参数说明:<meta charset="utf-8">必须放在 head 最前面,否则中文可能乱码。lang="zh-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 秒一次的定时器会堆叠出一串请求,服务端压力翻倍。after=lastId做增量拉取,避免每次全量返回历史消息。失败只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而不是restart,reload 是平滑重载,正在处理的问诊请求不会被打断。迁移脚本失败时显式exit 1,让部署流程中断而不是继续。

4.2 日志切割脚本:别让问诊日志把磁盘写满

PHP 应用日志如果不切割,几个月就能把磁盘撑爆,尤其是问诊系统会记录大量请求参数。用 Shell 配合 logrotate 或者自己写一个按天切割的脚本都行。

#!/usr/bin/env bash set -euo pipefail LOG_DIR="/var/log/consult" KEEP_DAYS=14 # 把当天日志归档,文件名带日期 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_DAYS=14按合规和排查需求定,问诊日志涉及患者信息,保留期不宜过长。find -mtime +14表示修改时间超过 14 天的文件,注意是「天」不是「秒」。

5. 避坑与排查:问诊系统上线后最容易翻车的 5 个点

5.1 现象:患者刷新页面后消息重复出现

原因:前端lastId存在内存变量里,刷新后归零,轮询又从第一条开始拉。解决:把lastId存到sessionStorage,初始化时读取;或者服务端返回时带上会话的last_message_id,前端首次加载直接用它做游标。

5.2 现象:两个医生同时点「接诊」,都提示成功

原因:接诊接口只判断了status='pending',但没带doctor_id条件,或者用了先查后改的两步操作。解决:改成单条条件 UPDATE,用rowCount()判断是否真的抢到,参考 2.3 的写法。两步操作在并发下必然出问题。

5.3 现象:中文消息在数据库里变成问号

原因:连接字符集不是utf8mb4,或者建表时用了utf8。解决:PDO 连接串加charset=utf8mb4,表也统一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 DB="consult" USER="consult_ro" # 只读账号,巡检不需要写权限 # 1. 超过 30 分钟未接诊的会话 mysql -u"$USER" -p"$DB" -N -e " SELECT COUNT(*) FROM consultation WHERE status='pending' 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.status='chatting' 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 的组合不新,但胜在每一层都看得见、改得动,适合作为问诊系统的起点。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 7:27:10

Cadence Allegro Padstack Editor表贴焊盘定义规范与实操指南

之前在做 PCB 封装库维护时&#xff0c;经常被 SMD 焊盘定义不规范的问题困扰&#xff1a;有人把焊盘层放在 TOP 层&#xff0c;有人在默认的 PACKAGE GEOMETRY 里画焊盘&#xff0c;还有人搞不清 Padstack Editor 和封装编辑器之间的分工。其实只要弄清楚焊盘栈&#xff08;Pa…

作者头像 李华
网站建设 2026/10/7 7:24:22

PMSM无感FOC实战:Ud/Uq物理本质与HFI频率工程选型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华