跳转到内容
Maple

菜单

将某个门店的完整菜单作为一个 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。

最小菜单

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

Terminal window
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,这对于基于时间的规则几乎总是不对的。时区在您第一次发布时确定,因此请一开始就设置正确。

读取回来

Terminal window
curl $MAPLE_BASE/locations/{locationId}/menu \
-H "Authorization: Bearer $MAPLE_KEY"

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

了解发布何时生效

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

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

如何订阅并验证这些事件,请参阅Webhook。

下一步