OCI SWE · VO Project Deep Dive + Coding

Oracle 医院预约 API:1000 医生的首个可用 15 分钟槽

基于 Oracle OCI 长帖的明确约束,设计跨 POST 调用保持状态、原子占位和幂等重试的预约接口。

题目重点:基于 Oracle OCI 长帖的明确约束,设计跨 POST 调用保持状态、原子占位和幂等重试的预约接口。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 医生 ID 为 0–999;一天 8 小时共 32 个起始时间槽,最后一个 16:45–17:00。日期为医生所在时区的本地日期,跨夏令时的时区转换在 API 边界处理。
  • 训练版请求带唯一 requestId 作为幂等键;同键重试返回原预约,同键改医生/日期报错。题目未明确幂等协议,这是生产扩展。
  • 下面代码是单进程内存参考;“跨 API 调用保持状态”满足同一实例内的多个调用。多实例或重启后持久性需数据库事务。

解题路线

  1. 以 (doctorId,date) 为键保存 32 位占用位图。按索引 0..31 扫描第一位 0,设置后返回对应 HH:MM;一天全满时返回无空位。
  2. 占位与 requestId 幂等记录必须在同一原子边界内;训练版用锁,生产环境用数据库唯一约束、行锁或乐观版本 CAS,避免两个请求同时预订同一槽。
  3. POST /appointments 的请求至少带 doctorId、localDate、requestId;成功返回 doctorId、日期、槽位与预约 ID,满额返回明确冲突码。医生不存在与非法日期分别验证。
  4. 部署多副本时把状态从进程移到持久共享存储,按医生和日期分片;防止医生排班变化后继续出售已关闭槽位。监控重复请求率、冲突、耗时和满额率。

手动走一遍

同一医生同一天连续两个不同 requestId 返回 09:00 与 09:15;重试第一个 requestId 仍返回 09:00,不再占新槽。另一医生同日独立从 09:00 开始。第 33 个不同请求返回无空位。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

"""In-memory reference for a 9:00-17:00, 15-minute first-slot booking API."""

from datetime import date
from threading import Lock


class HospitalBooking:
    SLOTS_PER_DAY = 32

    def __init__(self, doctor_count: int = 1000):
        if doctor_count <= 0:
            raise ValueError("doctor_count must be positive")
        self.doctor_count = doctor_count
        self.occupied: dict[tuple[int, date], int] = {}
        self.requests: dict[str, tuple[int, date, str]] = {}
        self.lock = Lock()

    def book(self, doctor_id: int, day: date, request_id: str) -> str | None:
        if not 0 <= doctor_id < self.doctor_count:
            raise ValueError("unknown doctor")
        if not request_id:
            raise ValueError("request_id is required")
        with self.lock:
            previous = self.requests.get(request_id)
            if previous is not None:
                if previous[:2] != (doctor_id, day):
                    raise ValueError("idempotency key reused for a different booking")
                return previous[2]
            key = (doctor_id, day)
            mask = self.occupied.get(key, 0)
            for index in range(self.SLOTS_PER_DAY):
                bit = 1 << index
                if mask & bit == 0:
                    mask |= bit
                    self.occupied[key] = mask
                    minute = 9 * 60 + index * 15
                    slot = f"{minute // 60:02d}:{minute % 60:02d}"
                    self.requests[request_id] = (doctor_id, day, slot)
                    return slot
            return None

复杂度

每个预约最多检查 32 个槽,固定营业规则下为 O(1) 时间;D 个医生、H 个有记录日期的状态存储为 O(DH),幂等记录随成功请求数增长。训练版全局锁会限制并发,生产应按医生日期细化事务粒度。

易错点与追问

容易失分的边界

  • 先读第一空槽再无锁写入,两个并发 POST 可能拿到同一个时间。
  • 把下午 17:00 当成可预约起点,实际最后起点是 16:45。
  • 只存在进程内存却宣称多实例、进程重启后仍保持预约。
  • 不处理重试,网络超时后客户再次 POST 被扣两个槽。

继续深挖

  • 医生临时请假或已有预约取消时,位图与幂等记录如何更新?
  • 跨时区患者看到的预约时间怎样避免夏令时歧义?
  • 如果必须按专业或地点搜索首个可用医生,索引和锁粒度如何变化?
✓已复制微信号!