## 一、概述

设备与平台之间有两种交互通道，互为补充：

| 通道 | 方向 | 用途 | 适用场景 |
|------|------|------|----------|
| **MQTT** | 平台 → 设备 | 人脸库下发（全量/增量） | 人脸库变更时批量推送、设备弱网/离线回传 |
| **MQTT** | 设备 → 平台 | 通行记录上报 | 设备离线后批量回传通行记录 |
| **HTTP** | 设备 → 平台 | 实时刷脸/刷卡通行 | 闸机类设备实时鉴权、落库 |
| **HTTP** | 设备 → 平台 | 人脸库查询 | 设备拉取指定设备的人脸信息 |

**核心能力说明：**
- 刷脸抓拍图 `snapImage`（base64）：设备随通行记录上报，平台落库并上传 OSS/MinIO。
- 识别相似度 `confidence`（字符串）：设备随通行记录上报，平台落库。
- 设备 `passDirection` 通行方向（整型枚举：1=进，2=出），用于通行通知。
- 消费者 `studentStatus` 在校状态（1=在校，0=离校），随第三方照片同步。
- 临时用户通行控制：`isRestricted` / `restrictionStartTimeStr` / `restrictionEndTimeStr` / `maxAllowedCalls` / `callsMade`（限时 / 限次）。

---

## 二、MQTT 连接信息

设备需连接平台提供的 MQTT Broker（地址、用户名、密码由平台方下发，设备端按自有运行环境配置）。

**设备需要关注的主题：**

| 方向 | 主题 | 说明 |
|------|------|------|
| 平台 → 设备（订阅） | `schoolFaceTopic/all:{设备sn}` | 全量刷新指令 |
| 平台 → 设备（订阅） | `schoolFaceTopic/add:{设备sn}` | 人脸增量新增 |
| 平台 → 设备（订阅） | `schoolFaceTopic/remove:{设备sn}` | 人脸增量移除 |
| 设备 → 平台（发布） | `schoolFaceTopic/accessRecord/{设备sn}` | 通行记录上报 |

> `{设备sn}` = 设备编码 `equipmentCode`，即 HTTP 接口中的 `deviceID`。
> 设备亦可订阅 `schoolFaceTopic/projectAddress/query` 响应以获取项目地址配置，本对接文档不展开。

---

## 三、平台 → 设备：人脸库下发（三个主题）

人脸库变更时，平台通过 MQTT 向设备推送增量（或全量刷新）。**主题前缀固定为 `schoolFaceTopic`，后接动作与设备编码（用 `:` 连接）。**

| 主题 | 触发时机 | 消息体 |
|------|----------|--------|
| `schoolFaceTopic/all:{设备sn}` | 人脸库需要全量刷新（如设备首次接入、强制同步） | `{"RefreshFaces":true}` |
| `schoolFaceTopic/add:{设备sn}` | 有新人脸加入该设备通行范围 | 新增的 `FaceRecognition` 对象数组 |
| `schoolFaceTopic/remove:{设备sn}` | 有人脸移出该设备通行范围 | 移除的 `FaceRecognition` 对象数组 |

### 3.1 全量刷新（all）

平台向 `schoolFaceTopic/all:{设备sn}` 发布：

```json
{"RefreshFaces":true}
```

设备收到后，应主动调用 **`/api/face/getFaceByEquipment`**（见第五章）按设备编码拉取完整人脸列表覆盖本地。

### 3.2 增量新增（add）

平台向 `schoolFaceTopic/add:{设备sn}` 发布 `FaceRecognition` 数组，示例：

```json
[
  {
    "cardCode": "239C704D0100010101FFFFFFFFFFFFFF",
    "equipmentId": 10831,
    "faceUrl": "https://sharedlife-yhtschool.oss-cn-beijing.aliyuncs.com/2025/09/08/3af3209830854cd184a17d138258263e.jpg",
    "id": 164,
    "isTemp": false,
    "loginName": "18849351456",
    "userName": "马浩然"
  }
]
```

### 3.3 增量移除（remove）

平台向 `schoolFaceTopic/remove:{设备sn}` 发布需删除的 `FaceRecognition` 数组，示例：

```json
[
  {
    "cardCode": "239C704D0100010101FFFFFFFFFFFFFF",
    "equipmentId": 10831,
    "faceUrl": "http://sharedlife-yhtschool.oss-cn-beijing.aliyuncs.com/2024/11/14/a78dbff01e6b48b38b31e94252cdb594.png?Expires=999725285900&OSSAccessKeyId=LTAIBVqUQrV6kQax&Signature=ZSDB7MFRVIwvQZ1cSmxensgxvKY%3D",
    "id": 53,
    "isTemp": false,
    "loginName": "18849351456",
    "userName": "马浩然"
  }
]
```

### 3.4 FaceRecognition 字段说明（下发与查询通用）

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Long | 人脸记录主键 |
| `equipmentId` | Long | 设备 ID |
| `loginName` | String | 登录名（学号/手机号） |
| `userName` | String | 姓名 |
| `faceUrl` | String | 人脸照片地址（OSS） |
| `cardCode` | String | 卡号（刷卡识别用） |
| `isTemp` | Boolean | `true`=临时用户，`false`=正式用户 |
| `isRestricted` | Boolean | 是否限时/限次（临时用户） |
| `restrictionStartTimeStr` / `restrictionEndTimeStr` | String | 当日可通行时间段 |
| `maxAllowedCalls` / `callsMade` | Integer | 最大通行次数 / 已通行次数 |

---

## 四、设备 → 平台：通行（刷脸/刷卡）记录上报

### 4.1 方式 A：HTTP 实时接口

跨域已放开（`@CrossOrigin(origins = "*")`），设备可直接调用。

#### （1）刷脸使用 `POST /api/face/useFace`

请求体（`Content-Type: application/json`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceID` | String | 是 | 设备编码 |
| `loginName` | String | 是 | 登录名（刷脸识别结果） |
| `isTemp` | Boolean | 否 | 是否临时用户，默认 `false` |
| `snapImage` | String | 是 | 抓拍人脸 base64（原文落库并上传 OSS/MinIO） |
| `confidence` | String | 是 | 识别相似度（如 `"0.935"` 或 `"95.6"`） |
| `passTime` | String | 否 | 通行时间（设备端识别时间），格式 `yyyy-MM-dd HH:mm:ss`；不传则取服务器接收时间 |

正式用户示例：

```json
{
  "deviceID": "1c54e6124e8f",
  "loginName": "18849351456",
  "isTemp": false,
  "snapImage": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...",
  "confidence": "0.935",
  "passTime": "2026-07-23 10:40:00"
}
```

临时用户示例：

```json
{
  "deviceID": "1c54e6124e8f",
  "loginName": "15210756478",
  "isTemp": true,
  "snapImage": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...",
  "confidence": "0.912",
  "passTime": "2026-07-23 10:42:10"
}
```

> 说明：`useFace` 会做**通行规则校验**。正式用户需命中该设备的任意一条通行规则（消费者状态/学校/校区/院系/专业/班级/楼宇/楼层/房间/储物柜等），否则拒绝；临时用户需在其通行范围、时限、限次内。校验不通过不落库。

#### （2）刷卡使用刷脸设备 `POST /api/face/useFaceByCardCode`

通过**卡编号**在刷脸/通行设备上识别，平台按卡号解析出消费者后再走 `useFace` 逻辑。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceID` | String | 是 | 设备编码 |
| `cardCode` | String | 是 | 卡编号 |

```json
{
  "deviceID": "1c54e6124e8f",
  "cardCode": "239C704D0100010101FFFFFFFFFFFFFF"
}
```

> 平台逻辑：按 `cardCode` 查找消费者 → 设置 `loginName` → 复用 `useFace` 的通行规则校验与落库。找不到用户返回 `未找到用户`。

#### 4.2 方式 B：MQTT 通行记录上报

设备发布到主题 `schoolFaceTopic/accessRecord/{设备sn}`，消息体为 JSON 字符串：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `loginName` | String | 与 `cardCode` 二选一 | 登录名 |
| `isTemp` | Boolean | 否 | 是否临时用户 |
| `cardCode` | String | 与 `loginName` 二选一 | 卡号 |
| `snapImage` | String | 是 | 抓拍人脸 base64 |
| `confidence` | String | 是 | 识别相似度 |
| `passTime` | String | 否 | 通行时间（设备端识别时间），格式 `yyyy-MM-dd HH:mm:ss`；不传则取服务器接收时间 |

登录名方式：

```json
{
  "loginName": "20210001",
  "isTemp": false,
  "snapImage": "/9j/4AAQSkZJRgABAQAAAQABAAD...",
  "confidence": "0.912",
  "passTime": "2026-07-23 10:40:00"
}
```

卡号方式：

```json
{
  "cardCode": "100200300",
  "snapImage": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...",
  "confidence": "95.6",
  "passTime": "2026-07-23 10:41:30"
}
```

> 平台回调中：必须携带 `loginName` 或 `cardCode` 之一，否则丢弃。记录通过 `saveAccessRecordFromMqtt` 直接落库（不做通行规则校验，仅做设备存在性、用户解析、抓拍图上传、安防平台推送）。

### 4.3 返回码对照（HTTP 方式）

| 返回 msg | 含义 |
|----------|------|
| `保存成功！` | 上报成功（可能携带已通行次数） |
| `未找到设备！` | `deviceID` 对应的设备不存在 |
| `未找到用户！` | `loginName` / `cardCode` 查不到人 |
| `未配置通行规则！` | 该设备未配置通行规则，拒绝正式用户 |
| `用户不符合通行规则！` | 正式用户未命中任何规则 |
| `临时用户不在通行范围内！` | 临时用户未授权该设备 |
| `临时用户不在可通行时间内！` | 临时用户超出限时窗口 |
| `临时用户超过通行次数！` | 临时用户已达限次 |
| `卡编号不能为空` | 入参校验失败 |

---

## 五、平台 → 设备：人脸信息查询

### 5.1 按设备获取人脸信息 `POST /api/face/getFaceByEquipment`

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `deviceID` | String | 是 | 设备编码 |
| `isTemp` | Boolean | 否 | 不传=正式+临时；`true`=仅临时；`false`=仅正式 |

```json
{ "deviceID": "1c54e6124e8f" }
```

返回 `FaceRecognition` 数组（字段见 3.4），用于设备端全量刷新本地人脸库。

### 5.2 增量轮询 `GET /api/face/faceChange?code={设备编码}`

返回增量集合，供未使用 MQTT 的设备轮询：

```json
{
  "add": [ ...FaceRecognition数组... ],
  "remove": [ ...FaceRecognition数组... ]
}
```

### 5.3 查询 24 小时变动记录 `GET /api/face/getFaceChangeRecords?deviceID={设备编码}`

返回过去 24 小时该设备的 `ADD` / `REMOVE` 变动明细：

```json
{
  "addList": [ ... ],
  "removeList": [ ... ]
}
```
