Files
DishMatch/配比秤-采样模式-API文档.md
mazengfei 6d3c5490a4 feat(food-prepare): 增加食材信息录入卡及相关接口支持
- 新增手动录入食材信息的UI布局,包含食材名称搜索、主辅材选择及用量输入
- 整合采样模式下的食材搜索弹窗,支持食材模糊查询与净材种类选择
- 新增相关网络接口调用,实现食材搜索、菜品搜索、菜品构成查询及采样历史等功能
- 优化食材列表展示,支持采样历史审核状态和熟重显示,增加颜色区分
- 修改PrepareFoodActivity,采样模式启用手动新增食材功能,包含添加、重置操作
- 调整Adapter和ViewModel,兼容新接口数据模型,支持分页加载和状态回调
- 修正接口路径和网络请求地址,统一使用nutrition/neglect路径前缀
- 删除冗余代码和未使用的旧接口代码,简化搜索逻辑及界面响应机制
2026-09-10 13:41:35 +08:00

612 lines
19 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 文档
> 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, // 必填 — 食材idnut_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_fooddata_source=2(配比秤采样) / audit_status=1(待审核) / status=0(下架)
5. 保存熟重 nut_food_cookoutput_weight = foodWeight
6. 保存食材构成 nut_food_compositionmater_id + use_weight + is_main
7. 根据构成自动计算营养指标 nut_food_nutri + 膳食宝塔 nut_food_pagoda
8. 发起审核记录 nut_food_audit_recordaudit_status=1(待审核)
```
### 处理流程(制作模式 cookMode=1
```
1. 校验食材构成非空 + materId 非空且唯一
2. 校验菜品存在(foodId 必填)
3. 标记菜品 nut_food.data_source = 3(配比秤制作)
4. 保存制作记录 nut_food_matching_usedevice_code + food_id + food_name + meal_type + day + food_weight(熟重)
5. 保存制作明细 nut_food_matching_use_itemmater_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 / 1000g→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 = 已完成(每次提交都置此状态,允许继续烹制累加)
```