Dish_Recognition/docs/API服务需求文档.md

362 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 菜品识别 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。