Skip to content

预约服务:改期、批量提醒与迟到处理 ​

预约提醒由预约的开始时间产生,不依赖参与者登录。登录时展示的是当前日程;后台提醒则覆盖用户不在线时的触达。

示例规则是“预约确认后,在开始前 24 小时和 30 分钟提醒,取消后停止发送”。如果预约创建时距开始只剩 20 分钟,可以只发送确认消息,不再补齐已经错过的两档提醒。

从一条提醒需求推导预约模型 ​

以会议室预约为例:用户约好周五 10:00 的场次,希望提前收到提醒。只保存手机号与发送时间,可以完成一次发送;但会议改到 11:00 后,系统还需要知道这条提醒属于哪个场次的哪个版本。

候选方案能解决什么需要补充的约束
按手机号和时间保存发送任务一次性人工通知无法自然关联改期、取消和参与人变化
定期扫描近期预约发现当前有效日程提醒阶段去重、窗口补扫和参与人展开
确认或改期时保存提醒计划追踪每轮日程变化旧任务识别、成员变化和任务恢复

本篇用“场次名额、预约记录、参与人关系、提醒计划”分别保存事实。占位是否成功属于预约事务;提醒是否发送属于通知流程。把它们分开,才能在短信服务故障时保留有效预约。

改期与提醒的执行时序 ​

改期先作为一个原子业务操作提交,提醒展开和渠道发送随后处理。这里只画“新场次可用”的成功改期,名额不足时整个改期回滚。

展开任务与渠道发送可以由不同 Worker 承担,图中合并为一个角色以突出顺序。成员独立的通知结果使一人失败不会触发整场重复发送。

预约需求与验收约束 ​

提醒之前还要保证预约本身有效。示例采用固定场次与名额模型:同一请求不能重复占位,确认成功才安排提醒,改期必须成功占到新场次后才能释放旧名额。

这里一次会议预约占一个资源名额,可以关联多个通知参与人。若课程按实际人数占座,则应按席位数量扣减并保存对应占位关系,不能把下面的减 1 原样用于团体订课。

业务问题示例决策实现约束
一个场次是否允许多人预约允许,最多为场次配置的容量名额扣减与预约确认同事务
预约是否可修改开始前可改,是否收费另行定义接口携带预期版本
新参与人加入时是否补提醒只补仍有价值的阶段按参与人创建独立意图
通知多久算迟到示例要求开始前完成提交提醒带最晚发送时间
所有渠道失败怎么办页面保留日程,记录失败供查询预约有效性不依赖通知成功

如果业务是医生、房间等任意时间段互斥预约,需要额外的区间冲突约束;下面的名额扣减只适用于预先划分好的场次。

保存预约时间和版本 ​

数据用途
appointment_id、version区分同一预约的多次改期
starts_at、timezone保存确定的开始时刻与展示时区
statusconfirmed、cancelled、completed 等状态
参与人关系与版本决定谁有资格收到这一轮提醒
提醒阶段before_24h、before_30m,分别去重

跨时区预约不能只存“上午 9 点”。日历规则需要对应 IANA 时区,实际执行对应明确时刻。重复预约先生成具体场次,再为每场安排任务,避免改动一场后意外影响整个系列。

建议分别保存 appointment_slots、bookings、booking_participants、reminder_plans 与通知记录。预约请求键在用户范围内唯一;参与人关系使用稳定成员 ID,离开后重新加入时产生新成员轮次,避免沿用旧关系版本。

接口或处理器输入输出与边界
创建预约场次、参与人、请求幂等键预约 ID、版本和确认结果
改期预约 ID、目标场次、预期版本成功后的新版本;冲突需重新读取
取消预约 ID、预期版本、请求键幂等取消并释放原占位
查询日程预约 ID最新时间、状态和提醒记录
提醒展开 Worker提醒计划 ID、预约版本、成员扫描检查点分批创建参与人通知意图

确认、改期与取消怎样触发 ​

确认预约时,在同一事务保存预约和提醒任务,或者保存 Outbox 事件。改期时增加版本并创建新计划;旧任务执行时发现版本不一致,记录 skipped。

提醒任务开始处理时,依次检查:

  1. 预约仍然确认,时间和版本与本次任务一致。
  2. 参与者仍在名单中,且没有取消这一路提醒。
  3. 当前时间仍处于可接受的提醒窗口。
  4. 这一参与者、这一场次、这一提醒阶段没有完成同渠道发送。

检查与外部发送之间仍有并发窗口。已经提交的短信不一定能撤回;消息中的链接应读取最新预约信息,不把旧任务参数当成当前日程。

事务与事件发布的衔接见 Outbox 模式,它解决本地提交与可靠投递之间的窗口,不保证外部通知恰好发送一次。

创建预约的事务 ​

以下 PostgreSQL 18 片段在已有表结构下表达名额条件,:参数名 由驱动绑定:

sql
UPDATE appointment_slots
SET remaining = remaining - 1
WHERE id = :slot_id
  AND tenant_id = :tenant_id
  AND starts_at > statement_timestamp()
  AND remaining > 0
RETURNING id, starts_at;

应用事务先取得请求幂等键的处理资格,再执行名额更新。更新成功后创建预约、成员和提醒计划,一起提交;任何一步失败都回滚扣减。重复请求等待原事务结果或返回已保存的预约,不再扣一次名额。

改期时按固定顺序锁定新旧场次与预约记录,校验版本和可修改时间。占用新名额、释放旧名额、增加预约版本和保存新提醒计划一起提交。取消也只有第一次从有效状态转换时释放名额。

remaining > 0 让最后一个名额的竞争发生在数据库写入时,而不是依赖两个请求各自读到的剩余数量。请求幂等键则处理另一种问题:同一个用户因响应丢失重发。库存条件不能代替请求去重,反过来也一样。

提醒计划如何展开 ​

text
领取已到时间的 reminder_plan
读取预约,检查版本、状态与最晚发送时间
分批读取仍有效的参与人:
  保存场次 + 预约版本 + 成员轮次 + 提醒阶段对应的通知意图
  与该批展开检查点一起提交
展开完成后结束计划
渠道 Worker 再次检查预约与参与人,提交仍有效的通知

展开期间新增的成员,不能只靠旧检查点覆盖:加入操作需要补建仍适用的通知,或使计划按新成员水位再次核对。移除成员则让已建立但尚未发送的意图在校验时退出。

1000 人同一时间预约怎样处理 ​

需要先区分 1000 个独立预约,还是一场有 1000 名参与者的会议。

情况拆分单位结果记录
1000 个独立预约每个预约与提醒阶段各自检查时间、状态和版本
一场 1000 人会议场次提醒计划再展开参与人每个参与者、每个渠道独立保存

展开参与人时分批读取并保存通知任务,分页推进与任务保存原子衔接,或通过稳定业务键安全重扫。不要在一个任务里循环发完 1000 人再整体确认,进程中断后无法安全区分谁已收到。

提醒有明确的最晚发送时间。消费者按渠道限速,必要时在允许窗口内提前错峰;若额度不足以在会前完成,应提前发现容量缺口,而不是临时在整点无限提高并发。具体容量计算见会员批量处理。

执行晚了怎么办 ​

到达时刻示例处理
原定前 24 小时,实际前 23 小时若仍有价值,可发送实际剩余时间
两档提醒同时积压到开始前 10 分钟合并为一次仍适用的提醒
预约已经开始或结束跳过过时的“即将开始”文案
已取消但取消事件尚未消费发送前读取当前预约状态,阻止旧任务

“实际剩余时间”应在发送前计算,不能直接沿用创建任务时的“还有 24 小时”。

一次改期失败与成功的记录演变 ​

假设预约 B17 已占场次 S10,预约版本为 3,用户请求改到 S11:

操作应保留的记录原因
第一次改期,S11 已满B17 仍关联 S10,版本仍为 3改期失败不应丢掉原位置
S11 释放名额后重新提交有效请求新占位、旧释放、B17 版本 4 和新计划一起提交避免只改时间却漏释放或漏提醒
版本 3 的提醒到达检测到当前版本 4,跳过旧任务身份仍在,但已失效
为版本 4 展开 1000 人,处理到 500 人退出前 500 人的意图与检查点已经持久化恢复只需继续或安全重扫
某成员在发送前退出其未发送意图跳过,其他成员继续一个成员变化不应取消整场提醒

如果取消与发送竞争,发送前的状态判断只能阻止尚未交给渠道的动作。测试时应分别观察“取消成功”与“外部消息是否已经提交”,而不是把两者当成一个原子结果。

应验证的结果 ​

场景应断言的结果
最后一个名额被两个请求同时争抢只有一个预约确认,剩余名额不小于零
创建请求成功但响应丢失相同请求键返回同一预约,不再次占位
改期目标已满原预约与原名额保持有效
展开到第 500 人后进程退出恢复后继续或安全重扫,不重复已建意图
发送前移除成员或取消预约未发送意图跳过;已提交渠道的结果可追踪
积压跨过预约开始时间过时提醒停止发送并记录原因

监控确认成功率、名额冲突、最老待展开计划、开始前提交成功比例与渠道失败。任务成功表示处理了对应提醒意图,用户是否真正看到需要独立回执。

腾讯会议集成方案中的重复通知入口 ​

腾讯会议的教育场景 API 集成白皮书介绍了课程、答辩和宣讲等预约场景。宣讲方案特别指出:创建会议的 API 响应与会议创建 Webhook 都可作为通知入口;如果其他流程也依赖 Webhook,建议统一由回调触发通知,避免发两次。文档还区分取消单次周期会议与取消整个系列。

对应到本文,应先统一业务事件身份,再生成通知意图。API 成功响应、回调重投和补查结果可以汇入同一入口,用“预约身份 + 版本 + 通知阶段 + 接收人”识别相同动作。仅对每个 HTTP 请求分别去重,无法识别两条不同入口携带的同一业务事实。

周期课程还需要系列 ID 与单次预约 ID:取消一节课只失效该次提醒,调整系列规则则明确影响哪些未来实例。对于 1000 名参与者,保存一次预约事实后分批展开通知,不在创建会议请求中同步发送 1000 次。这些数据模型和批量策略是本文据集成场景提出的实现建议;白皮书没有披露腾讯会议内部的提醒调度算法。

腾讯会议回调协议:确认接收要放在正确的位置 ​

腾讯会议的企业回调服务说明规定了响应时间、重试和成功响应格式。它要求的不只是 HTTP 200,还包括约定的响应正文;超时或响应不符合要求会重试。因此,一个通用 Webhook 框架即使自动返回 200,也可能无法完成该接口的成功确认。

应用可以在完成来源校验和接收记录持久化后,快速按协议确认,再异步展开参与人提醒。若在回调请求里同步通知 1000 名参与者,很容易超过供应商的等待时间,让重投与原请求同时展开同一批通知。重复事件应命中原接收结果,通知去重仍按业务身份进行。

如果接收记录没有保存成功,就不能先确认再依赖进程内队列补做;如果供应商的有限重试已经结束,则需要查询或人工核对来恢复。这一设计也适用于预约修改和取消事件,但每类事件的身份与状态转换应分别定义。

参考资料 ​