收货记录
收货查验和收货时拍下的照片:两个集合、每一个字段,以及它们在后厨现场意味着什么。
一条收货记录就是一次收货查验:一名员工接收一批订货、记录温度,并决定是否接收 它。这就是食品安全检查员会索取的那份记录。收货时拍下的照片是它旁边的第二个 集合。
| 集合 | 权限范围 | 一条记录是什么 |
|---|---|---|
deliveries | deliveries:read | 一次收货查验,每个产品带一条温度读数。 |
delivery-pictures | delivery-pictures:read | 在一次收货中拍下的一张照片,指回那次收货。 |
两者都是普通的集合:data 外面是同一个快照信封,同样的端点、
同样的游标、同样的 90 天规则。它们带着这两个集合开通时应用里仍保有的收货历史,
而不只是那之后记下的内容。
deliveries
| 字段 | 类型 | 含义 |
|---|---|---|
timestamp | string, 必填 | 查验发生的时间,以字符串表示的 Unix 毫秒时间戳。 |
isCompliant | boolean, 必填 | 员工对整批收货的判定。 |
supplierDeliveriesId | string | null | 货物来自的那家供应商:一条 suppliers 记录的 id。 |
temperatureRecords | array | null | 每个被测量的产品一条。见下文。 |
nonComplianceReasons | string[] | null | 这批货被拒收、或被有保留地接收的原因。由员工自己写的自由文本;合规时为空。 |
correctiveActions | string[] | null | 为此做了什么。不需要做什么时为空。 |
commentary | string | null | 自由文本备注。常常是 null。 |
userId | string | null | 收下这批货的那名员工,在应用记下了一个的时候。 |
temperatureRecords[]
| 字段 | 类型 | 含义 |
|---|---|---|
product | string | 测量的是什么,按员工给它起的名字。绝不为空。 |
lotNumber | string | null | 批号或批次,在它被采集到的时候。 |
value | number | null | 读数。产品在没有测量的情况下被登记时为 null。 |
unit | "C" | 始终是摄氏度。见下文。 |
温度始终是摄氏度。 一家应用以华氏度记录的餐厅,发来的仍是华氏度读数,它们在
到达时、存储之前就被换算。每一条的 unit 都是 "C",所以使用方永远不必换算,也
不必去猜。
{
"id": "8f2c1b04-0d5a-4b7e-9f31-6ad2c0e77a51",
"collection": "deliveries",
"deleted": false,
"capturedAt": "1787932805120",
"receivedAt": "1787932911004",
"sequence": "88",
"data": {
"timestamp": "1787932800000",
"isCompliant": false,
"supplierDeliveriesId": "supplier-7",
"temperatureRecords": [
{ "product": "Poulet fermier", "lotNumber": "L2291", "value": 6.4, "unit": "C" },
{ "product": "Farine T55", "lotNumber": null, "value": null, "unit": "C" }
],
"nonComplianceReasons": ["Température trop élevée"],
"correctiveActions": ["Produit refusé", "Fournisseur prévenu"],
"commentary": "Camion en retard, rupture de chaîne du froid probable",
"userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
}
}
delivery-pictures
一张标签、一个破损的托盘、一支抵着展示柜的探针:这些照片就是 isCompliant 背后的
证据。每一张都是一条指向其收货记录的记录,而且它带着一个文件,所以字节内容来自
assets 端点。
| 字段 | 类型 | 含义 |
|---|---|---|
timestamp | string, 必填 | 这张照片是什么时候拍的,以字符串表示的 Unix 毫秒时间戳。 |
deliveryId | string | null | 它所属的那条 deliveries 记录。 |
part | number | null | 应用把一张照片分成几部分存储时,这条记录在其中的位置。请原样带着它,而不要去解读它。 |
groupId | string | null | 把那几部分绑在一起。共用一个 groupId 的记录是同一张照片。 |
asset | object, 必填 | 点出那个文件:objectKey 和 status 始终都有,一旦上传完成,还会有 contentType、byteLength 和 sha256。 |
{
"id": "c61e0a7d-35b2-4f89-a1d4-7e90b2c45f18",
"collection": "delivery-pictures",
"deleted": false,
"capturedAt": "1787932861240",
"receivedAt": "1787932990517",
"sequence": "143",
"data": {
"timestamp": "1787932860000",
"deliveryId": "8f2c1b04-0d5a-4b7e-9f31-6ad2c0e77a51",
"part": 4,
"groupId": "b4d81f0a-c2e7-4a55-9d10-6f3e8a2b7c91",
"asset": {
"objectKey": "delivery-pictures/b4d81f0a/4.jpg",
"status": "uploaded",
"contentType": "image/jpeg",
"byteLength": 902450,
"sha256": "9f2a…"
}
}
}
字节内容要再调一次,针对这条记录自己的 id:
GET /v1/restaurants/{restaurantId}/collections/delivery-pictures/records/{recordId}/assets
它返回一组有效期十五分钟的签名 URL,与集合那一页上描述的完全 一样。对一张照片的任何一部分发起请求,你拿到的都是同一个文件:API 会替你解析这个 分组,所以你不需要弄清楚文件在哪一部分里。
要展示一次收货的照片,就遍历 delivery-pictures,留下 deliveryId 等于那条收货
记录 id 的记录,每个 groupId 留一条。
正确地读它
isCompliant 是记录,不是计算结果。 它是收货的那个人做出的决定,可能与你只看
温度会得出的结论不一致:一次用表面探针取得的读数、一个有自己阈值的产品、一次
临场判断。请把它呈现为他们的判定。如果你想要自己的判定,把它算在他们的旁边,并
标明那是你的。
自由文本字段就是自由文本。 nonComplianceReasons、correctiveActions 和
product 是员工在营业中打出来的,用的是这家餐厅的语言,带着一个忙碌周二的拼写。
不要拿它们建枚举,不要用它们做连接键,也不要假定它们是法语。
value 可以是 null。 一个产品被登记却没有读数是正常的。它往往是干货。把
null 当作「未测量」,绝不要当作 0。
时间戳是字符串。 timestamp 是 Unix 毫秒时间戳的十进制字符串,因为整个
BackResto 平台就是这样存时间戳的。Number(timestamp) 今天是安全的,往后几十万年
也是安全的,但不要让一个 JSON 解析器悄悄把它转成数字,然后再以丢了精度的样子写
回去。
一张照片可能晚于它的收货记录到达。 手机在能上传的时候才上传照片,有时是在查验 本身之后几分钟。一条暂时还没有照片的收货记录,可能只是还有一张在路上;在断定它 没有照片之前,晚些再遍历一次。