场所与人员

这家门店、它的员工、它的区域和它的机器——每一次温度读数都挂在上面的那张固定地图。

这些是不会动的东西:餐厅本身、在里面工作的人、它被划分成的那些区域,以及那些把 食物保持在某个温度上的机器。它们没有一个是一次查验。

但它们仍然决定了那些查验意味着什么。一个温度就是一个数字, 不带任何判定,直到你从它所测的那台设备上读出阈值;而一次读数之所以属于这栋建筑的 某一部分,只是因为那台设备说了它站在哪个区域。把这一页弄错了,下游的每一个数字都 会悄无声息地错掉。

集合权限范围一条记录是什么
restaurantsrestaurants:read门店本身:名称、地址、休息日、设置。
usersusers:read一个记录查验的员工账号。
areasareas:read门店被划分成的一个区域——厨房、冷库、吧台。
equipmentequipment:read一台冰箱、一台冷冻柜、一台冷链设备,带着它的阈值。
sensorssensors:read一个无线探头,按 MAC 地址标识。
cooking-equipmentcooking-equipment:read一台烤箱或其他加热设备,带着它的阈值。

这六个共用同一个快照信封iddeletedcapturedAtreceivedAtsequence 以及其余字段,都摆在下文描述的 data 旁边。

restaurants

你的密钥所限定的那家门店,以记录的形式呈现。每家餐厅一条,所以这个集合通常就只有 一页。

字段类型含义
namestring, 必填门店的名称,就是应用里显示的那个。
addressstring, 必填邮寄地址,作为一个字符串。
closingDaysstring[], 必填门店歇业的那些日子。普通字符串,schema 不对它们做约束。
exceptionalClosuresobject[]一次性的歇业,每一条是一个带 fromto 的对象。
detailedAddressobject | null拆成各部分的地址,在应用是那样存着的时候。
deliveryAddressstring | null货物送到哪里,在它与 address 不同的时候。
deliveryDetailedAddressobject | null同一个东西,拆成各部分。
preferredLanguagestring | null这家餐厅工作时用的语言。
multiAreaboolean | null这家门店是否被分成不止一个区域。
settingsobject | null应用设置。这个结构是应用的,而且它会变。
{
  "id": "e5a1d3c9-7b40-4a28-9df6-18c0b2e46a75",
  "collection": "restaurants",
  "deleted": false,
  "capturedAt": "1789300481002",
  "receivedAt": "1789300489517",
  "sequence": "7",
  "data": {
    "name": "Le Comptoir de Nanterre",
    "address": "12 rue des Anciennes Mairies, 92000 Nanterre",
    "closingDays": ["sunday"],
    "preferredLanguage": "fr",
    "multiArea": true
  }
}

这个示例只展示了运营的那一半。在它旁边,记录还带着商务的那一半:subscriptionsubscriptionscompanyNamebillingAddressbillingEmailtvaNumbertrialEndDatecustomerIddiscount。它们确实存在,它们在 OpenAPI 文档里, 而这一页不会带着你一个个讲它们。

读运营那些字段,别碰商务那些。 它们描述的是这家餐厅与我们之间的合同,不是它 的厨房。它们不是一套计费 API,它们改变的理由与食品安全毫无关系,而一个把客户的 订阅状态回显给客户看的集成,是一张等着被开出来的工单。

closingDays 是一个普通字符串的列表。 schema 没有给它们任何形状,所以读它们、 显示它们就好;不要对它们做分支,也不要假定某一种写法或大小写。

users

那些记录查验的员工账号。一条温度记录userId 指向的就是 这里。

字段类型含义
firstNamestring, 必填名,按录入的样子。
lastNamestring, 必填姓,按录入的样子。
emailstring, 必填这个账号的邮箱地址。
phoneNumberstring | null一个联系电话,在给了一个的时候。
modulesstring[]这个人用的是应用的哪些部分。自由字符串。
preferredNotificationTimestring | null这个人要求在什么时候被提醒。
isChildrenboolean | null一个内部标记。schema 没有赋予它任何含义;不要在它上面搭东西。
isDeletedboolean | null应用自己的停用标记——而且在这里可以为 null,和别处不一样。
{
  "id": "2774953d-8d9b-4a68-8ec4-1209edd90777",
  "collection": "users",
  "deleted": false,
  "capturedAt": "1789315002664",
  "receivedAt": "1789315010218",
  "sequence": "31",
  "data": {
    "firstName": "Amina",
    "lastName": "Berthier",
    "email": "amina.berthier@example.test",
    "phoneNumber": null,
    "modules": ["temperatures", "cleaning"],
    "isDeleted": false
  }
}

这是个人数据。 emailphoneNumber 以及那两个姓名字段能识别出一个真实的 人,而它们出现在这个 API 里只有一个理由:一条食品安全记录必须说清是谁做的那次 查验。这是它们在这里唯一的职责。

所以请存 id,在需要一个显示名的时候再去解它。不要因为联系方式恰好出现在载荷 里,就把它们抄进你自己的表、你的日志、你的分析事件或者某个第三方工具。餐厅才是 他们员工数据的控制者,而你每做一份副本,就是他们从此要为之负责的一份。

isDeleted 在这个集合上可以为 null,而在别处它是必填的,所以请把一个缺失的标记 当作「未停用」,而不要让它不小心以某种类假值的方式漏过去。它与信封上的 deleted 之间的区别,在基础目录那一页上有解释

areas

餐厅的一个区域。两个字段,而它的重要性远超两个字段所暗示的。

字段类型含义
namestring, 必填餐厅给这个区域起的名字——厨房、冷库、吧台。
isDeletedboolean, 必填应用的停用标记。见基础目录那一页
{
  "id": "9c2b17ae-4d80-4f55-b3e1-0a6c8f4d2b19",
  "collection": "areas",
  "deleted": false,
  "capturedAt": "1789301120440",
  "receivedAt": "1789301126871",
  "sequence": "12",
  "data": {
    "name": "Chambre froide",
    "isDeleted": false
  }
}

一切按区域分组,都是靠 areaId 设备、加热设备、炸锅和清洁任务都带着一个, 而它是一次读数与这栋建筑某一部分之间唯一的关联。记录本身 上面没有 areaId:你得经由设备才走得到那里。

areaId 在它出现的每一个地方都可以为 null。一台没有区域的冰箱是正常的——它意味着 没有人给它指派过,通常是在一家 multiArea 为 false 的单间门店里。把这些归到 「未指派」下面,而不是把它们丢掉。

equipment

冰箱、冷冻柜和其他冷链设备。决定一个温度是否可接受的,正是这条记录。

字段类型含义
indexinteger, 必填餐厅在自己那份清单里给它的位置。显示顺序,不是一个标识符。
typestring, 必填它是哪一类设备。一个不受约束的字符串——读它,不要对它做分支。
namestring, 必填厨房管这台设备叫什么。
minnumber | null可接受的最低温度。
maxnumber | null可接受的最高温度。
areaIdstring | null它所在的那个区域。
equipmentSensorIdstring | null装在它上面的那个传感器,在有一个的时候。
statestring | null这台设备当前的状态,一个不受约束的字符串。
outOfRangeCountnumber | null应用维护的一个超出范围读数的计数。
isDeletedboolean, 必填应用的停用标记。
{
  "id": "84ce5c13-3237-40d4-901a-cbba59a6406f",
  "collection": "equipment",
  "deleted": false,
  "capturedAt": "1789302455901",
  "receivedAt": "1789302461330",
  "sequence": "58",
  "data": {
    "index": 2,
    "type": "freezer",
    "name": "Congélateur bas",
    "min": -22,
    "max": -18,
    "areaId": "9c2b17ae-4d80-4f55-b3e1-0a6c8f4d2b19",
    "equipmentSensorId": "b31f7d64-0e29-42ca-9a57-51d3c8e07f28",
    "state": "ok",
    "outOfRangeCount": 0,
    "isDeleted": false
  }
}

minmax 是你能拿到的唯一判定。 一条温度记录带着 一个值、一个时间和它所测的那台设备——没有任何东西说它是不是没问题。-14.4 °C 在 冰箱里是一场危机,在冷冻柜里则是一个平常的星期二,而把两者分开的正是这条记录。你 搭出来的每一个合规数字,都要连接到这里。

它们同样可以为 null。当 minmax 缺席时,你就没有阈值,而对那次读数诚实的 回答是「未知」,不是「在范围内」。请在你的界面里这么说,而不是默认给一个绿色。

阈值是当前的那一套,不是当时适用的那一套。 快照给你的是今天的 minmax;三月的一次读数,当时是拿三月设的那套值来判断的,而餐厅随时都可以改它们。 如果你拿今天的数字去重新评估旧的读数——而这也是这个 API 唯一允许你做的事——请把 结果标注成你的重算结果,而不是厨房当时看到的东西。

typestate 是不受约束的字符串。 在 schema 里两者都只是单个的自由文本 字段,背后没有任何清单。把它们显示出来,非要分组就分组,但绝不要让一个不认识的值 从一个 switch 里漏过去、掉进沉默。

outOfRangeCount 是应用的数字,不是你的。 schema 没有说它覆盖的是哪个时间 窗,而你也没法从这里的快照把它重建出来。把它当作来自应用的一个信号;如果你需要一 个自己站得住脚的计数,就从那些读数和阈值算出你自己的,并且标明它是你的。

equipmentSensorId 从设备指向它的传感器。传感器记录则反过来指回来。两边都可以为 null,而且不会因为一边填了,另一边就保证也填了。

sensors

那些无线探头。两个字段,都是可选的。

字段类型含义
macAddressstring | null这个探头的硬件地址——你靠它把一台物理设备和另一台区分开。
sensorEquipmentIdstring | null这个传感器看着的那台设备。
{
  "id": "b31f7d64-0e29-42ca-9a57-51d3c8e07f28",
  "collection": "sensors",
  "deleted": false,
  "capturedAt": "1789302501764",
  "receivedAt": "1789302509002",
  "sequence": "61",
  "data": {
    "macAddress": "F4:12:9D:3A:77:0B",
    "sensorEquipmentId": "84ce5c13-3237-40d4-901a-cbba59a6406f"
  }
}

请留意这两个字段名,它们互为镜像,很容易写反:设备带的是 equipmentSensorId, 传感器带的是 sensorEquipmentId。读错那一个,你会拿到 undefined,而且没有任何 报错。

这里没有名字,没有电量,也没有最后一次出现的时间。一个传感器上报了什么,在 temperature-readings 上,而那些是以 equipmentId 为键 的,不是以传感器为键——所以一个已经哑掉的探头,看起来和一个没有什么可上报的探头 一模一样。

cooking-equipment

烤箱、保温柜,以及热的那一侧的其余东西。和 equipment 是同一个想法,只是把冷链 那套机件拿掉了。

字段类型含义
indexinteger, 必填在餐厅自己那份清单里的显示顺序。
typestring, 必填它是哪一类设备。不受约束的字符串。
namestring, 必填厨房管这台设备叫什么。
minnumber | null可接受的最低温度。
maxnumber | null可接受的最高温度。
areaIdstring | null它所在的那个区域。
isDeletedboolean, 必填应用的停用标记。
{
  "id": "3d90f4c8-62b7-4e13-8a05-ff71d6b9c204",
  "collection": "cooking-equipment",
  "deleted": false,
  "capturedAt": "1789303880115",
  "receivedAt": "1789303887640",
  "sequence": "64",
  "data": {
    "index": 1,
    "type": "oven",
    "name": "Four à sole",
    "min": 63,
    "max": 260,
    "areaId": "9c2b17ae-4d80-4f55-b3e1-0a6c8f4d2b19",
    "isDeleted": false
  }
}

没有 state,没有 outOfRangeCount,也没有传感器:加热设备是由人来读的,所以在 两次查验之间没有任何东西盯着它们。它们的读数是 cooking-temperature-records,那些记录用 cookingEquipmentId 指到这里——一个和冷链那边不同的字段名,也是客户端最常悄无声 息地什么都读不到的地方。

读这张地图

这一页上的所有东西都很小、变得很慢,而且要先有它们,其他任何集合才说得通。

  • 先加载它,然后留住它。 在你去走任何一个记录集合之前,先走 areasequipmentcooking-equipmentsensors,并且从你自己的副本里去解名字和 阈值,而不是每条记录解一次。
  • 但还是要刷新它。 设备会被改名,阈值会被修正,一台冰箱会挪到另一个区域。一份 取过一次、之后再也没走过的副本,会一路跑偏,却从来不会报错。
  • 留 id,不要留副本——对人尤其如此。userId,在渲染的时候再去解名字。见 上面那条关于个人数据的说明。
  • 两个删除标记,到处都要看。 信封上的 deleteddata 里的 isDeleted 是 不同的东西,而基础目录那一页解释了哪个是哪个。

最后更新于 2026-09-20。