362 lines
13 KiB
Markdown
362 lines
13 KiB
Markdown
# 菜品识别 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 <token>` 或基地密钥 |
|
||
| `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://<LAN>: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://<LAN>: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。
|