# 菜品识别管理系统第三方接口文档 版本:v1.3 更新时间:2026-10-13 ## 1. 接口地址 生产环境基础地址: ```text https://cpsb.ruimeida.cc/ ``` 基地和菜品查询接口使用 `GET` 请求;识别结果、无法识别样本和菜品图库样本写入接口使用 `POST` 请求;菜品学习状态和设备状态更新使用 `PATCH` 请求。 ## 2. 接口认证 接口使用独立的 API Key 认证,不使用后台用户登录账号密码。 每次请求都必须携带请求头: ```http X-API-Key: ``` 请向系统管理员获取正式 API Key,不要将 API Key 写入前端页面、公开代码仓库或日志中。 ## 3. 写入菜品识别结果 该接口接收一次识别任务的主信息和菜品明细,供第三方识别程序回传结果。接口使用 `request_id` 做幂等键:同一个 `request_id` 再次提交时会更新主记录,并完整替换该任务的菜品明细,不会产生重复订单。 ### 请求 ```http POST /api/public/recognition-orders Content-Type: application/json X-API-Key: ``` 完整地址: ```text https://cpsb.ruimeida.cc/api/public/recognition-orders ``` ### 请求体示例 ```json { "request_id": "REQ_20260916_000001", "base_name": "上海路基地", "device_code": "DEV-0018", "capture_time": "2026-09-16 12:34:56", "source_image": "https://example.com/images/source.jpg", "labeled_image": "https://example.com/images/labeled.jpg", "dish_count": 2, "dishes": [ { "index": 1, "name": "红烧鸡块", "score": 0.96, "is_unknown": false, "suggested_name": null, "box": [120, 80, 460, 390], "crop_image": "https://example.com/images/crop-1.jpg", "text": "识别为红烧鸡块,相似度 0.96" }, { "index": 2, "name": null, "score": 0.42, "is_unknown": true, "suggested_name": "辣椒炒肉", "box": [500, 90, 820, 400], "crop_image": "https://example.com/images/crop-2.jpg", "text": "低于阈值,建议人工确认" } ] } ``` ### Body 字段 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `request_id` | string | 是 | 唯一请求号,建议使用 `REQ_` 加毫秒时间戳或业务流水号 | | `base_name` | string | 是 | 基地名称,必须与基地信息中的 `name` 完全一致,例如 `上海路基地` | | `device_code` | string | 是 | 设备编号,对应设备列表中的设备编号 | | `capture_time` | string | 是 | 采集时间,格式为 `YYYY-MM-DD HH:mm:ss`,也支持 ISO 8601 字符串 | | `source_image` | string \| null | 否 | 原图路径或 URL;没有时传 `null` 或空字符串 | | `labeled_image` | string \| null | 否 | 标注预览图路径或 URL;没有时传 `null` 或空字符串 | | `dish_count` | int | 是 | 菜品区域数,必须等于 `dishes.length`,不能小于 0 | | `dishes` | array | 是 | 菜品识别明细数组 | ### `dishes[]` 字段 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `index` | int | 是 | 从 1 开始的顺序号,与标注预览图中的编号/框对应,任务内不可重复 | | `name` | string \| null | 是 | 识别菜名;未知菜品时传 `null` | | `score` | number \| null | 否 | 相似度,建议范围为 0~1;未知或没有分数时传 `null` | | `is_unknown` | boolean | 是 | 是否未达阈值或未知,未知时为 `true` | | `suggested_name` | string \| null | 否 | 未知菜品的近似菜名建议 | | `box` | number[4] \| null | 否 | 像素坐标 `[x1, y1, x2, y2]` | | `crop_image` | string \| null | 否 | 当前菜品裁剪图路径或 URL | | `text` | string \| null | 否 | 可读摘要文本,仅用于调试或展示 | ### cURL 示例 ```bash curl -X POST "https://cpsb.ruimeida.cc/api/public/recognition-orders" \ -H "Content-Type: application/json" \ -H "X-API-Key: " \ -d @recognition-order.json ``` ### 成功返回 ```json { "message": "识别结果已保存", "item": { "request_id": "REQ_20260916_000001", "base_name": "上海路基地", "device_code": "DEV-0018", "capture_time": "2026-09-16 12:34:56", "dish_count": 2 } } ``` ### 校验与存储说明 1. `base_name` 必须传启用基地的名称,并且需要与基地信息中的 `name` 完全一致;接口会在服务端匹配基地编码保存关联关系。 2. `device_code` 必须传设备列表中的设备编号。 3. `dish_count` 必须与明细条数一致;`index` 在同一请求内必须唯一且从 1 开始。 4. `box` 只能是 4 个数字,顺序为 `[x1, y1, x2, y2]`,单位为原图像素。 5. 识别原图、标注图和裁剪图由第三方自行保存,接口只保存传入的路径或 URL。 6. 数据分别保存于 `recognition_orders` 和 `recognition_order_dishes` 两张表,删除或更新主记录时会同步处理明细。 7. 新版接口使用 `base_name`,不再要求第三方传基地编码;服务端会根据基地名称匹配并保存内部关联关系。历史版本的 `base_id` 仅作兼容处理,不建议新接入继续使用。 ## 4. 上报无法识别样本 第三方识别程序在识别到未知菜品后调用该接口,上报的数据进入后台「数据管理 → 识别数据管理 → 无法识别统计」列表。管理员可在该页面勾选裁剪图、填写菜名后提交手动学习,学习提交会转发到识别端学习接口(`POST /api/client/manual-learn`)。 ### 请求 ```http POST /api/public/unrecognized-samples Content-Type: application/json X-API-Key: ``` 完整地址: ```text https://cpsb.ruimeida.cc/api/public/unrecognized-samples ``` ### 请求体示例 ```json { "request_id": "REQ_20260916_000002", "order_no": "HW26091612341817", "base_name": "上海路基地", "device_code": "DEV-0018", "capture_time": "2026-09-16 12:34:46", "image_url": "https://example.com/images/source.jpg", "unknown_count": 2, "suggested_names": ["梅菜扣肉", "黑米饭"], "crops": [ { "index": 1, "image_url": "https://example.com/images/crop-1.jpg", "suggested_name": "梅菜扣肉", "score": 0.62 }, { "index": 2, "image_url": "https://example.com/images/crop-2.jpg", "suggested_name": null, "score": 0.41 } ] } ``` ### Body 字段 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `request_id` | string | 否 | 唯一请求号,作为幂等键;传入后同一 `request_id` 重复上报会覆盖主记录并重建明细 | | `order_no` | string | 是 | 业务单号,后台无法识别统计与手动学习接口使用 | | `base_code` | string | 否 | 基地编码,与 `base_name` 至少传一个;服务端匹配启用基地后保存关联 | | `base_name` | string | 否 | 基地名称,需与基地信息中的 `name` 一致;与 `base_code` 同时传入时优先按编码匹配 | | `device_code` | string | 是 | 设备编号 | | `capture_time` | string | 是 | 采集时间,格式为 `YYYY-MM-DD HH:mm:ss`,也支持 ISO 8601 字符串 | | `image_url` | string | 是 | 整盘原图路径或 URL | | `unknown_count` | number | 否 | 未知菜品数量;不传或非法时按 `crops.length` 保存 | | `suggested_names` | string[] | 否 | 建议菜名列表,在列表页「建议菜名」列展示 | | `crops` | array | 否 | 未知菜品裁剪图明细,供后台学习弹框逐盘学习 | ### `crops[]` 字段 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `index` | int | 否 | 从 1 开始的顺序号,缺省按数组位置生成;同一样本内不可重复 | | `image_url` | string \| null | 否 | 当前未知菜品的裁剪图路径或 URL | | `suggested_name` | string \| null | 否 | 近似菜名建议;后台学习弹框会预填到菜名输入框 | | `score` | number \| null | 否 | 相似度,范围 0~1;后台学习弹框展示为置信度 | ### cURL 示例 ```bash curl -X POST "https://cpsb.ruimeida.cc/api/public/unrecognized-samples" \ -H "Content-Type: application/json" \ -H "X-API-Key: " \ -d @unrecognized-sample.json ``` ### 成功返回 ```json { "message": "无法识别样本已保存", "item": { "id": 1, "order_no": "HW26091612341817", "unknown_count": 2 } } ``` ### 校验与存储说明 1. `order_no`、`device_code`、`capture_time`、`image_url` 为必填,缺失或格式无效返回 `400`。 2. `base_code` / `base_name` 传入时服务端会匹配启用状态的基地并保存编码和名称;匹配不到时按传入原值保存,不影响入库。 3. `score` 超出 0~1、`index` 重复或非法时返回 `400`。 4. 传 `request_id` 时按幂等处理:重复上报覆盖主记录并完整替换 `crops` 明细;不传 `request_id` 时每次上报都会新增一条样本。 5. 数据保存于 `unrecognized_samples` 和 `unrecognized_sample_crops` 两张表。 6. 后台无法识别统计列表支持按时段、日期范围、基地、餐段、学习状态(默认未学习)和关键字筛选;餐段按 `capture_time` 落入系统餐段配置的时间范围判定。 ## 5. 上报菜品图库样本(近期学习菜品) 识别端在学菜完成后调用该接口,把「菜名 ↔ 样本图片」映射上报给管理系统,数据展示在后台「AI 自主学习管理 → 近期学习菜品」页面,详情弹框中展示每个菜品的名称和多张样本图片。 ### 请求 ```http POST /api/public/dish-gallery/samples Content-Type: application/json X-API-Key: ``` 完整地址: ```text https://cpsb.ruimeida.cc/api/public/dish-gallery/samples ``` ### 请求体示例(单菜) ```json { "request_id": "REQ_20260916_120001", "base_name": "上海路基地", "device_code": "DEV-0018", "learned_at": "2026-09-16 12:00:01", "name": "梅菜扣肉", "code": "DISH-001", "category": "成品", "images": [ "https://example.com/template/mc/20260916_120001_01.jpg", "https://example.com/template/mc/20260916_120001_02.jpg" ] } ``` ### 请求体示例(一次报多个菜) ```json { "request_id": "REQ_20260916_120001", "base_name": "上海路基地", "learned_at": "2026-09-16 12:00:01", "dishes": [ { "name": "梅菜扣肉", "code": "DISH-001", "category": "成品", "images": ["https://example.com/template/mc/20260916_120001_01.jpg"] }, { "name": "黑米饭", "category": "成品", "images": ["https://example.com/template/hmf/20260916_120001_02.jpg"] } ] } ``` ### Body 字段 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `request_id` | string | 否 | 请求号,与菜品名称一起作为幂等键;不传时服务端自动生成 | | `base_code` | string | 否 | 基地编码,与 `base_name` 至少传一个;服务端匹配启用基地后保存关联 | | `base_name` | string | 否 | 基地名称,需与基地信息中的 `name` 一致 | | `device_code` | string | 否 | 设备编号 | | `learned_at` | string | 是 | 学习完成时间,格式为 `YYYY-MM-DD HH:mm:ss`,也支持 ISO 8601 字符串;兼容 `capture_time` 字段名 | | `name` | string | 单菜必填 | 菜品名称(单菜格式) | | `code` | string | 否 | 菜品编码(单菜格式) | | `category` | string | 否 | 分类,默认 `成品`,建议传 `成品` 或 `配菜` | | `images` | string[] | 单菜必填 | 样本图片路径或 URL 数组,至少一张(单菜格式) | | `dishes` | array | 多菜必填 | 多菜品格式;与 `name` 同时传入时优先使用 `dishes` | ### `dishes[]` 字段 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `name` | string | 是 | 菜品名称 | | `code` | string | 否 | 菜品编码 | | `category` | string | 否 | 分类,默认 `成品` | | `images` | string[] | 是 | 样本图片路径或 URL 数组,至少一张 | ### cURL 示例 ```bash curl -X POST "https://cpsb.ruimeida.cc/api/public/dish-gallery/samples" \ -H "Content-Type: application/json" \ -H "X-API-Key: " \ -d @dish-gallery-sample.json ``` ### 成功返回 ```json { "message": "图库样本已保存", "item": { "request_id": "REQ_20260916_120001", "count": 2 } } ``` ### 校验与存储说明 1. `learned_at`(或 `capture_time`)缺失或格式无效返回 `400`。 2. 单菜格式必须同时传 `name` 和非空 `images`;多菜格式每个菜品的 `name` 和 `images` 同样必填,否则返回 `400`。 3. 幂等键为 `request_id + 菜品名称`:同一组合重复上报时更新样本(图片、分类、学习时间等以最新为准);上报未携带 `code` 时保留原编码。 4. 数据保存于 `dish_gallery_samples` 表。 5. 后台「近期学习菜品」列表按学习时间倒序展示,支持时段、日期范围、基地和关键字筛选;点「详情」按 `request_id` 展示该次学习的全部菜品及多张样本图片。 ## 6. 后台查询识别结果 以下接口用于系统后台的统计列表和详情查询,使用登录用户的 JWT,不使用第三方 API Key。第三方系统如需查询自身回传的数据,建议由业务服务端代理调用,不要把管理员 JWT 暴露给浏览器。 ### 识别统计列表 ```http GET /api/recognition-stats Authorization: Bearer ``` 可选参数:`period`(`today`、`yesterday`、`week`、`month`)、`baseCode`、`keyword`、`startDate`、`endDate`、`page`、`pageSize`。`pageSize` 支持 10、20、50。 ### 识别结果详情 ```http GET /api/recognition-orders/{request_id} Authorization: Bearer ``` 详情返回主记录和 `dishes` 明细,主记录返回 `base_name`、`device_code`,不返回或展示基地编码 `base_id`;明细包含 `index`、`name`、`score`、`is_unknown`、`suggested_name`、`box`、`crop_image`、`text`。 ## 7. 更新设备状态信息 第三方设备程序使用该接口按设备编号更新设备状态、心跳更新时间和菜谱当前日期。接口使用 API Key 认证。 ### 请求 ```http PATCH /api/public/devices/{device_code}/status Content-Type: application/json X-API-Key: ``` 完整地址示例: ```text https://cpsb.ruimeida.cc/api/public/devices/DEV-0018/status ``` ### 请求体 ```json { "status": "在线", "updated_at": "2026-09-16 14:30:00", "recipe_date": "2026-09-16" } ``` ### 参数说明 | 参数 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | 路径 `device_code` | string | 是 | 设备编号,对应设备列表中的设备编号 | | `status` | string | 是 | 设备状态,支持 `在线`、`离线`,也支持 `online`、`offline` | | `updated_at` | string \| null | 否 | 设备最后更新时间,格式为 `YYYY-MM-DD HH:mm:ss`;不传时使用服务器当前时间 | | `recipe_date` | string \| null | 否 | 菜谱当前日期,格式为 `YYYY-MM-DD`;不传时保留原值,传 `null` 或空字符串可清空 | ### cURL 示例 ```bash curl -X PATCH "https://cpsb.ruimeida.cc/api/public/devices/DEV-0018/status" \ -H "Content-Type: application/json" \ -H "X-API-Key: " \ -d '{"status":"在线","updated_at":"2026-09-16 14:30:00","recipe_date":"2026-09-16"}' ``` ### 成功返回 ```json { "message": "设备状态信息已更新", "item": { "id": 1, "code": "DEV-0018", "status": 1, "status_label": "在线", "updated_at": "2026-09-16 14:30:00", "recipe_date": "2026-09-16" } } ``` 设备列表查询接口 `GET /api/devices` 返回的字段包括 `status`、`status_label`、`updated_at`、`recipe_date`,其中 `status=1` 表示在线,`status=0` 表示离线。设备状态和时间信息保存于 `devices` 表。 ## 8. 菜品信息接口 菜品接口使用 API Key 认证,返回菜品名称、菜品分类、学习状态、价格等信息。 ### 8.1 获取菜品信息 ```http GET /api/public/dishes X-API-Key: ``` 完整地址: ```text https://cpsb.ruimeida.cc/api/public/dishes ``` 查询参数均为可选,不传参数时返回全部菜品: | 参数 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `name` | string | 否 | 按菜品名称模糊查询,例如 `鸡` | | `category` | string | 否 | 按菜品分类精确查询,例如 `素菜`;传 `全部` 等同于不筛选 | 查询全部菜品: ```bash curl -X GET "https://cpsb.ruimeida.cc/api/public/dishes" \ -H "X-API-Key: " ``` 按菜品名称和分类查询: ```bash curl -G "https://cpsb.ruimeida.cc/api/public/dishes" \ -H "X-API-Key: " \ --data-urlencode "name=鸡" \ --data-urlencode "category=大荤" ``` 返回示例: ```json { "total": 1, "items": [ { "id": 1, "name": "红烧鸡块", "code": "DISH-001", "category": "大荤", "price": 4, "status": "已学习", "confidence": 96.5, "picks": 3809, "created_at": "2026-09-16 10:00:00", "updated_at": "2026-09-28 09:30:00" } ] } ``` ### 8.2 按菜品名称更新学习状态 ```http PATCH /api/public/dishes/learning-status Content-Type: application/json X-API-Key: ``` 完整地址: ```text https://cpsb.ruimeida.cc/api/public/dishes/learning-status ``` 请求体: ```json { "name": "红烧鸡块", "status": "已学习" } ``` | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `name` | string | 是 | 菜品名称,按名称精确匹配 | | `status` | string | 是 | 学习状态,只能是 `已学习` 或 `未学习`;也兼容 `learned`、`unlearned` | cURL 示例: ```bash curl -X PATCH "https://cpsb.ruimeida.cc/api/public/dishes/learning-status" \ -H "Content-Type: application/json" \ -H "X-API-Key: " \ -d '{"name":"红烧鸡块","status":"已学习"}' ``` 成功返回: ```json { "message": "菜品学习状态已更新", "item": { "id": 1, "name": "红烧鸡块", "code": "DISH-001", "category": "大荤", "price": 4, "status": "已学习", "confidence": 96.5, "picks": 3809, "created_at": "2026-09-16 10:00:00", "updated_at": "2026-09-28 09:30:00" } } ``` 菜品名称不存在时返回 `404`;状态不合法或菜品名称为空时返回 `400`。 ## 9. 获取全部基地信息 ### 请求 ```http GET /api/public/bases ``` 完整地址: ```text https://cpsb.ruimeida.cc/api/public/bases ``` ### cURL 示例 ```bash curl -X GET "https://cpsb.ruimeida.cc/api/public/bases" \ -H "X-API-Key: " ``` ### 返回示例 ```json { "items": [ { "id": 1364, "branchName": "安仁街分部", "name": "上海路基地", "code": "上海路基地-1788059884828-e55vu", "province": "", "city": "", "district": "", "address": "", "contact": "", "phone": "", "longitude": null, "latitude": null, "description": "", "status": 1, "sortOrder": 0 } ] } ``` ### 字段说明 | 字段 | 类型 | 说明 | | --- | --- | --- | | `id` | number | 基地 ID,可用于排餐查询的 `baseId` 参数 | | `branchName` | string | 分部名称 | | `name` | string | 基地名称,可用于排餐查询的 `baseName` 参数 | | `code` | string | 基地编码,可用于排餐查询的 `baseCode` 参数 | | `province` / `city` / `district` | string | 所属地区 | | `address` | string | 详细地址 | | `contact` | string | 联系人 | | `phone` | string | 联系电话 | | `longitude` / `latitude` | number \| null | 经纬度 | | `description` | string | 基地备注 | | `status` | number | `1` 启用,`0` 禁用 | | `sortOrder` | number | 排序值 | ## 10. 获取当天排餐信息 ### 请求 ```http GET /api/public/meal-schedules/today ``` 兼容地址: ```http GET /api/meal-schedules/today ``` 接口默认返回中国时区(`Asia/Shanghai`)当天的数据,并包含所有基地、早餐、中餐和晚餐。 ### 查询参数 所有查询参数均为可选: | 参数 | 类型 | 说明 | | --- | --- | --- | | `date` | string | 指定日期,格式为 `YYYY-MM-DD`;不传时使用中国时区当天日期 | | `baseName` | string | 按基地名称精确查询,推荐使用;中文需要进行 URL 编码 | | `baseId` | number | 按基地 ID 查询 | | `baseCode` | string | 按基地编码精确查询 | 基地筛选参数只需要传一个。如果同时传多个参数,优先级为:`baseId` > `baseCode` > `baseName`。 ### 调用示例 查询当天所有基地: ```bash curl -X GET "https://cpsb.ruimeida.cc/api/public/meal-schedules/today" \ -H "X-API-Key: " ``` 按基地名称查询: ```bash curl -G "https://cpsb.ruimeida.cc/api/public/meal-schedules/today" \ -H "X-API-Key: " \ --data-urlencode "baseName=上海路基地" ``` 查询指定日期和基地: ```bash curl -G "https://cpsb.ruimeida.cc/api/public/meal-schedules/today" \ -H "X-API-Key: " \ --data-urlencode "date=2026-09-16" \ --data-urlencode "baseName=上海路基地" ``` ### 返回示例 ```json { "date": "2026-09-16", "timezone": "Asia/Shanghai", "mealPeriods": [ { "id": 1, "name": "早餐", "startTime": "07:00", "endTime": "09:00" }, { "id": 2, "name": "中餐", "startTime": "11:30", "endTime": "13:30" }, { "id": 3, "name": "晚餐", "startTime": "17:30", "endTime": "19:00" } ], "bases": [ { "id": 1364, "code": "上海路基地-1788059884828-e55vu", "name": "上海路基地", "branchName": "安仁街分部", "status": 1, "meals": [ { "mealPeriodId": 1, "mealPeriodName": "早餐", "startTime": "07:00", "endTime": "09:00", "dishes": [] }, { "mealPeriodId": 2, "mealPeriodName": "中餐", "startTime": "11:30", "endTime": "13:30", "dishes": [ { "name": "米粉蒸肉", "price": 4, "learningStatus": "未学习", "sortOrder": 0 } ] }, { "mealPeriodId": 3, "mealPeriodName": "晚餐", "startTime": "17:30", "endTime": "19:00", "dishes": [] } ] } ] } ``` ### 排餐字段说明 | 字段 | 类型 | 说明 | | --- | --- | --- | | `date` | string | 查询日期 | | `timezone` | string | 日期使用的时区 | | `mealPeriods` | array | 当天配置的餐段列表 | | `bases` | array | 基地列表 | | `bases[].meals` | array | 当前基地的各餐段;没有菜品时仍返回该餐段 | | `meals[].mealPeriodId` | number | 餐段 ID | | `meals[].mealPeriodName` | string | 餐段名称 | | `meals[].startTime` / `endTime` | string | 餐段开始、结束时间,格式为 `HH:mm` | | `meals[].dishes` | array | 当前基地当前餐段的菜品 | | `dishes[].name` | string | 菜品名称 | | `dishes[].price` | number | 菜品价格 | | `dishes[].learningStatus` | string | `已学习` 或 `未学习` | | `dishes[].sortOrder` | number | 菜品在餐段中的顺序 | ## 11. HTTP 状态码 | 状态码 | 说明 | | --- | --- | | `200` | 请求成功 | | `400` | 请求参数格式错误,例如日期或 `baseId` 格式错误 | | `401` | API Key 缺失或错误 | | `503` | 服务端尚未配置第三方 API Key | | `500` | 服务端内部错误 | 错误返回格式: ```json { "message": "第三方接口认证失败" } ``` ## 12. 对接建议 1. 第三方系统应将 API Key 保存在服务端配置中,不要放在浏览器前端代码中。 2. 建议每次调用排餐接口时传入 `baseName` 或 `baseId`,减少返回数据量。 3. 排餐接口会返回没有菜品的餐段,第三方系统应将 `dishes: []` 识别为当天该基地该餐段暂无排餐。 4. 日期不传时按 `Asia/Shanghai` 计算,不要使用第三方服务器的本地时区替代接口返回的 `date`。