菜单
发布菜单
将某个门店的完整菜单作为一个 JSON 文档发送,以您自己的稳定 ID 为键,然后再读取回来。发布是基于差异的,因此重复发布是安全的。
门店的菜单是顾客点餐所依据的目录。您将它作为一个 JSON 文档发布,其中描述整个菜单——分类、菜品、规格、加料选项,以及适用的税费定义。Maple 以您分配的外部 ID 作为全部依据,因此每个订单行项目上的 menu_entity_id 会直接对应回您自己的商品目录。
发布需要 menus:write 权限,并且与该门店之间存在有效连接。读取需要 menus:read。有关连接,请参阅Maple 如何运作。
菜单的结构
菜单是一棵树,旁边还有共享的定义:
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。
最小菜单
最小的有效文档包含一种货币,以及一个包含一个菜品、一个规格的分类:
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”只需维护一处,即可用于所有饮品:
{ "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" })。不要在同一个条目中混用这两种形式——这样的负载会被拒绝,而不是被悄悄截断。菜品、规格和单个选项都可以带有加料组,因此您可以在菜单需要的地方嵌套选项。
发布语义
每次发布都会把整个菜单作为期望状态发送。Maple 会将其与当前菜单进行差异比较(以 externalId 为键),并将结果作为新版本应用。由此衍生出几条规则:
- 文档就是整个菜单。 您发送的内容就会成为菜单。您之前发布过、但在后续发布中遗漏的对象会被移除——要保留某个对象,每次都要包含它。
stockStatus是例外。 它是字段级的值:在您_确实_包含的对象上省略它,会保留该对象当前的库存状态(新对象默认为in_stock)。因此,将某个菜品标记为售罄(86)仍然是一次包含所有对象的完整发布——只是您设置了一个字段。- 标识就是您的
externalId。 相同的externalId在多次发布中更新同一个对象;新的externalId会创建新的对象。请使用在您的系统中稳定的 ID(SKU 或主键),而不是显示名称。 - 顺序会被保留。 分类、菜品、规格和选项会按照您发送的顺序显示。
- 校验是全有或全无。 只要有任何无效内容,您就会收到列出所有问题的
400,菜单保持原样不变。请全部修复后重新发送。 - 完全相同的重复发布不会产生任何影响。 发送相同的文档不会改变任何内容,也不会产生多余的变动,因此您可以在每次菜单变更时放心发布。
- 每次变更都会生成版本。 内容变更会产生新版本;门店始终使用当前版本。
由于文档就是完整的期望状态,最简单的集成方式是:每当您的数据源变化时,重新发布整个菜单。您无需自己计算差异——Maple 会完成。
设置可用时间的时区
可用时间窗口(时段菜单和欢乐时光)按菜单的时区计算。请通过顶层的 timezone 字段设置——使用 IANA 名称,例如 America/New_York:
{ "currency": "USD", "timezone": "America/New_York", "categories": [ /* … */ ] }如果省略,菜单的时区默认为 UTC,这对于基于时间的规则几乎总是不对的。时区在您第一次发布时确定,因此请一开始就设置正确。
读取回来
curl $MAPLE_BASE/locations/{locationId}/menu \ -H "Authorization: Bearer $MAPLE_KEY"返回当前已发布的菜单——分类、带价格的菜品和规格、共享加料组,以及税费和折扣定义——每个对象都会回显您发布时使用的 externalId。没有已发布菜单的门店会返回 404。
了解发布何时生效
订阅菜单同步事件,以便在已发布的菜单传播时收到通知:
| 事件 | 含义 |
|---|---|
menu.sync.completed |
某个门店的菜单同步已完成 |
menu.sync.failed |
某个门店的菜单同步失败 |
如何订阅并验证这些事件,请参阅Webhook。