# 菜品识别 API 服务需求文档 | 项目 | 说明 | |------|------| | 文档版本 | v1.1 | | 日期 | 2026-09-15 | | 关联客户端 | Dish_Recognition(海康 / USB 结算机客户端) | | 客户端实现参考 | `service/result_api.py` | | 总服务器 | `central_server/`(`python start.py` 或 `python -m central_server`) | | 状态 | 客户端已实现「先传图再上报」;总服务器可单独部署 | --- ## 1. 背景与目标 食堂客户端完成「YOLO 检盘 → 特征匹配认菜」后,需将结构化识别结果上报至业务侧 API,供: - 收银 / 结算机展示与记账对接 - 总部汇总多基地识别数据 - 未知菜品后续运营处理(可选人工审核) **本期 API 服务范围**:接收图片、落对象存储、返回 URL;再接收识别结果 JSON 并落库。 **对接拓扑**:工位机只连总库/中心 API;业务系统只连总库,不直连工位。 **明确不做**:真实支付扣款、会员积分、打印票据(可由下游系统消费本 API 数据实现)。 --- ## 2. 角色与系统边界 ``` ┌─────────────────┐ 1) 上传图片 ┌──────────────────┐ │ 工位机客户端 │ ─────────────────────► │ 中心 API / 总库 │ │ Dish_Recognition│ 2) 上报 JSON(URL) │ 本文档 │ └────────┬────────┘ └────────┬─────────┘ │ 模板同步 │ ▼ ▼ ┌─────────────────┐ ┌──────────────────┐ │ MySQL 总库 │ ◄──── 流水 / URL ───── │ 对象存储 / 静态 │ │ 模板·基地·记录 │ │ media_store 或 OSS│ └─────────────────┘ └──────────────────┘ ▲ │ 只对接总库 ┌──────┴──────┐ │ 你方业务系统 │ └─────────────┘ ``` | 系统 | 职责 | |------|------| | 客户端 | 识别、本地 SQLite 缓存;先上传图片再上报 URL | | 中心 API | 鉴权、收图、静态访问、校验、结果落库、查询 | | MySQL 总库 | 模板权威数据;识别记录可同步写入 | | 对象存储 / 静态服务 | 原图 / 标注图 / 裁剪图,对外提供 HTTP URL | --- ## 3. 功能需求 ### 3.1 识别结果上报(P0 · 必须) | 项 | 要求 | |----|------| | 接口 | `POST /api/dish/recognize/result` | | 触发时机 | 客户端每次选图/拍照识别完成后(异步发送) | | 幂等 | 以 `request_id` 为幂等键;重复上报应返回成功且不重复计费/重复插有效流水 | | 响应时延 | P99 < 500ms(不含大图上传);超时客户端按 5s 配置 | | 失败策略 | 客户端失败时本地落 mock 文件;服务端应返回明确错误码便于重试 | ### 3.2 识别记录查询(P1 · 建议) | 接口 | 说明 | |------|------| | `GET /api/dish/recognize/records` | 按基地、时间范围分页查询 | | `GET /api/dish/recognize/records/{request_id}` | 按请求号查单条详情 | ### 3.3 图片资源(P0 · 必须:先上传再上报) 工位机本机路径跨机不可用。约定 **方案 A**: 1. `POST /api/dish/media/upload` 上传 `source` / `labeled` / `crop`,返回可访问 `url` 2. `POST /api/dish/recognize/result` 的 `source_image` / `labeled_image` / `dishes[].crop_image` **填 URL**,不再填本地路径 本地对象存储:`central_server/data/media/`,由 `python start.py` 提供 `GET /media/...`。 生产可把同一目录挂到 Nginx / OSS,或把上传接口改写为直传对象存储,**客户端字段不变**。 ### 3.4 未知菜审核(P2 · 可选) 客户端本地有「待学习库」;总部侧可扩展: | 接口 | 说明 | |------|------| | `GET /api/dish/unknown/pending` | 拉取未知菜列表 | | `POST /api/dish/unknown/{id}/approve` | 确认菜名并回写模板策略 | | `POST /api/dish/unknown/{id}/reject` | 忽略 | > 客户端 ORM 已有 `PendingApproval` 模型雏形,服务端可与之对齐。 ### 3.5 健康检查(P0) | 接口 | 说明 | |------|------| | `GET /api/health` | 存活探测;客户端/网关探活 | --- ## 4. 接口详细设计 ### 4.1 上报识别结果 **请求** - Method:`POST` - Path:`/api/dish/recognize/result` - Content-Type:`application/json` - Header(建议): | Header | 必填 | 说明 | |--------|------|------| | `Authorization` | 建议 | `Bearer ` 或基地密钥 | | `X-Base-Code` | 可选 | 与 body.`base_id` 一致,便于网关路由 | | `Content-Type` | 是 | `application/json` | **Body 字段(与客户端 `build_result_payload` 对齐)** | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `request_id` | string | 是 | 唯一请求号,格式建议 `REQ_{毫秒时间戳}` | | `base_id` | string | 是 | 基地编码,如 `BASE_LOCAL_01` | | `capture_time` | string | 是 | `YYYY-MM-DD HH:mm:ss` | | `source_image` | string | 否 | 原图路径或 URL | | `labeled_image` | string | 否 | 标注预览图路径或 URL | | `dish_count` | int | 是 | 菜品区域数,应等于 `dishes.length` | | `dishes` | array | 是 | 菜品明细,见下表 | **`dishes[]` 元素** | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `index` | int | 是 | 从 1 起的序号,与预览图编号/框对应 | | `name` | string \| null | 是 | 识别菜名;未知时为 `null` | | `score` | number \| null | 否 | 相似度,建议 0~1 | | `is_unknown` | bool | 是 | 是否未达阈值 / 未知 | | `suggested_name` | string \| null | 否 | 未知时的近似菜名建议 | | `box` | number[4] \| null | 否 | `[x1,y1,x2,y2]` 像素坐标 | | `crop_image` | string \| null | 否 | 裁剪图路径或 URL | | `text` | string \| null | 否 | 可读摘要文本(调试用) | **请求示例** ```json { "request_id": "REQ_1726296161000", "base_id": "BASE_LOCAL_01", "capture_time": "2026-09-14 14:22:41", "source_image": "https://cdn.example.com/cap/xxx.jpg", "labeled_image": "https://cdn.example.com/detect/xxx.jpg", "dish_count": 2, "dishes": [ { "index": 1, "name": "土豆丝", "score": 0.86, "is_unknown": false, "suggested_name": null, "box": [120, 80, 460, 400], "crop_image": "https://cdn.example.com/cutting/1.jpeg", "text": "土豆丝 0.860" }, { "index": 2, "name": null, "score": 0.42, "is_unknown": true, "suggested_name": "炒青菜", "box": [500, 90, 820, 410], "crop_image": "https://cdn.example.com/cutting/2.jpeg", "text": "未知 · 近似 炒青菜" } ] } ``` **成功响应**(建议) ```json { "code": 0, "message": "ok", "data": { "request_id": "REQ_1726296161000", "accepted": true, "duplicate": false } } ``` **错误响应**(建议) ```json { "code": 40001, "message": "dish_count 与 dishes 长度不一致", "data": null } ``` | HTTP | code(示例) | 含义 | |------|--------------|------| | 200 | 0 | 成功(含幂等重复) | | 400 | 40001 | 参数校验失败 | | 401 | 40100 | 未授权 | | 409 | 40900 | 业务冲突(可选) | | 500 | 50000 | 服务内部错误 | --- ### 4.2 查询识别记录(P1) `GET /api/dish/recognize/records?base_id=BASE_01&from=2026-09-01&to=2026-09-14&page=1&page_size=20` 返回字段建议对齐表 `recognition_records`: `request_id`、`base_code`、`capture_time`、`dish_count`、`unknown_count`、`summary`、`source_image`、`labeled_image`、`result_json` --- ### 4.3 媒体上传(P0) `POST /api/dish/media/upload` `Content-Type: multipart/form-data` | 字段 | 必填 | 说明 | |------|------|------| | `file` | 是 | 图片文件(jpg/png/bmp/webp,默认上限 12MB) | | `type` | 是 | `source` / `labeled` / `crop` | | `base_id` | 建议 | 基地编码 | | `request_id` | 建议 | 关联某次识别,便于归档 | 成功响应: ```json { "code": 0, "message": "ok", "data": { "url": "http://127.0.0.1:9000/media/BASE_LOCAL_01/REQ_1726296161000/source_cap.jpg", "path": "BASE_LOCAL_01/REQ_1726296161000/source_cap.jpg", "type": "source" } } ``` 静态访问:`GET /media/{base_id}/{request_id}/{filename}` 归档规则:`media_store/{base_id}/{request_id}/{type}_{原文件名}` 客户端行为(`ResultApiClient`): 1. 生成 `request_id` 2. **默认**:启动本机 `media_server`(`:9100`),把 YOLO 裁剪图等复制进 `central_server/data/media/`,字段写成 `http://<局域网IP>:9100/media/{base}/{request_id}/crop_xxx.jpg` 3. 若显式设置了 `DISH_MEDIA_UPLOAD_URL`,则优先远程上传,失败再回落本机 media server 4. 再 `POST` 管理系统 `recognition-orders`(或本地 mock) 模拟模式不发结果 HTTP,但仍生成本机 media URL。若云端管理系统要拉取图片,本机需公网可达,或设置 `DISH_MEDIA_PUBLIC_BASE` 为可访问前缀(反代/穿透/OSS)。 --- ## 5. 非功能需求 | 类别 | 要求 | |------|------| | 可用性 | 核心上报接口可用性 ≥ 99.5% | | 并发 | 单集群支持 ≥ 50 QPS 上报(可按基地数扩展) | | 安全 | HTTPS;基地级鉴权;禁止匿名写 | | 审计 | 保留原始 JSON ≥ 90 天;可按 `request_id` 追溯 | | 日志 | 记录 `request_id`、`base_id`、耗时、HTTP 状态 | | 兼容 | 字段向后兼容;新增字段客户端可忽略 | --- ## 6. 与客户端配置对齐 | 环境变量 | 含义 | 默认 | |----------|------|------| | `DISH_RESULT_API_MOCK` | `1` 写本地 mock,不发 HTTP | `0`(真实上报) | | `DISH_RESULT_API_URL` | 真实上报地址 | 管理系统 `…/recognition-orders` | | `DISH_MEDIA_UPLOAD_URL` | 远程传图地址;**空则不走远程,用本机 media server** | 空 | | `DISH_MEDIA_SERVER_ENABLED` | 是否启本机静态图服务 | `1` | | `DISH_MEDIA_SERVER_PORT` | 本机 media 端口 | `9100` | | `DISH_MEDIA_PUBLIC_BASE` | 对外可访问的 `/media` 前缀(云端拉图时必配公网) | 自动 `http://:9100/media` | | `DISH_MEDIA_HOST` | 强制写入 URL 的主机名/IP | 空则自动探测局域网 IP | | `DISH_UPLOAD_BEFORE_REPORT` | `1` 先落 URL 再上报 | `1` | | `DISH_API_TOKEN` / `DISH_RESULT_API_KEY` | Bearer / X-API-Key | 管理系统 Key | | `DISH_RESULT_API_TIMEOUT` | 结果上报超时秒数 | `10` | | `DISH_MEDIA_UPLOAD_TIMEOUT` | 远程单张图上传超时 | `20` | | `DISH_RESULT_BASE_ID` / `DISH_BASE_CODE` | 上报基地 | `BASE_LOCAL_01` | 联调步骤: 1. 启动客户端(会自动起 `:9100` media server) 2. 识别一次,日志应出现 `crop -> http://:9100/media/.../crop_….jpg` 3. 浏览器打开该 URL 应能看到裁剪图;上报 JSON 中 `dishes[].crop_image` 为该 HTTP 路径 4. 若管理系统在公网:配置 `DISH_MEDIA_PUBLIC_BASE` 为可外网访问的前缀,或设置 `DISH_MEDIA_UPLOAD_URL` 接到 OSS --- ## 7. 数据落库建议 可复用或扩展现有 `recognition_records`: | 列 | 说明 | |----|------| | `request_id` | 幂等键,唯一索引 | | `base_code` / `source_base_id` | 基地 | | `capture_time` | 抓拍时间 | | `source_image` / `labeled_image` | 建议存 URL | | `dish_count` / `unknown_count` | 统计 | | `summary` | 菜名摘要 | | `result_json` | 完整 dishes JSON | --- ## 8. 验收标准 - [x] 合法 payload 上报返回 `code=0`,且库中可查到对应 `request_id`(本地 `api_server`) - [x] 相同 `request_id` 重复上报不产生重复有效业务单据 - [x] `is_unknown=true` 且 `name=null` 可正常入库 - [x] `dish_count` 与数组长度不一致时返回 4xx - [ ] 配置 `DISH_API_TOKEN` 后未鉴权请求被拒绝 - [ ] 客户端关闭 mock 后真实 HTTP 联调通过(抽样 ≥ 20 次) - [x] 图片上传后结果中的 URL 可通过 `GET /media/...` 访问 --- ## 9. 分期建议 | 阶段 | 内容 | |------|------| | MVP | 健康检查 + 图片上传 + 静态访问 + 结果上报 + 幂等落库 | | 二期 | 记录查询分页 / 时间筛选;生产 OSS 直传 | | 三期 | 未知菜总部审核流 + 统计看板 API | --- ## 10. 附录:客户端当前行为说明 - 默认 **mock**:不发 HTTP;图片复制到 `media_store/`,JSON 写入 `api_mock_results/REQ_*.json`(字段已是 URL) - 真实模式:先 `multipart` 上传图片,再 `POST` JSON;失败时仍写一份本地文件便于排错 - 默认 **异步**上报(不阻塞 UI);本地 SQLite 仍保存本机路径便于预览 - 识别同时写入本地 SQLite `recognition_records`;MySQL 可用时客户端也会尝试同步写总库(与 HTTP API 并行,服务端实现时注意职责划分,避免双写冲突) --- ## 11. 待确认事项(给产品/后端) 1. 鉴权方式:已支持可选 `DISH_API_TOKEN`(Bearer / X-Api-Token);生产是否改 JWT / 签名? 2. 生产对象存储:继续用本机 `media_store` + Nginx,还是 OSS(接口形态已对齐,只换上传实现)? 3. 识别结果是否需要回推收银金额(单价是否由服务端维护)? 图片必须先上传再上报已冻结为协议。确认其余项后可发 v1.2。