简介这套PHP开源中医诊所后台管理项目面向PHP Web开发学习者、医疗信息化从业者及需要二次开发诊所系统的开发者提供带预配置数据库的完整管理平台源码可用于理解预约、患者、药品库存等业务模块的后台实现。压缩包共2000个文件、约12.45MB其中PHP文件承担后端逻辑与接口HTML、CSS、Less及大量JavaScript构成前端界面与交互资源SQL数据库文件、JSON配置和Markdown文档便于部署与查阅整体目录保留了较完整的工程结构。已有933人学习说明其在PHP后台与医疗管理场景中具有一定参考热度。项目涉及MySQL数据存储、MVC分层、登录认证与角色权限控制、RESTful API设计、数据安全、错误日志与性能优化等知识点读者可据此梳理从请求处理到页面渲染的完整链路并在此基础上调整科室、药品和预约流程完成符合自身业务需求的定制开发。1. 一套带库的中医诊所后台能省掉多少手工活接手小规模中医诊所的信息化需求时最省事的做法不是从零写一套而是先找一套 PHP 开源的中医诊所后台管理源码来改。这类项目通常把患者档案、挂号登记、开方、药品库存、收费统计都做在同一个后台里并且附一份可以直接导入的数据库脚本——「带数据库」这几个字的分量就在这儿表结构是现成的导入之后整套数据流就摆在你面前。它适合两类人想给诊所快速搭一套能用的系统、改动要短平快的开发者以及拿它练手 PHP 的人因为代码多是原生 SQL 直出逻辑全在明面上没有框架那层黑匣子。至于要对接医保结算、多门店连锁、前后端分离的这套骨架撑不住改到最后不如重写。2. 技术栈与目录结构PHP 单体应用的四层骨架2.1 技术栈盘点为什么诊所系统还在用原生 PHP这类 PHP 开源中医诊所后台常见的技术组合是 PHP 7.x MySQL 5.7 原生 PDO 或 mysqli 一套 Bootstrap 系的后台模板 jQuery。没有 Composer 依赖没有路由分发器一个.php文件对应一个页面表单 POST 到xxx_save.php处理完header(Location: ...)跳回来。看着土但对这个场景反而是优势诊所的电脑配置一般运维就是老板自己或者一个兼职出问题时能靠看一眼 URL 和文件目录定位而不是翻日志找路由。PHP 版本上要留个心眼。老源码包里常见mysql_connect()这种 PHP 5 时代的函数PHP 7.0 之后就彻底移除了直接跑会报Call to undefined function。稳妥的做法是先用 PHP 7.2 或 7.4 试跑通再考虑往 8.x 迁移真要迁移mysql_*全家桶得换成 PDO这是个体力活不在第一轮改造范围内。数据库这边中医诊所的数据特点是「字典多、明细多」药品字典、科室字典、治法字典、剂型字典一张处方下面挂着十几味药的明细。MySQL 5.7 的utf8mb4是必须的因为患者姓名、药材别名里出现生僻字很常见utf8实际是 utf8mb3存不下四字节字符会出现导入成功但查询出来是问号的情况。前端部分通常是一个左侧菜单 顶部面包屑的后台模板列表页用 jQuery 的 ajax 拉数据、弹层编辑。这类模板的耦合度不高想换皮基本是改static/下的 css 和模板头尾两个 include 文件不影响业务逻辑。2.2 目录结构与一次请求的流向解压之后目录大致是这样一套结构。不同发行版本的文件夹名会有出入但职责划分基本一致clinic-admin/ ├── index.php # 前台/入口多数包会直接跳到 admin/ ├── config/ │ └── database.php # 数据库账号密码部署时唯一必改的文件 ├── include/ │ ├── db.php # PDO 连接封装 │ ├── auth.php # 登录态校验业务页第一行 require │ ├── func.php # 公共函数分页、转义、金额格式化 │ └── header.php/footer.php # 后台模板头尾 ├── admin/ │ ├── login.php # 登录页与登录处理 │ ├── index.php # 后台首页通常是几个统计卡片 │ ├── patient_*.php # 患者档案列表/新增/编辑/详情 │ ├── registration_*.php # 挂号登记 │ ├── prescription_*.php # 开方主表 明细 │ ├── medicine_*.php # 药品字典与库存 │ ├── charge_*.php # 收费与退费 │ └── report_*.php # 统计报表 ├── static/ # css / js / 字体 / 后台模板皮肤 ├── uploads/ # 患者头像、检查单照片、导入的 Excel ├── install/ # 部分包带安装向导装完必须删 └── clinic.sql # 建表 初始数据的完整脚本一次典型请求的流向是浏览器请求admin/prescription_add.php→ 文件第一行require include/auth.php判断$_SESSION[uid]是否存在不存在就header(Location: login.php)→require include/db.php拿到 PDO 单例 → 查患者、查医生、查药品字典填充下拉框 →include header.php输出模板 → 表单提交到prescription_save.php→ 写库 → 跳回列表页。这条链路没有任何隐藏环节好处是排查快坏处是每个业务文件都要重复写鉴权和连接。二次开发时我一般会在include/auth.php里顺手加上统一的权限判断和操作日志一处改动全局生效比在每个文件里加if靠谱得多。提示install/目录装完一定要删。留在服务器上等于给数据库结构开了个公开的重装入口这类老项目在这上面的翻车案例不少。2.3 数据表设计中药处方为什么不能一张表存完导入clinic.sql之后核心表大概有十来张。把关键的几张理清楚后面的模块就都好读了表名作用关键字段注意点patient患者档案namephonegenderallergyphone建索引做建档去重doctor医生namedept_idtitle与系统登录账号分开医生不一定能登录registration挂号记录patient_iddoctor_idfeestatus状态区分待诊/已诊/退号prescription处方主表patient_iddoctor_iddiagnosisstatusstatus决定是否计入营收prescription_item处方明细prescription_idmedicine_idqtydose一味药一行别塞进逗号串medicine药品字典nameunitstockretail_price库存字段直接放主表简单但要注意并发stock_record出入库流水medicine_idtypeqtybeforeafter对账用的后悔药必须记sys_user后台账号usernamepassword_hashrole_id密码用password_hash存的才安全中医场景里有两处和西医系统不一样改的时候特别容易踩一是处方明细要记「剂量 单位 煎法」同样是黄芪9g 和 30g 是两回事先煎后下也得留痕二是收费常按「帖」计一帖包含多味药金额合计在明细上算、在主表上冗余一份报表才跑得快。患者表建议最少这么建字段不多但够用CREATE TABLE patient ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 患者姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, age SMALLINT NOT NULL DEFAULT 0 COMMENT 建档时录年龄生日非必填, phone VARCHAR(20) NOT NULL COMMENT 手机号建档去重键, id_card VARCHAR(32) DEFAULT NULL, addr VARCHAR(128) DEFAULT NULL, allergy VARCHAR(255) DEFAULT NULL COMMENT 过敏史开方页要红字提示, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 软删除, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_phone (phone), KEY idx_created (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者档案;allergy这个字段值得单独说一句它在列表页不显眼但在开方页应该被查出来顶在患者信息旁边。很多开源包只存不用二次开发时把它接进开方页是性价比很高的一次改造。deleted做软删除而不是物理删除是因为处方和收费单会关联患者 ID真删了历史单据就成孤儿数据了。3. 核心模块拆解从挂号建档到开方扣库存的完整链路3.1 数据库连接与登录鉴权全局只有一处密码原生的老包常见写法是每个文件顶部写一遍mysqli_connect(localhost,root,123456,clinic)。能跑但改密码要全项目搜替换。我一般先把连接收敛到配置文件 单例函数两层业务文件只调db()?php // config/database.php —— 部署时唯一要改的文件 return [ host 127.0.0.1, port 3306, dbname clinic, user clinic_user, pass change_me, charset utf8mb4, ];?php // include/db.php —— 单例 PDO一次请求复用同一个连接 function db(): PDO { static $pdo null; if ($pdo instanceof PDO) { return $pdo; } $cfg require __DIR__ . /../config/database.php; $dsn mysql:host{$cfg[host]};port{$cfg[port]};dbname{$cfg[dbname]};charset{$cfg[charset]}; $pdo new PDO($dsn, $cfg[user], $cfg[pass], [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, // 出错抛异常别静默失败 PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, // 统一取关联数组 PDO::ATTR_EMULATE_PREPARES false, // 用真预处理整数参数不会被当字符串 ]); return $pdo; }ATTR_EMULATE_PREPARES false这一行的作用是让 MySQL 服务端做预处理好处是参数类型准确、注入面更小代价是不能用同名占位符多次。charsetutf8mb4写在 DSN 里而不是靠SET NAMES是因为它连建连阶段就生效不会出现前几条查询乱码后几条正常这种玄学现象。登录部分要注意两件事密码比对用password_verify而不是明文或 md5 比较登录成功后调session_regenerate_id(true)避免会话固定攻击。?php // admin/login.php —— 登录处理片段 session_start(); require __DIR__ . /../include/db.php; $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; $stmt db()-prepare(SELECT id, username, password_hash, role_id, status FROM sys_user WHERE username ? LIMIT 1); $stmt-execute([$username]); $user $stmt-fetch(); // password_verify 对应建库时用 password_hash 写进去的密文 if (!$user || !password_verify($password, $user[password_hash])) { exit(scriptalert(账号或密码错误);history.back();/script); } if ((int)$user[status] ! 1) { exit(scriptalert(账号已被停用);history.back();/script); } session_regenerate_id(true); // 防会话固定 $_SESSION[uid] $user[id]; $_SESSION[role] $user[role_id]; header(Location: index.php);参数说明$user[status]是启用标记1 为正常role_id存到 session 里供include/auth.php做菜单和操作权限判断。如果导入的初始账号在库里是 md5 密文老包常见两个选择写个一次性脚本把所有账号重算成password_hash或者在登录时判断密文长度是 32 位就按 md5 比对、成功后顺手升级成新密文。后者更平滑用户无感。3.2 患者建档与挂号登记去重比录得快更重要诊所前台最常干的事就是建档案。手工录得快不重要重要的是别同一病人建出三份档案否则复诊时病史就断了。做法是拿手机号做幂等键插入前先查?php // admin/patient_save.php require __DIR__ . /../include/auth.php; // 未登录直接跳登录页 require __DIR__ . /../include/db.php; $data [ name trim($_POST[name] ?? ), gender (int)($_POST[gender] ?? 0), age (int)($_POST[age] ?? 0), phone trim($_POST[phone] ?? ), id_card trim($_POST[id_card] ?? ), addr trim($_POST[addr] ?? ), allergy trim($_POST[allergy] ?? ), ]; if ($data[name] || $data[phone] ) { exit(json_encode([code 1, msg 姓名和手机号必填], JSON_UNESCAPED_UNICODE)); } // 手机号去重已存在就返回老档案 ID让前端跳去详情页 $stmt db()-prepare(SELECT id FROM patient WHERE phone ? AND deleted 0 LIMIT 1); $stmt-execute([$data[phone]]); if ($oldId $stmt-fetchColumn()) { exit(json_encode([code 2, msg 该手机号已建档, id (int)$oldId], JSON_UNESCAPED_UNICODE)); } $sql INSERT INTO patient (name, gender, age, phone, id_card, addr, allergy, created_at) VALUES (:name, :gender, :age, :phone, :id_card, :addr, :allergy, NOW()); db()-prepare($sql)-execute($data); echo json_encode([code 0, msg 建档成功, id (int)db()-lastInsertId()], JSON_UNESCAPED_UNICODE);逻辑上有两点值得留意。第一execute($data)能直接跑是因为命名占位符和数组键名一一对应但要求数组里不能有多余的键多一个就会报Invalid parameter number——这是新手最常撞的报错看到了先检查数组。第二重复建档时返回code2和老的id前端拿到后可以直接跳详情页并提示「已存在档案」比弹个「手机号重复」然后让用户自己去找体验好很多。挂号是在这个基础上再插一条registration带上医生、科室、挂号费。挂号费建议落库时不覆盖原值改费用走一条新的流水否则月底对账时前台说收了多少、系统显示收了多少两边扯不清。3.3 开方与库存扣减事务的边界画在哪这是整套系统最容易出事的地方。开一张方子要往主表插一条、往明细表插十几条、还要把每味药的库存减掉。任何一步失败都不该留下半张处方。稳妥的写法是一个事务包住全部并且库存用带条件的 UPDATE?php // admin/prescription_save.php —— 开方 扣库存必须同一个事务 require __DIR__ . /../include/auth.php; require __DIR__ . /../include/db.php; $patientId (int)($_POST[patient_id] ?? 0); $doctorId (int)($_POST[doctor_id] ?? 0); $diagnosis trim($_POST[diagnosis] ?? ); // items 形如 [[medicine_id12,qty10,dose9g,decoction先煎], ...] $items $_POST[items] ?? []; if ($patientId 0 || !$items) { exit(json_encode([code 1, msg 患者或处方明细为空], JSON_UNESCAPED_UNICODE)); } $pdo db(); try { $pdo-beginTransaction(); // 1. 主表 $pdo-prepare(INSERT INTO prescription (patient_id, doctor_id, diagnosis, status, created_at) VALUES (?, ?, ?, 0, NOW())) -execute([$patientId, $doctorId, $diagnosis]); $pid (int)$pdo-lastInsertId(); // 2. 明细与库存 $insItem $pdo-prepare(INSERT INTO prescription_item (prescription_id, medicine_id, qty, dose, decoction, price) VALUES (?, ?, ?, ?, ?, ?)); $updStock $pdo-prepare(UPDATE medicine SET stock stock - ? WHERE id ? AND stock ?); $getPrice $pdo-prepare(SELECT retail_price FROM medicine WHERE id ?); foreach ($items as $it) { $mid (int)$it[medicine_id]; $qty (float)$it[qty]; if ($mid 0 || $qty 0) { continue; } // 条件更新库存不足时影响行数为 0直接抛异常回滚 $updStock-execute([$qty, $mid, $qty]); if ($updStock-rowCount() 0) { throw new RuntimeException(药品 #{$mid} 库存不足); } $getPrice-execute([$mid]); $price (float)$getPrice-fetchColumn(); $insItem-execute([$pid, $mid, $qty, $it[dose] ?? , $it[decoction] ?? , $price]); } $pdo-commit(); echo json_encode([code 0, msg 开方成功, pid $pid], JSON_UNESCAPED_UNICODE); } catch (Throwable $e) { $pdo-rollBack(); echo json_encode([code 1, msg $e-getMessage()], JSON_UNESCAPED_UNICODE); }关键在UPDATE medicine SET stock stock - ? WHERE id ? AND stock ?。把「够不够」和「减多少」放进同一条 SQL由 InnoDB 的行锁保证原子性两个人同时开同一味药也不会扣成负数。如果先SELECT stock判断再UPDATE中间那一瞬间就是竞态窗口门诊高峰期一天开几十张方撞上一次就够对账到半夜。参数上qty用float强转是因为中药按克计算8.5g 这种小数很常见字段类型要对应DECIMAL(10,2)而不是INT。decoction煎法和dose单味剂量是中医特有信息别图省事拼进一个remark字段后面按「先煎类药材」统计用量就统计不出来了。3.4 收费与报表一条 SQL 撑起后台首页后台首页那几个统计卡片本质就是几条聚合查询。按医生看近 30 天开方量和金额-- 后台首页「医生工作量」卡片对应的查询 SELECT d.id, d.name AS doctor_name, COUNT(DISTINCT p.id) AS prescription_cnt, SUM(i.qty * i.price) AS amount FROM prescription p JOIN doctor d ON d.id p.doctor_id JOIN prescription_item i ON i.prescription_id p.id WHERE p.created_at DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND p.status 1 -- 1 已收费未收费的不计入营收 GROUP BY d.id, d.name ORDER BY amount DESC;status 1这个条件是营收口径的分水岭。很多包把「已开方」当成「已收费」结果报表金额永远比实际收款高。真正的口径应该是收费单落库之后回写处方状态或者干脆用收费表做主表来统计。改造时我一般会先跑一遍全表核对处方明细金额合计 vs 收费记录合计两个数对不上就说明中间某一步逻辑漏了。4. 把项目跑起来环境、建库、配置、登录后台的完整步骤4.1 运行环境与扩展检查Windows 下用一体化 PHP 集成环境面板最省事装好之后把站点根目录指到解压出来的文件夹Linux 下 Nginx 或 Apache 都行重点是 PHP 版本和扩展。需要确认的扩展有pdo_mysql、mbstring中文截取和编码处理、gd验证码、图片处理、fileinfo上传文件类型判断、openssl。写个临时文件跑一下就知道?php // check.php —— 部署前放到站点根目录跑一次跑完立刻删 $need [pdo_mysql, mbstring, gd, fileinfo, openssl]; foreach ($need as $ext) { printf(%-12s %s\n, $ext, extension_loaded($ext) ? OK : 缺失); } echo PHP 版本: . PHP_VERSION . \n;PHP 版本上先按源码包里的说明来没说明就从 7.2 试起报Call to undefined function mysql_connect()说明是 PHP 5 时代的老代码要么降到 5.6 跑不推荐很多新环境装不上要么把那几个文件改成 PDO。4.2 建库导数据与配置改写导入之前先建库并且一定要显式指定字符集别指望默认值# 先建库再导数据字符集必须写 utf8mb4 mysql -uroot -p -e CREATE DATABASE clinic DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入源码包里的数据库脚本 mysql -uroot -p clinic clinic.sql # 验证表数量和中文字段注释能正常显示就说明字符集没问题 mysql -uroot -p clinic -e SHOW TABLES; SELECT name, phone FROM patient LIMIT 3;导完改config/database.php。这里有个坑源码包里这个文件可能不叫这个名字也可能直接写在include/conn.php里搜一下mysqli_connect或new PDO就能找到真正的连接点。改的时候顺手给业务建一个独立数据库账号别用 root权限只给SELECT, INSERT, UPDATE, DELETE。4.3 目录权限与 php.ini 关键项Linux 部署最常见的失败原因不是代码是权限。上传目录和缓存目录必须让 Web 进程可写# 假设 Web 进程用户是 www-data按实际环境替换 chown -R www-data:www-data uploads runtime find uploads runtime -type d -exec chmod 755 {} \; find uploads runtime -type f -exec chmod 644 {} \;php.ini里这几项建议直接改掉处方照片、批量导入患者名单都指望它们; 上传限制和时区诊所场景下这几项基本必调 upload_max_filesize 20M post_max_size 24M max_execution_time 120 date.timezone Asia/Shanghaidate.timezone不设的后果是 PHP 时间比北京时间差 8 小时而 MySQL 的NOW()又是对的于是同一张处方上「开方时间」和「创建时间」能差一天。这种问题在测试环境看不出来上线后对账才暴露属于典型的血泪经验。4.4 登录后台后的第一轮初始化动作能打开登录页只是第一步登录进去之后有几件事必须马上做顺序别乱用源码包附带的初始账号登录改掉密码并把用不到的演示账号停用或删除。检查sys_user表里的密码字段如果是 md5 密文按第 3.1 节说的方法升级成password_hash如果源码里是明文比对那这个包的安全性得重新评估。删除install/目录和临时放的check.php。关掉错误显示display_errors Offlog_errors On错误写日志而不是打到页面上。在后台录一遍基础字典科室、医生、药品含单位和零售价否则开方页的下拉框全是空的看着像系统坏了。注意药品字典里的stock初始值别图省事全填一个大数。库存是这套系统里唯一会随业务持续变动的数字起点不准后面所有的进销存报表都是错的。5. 避坑与排查五个让新手上不了手的典型问题5.1 页面全白或直接 500错误日志里是 undefined function现象访问首页一片空白或者浏览器报 500error_log里能看到Call to undefined function mysql_connect()/mysqli_connect()。原因源码是 PHP 5 时代写的用的是mysql_*系列函数PHP 7.0 起已被移除或者 PHP 装好了但pdo_mysql、mysqli扩展没启用。解决先确认扩展php -m | grep -i mysql看有没有pdo_mysql和mysqli。如果两者都有还报错说明是老函数把相关文件的连接部分批量替换成 PDO至少把mysql_query→db()-query、mysql_fetch_array→fetch()这几类改掉。改之前先全局搜一遍函数名心里有个工作量预期。5.2 登录成功后刷新又回登录页现象账号密码明明对点登录跳到首页一刷新或者点任何菜单又弹回登录页。原因session写不进去。常见三种session.save_path指向的目录不存在或不可写include/下的某个文件在?php之前有 BOM 头或一个空格导致session_start()之前已经有输出、Set-Cookie头发不出去Nginx 的fastcgi_param没配好session 目录权限正常但进程用户不对。解决先看浏览器里有没有PHPSESSID这个 cookie没有就是头没发出去重点查 BOM。用grep -rl $\xEF\xBB\xBF include/ admin/能快速定位带 BOM 的文件改成无 BOM 的 UTF-8。有 cookie 但刷新就丢去看session.save_path目录权限改成 755 并确认属主是 Web 用户。5.3 中文名和药材名导入后变问号现象clinic.sql导入成功页面上显示的都是???或者空格或者前端录入正常导出的 Excel 里全是乱码。原因三个层面的字符集不统一——建库时用了默认的latin1或utf8mb3连接没有设charsetutf8mb4表和字段各自又是另一套。生僻字比如某些中药别名是四字节字符utf8mb3根本存不下会被截断成问号。解决按「库 → 表 → 连接 → 页面」四层统一。建库指定utf8mb4导入前用SHOW CREATE TABLE抽查几张表连接 DSN 里带charsetutf8mb4HTML 里加meta charsetutf-8。已经乱了的数据没法自动修只能重新导入所以导数据这一步最好在本地先练一遍。5.4 库存被扣成负数或者金额和收费对不上现象某味药的库存显示 -12g或者同一天的开方金额合计和实际收款差几十块。原因处方保存没有事务或者有事务但库存判断用了「先查再改」。明细里某一味药扣失败库存不足、药名被删、价格为 NULL前面的已经写进去了没有回滚就留下半张方子和一堆负数库存。金额对不上多半是status状态流转漏了未收费的处方被统计进了营收。解决按 3.3 节的写法用事务 条件更新任何一步rowCount() 0就抛异常回滚。已经在负数状态的库存用stock_record流水倒推一次实际入库量重新校准别直接改成 0。金额口径上把统计 SQL 里的status条件写死并且定期跑一次「处方合计 vs 收费合计」的核对查询。5.5 上传处方照片失败但页面什么都不提示现象选好图片点上传转一圈回到页面什么也没发生文件也没出现在uploads/里。原因两种情况最常见。一是uploads/没有写权限move_uploaded_file返回 false 但代码没判断返回值静默失败。二是图片超过upload_max_filesize或post_max_sizePHP 直接丢弃整个 POST 请求$_POST和$_FILES都是空的代码走不到任何分支。解决先在php.ini里把两个上限调大见 4.3 节再在保存逻辑里加返回值判断和$_FILES[file][error]的映射提示。错误码 1 和 2 都是超限3 是只上传了一部分4 是没选文件。把这个映射写成一个函数出错时直接把原因弹出来比事后翻日志快得多。6. 二次开发在现有骨架上加复诊提醒与扩展字段改造可以从两个成本最低、收益最明显的点切进去复诊提醒和患者扩展字段。先说复诊提醒。原表结构通常没有「下次复诊日期」加字段比新建表快-- 两列就够预计复诊时间和提醒状态别一上来就建提醒表 ALTER TABLE patient ADD COLUMN next_visit_date DATE DEFAULT NULL COMMENT 下次预计复诊日期 AFTER allergy, ADD COLUMN visit_reminded TINYINT NOT NULL DEFAULT 0 COMMENT 是否已提醒 AFTER next_visit_date, ADD KEY idx_next_visit (next_visit_date);开方保存成功后顺手回写代码加在事务 commit 之后?php // 开方成功后写复诊日期天数从前端传默认 7 天 $days max(1, (int)($_POST[next_days] ?? 7)); db()-prepare(UPDATE patient SET next_visit_date DATE_ADD(CURDATE(), INTERVAL ? DAY), visit_reminded 0 WHERE id ?) -execute([$days, $patientId]);首页加一个卡片查未来三天内该复诊但还没来的患者SELECT p.id, p.name, p.phone, p.next_visit_date, d.name AS doctor_name FROM patient p LEFT JOIN prescription pr ON pr.patient_id p.id AND pr.status 1 LEFT JOIN doctor d ON d.id pr.doctor_id WHERE p.deleted 0 AND p.next_visit_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 3 DAY) GROUP BY p.id ORDER BY p.next_visit_date ASC;这段 SQL 里GROUP BY p.id是必要的因为一个患者可能有多条已收费处方LEFT JOIN会放大行数。想要更严谨就改成子查询取最近一条处方的医生代价是慢一点诊所数据量下无所谓。再说扩展字段。体质辨识、舌象、脉象这类信息字段会越加越多直接往patient表加列会把主表撑成三十多列列表页SELECT *拖得越来越慢。用附属表更干净CREATE TABLE patient_ext ( patient_id INT UNSIGNED NOT NULL, body_type VARCHAR(32) DEFAULT NULL COMMENT 体质类型, tongue VARCHAR(64) DEFAULT NULL COMMENT 舌象, pulse VARCHAR(64) DEFAULT NULL COMMENT 脉象, updated_at DATETIME NOT NULL, PRIMARY KEY (patient_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者扩展信息;写入用INSERT ... ON DUPLICATE KEY UPDATE一次 SQL 解决新增和更新两种情况前端不用区分是建档还是编辑。改造完必须验一遍别改完就上线。我的验证清单是新建一个患者 → 挂号 → 开一张包含三味药的方子其中一味故意给个超库存的数量→ 确认报错且库存和处方数据都没落库 → 改小数量重开 → 收费 → 首页统计卡片金额增加 → 患者列表显示复诊日期。这套流程走通基本说明改动没有破坏原有链路。验证点预期结果失败时先看哪里超库存开方报库存不足主表和明细表都不新增prescription_save.php的事务与rowCount判断同一手机号重复建档返回 code2 和老档案 IDpatient_save.php的去重查询复诊日期回写patient.next_visit_date有值回写代码是否在 commit 之后、是否用了同一个$patientId首页提醒卡片三天内到期的患者出现next_visit_date是否为空、deleted是否为 0收费后统计金额只增一次统计 SQL 的status条件这类开源包最大的价值不是功能多全而是结构够直白你能在一个下午之内把一条业务链路读明白再动手改。我早年改这类系统时吃过一次亏没备份数据库就直接ALTER TABLE中途字段加错想回滚结果演示数据全没了只能对着 Excel 手工录回去。从那以后我每次动表结构之前都强制走一遍「导出 → 本地导入验证 → 再改线上」多花五分钟省一晚上的返工。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?