☰
微信小程序预约挂号系统从设计到上线的工程实践
2026/10/12 12:34:07 网站建设 项目流程

看到这个标题,我脑子里浮现出来的第一句话是:这又是一个被当成普通 CRUD 来做的预约系统。带编号的仓库名一看就是从项目平台上导出来的,标题后半截 “_ub8qq51t” 大概率是生成任务时自动带上的随机串,不影响理解。但真正让我想写这篇文章的原因,是“微信小程序 + 在线预约挂号”这个组合,市面上教程大多停留在“能跑通”阶段,而医院场景真正难的地方,全在跑通之后的那一层。

我去年参与过一个医疗信息化相关的线上预约模块,后端对接的是某综合医院的信息系统,项目本身是个模拟项目,但流程和问题都是真实的。这篇文章就按我当时的落地顺序来写:先讲需求怎么盘,再讲技术选型,接着拆号源引擎、权限与合规,最后说上线前后的那堆坑。适合正在做或者准备做预约挂号类小程序的开发者看,也适合产品经理读一读,理解为什么医院预约系统不能照搬票务系统。

1. 先把需求盘明白:挂号小程序和“预约工具”根本是两码事

1.1 用户只看到“挂号”,背后却站着三套角色

普通预约类产品(比如理发店、健身房)只需要处理“用户——商家”两个角色,而医院的预约挂号系统天然是三套角色在协作:

  • 患者端:查科室、查医生、选时间、下单、支付、退号、查报告。看起来就是一条线,但患者对“科室”完全没有概念,他们输入的是“肚子疼”“咳嗽三天”,所以搜索和推荐必须做症状联想。
  • 医院运营端:排班、放号、停诊、加号、改号。排班不是随便选的,要跟医院的出诊规则绑定,比如专家门诊每周固定几天、普通门诊每天多少人、复诊优先占多少号。
  • 平台管理端:账号审核、数据统计、黑名单、费率配置。这一端在项目初期经常被忽略,结果一到上线就抓瞎,医生排班错了得有后门改,用户闹纠纷得有后台查记录。

我当时建议把这三套角色分别做成不同的首页和权限菜单,而不是用一套页面加按钮去适配所有身份。这个决策在后期帮了大忙,审核时提交的“角色说明图”也能画得清清楚楚。

1.2 业务规则比功能清单更磨人:退号、停诊、爽约的连锁反应

这是全项目最容易被低估的部分。功能清单看着很清爽:挂号、支付、取消、查询。但每一条规则都会引发连锁反应,举个例子:

  • 退号规则:就诊前 24 小时可以线上退号,号源立刻释放;就诊前 2 小时以内不能退号,只能到医院窗口办。这个规则是为了防止黄牛占号,也为了医生不空等。
  • 停诊规则:医生临时停诊,已经挂号的号源要自动释放,患者要收到通知并允许一键转约到同科室其他医生。这个流程里最麻烦的是“一键转约”要做到号源再分配,不是简单的退费了事。
  • 爽约规则:挂了号没去也没退,累计一定次数要进冷静期,后续一段时间内不能在线预约。这个规则的实现要点是拿“就诊核销记录”来判断爽约,不能拿支付状态来判断。

我把这些规则画成了状态迁移表,后面整个系统的逻辑都以这张表为准。没有这一步,程序员写出来的代码就是各写各的,测试也测不全。

1.3 我最终梳理出的最小闭环

在动手写代码之前,我拉着团队成员把流程压缩到最核心的一条主线:查号 → 锁号 → 支付 → 取号/核销。再辅以三条支线:取消释放、停诊转移、异常兜底。

流程里有个关键认知:预约挂号不是“下单定座位”,而是“分配稀缺资源的时序协调”。号源不是无限库存,每个号对应一个真实医生一段时间内的一个就诊名额。这个认知决定了后面的数据模型、锁号逻辑、并发策略,全都不能照搬电商下单那一套。

2. 技术地基怎么打:框架怎么选?云开发够不够用?

2.1 原生、uni-app、Taro 三选一的真实理由

标题直接把平台写死了——微信小程序,所以不需要像做 App 那样纠结跨端。但在小程序技术栈内部还是有三条路:原生、uni-app、Taro。

我的结论是:如果团队只有一两个人,且只在微信生态内运营,选原生就够了。理由有三个:

  1. 原生小程序的调试和排错链路最短,不用隔着一层编译框架去猜问题。
  2. 预约挂号系统要跟微信支付、订阅消息、微信隐私授权、地理位置等原生能力深度绑定,原生 API 的适配天然最顺。
  3. 框架层为了跨端做的抽象,在遇到奇怪 bug 时会成为排查的阻碍。到了线上问题,你需要的是一眼能看懂的 js 执行栈,而不是编译映射后的模糊调用栈。

当然,如果你以后明确要同时发支付宝小程序、抖音小程序,可以考虑 uni-app。但代价是自定义组件、平台差异兼容、底层 API 封装这些地方都会多花时间,尤其是医疗场景里那种复杂的日期选择器和分时段排班组件,框架化之后反而不好做。

2.2 云开发够不够用:从一张数据表规划说起

预约挂号要不要自建后端?我的建议是分阶段看:

  • 原型验证和小规模内测,直接用微信云开发(云函数 + 云数据库 + 云存储)完全够,省去域名备案、HTTPS 证书、服务器运维这些杂事。
  • 正式上线并接入医院信息系统后,建议至少把核心数据(号源、订单、支付流水)放到自建后端管理的独立数据库里,云数据库可以保留给非核心业务。

原因很现实:医院信息科对接时通常要求对数据库有审计权,要能看到谁在什么时候改了什么数据。云数据库在数据导出和第三方审计上有一定限制,自建 MySQL/PostgreSQL 则随时可以拉日志。

我当时用到的核心数据表设计大致如下,贴出来作为参考:

表名核心字段作用
usersopenid, unionid, nickname, phone用户账号统一表
patientsuser_id, name, id_card, phone就诊人信息,一个账号可绑定多人
departmentsdept_name, clinic_type, introduction科室字典
doctorsdept_id, doctor_name, title, intro医生档案
schedulesdoctor_id, dept_id, work_date, time_slot, total_count, remain_count排班与号源库存
appointmentsschedule_id, patient_id, status, lock_expire_at, paid_at预约单,核心状态表
payment_ordersappointment_id, transaction_id, amount, refund_status支付与退款流水
operation_logsoperator, action, target_id, before, after, created_at无业务逻辑的审计日志
blacklistuser_id, reason, expire_at防刷与爽约隔离

有个容易踩的坑:很多人习惯把号源剩余量直接当成一个数字字段去 update 减 1,这在并发量上来以后会出问题。我当时用了一个更稳妥的方案:号源库存每放一个号就生成一条“号源明细记录”,预约时锁明细,而不是锁库存数。这样不但能避免并发下的超卖,还能追查每个号到底流到了谁手里。

2.3 登录、手机号与支付:先想“谁是你的实名对象”

微信小程序的登录链路是:wx.login()换 code,再由后端调 code2Session 拿 openid。这一步所有开发者都会做,但预约挂号的特殊性在于:业务对象是“就诊人”,不是“微信号”。

也就是说,A 用微信给妈妈挂号,支付的是 A 的钱,最后取号就诊的是妈妈。所以系统必须单独维护“就诊人”列表,并在挂号和支付时都做实名校验。当时我做的规则是:添加就诊人必须校验姓名和身份证号的匹配关系,一个微信号最多绑定 5 个就诊人,就诊人身份证号在数据库中加密存储,页面展示时做脱敏。

手机号授权部分,建议用微信getPhoneNumber能力,拿到加密 code 后由后端解密换取真实手机号。尽量不要在小程序端自己弹窗收集手机号输入,一来安全风险大,二来微信审核时对隐私收集的扣分点会集中在这里。

支付走微信支付,需要注意预约挂号的退费链路是“原路退回”,而不是直接给用户余额。原路退回受支付渠道清算周期影响,用户端会出现几天的延迟,所以产品文案和客服话术都要提前准备好。

3. 号源引擎:排班、锁号、释放与防刷的设计与实现

3.1 排班模型:科室-医生-日期-时段四层

排班是整个系统的地基,地基没打牢,后面全得跟着返工。我用的四级模型是这样的:

科室(departments)→ 医生(doctors)→ 排班(schedules)→ 时段号源(time_slots)

排班表里存的是“某天某医生在某个科室出诊”,时段是上午、下午、晚间或者按 30 分钟切分。每个时段再带一个总号数和剩余号数。一个医生一天可以有多条排班记录,比如上午 30 个号,下午 20 个号。

比较反直觉的一点是:排班数据最好不要让医生自己在小程序里填,而是做一个独立的排班管理页面给医院运营人员用。医生在移动端看自己的班次列表就够了,修改权限集中到管理端。这样可以避免医生误操作把明天的号全放出来,也方便医院做统一调控。

排班发布还有一个细节——节假日。医院有自己的门诊日历,法定节假日期间部分科室停诊,这个日历不能写死,要留一个可配置的后台入口,因为每年节假日的调休安排都不一样。

3.2 锁号状态机:从“可约”到“已完成”

预约单的状态我定义为八种,按顺序流转:

状态含义触发动作
INIT已创建,未锁定用户发起预约
LOCKED已锁号,待支付用户确认号源
PAID已支付支付回调成功
CONFIRMED已确认支付后自动确认,或人工确认
CANCELLED已取消用户退号或系统停诊释放
FINISHED已完成线下签到/核销
NOSHOW爽约超过核销时间且未取消
CLOSED已关闭超时未支付,号源释放

状态机的代码写出来不难,难的是每一步状态迁移都要同时做两件事:更新预约单状态 + 更新号源余量。这个“双写”如果做不好,就会出现用户手里有号但号源库里没有、或者号源库有号但用户无法下单的脏数据。

我的实现是用数据库事务包住两个写操作,并在操作日志中记录前后值。伪代码大致长这样:

// 云函数 lockAppointment 简化示意 const result = await db.runTransaction(async (transaction) => { const schedule = await transaction.get('schedules', scheduleId); if (schedule.remain_count <= 0) { throw new Error('号源不足'); } await transaction.update('schedules', { remain_count: schedule.remain_count - 1 }); await transaction.insert('appointments', { scheduleId, status: 'LOCKED', lockExpireAt: Date.now() + 10 * 60 * 1000 // 10分钟锁定期 }); });

在早期版本里,我也曾图省事先减号源、再插预约单,结果一次并发压测就暴露了问题:减了号源但预约单插入失败,号就凭空消失了。

3.3 过期释放、重复提交与超时支付的边界守卫

锁号不是锁死不放。用户选好号之后,我有意把它设计成一个 10 分钟的锁定期:这期间别人看到的是“已占用”,用户自己则进入收银台支付。如果 10 分钟内没有支付,定时任务要把号释放回去。

这个小机制借鉴了票务系统的做法,但要注意一个体验差异:医院用户群体里有很多老年人,10 分钟太短,很多老人支付流程会卡住。我当时把初版设成 15 分钟,之后通过后台参数动态调整,最终定为 15 分钟。

定时释放的实现很简单,云函数定时触发器每 5 分钟扫一次:

// 定时触发器:每5分钟执行一次 exports.main = async () => { const now = Date.now(); const expired = await db.collection('appointments') .where({ status: 'LOCKED', lockExpireAt: _.lt(now) }) .update({ status: 'CLOSED', closeReason: 'timeout', closedAt: now }); // 同时恢复对应 schedule 的号源余量 // 注意这里需要放在同一个事务逻辑里 return expired.stats.updated; };

重复提交问题也很典型:用户手抖连续点了两次“立即预约”,或者弱网环境下请求重发,就会生成两个预约单。解决办法是在预约接口加一个幂等键,用requestId做唯一约束,同一个 requestId 第二次请求直接返回第一次的结果,不产生新记录。排查线上问题时,requestId 还能帮你把用户操作、后端日志、支付回调串成一条线,这个习惯值得养起来。

3.4 防刷与公平性:不把黑产拒之门外,黄牛就会替你做推广

预约挂号一旦热门,必然有人动歪心思。我做的防刷不是一刀切拦截,而是分层设计:

  • 身份层:每个用户必须绑定真实就诊人信息,身份证验证不过不能挂号。
  • 频次层:同一就诊人一天内最多预约 2 个不同科室的号,同一时段只能有一个待就诊的预约。这个规则主要防止一个人把热门号全占上。
  • 行为层:同一个手机号 30 分钟内预约接口调用超过 10 次,触发验证码;同一 IP 高频请求走限流。
  • 隔离层:检测到频繁取消又预约的用户,进黑名单冷静期。冷静期设计成先不告知具体时长,避免被逆向破解规则。

公平性的话题在医院系统里尤其敏感,有些规则宁可在前端做提示也别不说。比如“同一天不能预约两个相同科室的号”,这个提示要在选号前就说明白,而不是等用户支付失败再告诉他,否则投诉率会直线上升。

4. 权限模型与隐私合规:医疗场景绕不开的两座山

4.1 小程序端角色权限与页面路由设计

这套系统里有三种小程序端角色,权限边界必须清楚:

  • 患者端:自己的预约、就诊人管理、订单支付、报告查询。
  • 医生端(可选):查看当日预约列表、维护停诊/加号。
  • 管理端(通常走后台网页,小程序只做弱管理):排班调整、号源投放、数据看板。

我当时把手术级的管理功能放在独立 Web 管理后台,小程序端只保留一个“医生工作台”的小页面,用来展示排班和停诊操作。原因是:小程序审核时,涉及在线问诊、处方等敏感能力,需要拿资质;如果只是预约查询,审核相对轻松。但既然是预约挂号系统,最好还是提前规划好资质要求。

权限控制的实现有三层:路由守卫检查登录态,接口层校验角色,数据层校验数据归属。只有登录态和路由层校验,很容易出现平行越权:用户 A 通过改 URL 参数访问用户 B 的预约单。我把所有数据查询都强制加了where: { userId: 当前用户id }作为默认过滤条件,才算补上这个洞。

4.2 数据脱敏、隐私声明与账号注销

医疗数据的敏感性不用多说,但具体到小程序里有几个必须兑现的要求:

  • 身份证号、手机号默认脱敏展示,中间几位打码;详情页查看需要再次验证。
  • 用户输入的健康描述、症状文字属于敏感个人信息,存储时做加密,导出时做去标识化。
  • 隐私政策必须有一个可访问的在线链接,并且内容要逐条对应实际收集的数据项。很多开发者在这里吃亏:前端弹窗收集了定位权限,隐私声明里却只写了一句“为了更好提供服务”,审核基本必驳回。

对了,账号注销功能不能漏。这是比较新的要求,用户要能在小程序里发起注销,后端在确认后删除或匿名化其个人信息,同时保留必要的交易记录用于对账和审计。我把“注销”做成一个异步流程:用户申请注销 → 7 天冷静期 → 到期自动删除个人身份字段 → 保留订单流水但切断身份关联。

4.3 医疗服务类目的审核准备

微信小程序的服务类目,预约挂号通常归在“医疗”类目下,需要上传对应的机构资质文件,常见的有《医疗机构执业许可证》或与医疗服务机构的合作协议。没有资质的主体想绕过类目去做挂号,基本会在提审环节卡住。

提审时我建议主动提交三样东西:测试账号(要带权限的,后台提前配好)、测试排班数据(不要用空数据演示,审核员一看到空白页就驳回)、一段演示录屏(覆盖查询、预约、支付、取消完整链路)。即使审核说明里没要求录屏,附上也能明显降低被追问的概率。医疗类审核严格,但审核员也愿意看到思路清晰的提交材料。

4.4 与医院内部系统对接的安全建议

预约挂号的终极形态是跟医院内部的号表打通,这样才不会出现线上约了号、线下没记录的事故。但这部分的对接安全容易踩坑:

第一,内网系统不能直接在公网暴露。稳妥方案是医院侧部署一个前置服务或接口网关,只开放必要接口,并且做来源 IP 白名单。

第二,消息内容要做双向校验。医院返回号源数据时,不仅要判断返回码,还要校验数据完整性,比如科室 ID 是否存在、医生 ID 是否匹配。我就是吃了“只看返回码”的亏,某次内部系统返回了空号源但代码继续往下走,导致用户挂到了一个实际不存在的医生。

第三,预留一个“手工模式”。对接稳定之前,先让医院运营人员在小程序后台手动维护排班,等接口跑稳定了再切换自动同步。自动同步必须保留一个定时任务做对账,比如每隔 10 分钟比对两边号源数,有差异就报警。

5. 上线前后的实战:审核、灰度、运维与那些“线上才知道”的坑

5.1 提审最容易忽视的四个小细节

很多团队在小程序提审时铩羽而归,原因往往不是技术问题,而是细节。这里列几个我亲身踩过的:

第一,隐私政策页面的链接必须在微信后台配置的“用户隐私保护指引”里能够直接打开,而且要用小程序根域名下的域名,不能用私有 IP 或临时链接。

第二,微信支付商户号要提前完成认证,而且商户号的经营范围必须和你的小程序服务类目一致,不一致的话审核会被打回“经营内容与资费类目不符”。

第三,所有测试用的号源和排班数据要提前准备好,并且保证测试账号能正常进入支付环节。审核员会真实走完付款流程再测试退款,如果你用的只是演示模式,会被要求重新提审。

第四,提交审核的小程序版本里不能出现“开发环境”“本地测试”这类字样,也不能有 console 的大量调试输出。看起来是小问题,但审核截图里一旦出现这种字样,会给人很不专业的印象。

5.2 灰度放号:不要第一天就把全部号源放出来

预约挂号的上线策略和普通 App 不一样,普通 App 可以全量发版再迭代,但挂号系统一旦出现数据错乱,影响的是真实患者的就诊安排。

我当时的做法是分三批灰度:

  • 第一批:试点一个科室(比如体检中心),号源量小,即使出问题影响面也小。
  • 第二批:开放儿科、内科等高频科室,同时开启“停诊转移”功能做全流程验证。
  • 第三批:全部科室开放,并开启排班自动同步。

灰度期间重点观察三个指标:支付成功率(低于 90% 就要查支付链路)、取消后号源恢复时间(超过 1 分钟说明异步任务有问题)、用户投诉内容里是否出现“明明有号却挂不上”的描述。

灰度放号的操作通过后台一个“放号开关”实现:所有科室默认不放号,排班数据正常维护但前端不展示,需要上线的科室点开开关即可。这个开关在正式系统里也很有用,节假日临时关停某个科室,不需要改代码,改配置就行。

5.3 早高峰放号那一分钟的排队与限流

医院放号时间通常很固定,比如每天早晨 7:00 放 7 天后的号。这就意味着系统峰值会在某一分钟集中爆发,之前看起来游刃有余的服务,到了这一分钟可能直接打挂。

我没有做特别复杂的秒杀架构,因为挂号并发量虽然集中,但总体数量远小于电商大促。我用了三个便宜有效的措施:

  1. 接口层限流:每个用户每分钟最多 60 次请求,每个 IP 每分钟 120 次,超过就返回友好提示“当前访问人数较多,请稍后重试”。
  2. 预约接口排队:用户点击“选号”时先进入一个排队页,由后端队列异步处理锁号请求,前端轮询结果。这个体验上的变化让用户觉得系统在认真处理,而不是卡死。
  3. 数据库连接池保护:高峰前把连接池调到上限,并对慢查询做兜底,确保排班查询接口响应时间控制在 500ms 以内。

实测下来,这三个措施在约 500 并发的情况下完全没有问题,削峰效果明显。如果你要面对的是几千并发,可能还得上 Redis 预扣库存、消息队列异步出票,但那样复杂度也上来了,得权衡。

5.4 停诊、退费与黑名单的兜底

线上系统跑起来之后,真正让人睡不着的不是开发期的 Bug,而是运营期的异常。最典型的几个场景:

医生临时停诊,系统要自动给受影响用户发订阅消息。这里有个被很多人忽略的限制:微信订阅消息需要用户每次点击授权“允许一次”,一次授权只能推送一条通知。所以停诊推送可能因为用户没授权而失败。我的兜底方案是:推送失败的用户自动进入短信队列,用文本短信再通知一次。

退费也要兜底。微信支付的原路退款到账需要 1-3 个工作日,用户会打电话问“为什么还没到”。我当时做了一整套退款状态跟踪:退款中、退款成功、退款失败,失败后自动触发人工客服单,客服确认后线下转账走医院流程。不要小看这个流程,退款失败率约占在线支付的 0.3%,听起来不高,但真实用户不会这么想。

黑名单机制上线后有个反向教训:某位老年用户因为不会操作,连续误取消 5 次后被系统自动拉黑了。后来我调整了规则,对绑定就诊人满 3 个月且无投诉记录的用户,冷静期自动缩短,并把判断逻辑从“次数”改成“频率”,同一用户 7 天内取消 5 次才触发,避免误伤。

5.5 操作日志与大对账

最后一条经验,也是我认为最值钱的一条:把所有业务操作都写日志,且不删日志。

我在系统里设计了一张 operation_logs 表,每个操作记录operator、action、targetId、before、after、createdAt、source(哪个端、哪个页面、哪个请求来源)。当时团队有人觉得这是浪费存储,结果上线第二周就救过一命:某用户反馈订单金额不对,查业务表没发现异常,最后是靠日志找到了支付回调被重复调用导致重复扣款的问题。

大对账的频率我定为一小时一次:对比预约单状态和支付平台的流水,找出“已支付但无预约单”“已预约但无支付记录”的异常数据。这类数据不需要太多,哪怕每天只有几条,也要在最早期发现,拖到月底再处理就是灾难。

说回我自己踩过最疼的一次坑:停诊转移功能上线前一天,测试发现预约单被重复转移了两次,用户最终没收到任何通知,但号源已经被扣掉两份。排查到凌晨才发现,是转移接口的重试机制和前端按钮的连续点击叠加触发了并发写。修好之后我在团队里立了一条规矩:所有涉及号源变更的接口,必须保证幂等,且操作日志必须记录清晰的重试标识,再遇到类似问题只需要按 requestId 排查链路。

做预约挂号类项目,真正的分水岭不是能不能写出来,而是能不能扛得住真实世界的规则与异常。把状态机定清楚、把边界守卫做好、把日志和兜底留足,比任何花哨的技术选型都重要。这套思路不只适用于挂号系统,凡是涉及稀缺资源分配和时序协调的项目,都可以直接套用。后续如果要做复诊、取药、报告查询,也只是在这个骨架上往里加模块的事。

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

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

立即咨询