13 KiB
菜品识别 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:
POST /api/dish/media/upload上传source/labeled/crop,返回可访问urlPOST /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 | 否 | 可读摘要文本(调试用) |
请求示例
{
"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": "未知 · 近似 炒青菜"
}
]
}
成功响应(建议)
{
"code": 0,
"message": "ok",
"data": {
"request_id": "REQ_1726296161000",
"accepted": true,
"duplicate": false
}
}
错误响应(建议)
{
"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 |
建议 | 关联某次识别,便于归档 |
成功响应:
{
"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):
- 生成
request_id - 默认:启动本机
media_server(:9100),把 YOLO 裁剪图等复制进central_server/data/media/,字段写成
http://<局域网IP>:9100/media/{base}/{request_id}/crop_xxx.jpg - 若显式设置了
DISH_MEDIA_UPLOAD_URL,则优先远程上传,失败再回落本机 media server - 再
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 |
联调步骤:
- 启动客户端(会自动起
:9100media server) - 识别一次,日志应出现
crop -> http://<LAN>:9100/media/.../crop_….jpg - 浏览器打开该 URL 应能看到裁剪图;上报 JSON 中
dishes[].crop_image为该 HTTP 路径 - 若管理系统在公网:配置
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. 验收标准
- 合法 payload 上报返回
code=0,且库中可查到对应request_id(本地api_server) - 相同
request_id重复上报不产生重复有效业务单据 is_unknown=true且name=null可正常入库dish_count与数组长度不一致时返回 4xx- 配置
DISH_API_TOKEN后未鉴权请求被拒绝 - 客户端关闭 mock 后真实 HTTP 联调通过(抽样 ≥ 20 次)
- 图片上传后结果中的 URL 可通过
GET /media/...访问
9. 分期建议
| 阶段 | 内容 |
|---|---|
| MVP | 健康检查 + 图片上传 + 静态访问 + 结果上报 + 幂等落库 |
| 二期 | 记录查询分页 / 时间筛选;生产 OSS 直传 |
| 三期 | 未知菜总部审核流 + 统计看板 API |
10. 附录:客户端当前行为说明
- 默认 mock:不发 HTTP;图片复制到
media_store/,JSON 写入api_mock_results/REQ_*.json(字段已是 URL) - 真实模式:先
multipart上传图片,再POSTJSON;失败时仍写一份本地文件便于排错 - 默认 异步上报(不阻塞 UI);本地 SQLite 仍保存本机路径便于预览
- 识别同时写入本地 SQLite
recognition_records;MySQL 可用时客户端也会尝试同步写总库(与 HTTP API 并行,服务端实现时注意职责划分,避免双写冲突)
11. 待确认事项(给产品/后端)
- 鉴权方式:已支持可选
DISH_API_TOKEN(Bearer / X-Api-Token);生产是否改 JWT / 签名? - 生产对象存储:继续用本机
media_store+ Nginx,还是 OSS(接口形态已对齐,只换上传实现)? - 识别结果是否需要回推收银金额(单价是否由服务端维护)?
图片必须先上传再上报已冻结为协议。确认其余项后可发 v1.2。