{
  "openapi": "3.0.3",
  "info": {
    "title": "智慧营养健康餐厅 - 设备端人脸学习",
    "version": "2026-10-10"
  },
  "paths": {
    "/p/api/facedata": {
      "post": {
        "summary": "人脸数据同步",
        "description": "## 接口概述\n\n设备拉取人脸数据。张家港采用设备端学习模式时，设备每 2 分钟调用一次全量接口：有 `face_token` 的记录直接保存；没有 `face_token` 的记录先下载照片、在设备端学习，再调用学习结果回传接口。\n\n本期仅调整全量拉取；`type=0` 的既有增量消息队列逻辑未改造，不能把它当作本期增量方案。\n\n## 接口信息\n\n| 项目 | 内容 |\n| --- | --- |\n| 接口地址 | `/p/api/facedata` |\n| 请求方法 | POST |\n| Content-Type | `application/x-www-form-urlencoded` |\n| 接口版本 | 当前张家港分支；接口路径没有独立版本号 |\n\n## 请求头（Header）\n\n| 参数名 | 是否必填 | 说明 |\n| --- | --- | --- |\n| `Content-Type` | 是 | `application/x-www-form-urlencoded` |\n| `Access-Token` | 否 | Apifox 历史接口保留的可选头；当前方法未校验此头，不能将其视为设备认证。 |\n\n## 请求参数（Body）\n\n| 参数名 | 类型 | 是否必填 | 描述 | 示例值 |\n| --- | --- | --- | --- | --- |\n| `code` | String | 是 | 已登记且未删除的设备编码 | `FACE-DEVICE-001` |\n| `type` | Integer | 否 | `1` 全量；`0` 增量；默认 `0`，其他值按 `0` 处理 | `1` |\n| `staff_uuid` | String | 否 | 指定人员 UUID；全量时仅返回此人员 | `11111111-2222-3333-4444-555555555555` |\n\n全量请求示例：`code=FACE-DEVICE-001&type=1`。\n\n## 响应格式\n\n全量查询成功，设备端学习模式；`feature` 和 `face_token` 为空表示尚未学习：\n\n```json\n{\n  \"code\": 0,\n  \"message\": \"查询成功\",\n  \"data\": {\n    \"face_data\": [\n      {\n        \"id\": 123,\n        \"staff_uuid\": \"11111111-2222-3333-4444-555555555555\",\n        \"head_img_url\": \"http://example.invalid/static/upload/face/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jpg\",\n        \"feature\": \"\",\n        \"face_token\": \"\",\n        \"name\": \"示例人员\",\n        \"hardware_uuid\": \"66666666-7777-8888-9999-000000000000\",\n        \"status\": 1\n      }\n    ]\n  },\n  \"face_learning_mode\": 2,\n  \"trace_id\": \"<本次请求追踪ID>\"\n}\n```\n\n服务端学习模式成功响应的 `face_learning_mode` 为 `1`，全量只返回已有非空 `feature` 的有效照片。无匹配人员时 `face_data` 可为空数组。\n\n查询失败示例（未登记设备）：\n\n```json\n{\"code\":1,\"message\":\"设备不存在\",\"data\":null,\"trace_id\":\"<本次请求追踪ID>\"}\n```\n\n缺少 `code` 时返回 `{\"code\":1,\"message\":\"参数错误\",\"data\":null,\"trace_id\":\"<本次请求追踪ID>\"}`。业务失败响应不保证带 `face_learning_mode`，但同样带 `trace_id`。\n\n## 状态码说明\n\n| HTTP 状态 | `code` | 描述 | 设备处理 |\n| --- | --- | --- | --- |\n| 通常 200 | `0` | 查询成功 | 读取最外层 `face_learning_mode`，再处理 `data.face_data`。 |\n| 通常 200 | `1` | 参数错误、设备不存在等业务失败 | 不更新本地人脸库；排查参数和设备登记后重试。 |\n\n`face_learning_mode=1` 表示服务端学习，`2` 表示设备端学习。设备端模式只有 `face_token` 为空时才需要学习；学习成功后回传。全量拉取会把该设备待处理的人脸消息标记已处理，并更新设备同步时间。HTTP 200 不等于业务成功。\n\n## 响应字段说明\n\n| 字段名 | 类型 | 描述 |\n| --- | --- | --- |\n| `code` | Integer | 顶层业务码，`0` 成功，`1` 失败。 |\n| `message` | String | 查询结果或错误原因。 |\n| `data` | Object / Null | 成功时包含 `face_data`；失败时为 `null`。 |\n| `data.face_data` | Array | 有有效照片且人员未删除的人脸列表。 |\n| `data.face_data[].id` | Integer | `ydy_staff.id`，回传时作为 `staff_id`。 |\n| `data.face_data[].staff_uuid` | String | 人员 UUID。 |\n| `data.face_data[].head_img_url` | String | 完整照片 URL；回传时原样传入。 |\n| `data.face_data[].feature` | String | 人脸特征值；设备端未学习时可为空。 |\n| `data.face_data[].face_token` | String | 对应 `baidu_face_token`；设备端未学习时可为空。 |\n| `data.face_data[].name` | String | 人员姓名。 |\n| `data.face_data[].hardware_uuid` | String | 人脸设备关联 UUID。 |\n| `data.face_data[].status` | Integer | 全量有效人脸当前为 `1`。 |\n| `face_learning_mode` | Integer | 仅成功响应保证返回；`1` 服务端学习，`2` 设备端学习。 |\n| `trace_id` | String | 本次请求的追踪 ID，成功与业务失败响应均返回。 |\n\n## trace_id 说明\n\n项目自定义 `json()` 响应函数会统一补充 `trace_id`，成功与业务失败均固定返回。排查时提供该 ID、设备编码和调用时间，即可关联服务端日志；不要在工单中传播真实人脸特征值。Docker 内对缺少 `code` 的真实 HTTP 请求已验证错误响应包含该字段。",
        "parameters": [
          {
            "name": "Access-Token",
            "in": "header",
            "required": false,
            "description": "保留现有 Apifox 可选头；此接口业务代码以设备编码查询设备，不校验该头。",
            "schema": {"type": "string"}
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["code"],
                "properties": {
                  "code": {"type": "string", "description": "设备编码，必须对应未删除设备。", "example": "FACE-DEVICE-001"},
                  "type": {"type": "integer", "enum": [0, 1], "default": 0, "description": "0=增量；1=全量。本期设备端学习使用 1。", "example": 1},
                  "staff_uuid": {"type": "string", "description": "可选，指定人员 UUID。", "example": "11111111-2222-3333-4444-555555555555"}
                }
              },
              "examples": {
                "fullSync": {"summary": "全量拉取", "value": {"code": "FACE-DEVICE-001", "type": 1}},
                "oneStaff": {"summary": "指定人员全量拉取", "value": {"code": "FACE-DEVICE-001", "type": 1, "staff_uuid": "11111111-2222-3333-4444-555555555555"}}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "业务结果请以顶层 code 判断。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["code", "message", "data", "trace_id"],
                  "properties": {
                    "code": {"type": "integer", "enum": [0, 1], "description": "0 成功，1 业务失败。"},
                    "message": {"type": "string"},
                    "trace_id": {"type": "string", "description": "项目 json() 响应函数统一补充的请求追踪 ID。"},
                    "face_learning_mode": {"type": "integer", "enum": [1, 2], "description": "仅成功响应保证返回；1 服务端学习，2 设备端学习。"},
                    "data": {
                      "nullable": true,
                      "type": "object",
                      "properties": {
                        "face_data": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {"type": "integer", "description": "人员 ID，学习结果回传使用。"},
                              "staff_uuid": {"type": "string"},
                              "head_img_url": {"type": "string", "format": "uri", "description": "完整照片 URL，回传时原样提交。"},
                              "feature": {"type": "string", "description": "特征值；设备端未学习时为空。"},
                              "face_token": {"type": "string", "description": "百度人脸 token；设备端未学习时为空。"},
                              "name": {"type": "string"},
                              "hardware_uuid": {"type": "string"},
                              "status": {"type": "integer", "description": "全量有效人脸为 1。"}
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "deviceLearning": {"summary": "设备端学习模式：待学习照片", "value": {"code": 0, "message": "查询成功", "data": {"face_data": [{"id": 123, "staff_uuid": "11111111-2222-3333-4444-555555555555", "head_img_url": "http://example.invalid/static/upload/face/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jpg", "feature": "", "face_token": "", "name": "示例人员", "hardware_uuid": "66666666-7777-8888-9999-000000000000", "status": 1}]}, "face_learning_mode": 2, "trace_id": "<本次请求追踪ID>"}},
                  "serverLearning": {"summary": "服务端学习模式", "value": {"code": 0, "message": "查询成功", "data": {"face_data": [{"id": 123, "staff_uuid": "11111111-2222-3333-4444-555555555555", "head_img_url": "http://example.invalid/static/upload/face/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jpg", "feature": "<特征值示例>", "face_token": "<face-token-示例>", "name": "示例人员", "hardware_uuid": "66666666-7777-8888-9999-000000000000", "status": 1}]}, "face_learning_mode": 1, "trace_id": "<本次请求追踪ID>"}},
                  "businessError": {"summary": "业务失败：设备不存在", "value": {"code": 1, "message": "设备不存在", "data": null, "trace_id": "<本次请求追踪ID>"}}
                }
              }
            }
          }
        },
        "x-apifox-status": "developing"
      }
    },
    "/p/api/facedataLearnResult": {
      "post": {
        "summary": "设备端人脸学习结果回传",
        "description": "## 接口概述\n\n设备在 `face_learning_mode=2` 时完成本地人脸学习后，把特征值和 token 回传服务端。服务端核对当前人员、照片和关联关系后，更新 `ydy_staff_face` 及对应的 `ydy_hardware_img`。\n\n## 接口信息\n\n| 项目 | 内容 |\n| --- | --- |\n| 接口地址 | `/p/api/facedataLearnResult` |\n| 请求方法 | POST |\n| Content-Type | `application/x-www-form-urlencoded` |\n| 接口版本 | 当前张家港分支；接口路径没有独立版本号 |\n\n## 请求头（Header）\n\n| 参数名 | 是否必填 | 说明 |\n| --- | --- | --- |\n| `Content-Type` | 是 | `application/x-www-form-urlencoded` |\n\n当前代码不校验设备密钥或 `Access-Token`，只使用请求体中的 `code` 确认未删除设备存在；生产环境须依靠内网访问控制。不能照搬其他接口的 `account`、`pwd`、`key` 请求头。\n\n## 请求参数（Body）\n\n| 参数名 | 类型 | 是否必填 | 描述 | 示例值 |\n| --- | --- | --- | --- | --- |\n| `code` | String | 是 | 设备编码，仅校验设备存在，不入库 | `FACE-DEVICE-001` |\n| `staff_id` | Integer | 是 | `/p/api/facedata` 的 `face_data[].id`，须大于 0 | `123` |\n| `head_img_url` | String | 是 | 原样提交全量接口返回的完整照片 URL；最长 1024 字节 | `http://example.invalid/static/upload/face/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jpg` |\n| `feature` | String | 是 | 设备学习得到的特征值；最多 1048576 字节 | `<特征值占位>` |\n| `baidu_face_token` | String | 是 | 设备学习得到的 token；最多 56 字节 | `<token占位>` |\n\n`feature` 和 `baidu_face_token` 为敏感数据，不要把真实值写入共享文档、聊天记录或问题工单。\n\n## 响应格式\n\n保存成功：\n\n```json\n{\"code\":0,\"message\":\"人脸学习结果已保存\",\"data\":null,\"trace_id\":\"<本次请求追踪ID>\"}\n```\n\n照片在设备学习期间发生更新时：\n\n```json\n{\"code\":1,\"message\":\"人脸照片已更新，请重新拉取\",\"data\":null,\"trace_id\":\"<本次请求追踪ID>\"}\n```\n\n未登记设备时：\n\n```json\n{\"code\":1,\"message\":\"设备不存在\",\"data\":null,\"trace_id\":\"<本次请求追踪ID>\"}\n```\n\n## 状态码说明\n\n| HTTP 状态 | `code` | 描述 | 设备处理 |\n| --- | --- | --- | --- |\n| 通常 200 | `0` | 同一照片的学习结果已保存 | 可继续同步；同一照片、同一结果重复回传可成功。 |\n| 通常 200 | `1` | 业务失败 | 按 `message` 处理；照片已更新或人脸已学习时重新全量拉取，不覆盖本地新数据。 |\n\n可能的失败文案包括：`当前未启用设备端人脸学习`、`参数错误`、`设备不存在`、`人员不存在`、`当前人脸数据不存在或不唯一`、`人脸照片已更新，请重新拉取`、`人脸已学习，请重新拉取`、`人脸照片关联异常`。异常时也可能返回通用的 `人脸学习结果回传失败`。HTTP 200 不等于业务成功。\n\n## 响应字段说明\n\n| 字段名 | 类型 | 描述 |\n| --- | --- | --- |\n| `code` | Integer | `0` 保存成功，`1` 失败。 |\n| `message` | String | 保存结果或失败原因。 |\n| `data` | Null | 当前固定为 `null`。 |\n| `trace_id` | String | 本次请求的追踪 ID，成功与业务失败响应均返回。 |\n\n校验照片 URL 时，服务端为库内相对路径补上站点前缀，再与请求中的完整 `head_img_url` 精确比较；不一致不写入。若已保存不同 token 或 feature，也拒绝覆盖。仅在设备端学习模式下允许回传。\n\n## trace_id 说明\n\n项目自定义 `json()` 响应函数会统一补充 `trace_id`，成功与业务失败均固定返回。排查时优先提供该 ID，并附设备编码、人员 ID 和调用时间；不要提供真实 `feature` 或 `baidu_face_token`。Docker 内对模式 1 下回传的真实 HTTP 请求已验证错误响应包含该字段。",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["code", "staff_id", "head_img_url", "feature", "baidu_face_token"],
                "properties": {
                  "code": {"type": "string", "minLength": 1, "description": "设备编码，仅检查设备是否存在，不入库。", "example": "FACE-DEVICE-001"},
                  "staff_id": {"type": "integer", "minimum": 1, "description": "人员 ID，来自 facedata 的 face_data[].id。", "example": 123},
                  "head_img_url": {"type": "string", "minLength": 1, "maxLength": 1024, "description": "原样使用 facedata 下发的完整 URL。", "example": "http://example.invalid/static/upload/face/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jpg"},
                  "feature": {"type": "string", "minLength": 1, "maxLength": 1048576, "description": "设备学习得到的人脸特征值；服务端限制为 1048576 字节。", "example": "<设备学习产生的特征值>"},
                  "baidu_face_token": {"type": "string", "minLength": 1, "maxLength": 56, "description": "设备学习得到的人脸 token；服务端限制为 56 字节。", "example": "<设备学习产生的人脸token>"}
                }
              },
              "examples": {
                "learnedFace": {"summary": "回传设备学习结果", "value": {"code": "FACE-DEVICE-001", "staff_id": 123, "head_img_url": "http://example.invalid/static/upload/face/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jpg", "feature": "<设备学习产生的特征值>", "baidu_face_token": "<设备学习产生的人脸token>"}}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "业务结果请以顶层 code 判断。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["code", "message", "data", "trace_id"],
                  "properties": {
                    "code": {"type": "integer", "enum": [0, 1]},
                    "message": {"type": "string"},
                    "trace_id": {"type": "string", "description": "项目 json() 响应函数统一补充的请求追踪 ID。"},
                    "data": {"nullable": true, "type": "object"}
                  }
                },
                "examples": {
                  "success": {"summary": "学习结果保存成功", "value": {"code": 0, "message": "人脸学习结果已保存", "data": null, "trace_id": "<本次请求追踪ID>"}},
                  "stalePhoto": {"summary": "照片已更新，需重新拉取", "value": {"code": 1, "message": "人脸照片已更新，请重新拉取", "data": null, "trace_id": "<本次请求追踪ID>"}},
                  "missingDevice": {"summary": "设备不存在", "value": {"code": 1, "message": "设备不存在", "data": null, "trace_id": "<本次请求追踪ID>"}}
                }
              }
            }
          }
        },
        "x-apifox-status": "developing"
      }
    }
  }
}
