> ## Documentation Index
> Fetch the complete documentation index at: https://docs.maple.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# 幂等性与重放安全

> Maple 如何在不使用幂等键的情况下保证重试安全：重放安全的决策、按差异应用的菜单、幂等重放，以及您这一侧的去重。

网络可能在请求中途失败。关键问题是：如果您重试，是否会有事情发生两次？在 Maple 中，您最常重试的写入操作都被设计为可安全重复，因此遇到超时，您无需先核对状态就可以直接重试。

<Note>
  此 API **没有 `Idempotency-Key` 请求头**。对于订单、菜单和事件，安全性是每个操作自身的属性（详见下文），无需逐个请求选择启用。唯一需要您发送键的地方是**预订和候位写入**，它们使用请求体中的 `idempotency_key`（见下文）。
</Note>

## 订单决策是重放安全的

重复发送订单决策（`accept`、`deny`、`ready`、`complete`、`cancel` 或 `status`）会返回相同的确认，不会重复产生副作用：

```json theme={null}
{ "object": "order_decision", "order_id": "ord_...", "decision": "accept", "status": "received" }
```

如果您发送了 `accept` 但响应超时，请再发送一次。订单只会被接受一次，无论哪种情况您都会收到 `{ "status": "received" }`。这样，一个简单的“遇到暂时性错误就重试”的循环就是正确的，您这一侧无需做任何额外记录。

## 发布菜单是一次差异应用

菜单发布描述的是整份菜单的目标状态，并以您的 `externalId` 为键，按与当前已发布内容的差异来应用：

* 重新发布**完全相同**的文档不会产生任何变化。
* 对象标识通过 `externalId` 在多次发布之间保持稳定，因此重新发布不会重建或频繁变动对象。
* 校验是全有或全无：返回 `400` 时不会更改任何内容，因此发布失败后菜单保持原样。

所以，如果发布超时，重新发送同一份文档是安全的。请参阅[发布菜单](/zh/developer-api/guides/publish-menu)。

## 预订写入携带 `idempotency_key`

创建、更改或取消预订（`POST /v1/bookings`、`POST /v1/bookings/{id}/change`、`POST /v1/bookings/{id}/cancel`），以场所身份取消预订（`POST /v1/bookings/{id}/venue_cancel`），以及为散客安排入座（`POST /v1/locations/{locationId}/walk_ins`），都需要在**请求体中**提供 `idempotency_key`，其范围限定在您的应用内。与候位加入一样，散客入座的键属于您的应用，在其所有门店间通用。键必须包含 1–255 个字符。对于 **Maple 原生预订**，事件记录会在同一个事务中记录该写入及其重放结果：

* 使用**相同的键和相同的请求**重试，会重放最初的结果：您会得到同一个 `booking`，不会重复预订、改期或取消。
* **相同的键搭配不同的请求**（不同的客人、时段或备注）会返回 `409`，并带有 `code: "idempotency_key_reused"`。请为每次要执行的写入生成新的键；UUID 即可。

重试会返回同一个预订标识，而不会重新应用变更；响应是从当前预订中读回的。键自首次占用起有 24 小时的重放窗口。重放会先于 `expected_version` 被检查，因为在原始写入成功后，`expected_version` 往往已经过期。

原生和第三方服务商的保留（hold）都会被持久化，并归获取它们的应用所有。重复提交同一个 `slot_token` 会重放该应用仍有效的保留，而不会再占用新的锁。其他应用无法使用或释放它，即使在共同授权的门店也是如此。未知的服务商保留在经过保守的 24 小时不活动隔离期后，会释放其配额预留，这与其保留的审计记录无关；已过期的操作无法通过重试恢复。

### 由第三方服务商支持的写入

对于 OpenTable，Maple 会在向服务商发起 I/O 之前，持久地占用该键和请求摘要，并使用同一个稳定的服务商请求 ID。待处理的更改或取消还会为镜像预订预留，以防其他合作伙伴写入。成功后，Maple 会同时记录镜像和重放结果。暂时性的镜像失败会同步重试；后台对账程序会修复未完成的工作。

服务商超时会在结果尚未确定时返回 `409 operation_in_progress`。请在 `details.retry_after` 秒后，使用相同的请求和键重试。在使用原始服务商请求 ID 重复执行预订写入之前，Maple 会先执行一次对账读取。已完成的写入会直接重放，不会再次调用服务商。

<Warning>
  未解决的待处理键在其 24 小时自动重放窗口结束后**不会**被回收。如果 `details.reason` 为 `replay_window_expired`，请停止自动重试，并携带 `details.operation_id` 联系 Maple。不要为了绕开不确定的结果而生成新的键。
</Warning>

预订配置则改用内容寻址的发布方式：相同的配置无需显式的键，也不会产生任何变化。状态类场所操作（confirm、decline、seat、complete、no-show）使用 `expected_version`，而不是幂等键；在得到不明确的响应后，请先读取该预订，再执行其他操作。场所侧取消和散客入座则需要 `idempotency_key`，与其他键一样，范围限定在您的应用内。

```json theme={null}
{ "hold_id": "bkh_…", "guest": { "first_name": "Alex", "last_name": "Quinn", "phone": "+12125551234" }, "idempotency_key": "6f1c…" }
```

## 重放事件是幂等的

`POST /v1/webhook_events/{eventId}/replay` 只会向尚未收到该事件的订阅重新投递。已经收到的订阅会被跳过，因此在故障恢复后重放，不会向正常的端点重复投递。

## 您这一侧：对投递去重

您必须自行添加幂等性的唯一地方是 **webhook 处理程序**。投递保证至少一次，因此同一个事件可能多次到达。请根据信封中的 `id` 去重：

```ts theme={null}
async function handleEvent(event: { id: string; type: string; data: unknown }) {
  if (await alreadyProcessed(event.id)) return; // no-op on a repeat
  await process(event);
  await markProcessed(event.id);
}
```

尽可能让处理工作本身也具备幂等性，例如以 Maple 的 `order_id` 作为您自己订单记录的键，这样重复的 `order.notification` 会更新记录，而不是产生重复。不要依赖事件顺序；当顺序很重要时，请通过 `GET /v1/orders/{orderId}` 进行核对。请参阅[Webhooks](/zh/developer-api/concepts/webhooks)。

## 总结

| 操作 | 可以安全重试吗？ | 原因 |
| - | - | - |
| 订单决策 | 是 | 重放安全；重复发送会返回 `received`，不会产生重复影响 |
| 菜单发布 | 是 | 按差异应用；重新发布相同内容不会产生变化 |
| 事件重放 | 是 | 跳过已投递的订阅 |
| 原生预订创建 / 更改 / 取消 | 是，需使用您的 `idempotency_key` | 相同的键 + 相同的请求体会重放；相同的键 + 不同的请求体返回 `409 idempotency_key_reused` |
| 散客入座和场所侧取消 | 是，需使用您的 `idempotency_key` | 相同的键 + 相同的请求体会重放同一个预订，即使其桌位已被占用；相同的键 + 不同的请求体（对于散客入座，还包括另一个门店）返回 `409 idempotency_key_reused` |
| 原生预订保留 | 在原保留仍有效期间 | 再次提交其 `slot_token` 会返回仍有效的保留 |
| 第三方服务商的预订写入 / 保留 | 使用相同的键 / 报价 | 持久占用及稳定的服务商请求 ID；未解决的结果可能需要对账 |
| 预订配置 | 是 | 相同的配置是内容寻址的空操作 |
| 接收 webhook | 由您去重 | 至少一次投递，请根据事件 `id` 去重 |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.