php+uniapp医院问诊挂号小程序开发实战:号源锁与防超卖设计
发布时间:2026/9/29 15:49:53来源:尧图网络
做医院问诊挂号小程序这套东西乍一看好像就是“一个预约表单一个支付”但真正落到哈尔滨中心医院这类三甲医院的场景里你会发现事情远没那么简单。实名制、分时段、号源锁、排队叫号、在线问诊、爽约处理、医保支付对接……随便哪个环节没想清楚上线第一天就能被门诊护士和患者一起骂到怀疑人生。这篇就围绕“phpuniapp哈尔滨中心医院问诊挂号预约系统用户移动端小程序”这套组合把我实际开发中踩过的坑、想明白的逻辑、以及可以直接抄作业的代码和表结构都捋一遍。适合正在做医疗类小程序、或者准备从零搭建问诊挂号系统的朋友参考尤其是后端用php、前端用uniapp这套技术栈的。1. 整体构思与技术选型为什么是phpuniapp这套组合1.1 技术选型的核心逻辑很多团队一听到“医院挂号系统”下意识就会往Java、Spring Cloud那套微服务上靠觉得医院项目就得上“大厂标配”。但现实是二三线城市的医疗信息化项目尤其是面向单个医院的问诊挂号系统预算和工期往往撑不起那套东西。php在这里的优势恰恰是“轻”和“快”——开发效率高、部署成本低、维护门槛低一个LAMP/Nginx环境就能跑起来而且对并发要求不算极端变态的挂号场景是够用的。uniapp这边就更不用说了一套代码编译到微信小程序、H5、App医院这种“患者用什么端都有”的场景不可能要求所有患者都装App微信小程序是绝对主力但偶尔医生端、院内大屏、公众号H5也要用uniapp一套代码全部覆盖开发成本和维护成本都压下来了。我最终的架构是这样后端phpThinkPHP 6框架提供RESTful API接口前端uniappvue3语法编译为微信小程序数据库MySQL 5.7InnoDB引擎utf8mb4字符集缓存Redis负责验证码、Token、号源锁这套组合我实测下来单机扛住每天几千单预约是没问题的真要上万的并发抢号比如早上8点放号那一下加个Redis队列和简单的限流也就够了不需要一上来就搞微服务。1.2 系统模块与角色边界问诊挂号系统看起来简单但涉及的角色至少四个患者、医生、挂号处/护士、管理员。用户移动端小程序主要服务的还是患者这个角色但后端的权限设计要把医生端和管理端的边界预留好否则后面扩展会很难受。移动端小程序核心功能我拆成了六大块用户体系微信授权登录、手机号绑定、实名认证医院展示科室列表、医生排班、医生详情挂号预约号源查询、分时段预约、号源锁定在线问诊图文咨询、历史问诊记录订单管理挂号单、取消挂号、退费流程个人中心就诊人管理、电子病历、消息通知这里最核心、最容易出问题的就是第3块——挂号预约。数据一致性、幂等性、超时处理全在这个环节。2. 核心业务逻辑与数据模型设计2.1 数据库表结构设计先上我最终落地的表结构这是一点一点踩坑踩出来的。挂号系统最核心的表就是排班表和号源表这两张表设计得不好后面写并发逻辑的时候会痛不欲生。-- 医生排班表 CREATE TABLE doc_schedule ( id int(11) NOT NULL AUTO_INCREMENT, doctor_id int(11) NOT NULL COMMENT 医生ID, dept_id int(11) NOT NULL COMMENT 科室ID, schedule_date date NOT NULL COMMENT 排班日期, period_type tinyint(1) NOT NULL COMMENT 时段1上午 2下午 3晚间, start_time time NOT NULL COMMENT 开始时间, end_time time NOT NULL COMMENT 结束时间, total_num int(11) NOT NULL DEFAULT 0 COMMENT 总号数, remain_num int(11) NOT NULL DEFAULT 0 COMMENT 剩余号数, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 状态1正常 0停诊, PRIMARY KEY (id), KEY idx_doctor_date (doctor_id, schedule_date), KEY idx_dept_date (dept_id, schedule_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班表;这里有个关键点remain_num是冗余字段通过total_num - 已预约数实时算也可以但高峰期每次都count一次太伤数据库所以我在排班表上直接存了剩余号数每次预约事务里UPDATE ... SET remain_num remain_num - 1 WHERE id ? AND remain_num 0这种乐观锁写法天然防超卖比先查后更新靠谱得多。-- 号源表号段明细 CREATE TABLE doc_register_num ( id int(11) NOT NULL AUTO_INCREMENT, schedule_id int(11) NOT NULL COMMENT 排班ID, period_no int(11) NOT NULL COMMENT 序号如1-50, period_time varchar(20) DEFAULT NULL COMMENT 建议就诊时间如08:00-08:15, is_booked tinyint(1) NOT NULL DEFAULT 0 COMMENT 是否已预约0未约 1已约 2锁定中, patient_id int(11) DEFAULT NULL COMMENT 锁定的患者ID, lock_expire datetime DEFAULT NULL COMMENT 锁定过期时间, PRIMARY KEY (id), KEY idx_schedule (schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT号源明细表;号源表是后来加上的。最初我天真地以为排班表有“剩余号数”就够了患者直接减1拿到一个号结果发现“号”这个概念在真实医院里不是抽象的——患者会问“我是第几个号”“我大概几点去合适”如果没有号段明细你就没法回答这个问题。2.2 分时段预约的排号规则哈尔滨中心医院这种规模的三甲门诊量是很大的。如果所有人都8点涌到诊室门口秩序直接就崩了。所以系统必须做分时段预约。我的规则是这样的上午时段 08:00-12:00下午 13:00-17:00每个时段按医生接诊速度拆成若干个15分钟的时间片每个时间片排2-3个号具体看医生看诊速度有的专家看一个号要20分钟这就不适合按4-6个号/小时来排号源生成的时候用一段php脚本根据排班自动生成// 根据schedule生成号源明细 public function generateNums($scheduleId) { $schedule Db::name(doc_schedule)-find($scheduleId); $periodStart strtotime($schedule[start_time]); $periodEnd strtotime($schedule[end_time]); $interval 15 * 60; // 15分钟一个时间片 $perSlice 2; // 每个时间片2个号 $periodNo 1; $data []; for ($t $periodStart; $t $periodEnd; $t $interval) { for ($i 0; $i $perSlice; $i) { $data[] [ schedule_id $scheduleId, period_no $periodNo, period_time date(H:i, $t) . - . date(H:i, $t $interval), is_booked 0 ]; } } Db::name(doc_register_num)-insertAll($data); // 更新排班表的总号数 Db::name(doc_schedule)-where(id, $scheduleId)-update([ total_num count($data), remain_num count($data) ]); }如果一个医生的排班周期是重复的比如每周二上午出诊排班管理后台可以直接“一键生成本月所有周二上午的排班”这个功能医生和护士都特别喜欢他们最烦的就是每周手工去录排班。2.3 号源锁定与防超卖机制这是整个系统最核心的并发问题。患者不是点了“预约”就直接扣号的中间要经过选择就诊人、确认信息、支付如果在线支付的话等环节整个过程可能有1-3分钟。如果患者一进来就扣号那他中途放弃或者卡在半路号就被白白占住了。我的方案是“两阶段锁定”第一阶段患者选完号源请求“锁定号源”接口。后端把doc_register_num对应记录的is_booked置为2锁定中patient_id写当前用户lock_expire设为当前时间15分钟。第二阶段患者确认支付/确认预约请求“确认预约”接口。后端校验号源仍是该患者锁定中然后把状态更新为1已预约同时扣减排班表的remain_num。如果15分钟内没确认锁定过期号源自动释放其他人就能约了。这个释放动作我用了两种机制兜底一是Redis的过期key回调但小程序端的定时性不一定完全可靠二是用户查询号源时顺带做一次清理把超过lock_expire且状态还是2的记录重置为0。还有一层的保险是数据库事务和乐观锁。确认预约的时候不能只校验“我是这个号的锁定人”还得防着极端情况——锁已经过期释放给了别人但患者这边的确认请求是15分钟前发出的在网络里堵了半天才到服务器。所以最终确认时的SQL要带上时间条件// 确认预约时的更新语句 $result Db::name(doc_register_num) -where(id, $numId) -where(patient_id, $patientId) -where(is_booked, 2) -where(lock_expire, , date(Y-m-d H:i:s)) -update([is_booked 1, booked_time date(Y-m-d H:i:s)]); if ($result) { // 扣减排班剩余号数 Db::name(doc_schedule) -where(id, $scheduleId) -where(remain_num, , 0) -setDec(remain_num); }注意setDec在ThinkPHP里生成的是UPDATE ... SET remain_num remain_num - 1不是先查再改所以带上remain_num 0条件就是天然的乐观锁这一句很关键千万别改成“先查剩余、再更新剩余”。2.4 在线问诊与图文消息的实现在线问诊这块我踩了一个很深的坑一开始想用WebSocket做即时通讯后来发现完全没必要。图文问诊不是实时聊天患者发一条消息、医生过十分钟回复一条这是常态。用WebSocket纯属给自己加负担。最终方案是消息存MySQL用uniapp的setInterval轮询 小程序订阅消息通知。患者端轮询最近10分钟的新消息有更新就刷新页面同时给医生端发订阅消息提醒。如果后面真要上实时音视频问诊那再单独接一个RTC服务图文问诊保持简单就好。消息表字段大致是msg_id、session_id、sender_type1医生 2患者、content、msg_typetext/image、create_time、is_read。列表查询就一条按会话分组带用户信息的SQL简单直接。3. 移动端小程序从登录到预约的完整链路3.1 微信登录与手机号绑定流程小程序的登录流程网上版本很多但医院场景有个特殊点必须实名制。不能像普通电商那样“微信拿个openid就能逛”。所以我的登录链路是两段式第一步wx.login拿code后端调微信接口换openid和session_key建立账号如果不存在就创建。第二步强制绑定手机号使用微信的getPhoneNumber能力后端解密拿到手机号后跟医院HIS里的患者档案做匹配。这里有一个体验细节如果直接强制用户一进来就输身份证号做实名认证流失率会很高。很多患者只是想先看看科室和医生排班不一定要马上挂号。所以我把“浏览”权限放开了用户未登录也能看科室和医生信息等他要预约的时候再强制走登录实名流程。实测这个设计是有效的小程序的跳出率明显低于一开始就强制登录的版本。3.2 首页与科室医生数据的加载策略医院类小程序首页有个特点信息多、更新频率低。科室列表、医院介绍、医生简介这些内容一周可能才变一次。但患者打开小程序却要频繁看这些页面。我的策略是三级缓存接口返回数据后在本地Storage存一份设置有效期1小时页面加载时先渲染缓存的旧数据用户感知就是秒开后台静默请求最新接口有新数据再覆盖这比直接加个loading等接口返回体验好得多。在移动端弱网环境下这个策略简直是救命稻草——很多患者是在医院大厅等号的时候才打开小程序的那个环境WiFi信号差、4G网络也可能满格但带宽不足如果每次都等网络返回用户早就不耐烦退出了。科室列表缓存代码大致这样async loadDeptList() { const cacheKey cache_dept_list; const cacheData uni.getStorageSync(cacheKey); if (cacheData cacheData.expire Date.now()) { this.deptList cacheData.data; } // 静默刷新 const res await request(/api/dept/list); if (res.code 0) { this.deptList res.data; uni.setStorageSync(cacheKey, { data: res.data, expire: Date.now() 3600 * 1000 }); } }注意缓存只在“列表页”和“首页”用到了挂号预约这种强时效性页面绝对不能缓存。患者看到某个医生有号进去预约却说“号源已失效”这种体验极其糟糕。所以我处理预约相关的接口都是强制走网络而且每次返回都会带关键数据和当前时间戳前端拿到后立刻校验。3.3 预约流程的状态机设计预约不是一个瞬间动作而是一个包含多个状态转换的长流程。我专门画了一个状态流转图来梳理逻辑这里用文字描述待选择状态用户浏览排班看到剩余号数此时不做任何锁定锁定中用户选择了具体号源后端锁定15分钟前端进入确认页待支付用户确认了就诊人和信息如果是需要在线支付的号进入支付环节已预约支付成功或免费号确认成功生成正式挂号单已完成就诊结束已取消用户主动取消或超时未支付系统取消前端uniapp在处理这个流程时需要特别注意“页面被销毁”的情况。患者从选择号源页跳到了确认页然后切到后台做别的事情过了10分钟再回来此时锁定可能即将过期。我处理的方式是在确认页的onShow生命周期里重新调用“查询号源锁定状态”的接口如果发现即将过期剩余2分钟就弹出提示让用户确认是否继续同时提供一个“重新锁定”的按钮。onShow() { this.checkLockStatus(); // 查询当前锁定状态和过期时间 }, checkLockStatus() { request(/api/register/lock-status, { numId: this.numId }).then(res { if (res.code 0) { const remainSeconds res.data.expire - Date.now() / 1000; if (remainSeconds 120) { uni.showModal({ title: 号源即将释放, content: 您的号源锁将在2分钟内失效请尽快确认或点击重新锁定。, confirmText: 重新锁定, success: (e) { if (e.confirm) this.lockNum(); } }); } } }); }3.4 接口请求封装与统一错误处理uniapp里我封装了一个统一的request方法对接后端php接口。基础配置挂在manifest.json里但环境区分要单独处理。开发环境我用了两个域名一个本地phpStudy指向的后端地址一个测试服务器地址。这个在uniapp里只需要在request封装文件里根据环境变量切换baseURL就行// utils/request.js const BASE_URL import.meta.env.DEV ? https://test-api.example.com : https://api.example.com; function request(url, data {}, method GET) { return new Promise((resolve, reject) { const token uni.getStorageSync(token); uni.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, Authorization: token ? Bearer token : }, success: (res) { const code res.data.code; if (code 0) { resolve(res.data); } else if (code 401) { // token过期跳登录页 uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/login }); reject(res.data); } else { // 统一错误提示 uni.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常请稍后重试, icon: none }); reject(err); } }); }); }第二次封装的时候我加了一个“错误码白名单”的机制。有些错误码是不需要弹Toast的比如用户在信息填写页点了个不存在的科室ID导致返回了一个“科室不存在”的错误这种明确不出现的状况直接静默处理就行了。不然整个操作过程会被一种又一种的弹窗打断体验很差。const silentCodes [404, 405]; // 这些错误码不弹提示 if (silentCodes.includes(code)) { resolve(res.data); // 由页面自己处理 }3.5 PHP后端接口的Token鉴权设计php后端用了ThinkPHP6框架接口鉴权用的是JWT思路但没完全按标准JWT来我简化了一下。加了一个Redis会话管理登录成功生成32位随机token存Rediskey为login_token_123值为用户信息有效期2小时。用户每次请求带着token后端从Redis查一下有没有、过期没有这样比纯粹无状态JWT多了一个“主动踢人下线”的能力——比如患者更换手机号或账号异常的时候后台管理员可以强制清除某个用户的登录态这在JWT标准里做起来很麻烦。// 中间件Token验证 public function handle($request, Closure $next) { $token $request-header(Authorization); $token str_replace(Bearer , , $token); if (empty($token)) { return json([code 401, msg 未登录]); } $value Redis::get(login_token_ . $token); if (empty($value)) { return json([code 401, msg 登录已过期]); } $user json_decode($value, true); // 刷新有效期 Redis::expire(login_token_ . $token, 7200); $request-user $user; return $next($request); }这里有个细节千万别忽略有效期刷新必须放在中间件里统一处理否则用户如果在看一个长页面超过2小时回来之后token就过期了。我在测试时发现如果只在登录和特定接口里刷新用户挂后台太久请求来了还是过期的。统一在中间件刷新之后这个问题就彻底消失了。4. 上线前后遇到的坑与解决方案4.1 号源被“黄牛”脚本抢注上线一周后我发现一个科室的号刚放出来5分钟就全部约满但实际去就诊的患者人数并没有增加。查日志发现同一批手机号在同一天频繁调用锁定接口。解决的思路分三层前端限制加图形验证码滑块拼图那种在“确认页面”出现连续两次锁定失败的时候触发。后端频控同一个患者ID每天最多锁定10次号源超过直接拒绝。同一个IP每天预约次数也有限制。行为识别用Redis记录用户在锁定页面的“停留时间”正常用户从选择号到点确认至少需要3秒以上脚本只需要几十毫秒。这个特征能筛掉一大半机器行为。这套三层防护上线之后异常预约量降了90%以上。老实说真要有人花大精力去逆向你的小程序总有办法但防住“一键脚本”这个级别的bug就够了。4.2 支付回调与HIS系统对接的最终一致性在线支付环节用的是微信支付但支付成功之后不能光改订单状态还要跟医院HIS系统同步。这就出现了一个经典的分布式一致性问题本地数据库和HIS系统两边都要更新任何一边失败都会出问题。我用的方案是“本地事务消息队列补偿”支付回调先落本地状态为“已支付待同步”同时往异步任务表里插入一条同步HIS的任务异步脚本定时扫描未同步的任务调HIS接口成功后更新状态如果调用失败任务保留脚本自动重试最多3次并告警这里最忌讳的是在微信支付回调里直接同步等HIS返回。微信回调有超时限制如果HIS恰好卡了5秒微信那边可能已经判定超时又开始重发回调了结果就是回调重复处理。所以一定要把本地确认动作和外部系统同步动作解耦开。4.3 微信审核被拒的问题医疗类小程序在微信审核这里卡得特别严。我第一版提交的时候因为“涉及医疗健康服务”但没有提供相关资质证明被拒了一次。后来把《医疗机构执业许可证》等资质文件加到小程序后台的“医疗健康”类目里同时在关于页加了一个“免责声明”和“互联网诊疗服务协议”。还有小程序里的所有医疗用词要小心——不能说是“在线诊断”只能说“医生在线咨询”不能承诺“治愈率”之类的字眼。这些细节过审之前一定要自查清楚。4.4 uniapp在小程序端的常见兼容性坑uniapp写代码一时爽到小程序端跑起来有些CSS和API的兼容性问题让人抓狂。我列几个印象最深的position: fixed在输入框弹出键盘时会错位微信小程序里键盘弹出时iOS上fixed元素会跟着跳动。解决方法是有输入框的页面上用page布局或者用uniapp的keyboard-height-change事件来手动调整位置。rpx在不同设备的表现rpx是按屏幕宽度750等分的理论上适配没问题但到了iPad或者折叠屏上350rpx的效果就怪怪的。后来我整个项目把设计稿按750宽度来切并且在page.json里限制了maxWidth在iPad上显示居中的PC风布局。canvas导出白图做医生签名板的时候遇到过一次在小程序的canvas上画完内容再导出图片出现白屏。原因是canvas绘制的时机不对必须在canvas完全渲染完成后再去uni.canvasToTempFilePath。我当时的解决的土办法是延迟300ms再导出治标但管用。后来发现正规解法是监听canvas的onCanvasReady回调再操作。web-view里打开的页面移动端web-view打开外部链接时底部会白一块。这个不是小程序的问题是web-view组件的高度自适应。最后用了一个简单粗暴的方案——整个页面用web-view占满全屏页面内自己适配。4.5 性能优化的三个实战做法移动端小程序的性能瓶颈不在CPU而在渲染和网络。我做了三个优化骨架屏预约确认页、订单列表页用骨架屏代替传统loading动画用户感知加载速度快很多。这个比转菊花圈体验好太多。图片压缩与CDN医生头像、医院环境图这些静态资源全部丢到CDN大小超过100KB的一律压缩到80%质量。实测下来首页图片加载耗时压缩了将近一半。分包加载小程序主包只放首页、登录、科室列表这些核心页面问诊聊天、个人中心、历史订单全部放分包里。分包加载不但省主包体积有2MB限制还能让首屏更快。4.6 前端防录屏的需求排班和问诊信息涉及患者隐私有时候医院会要求做防录屏。这个说实话在小程序端没有完美的拦截方案只能提高录屏门槛禁止系统截屏Android端可以通过wx.setScreenBrightness不正确的做法是限制截屏事件但微信小程序没有直接的API去禁止截屏。我能做的只能是用户禁止截屏在隐私协议里写明“请勿截屏分享他人”做一个自定义分享拦截禁止小程序被转发到群。关键页面隐藏敏感信息比如问诊会话里的身份证号、手机号默认打码显示点击“查看”才明文显示同时这个点击操作会被记录到日志里。这套措施不能完全杜绝截屏但至少能让人在录屏之前产生犹豫大多数情况下已经足够了。5. 一套能直接用的php接口示例代码5.1 获取医生排班接口前端预约的第一步是查排班。医生排班接口要返回的信息包括排班ID、日期、时段、剩余号数、医生基本信息、号源明细列表。这里有个取舍——号源明细具体是几点几号的号要不要一次性返回我最终选择的是第一次只返回“排班”级别的信息患者选好了某天的排班再调“号源明细”接口获取具体号段列表。因为一个医生的一个上午排班可能生成25个号全量返回虽然数据不大但在接口里做关联查询开发和维护成本都上去了而且大部分患者只需要知道“还有号”就点进来了并不需要关心具体是第几号。// 排班接口 public function schedule() { $doctorId input(doctor_id, 0, intval); $deptId input(dept_id, 0, intval); $date input(date, ); $map []; if ($doctorId) $map[doctor_id] $doctorId; if ($deptId) $map[dept_id] $deptId; if ($date) $map[schedule_date] $date; $map[status] 1; $list Db::name(doc_schedule) -alias(s) -join(doc_doctor d, s.doctor_id d.id) -field(s.id, s.schedule_date, s.period_type, s.start_time, s.end_time, s.total_num, s.remain_num, d.name as doctor_name, d.avatar, d.title) -where($map) -order(s.schedule_date, s.period_type) -select() -toArray(); // 判断号源是否已约满 foreach ($list as $item) { $item[is_full] $item[remain_num] 0; } return json([code 0, data $list]); }5.2 锁定号源接口public function lockNum() { $userId $request-user[id]; $numId input(num_id, 0, intval); $num Db::name(doc_register_num)-where(id, $numId)-find(); if (!$num) { return json([code 1, msg 号源不存在]); } if ($num[is_booked] 1) { return json([code 1, msg 该号源已被预约]); } if ($num[is_booked] 2 $num[patient_id] ! $userId) { // 别人锁着 return json([code 1, msg 号源正在被其他人预约]); } if ($num[is_booked] 2 $num[patient_id] $userId) { // 自己锁着续期 Db::name(doc_register_num)-where(id, $numId)-update([ lock_expire date(Y-m-d H:i:s, time() 15 * 60) ]); return json([code 0, msg 锁定成功]); } // 正常锁定 $lock Db::name(doc_register_num)-where(id, $numId)-where(is_booked, 0)-update([ is_booked 2, patient_id $userId, lock_expire date(Y-m-d H:i:s, time() 15 * 60) ]); if ($lock) { return json([code 0, msg 锁定成功]); } return json([code 1, msg 号源状态异常请刷新]); }注意锁定那里有个where(is_booked, 0)的条件这是防并发的最后一道锁。如果真的有两个请求同时命中同一个号源数据库的update会串行执行第二个因为状态已经不是0了更新不到数据返回“状态异常”从根上防止了重复锁定。5.3 确认预约接口确认预约时要注意“先检查排班剩余号数再扣减”和“更新号源状态”这两个操作的顺序。我的建议是先更新号源明细表把状态从锁定改为已约再扣减排班表的remain_num。因为号源明细是更精细的数据如果排班扣减成功但号源更新失败会出现“排班说没号了但号源还是锁着的”这种数据不一致。public function confirmRegister() { $userId $request-user[id]; $numId input(num_id, 0, intval); $patientId input(patient_id, 0, intval); // 就诊人ID Db::startTrans(); try { $num Db::name(doc_register_num)-where(id, $numId)-lock(true)-find(); if (!$num || $num[is_booked] ! 2 || $num[patient_id] ! $userId) { throw new \Exception(号源状态异常请重新选择); } if (strtotime($num[lock_expire]) time()) { throw new \Exception(号源锁定已过期请重新选择); } // 更新号源状态 Db::name(doc_register_num)-where(id, $numId)-update([ is_booked 1, patient_id $patientId, booked_time date(Y-m-d H:i:s) ]); // 扣减排班号数 $result Db::name(doc_schedule) -where(id, $num[schedule_id]) -where(remain_num, , 0) -setDec(remain_num); if (!$result) { throw new \Exception(排班号数已满); } // 创建预约订单 $orderId Db::name(register_order)-insertGetId([ order_no REG . date(YmdHis) . rand(1000, 9999), user_id $userId, patient_id $patientId, num_id $numId, schedule_id $num[schedule_id], status 1, // 已预约 create_time date(Y-m-d H:i:s) ]); Db::commit(); return json([code 0, data [order_id $orderId]]); } catch (\Exception $e) { Db::rollback(); return json([code 1, msg $e-getMessage()]); } }注意lock(true)这个用法它会在SELECT的时候加FOR UPDATE锁这样一来两个并发请求同时进入事务第二个会在SELECT那里等第一个提交或回滚不会读到中间状态。6. 常见问题速查表与避坑指南现象原因解决方案号源明明有点击却约不上号源明细状态更新了但排班表remain_num没扣减检查事务顺序确认预约必须保证两个表的状态在同一个事务里患者重复提交生成两条订单前端重复点击或网络重试后端加唯一约束/幂等键可以用num_id或lock_id做唯一键第一次插入成功第二次自然失败支付成功但订单还是待支付支付回调没到/处理失败写一个手动对账脚本定时拉取微信支付账单和订单表比对Token过期后用户又自动登录了Token刷新中间件没生效确保所有需要鉴权的接口都经过同一个中间件刷新逻辑统一小程序里图片显示不全图片太大或域名没配置合法域名上传的图片走CDN并且把CDN域名加到小程序后台的 downloadFile 合法域名输入框被键盘顶上去页面变形键盘弹出挤压fixed布局用adjust-positionfalse手动控制监听键盘高度动态padding基础库版本太老新API报错微信基础库不兼容在manifest.json里设置最低基础库版本建议至少2.30.0以上分页加载时列表抖动图片未设置固定宽高给image标签设置固定的宽高或使用modewidthFix用户切后台再回来锁定过期锁定有15分钟时限前端在onShow时主动查锁定状态快过期了弹窗提醒HIS系统同步失败导致两边数据不一致外部接口不可靠异步任务重试机制不要本地和HIS在一个事务里再说一个很多人容易忽略的点日志记录一定要完整。我在接口层写了一个全局的日志中间件所有请求和响应都存数据库或者文件出了问题可以直接顺着日志捋链路。尤其是预约和支付这类核心操作日志里要记录入参、出参、处理耗时、服务器时间、用户ID、设备信息。这套日志帮我解决的问题不计其数有一次排查用户投诉“我明明没预约成功但扣了钱”靠的就是日志里那笔支付回调和订单状态不对应的记录。7. 关于医疗类小程序的一些额外忠告做这类跟医疗沾边的系统技术层面的坑其实都好填难的是业务边界和合规。我在开发过程中被医院方反复提醒过几个问题写出来供参考不要碰“诊断”这个词。在线问诊里医生只能说“根据您描述的症状建议您来院进一步检查”不能说“您得的是某某病”。这不是医生不想说是合规要求不允许在线上做确诊。隐私协议一定要单独确认。小程序首次登录就要弹窗让用户勾选隐私协议收集的信息类型、用途、保存期限都得写清楚。微信后来对隐私协议收紧了没有这个弹窗相关接口会被限制。备份策略要提前定。用户预约数据非常重要必须每天做一次全量备份每小时做一次增量备份。我当时用的是云数据库自带的自动备份外加凌晨2点定时把备份文件转存到另一个存储桶。留一个紧急开关。整个系统要有一个“一键停诊”或者“全局关闭预约”的后台开关。比如某个医生临时出差、某天门诊停诊运维人员可以在后台一键把所有相关排班停掉而不是挨个去改排班状态。这个功能上线前就要做好别等出事了才加。关于设计上的反思如果重来一次我会把“患者档案”和“就诊人管理”这块的权限剥离得更清晰。现在患者账号下可以绑定多个就诊人比如老人帮孩子挂号、子女帮父母挂号但我一开始没考虑清楚谁能修改哪个就诊人的信息导致出现了“A患者能看到B患者的部分敏感信息”的隐患。虽然最后通过数据权限修正了但这类问题的风险等级很高设计之初就应该用“数据归属操作权限”双维度来控制。最后分享一个我自己反复用的排错技巧给每个订单号、每个操作加一个trace_id从最前端到后端到数据库一路带下去。这是从大型分布式系统里学来的思路放在单体php项目里同样好用。用户反馈问题的时候只要拿着order_no或者trace_id我三分钟内就能定位到问题是出在前端渲染、后端逻辑还是数据库层面。这个习惯帮我节省的时间比项目里任何一次技术选型优化都多。做医疗系统的项目心态要好因为用户都是带着焦虑来的任何一个技术小毛病都可能被放大成患者的糟糕体验。把每一个细节想透把每一类异常都提前兜住这就是我做这个系统最大的心得。
网站建设高端定制企业官网