# 配比秤(新设备)— 采样与制作模式 API 文档 > Controller: `NutRatioScaleController` > 路径前缀: `/neglect/ratio-scale`(Nacos 白名单 `/nutrition/neglect/**`,无需 Sa-Token) > 设备上下文通过请求头 `X-DEVICE-CODE` 解析(`TerminalContextHelper`,据此拿到食堂 id) > 日期: 2026-09-09 > > **序列化规则**:Long 类型字段响应中均为字符串(Jackson `ToStringSerializer`),`BigDecimal` 为普通数字 > **单位约定**: > - 提交构成的熟重 `foodWeight` 单位为 **克 (g)**(与 `nut_food_cook.output_weight` 口径一致) > - 食材称量用量 `useWeight` 单位为 **克 (g)** > - 烹制单生重 `rawWeight`、熟重 `cookedWeight` 单位为 **千克 (kg)** > - 食材实际用量 `actualQty` 单位为 **克 (g)** --- ## 业务背景 配比秤新增「采样模式」与「制作模式」,两者通过**同一个提交接口** `POST /constitute/save` 的 `cookMode` 字段区分: | cookMode | 模式 | 说明 | |:--------:|------|------| | 2 | 采样模式 | **从零建菜**:称量食材 → 新增菜品(生成构成/营养/宝塔)→ 进入「待审核」→ 后台审核通过后上架 | | 1 | 制作模式 | **基于已有菜品**:称量食材 → 记录熟重+构成快照(历史参考,不覆盖正式构成)→ 标记菜品为「配比秤制作」 | ### 制作方式枚举 | 值 | 名称 | 菜品对象 | 数据来源 data_source | |:--:|------|---------|---------------------| | 1 | 制作模式 | 已有菜品(foodId 必填) | 3(配比秤制作) | | 2 | 采样模式 | 新增菜品(foodName 必填) | 2(配比秤采样) | ### 采样菜品审核状态枚举 | 状态码 | 状态名 | 说明 | |:------:|--------|------| | 1 | 待审核 | 采样提交后的初始状态,后台可审核 | | 2 | 已通过 | 审核通过,菜品上架生效 | | 3 | 已驳回 | 审核驳回,菜品保持下架,可重新采样同名菜品 | ### 食材分类枚举 | 分类 | 名称 | 说明 | |:----:|------|------| | 1 | 主材 | 菜品主要食材 | | 2 | 辅材 | 菜品辅助食材 | | 3 | 调料 | 油盐酱醋等 | --- ## 接口清单 | 序号 | 接口 | 路径 | 用途 | |:----:|------|------|------| | 一 | 食材模糊搜索 | `POST /neglect/ratio-scale/mater-search` | 称量前检索食材(设备对应食堂食材库) | | 二 | 搜菜品 | `POST /neglect/ratio-scale/food-search` | 制作模式选已有菜品(按名称模糊查上架菜品) | | 三 | 查菜品构成 | `GET /neglect/ratio-scale/food/{foodId}/composition` | 制作模式查看已有菜品的主辅料+调料构成 | | 四 | 提交构成 | `POST /neglect/ratio-scale/constitute/save` | 统一提交,cookMode 区分制作/采样 | | 五 | 采样历史 | `POST /neglect/ratio-scale/sample/history` | 查询本食堂采样提交历史(默认当天) | | 六 | 制作历史 | `POST /neglect/ratio-scale/make/history` | 按设备+日期查询制作记录分页 | | 七 | 制作历史图表 | `GET /neglect/ratio-scale/make/history/{foodId}` | 按菜品id查制作历史图表数据 | | 八 | 烹饪完成 | `POST /neglect/ratio-scale/cook-orders/complete` | 按单烹制,任意状态提交均累加 | --- ## 一、食材模糊搜索 ``` POST /neglect/ratio-scale/mater-search Content-Type: application/json X-DEVICE-CODE: <设备编码> ``` 称量食材前,从食材库检索食材(设备对应食堂食材)。 ### 请求体 ```json { "keyword": "萝卜", // 选填 — 模糊匹配食材名称和别名 "pageNum": 1, // 必填 — 页码 "pageSize": 20 // 必填 — 每页条数 } ``` ### 响应 ```json { "code": "00000", "msg": "操作成功", "data": [ { "id": "1979825467306471454", // 食材id "materName": "樱桃萝卜", // 食材名称 "materCode": "04041012", // 食材编码 "alias": null, // 别名 "materFirstClass": "04", // 食材一级分类 "materSecondClass": "043", // 食材二级分类 "materUrl": null, // 食材图片url "canteenId": "2" // 归属食堂id } ], "total": 1 } ``` ### 字段说明 | 字段 | 类型 | 说明 | |------|------|------| | id | Long → String | 食材id(提交构成 items.materId 使用) | | materName | String | 食材名称 | | materCode | String | 食材编码(一级+二级+自编码,全局唯一) | | alias | String | 别名 | | materFirstClass | String | 食材一级分类 | | materSecondClass | String | 食材二级分类 | | materUrl | String | 食材图片url | | canteenId | Long → String | 归属食堂id | ### 逻辑说明 - 数据源:`nut_mater_base` - 过滤:`canteen_id` = 当前食堂(由 `X-DEVICE-CODE` 解析) - 关键词模糊匹配 `mater_name` 和 `alias` - 排序:`mater_name ASC` --- ## 二、搜菜品 ``` POST /neglect/ratio-scale/food-search Content-Type: application/json X-DEVICE-CODE: <设备编码> ``` 制作模式下,检索本食堂已上架的菜品,供后厨选择目标菜品。 ### 请求体 ```json { "keyword": "西兰花", // 选填 — 菜品名称或编号模糊搜索 "pageNum": 1, // 必填 — 页码 "pageSize": 20 // 必填 — 每页条数 } ``` ### 响应 ```json { "code": "00000", "msg": "操作成功", "data": [ { "foodId": "2091839370283118593", // 菜品id "foodCode": "FOOD00012", // 菜品编号 "foodName": "清炒西兰花" // 菜品名称 } ], "total": 1 } ``` ### 逻辑说明 - 数据源:`nut_food`(`status=1` 上架) - 过滤:`canteen_id` = 当前食堂 - 关键词模糊匹配 `food_name` 和 `food_code` - 排序:`id DESC` --- ## 三、查菜品构成 ``` GET /neglect/ratio-scale/food/{foodId}/composition X-DEVICE-CODE: <设备编码> ``` 制作模式下,根据菜品 id 查询已有菜品的主辅料+调料构成及熟重。 ### 响应 ```json { "code": "00000", "msg": "操作成功", "data": { "foodId": "2091839370283118593", // 菜品id "foodName": "清炒西兰花", // 菜品名称 "foodWeight": 450.0, // 熟重(g),从 nut_food_cook.output_weight 反查 "ingredients": [ { "materId": "1979825467306471454", // 食材id "materName": "樱桃萝卜", // 食材名称 "useWeight": 300.0, // 用量(g) "isMain": 1, // 1=主材 / 2=辅材 / 3=调料 "isMainName": "主材" } ] } } ``` ### 逻辑说明 - 数据源:`nut_food_composition`(按 `food_id` 查全部构成) - 食材名称从 `nut_mater_base` 批量反查 --- ## 四、提交构成(cookMode 区分采样/制作) ``` POST /neglect/ratio-scale/constitute/save Content-Type: application/json X-DEVICE-CODE: <设备编码> ``` **统一提交接口**,通过 `cookMode` 区分采样/制作。 ### 请求体 ```json { "cookMode": 2, // 必填 — 1=制作模式 / 2=采样模式 "foodId": 2091839370283118593, // 制作模式必填 — 已有菜品id "foodName": "清炒西兰花", // 采样模式必填 — 菜品名称 "foodWeight": 500, // 必填 — 熟重(g),必须 > 0 "mealType": 2, // 选填 — 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐 "items": [ // 必填 — 食材构成列表,至少1条 { "materId": 20001, // 必填 — 食材id(nut_mater_base.id) "useWeight": 300, // 必填 — 食材称量用量(g),必须 > 0 "isMain": 1 // 必填 — 食材分类: 1=主材 / 2=辅材 / 3=调料 }, { "materId": 30001, "useWeight": 50, "isMain": 3 } ] } ``` ### 响应 ```json { "code": "00000", "msg": "操作成功", "data": null } ``` ### 处理流程(采样模式 cookMode=2) ``` 1. 校验食材构成非空 + materId 非空且唯一 2. 校验菜品名称在同食堂下唯一(排除已驳回菜品,允许驳回后重新采样同名) 3. 生成菜品编号(FOOD + 5位序号) 4. 保存菜品 nut_food:data_source=2(配比秤采样) / audit_status=1(待审核) / status=0(下架) 5. 保存熟重 nut_food_cook:output_weight = foodWeight 6. 保存食材构成 nut_food_composition:mater_id + use_weight + is_main 7. 根据构成自动计算营养指标 nut_food_nutri + 膳食宝塔 nut_food_pagoda 8. 发起审核记录 nut_food_audit_record:audit_status=1(待审核) ``` ### 处理流程(制作模式 cookMode=1) ``` 1. 校验食材构成非空 + materId 非空且唯一 2. 校验菜品存在(foodId 必填) 3. 标记菜品 nut_food.data_source = 3(配比秤制作) 4. 保存制作记录 nut_food_matching_use:device_code + food_id + food_name + meal_type + day + food_weight(熟重) 5. 保存制作明细 nut_food_matching_use_item:mater_id + mater_name + use_weight + is_main(构成快照) 6. 不覆盖菜品的正式构成/营养/宝塔(历史参考) ``` ### 异常场景 | 场景 | 错误信息 | |------|---------| | 食材构成列表为空 | `请完善菜品构成信息` | | 食材id为空 | `菜品食材不能为空` | | 食材id重复 | `菜品食材不能重复,重复食材id:xxx` | | 制作方式非法 | `不支持的制作方式:xxx` | | 采样:同食堂已存在同名菜品(非驳回) | `该食堂下已存在同名餐品` | | 采样:食材id无效 | `存在无效的食材id` | | 制作:菜品不存在 | `菜品不存在` | | 熟重/用量 ≤ 0 | 参数校验:`熟重必须大于0` / `食材用量必须大于0` | ### 涉及数据表 **采样模式(cookMode=2)** | 表名 | 操作 | 说明 | |------|:--:|------| | `nut_food` | 新增 | 采样菜品主表(采样来源 + 待审核 + 下架) | | `nut_food_cook` | 新增 | 保存熟重 | | `nut_food_composition` | 新增 | 食材构成 | | `nut_food_nutri` | 新增 | 每100g营养指标(自动计算) | | `nut_food_pagoda` | 新增 | 膳食宝塔(自动计算) | | `nut_food_audit_record` | 新增 | 待审核记录 | **制作模式(cookMode=1)** | 表名 | 操作 | 说明 | |------|:--:|------| | `nut_food` | 更新 | 标记 data_source=3(配比秤制作) | | `nut_food_matching_use` | 新增 | 制作记录(熟重记录) | | `nut_food_matching_use_item` | 新增 | 制作明细(构成快照) | --- ## 五、采样历史 ``` POST /neglect/ratio-scale/sample/history Content-Type: application/json X-DEVICE-CODE: <设备编码> ``` 查询本食堂(由 `X-DEVICE-CODE` 解析)的采样提交历史,默认查询当天。 ### 请求体 ```json { "pageNum": 1, // 必填 — 页码 "pageSize": 10, // 必填 — 每页条数 "keyword": "西兰花", // 选填 — 菜品名称模糊搜索 "auditStatus": 1, // 选填 — 审核状态筛选: 1=待审核 / 2=已通过 / 3=已驳回 "sampleDate": "2026-09-09" // 选填 — 采样日期,不传默认当天 } ``` ### 响应 ```json { "code": "00000", "msg": "操作成功", "data": [ { "foodId": "2097486806812893186", // 菜品id "foodCode": "FOOD00030", // 菜品编号 "foodName": "清炒西兰花", // 菜品名称 "foodWeight": 500.0, // 熟重(g) "auditStatus": 1, // 审核状态: 1=待审核 / 2=已通过 / 3=已驳回 "auditStatusName": "待审核", // 审核状态中文 "auditRecordId": "2097486809375612929", // 审核记录id "createTime": "2026-09-09 08:46:39" // 提交时间 } ], "total": 1 } ``` ### 逻辑说明 - 数据源:`nut_food`(`data_source=2` 配比秤采样) - `canteenId` 由 `X-DEVICE-CODE` 解析终端后自动限定 - 排序:`create_time DESC` - `foodWeight` 从 `nut_food_cook.output_weight` 反查 - `auditRecordId` 从 `nut_food_audit_record` 反查 --- ## 六、制作历史 ``` POST /neglect/ratio-scale/make/history Content-Type: application/json X-DEVICE-CODE: <设备编码> ``` 查询本设备(由 `X-DEVICE-CODE` 解析)的制作记录。 ### 请求体 ```json { "pageNum": 1, // 必填 — 页码 "pageSize": 10, // 必填 — 每页条数 "day": "2026-09-09" // 选填 — 制作日期,不传查全部 } ``` ### 响应 ```json { "code": "00000", "msg": "操作成功", "data": [ { "id": "2097499999999999999", // 制作记录id "foodId": "2091839370283118593", // 菜品id "foodName": "清炒西兰花", // 菜品名称 "foodWeight": 500.0, // 熟重(g) "mealType": 2, // 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐 "mealTypeName": "午餐", "day": "2026-09-09", // 制作日期 "createTime": "2026-09-09 08:46:39" } ], "total": 1 } ``` ### 逻辑说明 - 数据源:`nut_food_matching_use` - 过滤:`device_code` = 当前设备(由 `X-DEVICE-CODE` 解析) - 排序:`create_time DESC` --- ## 七、制作历史图表(按菜品id) ``` GET /neglect/ratio-scale/make/history/{foodId} X-DEVICE-CODE: <设备编码> ``` 根据菜品 id 查询该菜品的所有制作记录,返回图表数据(对齐借鉴项目 `queryGoodsInfoByFoodIdMatchingList`),用于前端渲染制作趋势图。 ### 响应 ```json { "code": "00000", "msg": "操作成功", "data": [ { "xAxes": [ {"goodId": "", "goodName": "时间"}, {"goodId": "", "goodName": "熟重"}, {"goodId": "20001", "goodName": "樱桃萝卜"}, {"goodId": "30001", "goodName": "酱油"} ], "yAxes": [ {"value": "2026-09-09 08:46:39"}, {"value": 500.0}, {"value": 300.0}, {"value": 50.0} ] } ] } ``` ### 字段说明 | 字段 | 说明 | |------|------| | xAxes | 横轴模板:时间、熟重、该菜品所有出现过的食材(去重保序),所有记录共用 | | xAxes[].goodId | 食材id(时间/熟重项为空字符串) | | xAxes[].goodName | 项名称(时间/熟重/食材名称) | | yAxes | 纵轴值,与 xAxes 一一对应 | | yAxes[].value | 值:时间项为时间戳,其余为数值(缺失食材补 0) | ### 逻辑说明 - 数据源:`nut_food_matching_use`(主表)+ `nut_food_matching_use_item`(子表) - 主表按 `food_id` 过滤,`create_time DESC` 排序 - 子表按 `matching_use_id` 批量查询并分组(消除 N+1) - 时间项取 `create_time`,熟重项取 `food_weight`,食材项取 `use_weight`(缺失补 0) --- ## 八、烹饪完成(支持再次烹制) ``` POST /neglect/ratio-scale/cook-orders/complete Content-Type: application/json X-DEVICE-CODE: <设备编码> ``` 按单烹制提交。**改造点**:原逻辑一次性完成(`待烹制 → 已完成`),现支持**任意状态下再次烹制**——每次提交累加份数/生重/熟重并留存一条流水,不校验烹制状态,需求达标后仍可继续烹制(应对菜不够卖接着做的场景)。 ### 请求体 ```json { "cookOrderId": 10001, // 必填 — 烹制单id "portions": 10, // 选填 — 本次烹制份数,不传则按申领单/需求份数兜底 "cookStart": "2026-09-09 10:00:00", // 选填 — 本次烹制开始时间 "cookedWeight": 3.5, // 必填 — 本次熟重(kg) "items": [ // 必填 — 主辅材实际用量列表,至少1条 { "materId": 20001, // 必填 — 食材id "actualQty": 5000 // 必填 — 实际用量(g) } ], "seasonings": [ // 选填 — 调料用量列表 { "materId": 30001, "actualQty": 15.0 } ] } ``` ### 响应 ```json { "code": "00000", "msg": "操作成功", "data": null } ``` ### 状态流转规则(改造核心) ``` 每次提交后(不校验状态,任何状态下提交均累加): cooked_portions 累加 += 本次份数 raw_weight 累加 += 本次生重 cooked_weight 累加 += 本次熟重 cook_status → 2(已完成,每次提交都置此状态,允许继续烹制累加) 生熟比 = 累计熟重 / 累计生重(用累加后的值) 时间语义: cook_start = 首次烹制开始时间(后续烹制不覆盖) cook_end = 最后一次烹制结束时间(每次更新) ``` ### 处理流程 ``` 1. 校验烹制单存在 + 食堂权限(不校验烹制状态,任何状态下提交均累加数据) 2. 本次烹制份数 = 入参 portions(优先)→ 申领单 quantity → 需求份数 → 1 3. 首次烹制(cooked_portions=0)才完善组配任务并完成 4. 计算本次生重(Σ actualQty / 1000,g→kg) 5. 计算生熟比 = 累计熟重 / 累计生重(用累加后的值) 6. 写烹制流水 nut_prod_cook_flow(本次生重/熟重/份数/起止时间) 7. 保存食材明细 + 调料(首次更新预生成明细,再次插入新明细,均关联 flow_id) 8. 累加主表:份数/生重/熟重累加 + 状态置已完成 + 起止时间 ``` ### 异常场景 | 场景 | 错误信息 | |------|---------| | 烹制单不存在 | `烹制单不存在` | | 首次烹制未关联组配任务 | `烹制单未关联组配任务,无法开始烹制` | | 组配任务不存在 | `组配任务不存在,comboNo=xxx` | ### 涉及数据表 | 表名 | 操作 | 说明 | |------|:--:|------| | `nut_prod_cook_order` | 查询 + 更新 | 累加份数/生重/熟重 + 状态置已完成 | | `nut_prod_cook_flow` | 新增 | **烹制流水**,每次烹制一条 | | `nut_prod_cook_ingredient` | 查询 + 更新/新增 | 首次更新预生成明细,再次插入新明细,均带 `flow_id` | | `nut_prod_combo_task` / `_item` | 查询 + 更新 | 仅首次烹制时完善组配 | | `nut_sup_internal_supply` / `nut_sup_meal_package` / `_ingredient` | 查询 | 解析溯源码与份数 | | `nut_mater_base` / `nut_food_composition` | 查询 | 食材编码/名称/分类 | --- ## 数据链路图(采样模式 cookMode=2) ``` nut_mater_base (食材库) │ │ 采样称量 mater_id + use_weight(g) ▼ POST /constitute/save (cookMode=2) │ ├── 生成 nut_food (采样来源 + 待审核 + 下架) ├── 生成 nut_food_cook (熟重 = foodWeight) ├── 生成 nut_food_composition (食材构成) ├── 自动计算 nut_food_nutri (每100g营养) + nut_food_pagoda (膳食宝塔) └── 生成 nut_food_audit_record (待审核) ``` ## 数据链路图(制作模式 cookMode=1) ``` nut_food (已有菜品) │ │ 制作称量 mater_id + use_weight(g) + 熟重 ▼ POST /constitute/save (cookMode=1) │ ├── 标记 nut_food.data_source = 3 (配比秤制作) ├── 生成 nut_food_matching_use (制作记录:熟重/餐次/日期/设备) └── 生成 nut_food_matching_use_item (构成快照,历史参考) (不覆盖 nut_food_composition / nut_food_nutri / nut_food_pagoda) ``` ## 数据链路图(再次烹制) ``` nut_prod_cook_order (烹制单,累加数据) │ ├── POST /cook-orders/complete (每次烹制) │ ├── 生成 nut_prod_cook_flow (流水,每次一条) │ └── 生成/更新 nut_prod_cook_ingredient (明细,带 flow_id) │ └── 主表累加: cooked_portions / raw_weight / cooked_weight cook_start = 首次开始 / cook_end = 末次结束 cook_status = 已完成(每次提交都置此状态,允许继续烹制累加) ```