# 清洁

> 清洁计划、真正做过的那些任务，以及证明它做过的照片：三个集合，以及一条缺失的记录证明不了什么。

Source: https://developers.backresto.com/zh/docs/cleaning

应用里的清洁，是一份计划加上它的凭证。餐厅写下要清洁什么、在哪里、多久一次；员工
做完一项就勾掉一项；其中有些任务要求拍一张照片。三个集合，每一层一个，而它们之间
的连接才是大部分工作所在。

| 集合 | 权限范围 | 一条记录是什么 |
| --- | --- | --- |
| `cleaning-tasks` | `cleaning-tasks:read` | 计划里的一行：必须清洁什么、在哪里、多久一次、是否需要一张照片。 |
| `cleaning-task-records` | `cleaning-task-records:read` | 一项确实做过的任务：什么时候做的，谁做的。 |
| `cleaning-task-pictures` | `cleaning-task-pictures:read` | 证明它做过的那张照片。带文件。 |

这三个共用同一个[快照信封](https://developers.backresto.com/zh/docs/collections)：`id`、`deleted`、`capturedAt`、
`receivedAt`、`sequence` 以及其余字段，都摆在下文描述的 `data` 旁边。

## `cleaning-tasks`

计划本身。它们很少变（一家餐厅把计划写一次，等厨房变了再改它），所以这是你遍历得
最少、缓存得最久的那个集合。

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `index` | integer, **必填** | 这项任务在计划中的位置，按应用给它们排的顺序。 |
| `name` | string, **必填** | 必须清洁的是什么，用这家餐厅自己的说法。 |
| `description` | string \| null | 更长的说明，在有人写了的时候。 |
| `areaId` | string \| null | 它所属的那个[区域](https://developers.backresto.com/zh/docs/facilities)。在一家不把自己划分成区域的餐厅里为 `null`。 |
| `recurrence` | integer, **必填** | 这项任务多久轮到一次，就是一个普通整数。 |
| `requirePhotoProof` | boolean \| null | 任务要求拍照时为 `true`。`null` 表示应用从来没设置过它。 |
| `isDeleted` | boolean, **必填** | 应用自己的软删除。请读下面那条说明。 |

```json
{
  "id": "c9a4f7e2-1b83-4d05-a6f1-90e3b7c4d218",
  "collection": "cleaning-tasks",
  "deleted": false,
  "capturedAt": "1789601204773",
  "receivedAt": "1789601399042",
  "sequence": "1",
  "data": {
    "index": 3,
    "name": "Nettoyage de la trancheuse",
    "description": "Démonter la lame, dégraisser, désinfecter.",
    "areaId": "7d2c0b61-4e8a-49f3-9c27-5a1b8e60d3f4",
    "recurrence": 1,
    "requirePhotoProof": true,
    "isDeleted": false
  }
}
```

**`recurrence` 不带单位。** 它是一个整数，而记录并没有说它数的是什么。请照应用
显示它的样子显示它，而不要靠猜渲染出一句「每 1 天」。

**`isDeleted` 不是信封上的 `deleted`。** 一项被餐厅从计划里移走的任务，回来时是
`isDeleted: true` 且 `deleted: false`：它仍然是一条活着的记录，描述的是一行已经不
再适用的内容。两个都要筛，并且把被删掉的那些留着。老记录仍然指向它们。

## `cleaning-task-records`

一项做过的任务。有意做得很小：哪项任务、什么时候、谁。

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `timestamp` | string, **必填** | 这项任务是什么时候做的，以字符串表示的 Unix 毫秒时间戳。 |
| `cleaningTaskId` | string, **必填** | 它所满足的那条 `cleaning-tasks` 记录。 |
| `userId` | string \| null | 做这件事的那名[员工](https://developers.backresto.com/zh/docs/facilities)，在应用记下了一个的时候。 |

```json
{
  "id": "5f8b3d16-2c90-4a77-b4e8-31d0a9c65f7b",
  "collection": "cleaning-task-records",
  "deleted": false,
  "capturedAt": "1789775012558",
  "receivedAt": "1789775204910",
  "sequence": "2",
  "data": {
    "timestamp": "1789774800000",
    "cleaningTaskId": "c9a4f7e2-1b83-4d05-a6f1-90e3b7c4d218",
    "userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
  }
}
```

`cleaningTaskId` 是必填的，所以每条记录都有一项任务。反过来并不成立：一项任务可以
在计划里待上几周而没有任何记录对着它，而那正是一项每周任务在星期二时的正常状态。

## `cleaning-task-pictures`

那张照片。它带着一个文件，这让它成为这里唯一一个你没法只靠 records 端点就消费完
的集合。

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `timestamp` | string, **必填** | 这张照片是什么时候拍的，以字符串表示的 Unix 毫秒时间戳。 |
| `cleaningTaskRecordId` | string \| null | 它所证明的那条 `cleaning-task-records` 记录。可以为 null。见下文。 |
| `part` | number \| null | 没有文档说明，而且是一个小数，不是一个计数器。请原样带着它，而不要去解读它，就像[追溯标签](https://developers.backresto.com/zh/docs/traceability-labels)上的那条提醒一样。 |
| `groupId` | string \| null | 把那些照片绑在一起。共用一个 `groupId` 的记录属于同一份凭证。 |
| `asset` | object, **必填** | 点出那个文件：`objectKey` 和 `status` 始终都有，一旦上传完成，还会有 `contentType`、`byteLength` 和 `sha256`。 |

```json
{
  "id": "2d71c8a9-6e34-4b0f-8517-ac93e25d0b46",
  "collection": "cleaning-task-pictures",
  "deleted": false,
  "capturedAt": "1789775118330",
  "receivedAt": "1789775301774",
  "sequence": "3",
  "data": {
    "timestamp": "1789774860000",
    "cleaningTaskRecordId": "5f8b3d16-2c90-4a77-b4e8-31d0a9c65f7b",
    "part": 1,
    "groupId": "a03e5c88-71bd-4f92-b6d4-2e8f10c73a5b",
    "asset": {
      "objectKey": "cleaning-task-pictures/2d71c8a9/1.jpg",
      "status": "uploaded",
      "contentType": "image/jpeg",
      "byteLength": 812043,
      "sha256": "9f2a…"
    }
  }
}
```

字节内容要再调一次，针对这条记录自己的 id：

```http
GET /v1/restaurants/{restaurantId}/collections/cleaning-task-pictures/records/{recordId}/assets
```

它返回一组签名 URL，每个有效**十五分钟**，每个都自带授权，所以下载不需要任何请求
头。现在就把字节取下来，存字节而不要存链接，需要一个新 URL 的时候再调一次这个端
点。那些规则（什么会出现、`sha256` 是做什么用的、为什么 `contentType` 值得一读）
都在[集合那一页](https://developers.backresto.com/zh/docs/collections)上，而且在这里是一样的。

**`asset.status` 告诉你有没有字节可取。** 它是 `pending` 或 `uploaded`。一个
`pending` 的附件，是设备已经宣告、却还没送完的一张照片：记录是真的，文件还不是，
而 assets 端点作答时不会带上它。在你把一个空的附件列表当成一张从未拍过的照片之
前，先读这个状态。

**`cleaningTaskRecordId` 可以为 null。** 一张照片可以在不指向任何记录的情况下存
在，所以一个假定它总是在那儿的连接，会把照片丢在地上。把它们单独计数，而不要悄无
声息地扔掉。

## 把三个连起来

你几乎肯定想要的那个结构，是每做过一项任务一行，带上它在计划里的那一条和它的那些
照片：

- `cleaning-task-records.cleaningTaskId` → `cleaning-tasks.id`
- `cleaning-task-pictures.cleaningTaskRecordId` → `cleaning-task-records.id`

三次遍历，在你这边做连接。没有哪个端点会替你做这件事，也没有按任务或按日期的过
滤。你把每个集合整个走完，然后对账，就像[信封那一页](https://developers.backresto.com/zh/docs/collections)描述的
那样。

报表用 `capturedAt`，同步用 `receivedAt`：一台放在冷库里的平板会在找到信号时才上
传，而一张 09:00 拍下的照片可能 14:00 才到，比它所属的那条记录晚，也可能比它早。

## 一条缺失的记录证明不了什么

**计划里一条没有对应记录的条目，并不能证明这项任务被跳过了。** 它可能是在一台还没
上传的设备上做的，也可能是在为那家餐厅开启采集之前做的。一项标着
`requirePhotoProof: true` 却没有照片对着它的任务也一样：那张照片可能正在上传途中，
而一个还没上传完的附件是缺席，而不是坏掉。

所以不要拿这个 API 印出一个清洁完成率，然后把它叫作合规。你能诚实地说出来的，是
什么东西到了、以及什么时候到的。在你把一个数字摆到任何人面前之前，先看
[关于完整性的说明](https://developers.backresto.com/zh/docs/support)。
