场所与人员
这家门店、它的员工、它的区域和它的机器——每一次温度读数都挂在上面的那张固定地图。
这些是不会动的东西:餐厅本身、在里面工作的人、它被划分成的那些区域,以及那些把 食物保持在某个温度上的机器。它们没有一个是一次查验。
但它们仍然决定了那些查验意味着什么。一个温度就是一个数字, 不带任何判定,直到你从它所测的那台设备上读出阈值;而一次读数之所以属于这栋建筑的 某一部分,只是因为那台设备说了它站在哪个区域。把这一页弄错了,下游的每一个数字都 会悄无声息地错掉。
| 集合 | 权限范围 | 一条记录是什么 |
|---|---|---|
restaurants | restaurants:read | 门店本身:名称、地址、休息日、设置。 |
users | users:read | 一个记录查验的员工账号。 |
areas | areas:read | 门店被划分成的一个区域——厨房、冷库、吧台。 |
equipment | equipment:read | 一台冰箱、一台冷冻柜、一台冷链设备,带着它的阈值。 |
sensors | sensors:read | 一个无线探头,按 MAC 地址标识。 |
cooking-equipment | cooking-equipment:read | 一台烤箱或其他加热设备,带着它的阈值。 |
这六个共用同一个快照信封:id、deleted、capturedAt、
receivedAt、sequence 以及其余字段,都摆在下文描述的 data 旁边。
restaurants
你的密钥所限定的那家门店,以记录的形式呈现。每家餐厅一条,所以这个集合通常就只有 一页。
| 字段 | 类型 | 含义 |
|---|---|---|
name | string, 必填 | 门店的名称,就是应用里显示的那个。 |
address | string, 必填 | 邮寄地址,作为一个字符串。 |
closingDays | string[], 必填 | 门店歇业的那些日子。普通字符串,schema 不对它们做约束。 |
exceptionalClosures | object[] | 一次性的歇业,每一条是一个带 from 和 to 的对象。 |
detailedAddress | object | null | 拆成各部分的地址,在应用是那样存着的时候。 |
deliveryAddress | string | null | 货物送到哪里,在它与 address 不同的时候。 |
deliveryDetailedAddress | object | null | 同一个东西,拆成各部分。 |
preferredLanguage | string | null | 这家餐厅工作时用的语言。 |
multiArea | boolean | null | 这家门店是否被分成不止一个区域。 |
settings | object | 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
}
}
这个示例只展示了运营的那一半。在它旁边,记录还带着商务的那一半:subscription、
subscriptions、companyName、billingAddress、billingEmail、tvaNumber、
trialEndDate、customerId、discount。它们确实存在,它们在 OpenAPI 文档里,
而这一页不会带着你一个个讲它们。
读运营那些字段,别碰商务那些。 它们描述的是这家餐厅与我们之间的合同,不是它 的厨房。它们不是一套计费 API,它们改变的理由与食品安全毫无关系,而一个把客户的 订阅状态回显给客户看的集成,是一张等着被开出来的工单。
closingDays 是一个普通字符串的列表。 schema 没有给它们任何形状,所以读它们、
显示它们就好;不要对它们做分支,也不要假定某一种写法或大小写。
users
那些记录查验的员工账号。一条温度记录的 userId 指向的就是
这里。
| 字段 | 类型 | 含义 |
|---|---|---|
firstName | string, 必填 | 名,按录入的样子。 |
lastName | string, 必填 | 姓,按录入的样子。 |
email | string, 必填 | 这个账号的邮箱地址。 |
phoneNumber | string | null | 一个联系电话,在给了一个的时候。 |
modules | string[] | 这个人用的是应用的哪些部分。自由字符串。 |
preferredNotificationTime | string | null | 这个人要求在什么时候被提醒。 |
isChildren | boolean | null | 一个内部标记。schema 没有赋予它任何含义;不要在它上面搭东西。 |
isDeleted | boolean | 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
}
}
这是个人数据。 email、phoneNumber 以及那两个姓名字段能识别出一个真实的
人,而它们出现在这个 API 里只有一个理由:一条食品安全记录必须说清是谁做的那次
查验。这是它们在这里唯一的职责。
所以请存 id,在需要一个显示名的时候再去解它。不要因为联系方式恰好出现在载荷
里,就把它们抄进你自己的表、你的日志、你的分析事件或者某个第三方工具。餐厅才是
他们员工数据的控制者,而你每做一份副本,就是他们从此要为之负责的一份。
isDeleted 在这个集合上可以为 null,而在别处它是必填的,所以请把一个缺失的标记
当作「未停用」,而不要让它不小心以某种类假值的方式漏过去。它与信封上的 deleted
之间的区别,在基础目录那一页上有解释。
areas
餐厅的一个区域。两个字段,而它的重要性远超两个字段所暗示的。
| 字段 | 类型 | 含义 |
|---|---|---|
name | string, 必填 | 餐厅给这个区域起的名字——厨房、冷库、吧台。 |
isDeleted | boolean, 必填 | 应用的停用标记。见基础目录那一页。 |
{
"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
冰箱、冷冻柜和其他冷链设备。决定一个温度是否可接受的,正是这条记录。
| 字段 | 类型 | 含义 |
|---|---|---|
index | integer, 必填 | 餐厅在自己那份清单里给它的位置。显示顺序,不是一个标识符。 |
type | string, 必填 | 它是哪一类设备。一个不受约束的字符串——读它,不要对它做分支。 |
name | string, 必填 | 厨房管这台设备叫什么。 |
min | number | null | 可接受的最低温度。 |
max | number | null | 可接受的最高温度。 |
areaId | string | null | 它所在的那个区域。 |
equipmentSensorId | string | null | 装在它上面的那个传感器,在有一个的时候。 |
state | string | null | 这台设备当前的状态,一个不受约束的字符串。 |
outOfRangeCount | number | null | 应用维护的一个超出范围读数的计数。 |
isDeleted | boolean, 必填 | 应用的停用标记。 |
{
"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
}
}
min 和 max 是你能拿到的唯一判定。 一条温度记录带着
一个值、一个时间和它所测的那台设备——没有任何东西说它是不是没问题。-14.4 °C 在
冰箱里是一场危机,在冷冻柜里则是一个平常的星期二,而把两者分开的正是这条记录。你
搭出来的每一个合规数字,都要连接到这里。
它们同样可以为 null。当 min 或 max 缺席时,你就没有阈值,而对那次读数诚实的
回答是「未知」,不是「在范围内」。请在你的界面里这么说,而不是默认给一个绿色。
阈值是当前的那一套,不是当时适用的那一套。 快照给你的是今天的 min 和
max;三月的一次读数,当时是拿三月设的那套值来判断的,而餐厅随时都可以改它们。
如果你拿今天的数字去重新评估旧的读数——而这也是这个 API 唯一允许你做的事——请把
结果标注成你的重算结果,而不是厨房当时看到的东西。
type 和 state 是不受约束的字符串。 在 schema 里两者都只是单个的自由文本
字段,背后没有任何清单。把它们显示出来,非要分组就分组,但绝不要让一个不认识的值
从一个 switch 里漏过去、掉进沉默。
outOfRangeCount 是应用的数字,不是你的。 schema 没有说它覆盖的是哪个时间
窗,而你也没法从这里的快照把它重建出来。把它当作来自应用的一个信号;如果你需要一
个自己站得住脚的计数,就从那些读数和阈值算出你自己的,并且标明它是你的。
equipmentSensorId 从设备指向它的传感器。传感器记录则反过来指回来。两边都可以为
null,而且不会因为一边填了,另一边就保证也填了。
sensors
那些无线探头。两个字段,都是可选的。
| 字段 | 类型 | 含义 |
|---|---|---|
macAddress | string | null | 这个探头的硬件地址——你靠它把一台物理设备和另一台区分开。 |
sensorEquipmentId | string | 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 是同一个想法,只是把冷链
那套机件拿掉了。
| 字段 | 类型 | 含义 |
|---|---|---|
index | integer, 必填 | 在餐厅自己那份清单里的显示顺序。 |
type | string, 必填 | 它是哪一类设备。不受约束的字符串。 |
name | string, 必填 | 厨房管这台设备叫什么。 |
min | number | null | 可接受的最低温度。 |
max | number | null | 可接受的最高温度。 |
areaId | string | null | 它所在的那个区域。 |
isDeleted | boolean, 必填 | 应用的停用标记。 |
{
"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 指到这里——一个和冷链那边不同的字段名,也是客户端最常悄无声
息地什么都读不到的地方。
读这张地图
这一页上的所有东西都很小、变得很慢,而且要先有它们,其他任何集合才说得通。
- 先加载它,然后留住它。 在你去走任何一个记录集合之前,先走
areas、equipment、cooking-equipment和sensors,并且从你自己的副本里去解名字和 阈值,而不是每条记录解一次。 - 但还是要刷新它。 设备会被改名,阈值会被修正,一台冰箱会挪到另一个区域。一份 取过一次、之后再也没走过的副本,会一路跑偏,却从来不会报错。
- 留 id,不要留副本——对人尤其如此。 存
userId,在渲染的时候再去解名字。见 上面那条关于个人数据的说明。 - 两个删除标记,到处都要看。 信封上的
deleted和data里的isDeleted是 不同的东西,而基础目录那一页解释了哪个是哪个。