多数字面理解都会把“付费预约”想成一个很直的流程:访客选时段、点支付、付成功、日历里马上生成事件。企业版源码故意没有这么做。它先把信息落在 calendar.booking,让时段、答案、参与人和收费信息暂存在 booking 里;只有付款真的过账,或者满足最终确认条件后,才把 booking 转成正式的 calendar.event。
这不是多绕一步,而是在日历资源占用、支付结果不确定、门户回跳安全三件事之间做隔离。
主要参考的企业版源码入口:
enterprise/appointment_account_payment/controllers/appointment.pyenterprise/appointment_account_payment/controllers/payment.pyenterprise/appointment_account_payment/models/calendar_booking.pyenterprise/website_appointment_account_payment/views/appointment_templates_payment.xml
一、真正的跨模块链路是什么
这里至少串了四层:
- Website / Appointment 表单层:访客在网站选时段、填问题、带着 invite 参数进入支付页。
- Booking 暂存层:
_handle_appointment_form_submission()不直接建calendar.event,而是创建calendar.booking。 - Accounting / Payment 层:
_make_invoice_from_booking()先生成发票,支付页根据发票 access token 进入支付流程。 - Calendar 最终确认层:
_make_event_from_paid_booking()只在真正满足“已支付/可成会”后才生成事件,并再次过滤不可用 booking。
所以用户看到的是一个预约页,但系统内部其实是“网站表单 -> booking 暂存 -> 发票/支付 -> 日历成会”的四段式链路。
二、为什么必须先存 booking,不能直接占用日历
_handle_appointment_form_submission() 的注释说得很明确:这样做是为了避免把未确认、未支付的预约直接同步到日历。如果用户支付中断、支付失败、浏览器关掉,系统里留下来的应该是可回收的 booking,而不是已经污染员工日历或资源容量的正式事件。
calendar.booking 里保留了:
- 预约问题答案;
- 资源或员工占用草稿;
booking_token;- 发票关联;
- 目标产品与容量信息。
也就是说,booking 是跨 Appointment、Accounting、Calendar 的“中间态合同”。
三、支付完成后,为什么有时回摘要页,有时直接回事件页
回跳不是固定 URL,而是由 token 和状态共同决定:
appointment_post_payment()先通过发票access_token找到 booking;- 如果 booking 已经生成了
calendar_event_id,就跳到/calendar/view/<event_token>; - 如果支付失败、待处理,或事件还没生成,就跳回
/calendar_booking/<booking_token>/view摘要页。
这就是本文最重要的边界:
- invoice token 负责把支付结果安全地带回正确 booking;
- booking token 负责让访客回看自己的暂存预约摘要;
- event token 只在正式成会后才对外暴露。
三个 token 对应三种对象,不能混成一个“支付成功页链接”。
四、为什么支付成功也不等于一定能成会
_make_event_from_paid_booking() 在转事件前还会调用 _filter_unavailable_bookings()。这意味着即使用户付过钱,系统仍会重新看资源/员工在该时间窗内是否可用。
这一步解决的是并发边界:
- 访客提交 booking 到付款成功之间有时间差;
- 同一资源可能被别的流程先确认;
- 站点展示的“看起来可约”不等于最终“可成会”。
也因此文章标题里说“不是付款成功就锁时段”。支付是必要条件,但不是越过 availability 复核的许可证。
五、落地时最容易踩的坑
最常见的误判有四个:
- 误把
calendar.booking当成正式预约,脚本里直接拿它做运营统计; - 以为支付成功页一定能拿到最终事件链接;
- 忽略了 booking 的自动清理,导致把过期 booking 误当活数据;
- 把回跳做成单一 URL,结果 pending / failed payment 无法回到正确摘要页。
_gc_calendar_booking() 的存在也说明 booking 本来就是一个可清理的中间对象,而不是永久业务主表。
六、结论
企业版网站付费预约真正解决的不是“在线支付预约”这四个字,而是未确认预约如何安全跨过网站、会计和日历三个子系统。calendar.booking 把中间态隔离出来,invoice token、booking token 和 event token 又把支付后回跳分成三层身份,最终保证:
- 未支付时不会污染正式日历;
- 已支付时能精确回到正确对象;
- 并发冲突时仍保留最后一道 availability 复核。
DISCUSSION
评论区