> ## 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.

# 发布菜单

> 将某个门店的完整菜单作为一个 JSON 文档发送，以您自己的稳定 ID 为键，然后再读取回来。发布是基于差异的，因此重复发布是安全的。

门店的菜单是顾客点餐所依据的目录。您将它作为**一个 JSON 文档**发布，其中描述整个菜单——分类、菜品、规格、加料选项，以及适用的税费定义。Maple 以**您分配的外部 ID** 作为全部依据，因此每个订单行项目上的 `menu_entity_id` 会直接对应回您自己的商品目录。

发布需要 `menus:write` 权限，并且与该门店之间存在**有效连接**。读取需要 `menus:read`。有关连接，请参阅[Maple 如何运作](/zh/developer-api/how-maple-works)。

## 菜单的结构

菜单是一棵树，旁边还有共享的定义：

```
menu
├── categories[]          (required)
│   └── items[]
│       └── variations[]  (required: at least one — the price lives here)
│           └── modifierGroups[]   (inline, or a reference to a shared group)
├── modifierGroups[]      (shared definitions, referenced by groupId)
├── taxCategories[] / taxRates[]
├── fees[]
└── discounts[] / promotions[] / coupons[]
```

两条规则涵盖了模型的大部分内容：

* **每个对象都有一个 `externalId`**——这是您为它设置的稳定标识。由您来选择，Maple 绝不会更改。使用相同的 `externalId` 重新发布，会更新同一个对象。响应中还会生成 Maple 自己的 ID（`item_…`、`var_…`、`ctg_…`）供参考，但您应通过自己的 `externalId` 来定位对象。
* **价格在规格（variation）上。** 即使是最简单的菜品，也至少有一个规格（可称之为"Regular"）。所有价格都是**整数的最小货币单位**——`450` 即 \$4.50。

## 最小菜单

最小的有效文档包含一种货币，以及一个包含一个菜品、一个规格的分类：

```bash theme={null}
curl -X POST $MAPLE_BASE/locations/{locationId}/menu \
  -H "Authorization: Bearer $MAPLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "USD",
    "categories": [
      {
        "externalId": "cat-drinks",
        "name": "Drinks",
        "items": [
          {
            "externalId": "itm-latte",
            "name": "Latte",
            "variations": [
              { "externalId": "var-latte-reg", "name": "Regular", "price": { "amount": { "amountMinor": 450, "currency": "USD" } } }
            ]
          }
        ]
      }
    ]
  }'
```

发布成功后会返回最终的菜单，其中所有集合都已展开，并填入了 Maple ID。您发送的 `externalId` 会原样返回。

## 添加共享加料选项

在顶层定义一次加料组，然后在需要的地方通过 `groupId` 引用——这样"Milk options"只需维护一处，即可用于所有饮品：

```json theme={null}
{
  "currency": "USD",
  "modifierGroups": [
    {
      "externalId": "grp-milk",
      "name": "Milk",
      "minSelections": 0,
      "maxSelections": 1,
      "options": [
        { "externalId": "mod-whole", "name": "Whole milk" },
        { "externalId": "mod-oat", "name": "Oat milk", "price": { "amount": { "amountMinor": 75, "currency": "USD" } } }
      ]
    }
  ],
  "categories": [
    {
      "externalId": "cat-drinks",
      "name": "Drinks",
      "items": [
        {
          "externalId": "itm-latte",
          "name": "Latte",
          "variations": [{ "externalId": "var-latte-reg", "name": "Regular", "price": { "amount": { "amountMinor": 450, "currency": "USD" } } }],
          "modifierGroups": [{ "groupId": "grp-milk" }]
        }
      ]
    }
  ]
}
```

加料组条目要么是**内联定义**（包含 `name` 和 `options`），要么是**引用**（`{ "groupId": "grp-milk" }`）。不要在同一个条目中混用这两种形式——这样的负载会被拒绝，而不是被悄悄截断。菜品、规格和单个选项都可以带有加料组，因此您可以在菜单需要的地方嵌套选项。

<Tip>
  税类别和税率、费用（例如瓶装押金）、折扣、促销和优惠券都是可选的顶层集合，每个都通过各自的 `externalId` 定位，并通过 ID（`taxCategoryId`、`feeIds` 等）关联到菜品。[端点参考](/zh/developer-api/api-reference)记录了每个字段。
</Tip>

## 发布语义

每次发布都会把**整个菜单**作为期望状态发送。Maple 会将其与当前菜单进行差异比较（以 `externalId` 为键），并将结果作为新版本应用。由此衍生出几条规则：

* **文档就是整个菜单。** 您发送的内容就会成为菜单。您之前发布过、但在后续发布中遗漏的对象会被**移除**——要保留某个对象，每次都要包含它。
* **`stockStatus` 是例外。** 它是字段级的值：在您\_确实\_包含的对象上省略它，会保留该对象当前的库存状态（新对象默认为 `in_stock`）。因此，将某个菜品标记为售罄（86）仍然是一次包含所有对象的完整发布——只是您设置了一个字段。
* **标识就是您的 `externalId`。** 相同的 `externalId` 在多次发布中更新同一个对象；新的 `externalId` 会创建新的对象。请使用在您的系统中稳定的 ID（SKU 或主键），而不是显示名称。
* **顺序会被保留。** 分类、菜品、规格和选项会按照您发送的顺序显示。
* **校验是全有或全无。** 只要有任何无效内容，您就会收到列出所有问题的 `400`，菜单保持原样不变。请全部修复后重新发送。
* **完全相同的重复发布不会产生任何影响。** 发送相同的文档不会改变任何内容，也不会产生多余的变动，因此您可以在每次菜单变更时放心发布。
* **每次变更都会生成版本。** 内容变更会产生新版本；门店始终使用当前版本。

由于文档就是完整的期望状态，最简单的集成方式是：每当您的数据源变化时，重新发布整个菜单。您无需自己计算差异——Maple 会完成。

### 设置可用时间的时区

可用时间窗口（时段菜单和欢乐时光）按**菜单的时区**计算。请通过顶层的 `timezone` 字段设置——使用 IANA 名称，例如 `America/New_York`：

```json theme={null}
{ "currency": "USD", "timezone": "America/New_York", "categories": [ /* … */ ] }
```

如果省略，菜单的时区默认为 **UTC**，这对于基于时间的规则几乎总是不对的。时区在您**第一次**发布时确定，因此请一开始就设置正确。

## 读取回来

```bash theme={null}
curl $MAPLE_BASE/locations/{locationId}/menu \
  -H "Authorization: Bearer $MAPLE_KEY"
```

返回当前已发布的菜单——分类、带价格的菜品和规格、共享加料组，以及税费和折扣定义——每个对象都会回显您发布时使用的 `externalId`。没有已发布菜单的门店会返回 `404`。

## 了解发布何时生效

订阅菜单同步事件，以便在已发布的菜单传播时收到通知：

| 事件 | 含义 |
| - | - |
| `menu.sync.completed` | 某个门店的菜单同步已完成 |
| `menu.sync.failed` | 某个门店的菜单同步失败 |

如何订阅并验证这些事件，请参阅[Webhook](/zh/developer-api/concepts/webhooks)。

## 下一步

<CardGroup cols={2}>
  <Card title="接收并决定订单" icon="receipt" href="/zh/developer-api/guides/receive-orders">
    订单行项目通过您分配的 `externalId` 引用您的菜单。
  </Card>

  <Card title="完整菜单架构" icon="code" href="/zh/developer-api/api-reference">
    端点参考中每个菜单对象的每个字段。
  </Card>
</CardGroup>


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