冷却与食品处理流程
冷却、冷冻、复热与运输——记录一次温度转变的四个集合,以及一次没做完的转变长什么样。
一道慢慢冷下来的热菜,会在细菌繁殖的那个温度区间里待上好几个小时,所以规矩是把 它快速降下来,并且能拿得出证据说你做了。有四个集合记录这类转变。其中三个是时间 上的转变——降下来、再降下去、又升回去——第四个则是同一个想法,发生在空间上。
| 集合 | 权限范围 | 一条记录是什么 |
|---|---|---|
cooling | cooling:read | 一件被降到冷藏温度的热产品。 |
freezing | freezing:read | 一件被降到冷冻状态的产品。 |
reheating | reheating:read | 一件被升回供餐温度的冷产品。 |
transport | transport:read | 一件从一处被运到另一处的产品,带着两端各自的温度。 |
这四个共用同一个快照信封:id、deleted、capturedAt、
receivedAt、sequence 以及其余字段,都摆在下文描述的 data 旁边。
cooling
一次循环:一件产品,什么时候开始、当时有多热,什么时候结束、当时有多冷。
| 字段 | 类型 | 含义 |
|---|---|---|
timestamp | string, 必填 | 这条记录自己的时间戳,以字符串表示的 Unix 毫秒时间戳。不是这次循环的时钟——那是下面那两个日期。 |
product | string, 必填 | 被冷却的是什么,以文本记录。 |
beginDate | string, 必填 | 这次循环什么时候开始,以字符串表示的 Unix 毫秒时间戳。 |
beginTemperature | number, 必填 | 它开始时的温度。 |
endDate | string | null | 它什么时候结束。还没结束时为 null。 |
endTemperature | number | null | 它结束时的温度。与 endDate 一同为 null。 |
reminderId | string | null | 应用挂在这次循环上的那个提醒,在有一个的时候。 |
userId | string | null | 记录它的那名员工,在应用记下了一个的时候。 |
{
"id": "0e5d1a7c-9f24-4b13-8d60-c1a7f0b25e94",
"collection": "cooling",
"deleted": false,
"capturedAt": "1789779612441",
"receivedAt": "1789779910228",
"sequence": "1",
"data": {
"timestamp": "1789772400000",
"product": "Blanquette de veau",
"beginDate": "1789772400000",
"beginTemperature": 63.5,
"endDate": "1789779600000",
"endTemperature": 8.2,
"userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
}
}
product 是一个名字,不是一个引用。 这些记录上没有 productId,所以你没法在
不做字符串匹配的情况下把它们连到 products 上——而这些字符串是在营业中、用这家
餐厅的语言打出来的。
reminderId 是不透明的。 这个 API 里没有任何集合能解开它。如果它有助于你把
记录分组,就带着它;但别指望能查到它指向什么。
freezing
同样的结构,用于一件被降到冷冻、而不是降到冷藏温度的产品。逐字段看,它就是
cooling。
| 字段 | 类型 | 含义 |
|---|---|---|
timestamp | string, 必填 | 这条记录自己的时间戳。 |
product | string, 必填 | 被冷冻的是什么。 |
beginDate | string, 必填 | 这次操作什么时候开始。 |
beginTemperature | number, 必填 | 它开始时的温度。 |
endDate | string | null | 它什么时候结束,或者 null。 |
endTemperature | number | null | 它结束时的温度,或者 null。 |
reminderId | string | null | 同上。 |
userId | string | null | 是谁记录的。 |
记录里没有任何东西说它是这三者中的哪一个:那取决于它来自哪个集合,而信封的
collection 字段会把它复述出来。一条把三者并进同一张表的流水线,必须把那一列
留住。
reheating
一件被升回来的冷产品。结构还是那个,只是读的方向反过来:beginTemperature 是低
的那个,endTemperature 是高的那个。
| 字段 | 类型 | 含义 |
|---|---|---|
timestamp | string, 必填 | 这条记录自己的时间戳。 |
product | string, 必填 | 被复热的是什么。 |
beginDate | string, 必填 | 它什么时候进去的。 |
beginTemperature | number, 必填 | 它开始时的温度。 |
endDate | string | null | 它什么时候出来的,或者 null。 |
endTemperature | number | null | 它达到的温度,或者 null。 |
reminderId | string | null | 同上。 |
userId | string | null | 是谁记录的。 |
一次没有结尾的操作长这样:
{
"id": "b4c8e21d-3a76-4f58-9c02-77ad5e1b8f30",
"collection": "reheating",
"deleted": false,
"capturedAt": "1789783221907",
"receivedAt": "1789783402115",
"sequence": "2",
"data": {
"timestamp": "1789783200000",
"product": "Gratin dauphinois",
"beginDate": "1789783200000",
"beginTemperature": 4.1,
"endDate": null,
"endTemperature": null,
"userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
}
}
transport
同一种转变,发生在空间而不是时间上:产品从哪里出发、到达了哪里,两端各有一个 温度。
| 字段 | 类型 | 含义 |
|---|---|---|
timestamp | string, 必填 | 这条记录自己的时间戳。 |
product | string, 必填 | 被运输的是什么,以文本记录。 |
departureLocation | string, 必填 | 它从哪里出发,以文本记录。 |
departureTime | string, 必填 | 它什么时候出发,以字符串表示的 Unix 毫秒时间戳。 |
departureTemperature | number, 必填 | 出发时的温度。 |
arrivalLocation | string | null | 它到达了哪里。还没到达时为 null。 |
arrivalTime | string | null | 它什么时候到达,或者 null。 |
arrivalTemperature | number | null | 到达时的温度,或者 null。 |
reminderId | string | null | 同上。 |
userId | string | null | 是谁记录的。 |
{
"id": "f31b6a08-5c47-4de9-b1a5-6e0c9d247b82",
"collection": "transport",
"deleted": false,
"capturedAt": "1789786944610",
"receivedAt": "1789787101883",
"sequence": "3",
"data": {
"timestamp": "1789785000000",
"product": "Plats préparés — service traiteur",
"departureLocation": "Cuisine centrale",
"departureTime": "1789785000000",
"departureTemperature": 3.4,
"arrivalLocation": "Salle Montparnasse",
"arrivalTime": "1789786800000",
"arrivalTemperature": 5.9,
"userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
}
}
这两个地点是自由文本,既不是区域,也不是地址。它们就是那个人 打进去的东西;而到达那一端的可选程度,和到达温度完全一样:一条记录可以只带着一次 出发,别的什么都没有。
拿它们做事
一个为 null 的结尾,是两件不同的事。 endDate 和 endTemperature——以及
transport 上的 arrivalTime、arrivalTemperature 和 arrivalLocation——都可以
为 null,而记录不会告诉你是为什么。可能是一次在你读它的时候仍在进行的操作,也可能
是一次从来没有人去收尾的操作。两者看起来一模一样。
这件事在你要算时长的那一刻就变得要紧。这些日期是字符串,所以
endDate - beginDate 在一条已结束的记录上减得好好的,而在一条未结束的记录上,它
会把 null 强制转成零,然后交给你一个很大的负数,而不是一个错误。没有任何东西
抛出异常;你的
平均值只是错的。先筛出已结束的记录,把未结束的单独计数,并且自己决定一条未结束的
记录到了多久就不再算进行中、而开始算被丢下了——这个 API 里没有任何东西会替你做
这个判断。
用 beginDate 和 endDate 来算,而不是 timestamp。 这两个日期是这次操作的
时钟。timestamp 是这条记录自己的时钟,而一段时长不是用它做出来的。
按 capturedAt 排序,而不是 receivedAt。 一台放在冷库里的平板会在找到信号时
才上传,所以一次 09:00 开始的循环可能 14:00 才到。
信封那一页讲了完整的来龙去脉。
缺席证明不了什么。 一件没有冷却记录的产品,可能从来没被冷却过,也可能正躺在 一台还没上传的设备里。不要把这类记录的计数变成一个合规数字——见 那条提醒。