feat(ratio-scale): 新增烹饪时长采集及调料放置时间支持

- 配比秤采样提交接口新增字段 cookStart,传递烹饪开始时间
- 食材构成中调料项新增 putTime 字段,记录调料首次使用时间
- 设备端提交采样时食材构造调整,调料按名称匹配 putTime 填充
- 新增对烹饪时长的后端计算功能,时长单位为分钟自动向上取整
- 采样列表接口响应新增 cookStart 与 cookDuration 字段,用于显示烹饪时长
- 配比秤采样模式相关API文档同步更新,废弃制作模式接口及字段
- 烹饪完成接口要求强化传递 cookStart,后端基于此计算并更新烹饪时长
- 关联数据库迁移支持烹饪时长字段持久化及历史记录完善
This commit is contained in:
mazengfei
2026-09-21 14:21:17 +08:00
parent 7b66652c0b
commit 94bcdb3d9b
4 changed files with 295 additions and 426 deletions
@@ -130,7 +130,9 @@ data class ConstituteSaveItem(
/** 食材称量用量(g),必须 > 0 */ /** 食材称量用量(g),必须 > 0 */
val useWeight: Double = 0.0, val useWeight: Double = 0.0,
/** 食材分类: 1=主材 / 2=辅材 / 3=调料 */ /** 食材分类: 1=主材 / 2=辅材 / 3=调料 */
val isMain: Int = 0 val isMain: Int = 0,
/** 调料首次使用时间 yyyy-MM-dd HH:mm:ss,仅调料需要,主辅材留空;字段与制作模式 CookCompleteItem 对齐 */
val putTime: String? = null
) )
/** /**
@@ -141,6 +143,8 @@ data class ConstituteSaveItem(
data class ConstituteSaveRequest( data class ConstituteSaveRequest(
/** 菜品名称 */ /** 菜品名称 */
val foodName: String? = null, val foodName: String? = null,
/** 烹饪开始时间 yyyy-MM-dd HH:mm:ss,进入提交页面时刻;字段与制作模式 CookCompleteRequest 对齐 */
val cookStart: String? = null,
/** 熟重(g),必须 > 0 */ /** 熟重(g),必须 > 0 */
val foodWeight: Double = 0.0, val foodWeight: Double = 0.0,
/** 食材构成列表,至少 1 条 */ /** 食材构成列表,至少 1 条 */
@@ -174,6 +178,10 @@ data class SampleHistoryItem(
val foodName: String = "", val foodName: String = "",
/** 熟重(g) */ /** 熟重(g) */
val foodWeight: Double = 0.0, val foodWeight: Double = 0.0,
/** 烹饪开始时间,提交时未传则为 null */
val cookStart: String? = null,
/** 烹饪时长(分钟),后端按 提交时间-烹饪开始时间 向上取整计算;未传 cookStart 则为 null */
val cookDuration: Int? = null,
/** 状态: 1=实验中 / 2=已评测 / 9=已转化 */ /** 状态: 1=实验中 / 2=已评测 / 9=已转化 */
val status: Int = 0, val status: Int = 0,
/** 状态中文 */ /** 状态中文 */
@@ -563,13 +563,17 @@ class SubmitFoodActivity : BaseActivity() {
} }
// 仅保留用量 > 0 的食材;materId 优先取 materId,为空时回退 goodsId // 仅保留用量 > 0 的食材;materId 优先取 materId,为空时回退 goodsId
// 调料(isMain=3)按 goodsName 匹配首次使用时间填入 putTime,主辅材留空
val items = allGoods val items = allGoods
.filter { (it.useWeight ?: 0.0) > 0.0 } .filter { (it.useWeight ?: 0.0) > 0.0 }
.map { .map {
ConstituteSaveItem( ConstituteSaveItem(
materId = it.materId?.takeIf { mid -> mid.isNotEmpty() } ?: it.goodsId, materId = it.materId?.takeIf { mid -> mid.isNotEmpty() } ?: it.goodsId,
useWeight = it.useWeight ?: 0.0, useWeight = it.useWeight ?: 0.0,
isMain = it.materialType isMain = it.materialType,
putTime = if (it.materialType == 3) {
it.goodsName?.let { name -> seasoningFirstUseMap[name] }
} else null
) )
} }
if (items.isEmpty()) { if (items.isEmpty()) {
@@ -591,6 +595,7 @@ class SubmitFoodActivity : BaseActivity() {
val request = ConstituteSaveRequest( val request = ConstituteSaveRequest(
foodName = foodName, foodName = foodName,
cookStart = cookStartTime,
foodWeight = foodWeight, foodWeight = foodWeight,
items = items items = items
) )
+162 -422
View File
@@ -1,42 +1,45 @@
# 配比秤(新设备)— 采样与制作模式 API 文档 # 配比秤(新设备)— 采样模式 API 文档
> Controller: `NutRatioScaleController` > Controller: `NutRatioScaleController`
> 路径前缀: `/neglect/ratio-scale`Nacos 白名单 `/nutrition/neglect/**`,无需 Sa-Token > 路径前缀: `/neglect/ratio-scale`Nacos 白名单 `/nutrition/neglect/**`,无需 Sa-Token
> 设备上下文通过请求头 `X-DEVICE-CODE` 解析(`TerminalContextHelper`,据此拿到食堂 id > 设备上下文通过请求头 `X-DEVICE-CODE` 解析(`TerminalContextHelper`,据此拿到食堂 id
> 日期: 2026-09-09 > 日期: 2026-09-10
> >
> **序列化规则**:Long 类型字段响应中均为字符串(Jackson `ToStringSerializer`),`BigDecimal` 为普通数字 > **序列化规则**:Long 类型字段响应中均为字符串(Jackson `ToStringSerializer`),`BigDecimal` 为普通数字
> **单位约定** > **单位约定**
> - 提交构成的熟重 `foodWeight` 单位为 **克 (g)**(与 `nut_food_cook.output_weight` 口径一致) > - 提交采样的熟重 `foodWeight` 单位为 **克 (g)**
> - 食材称量用量 `useWeight` 单位为 **克 (g)** > - 食材称量用量 `useWeight` 单位为 **克 (g)**
> - 烹制单生重 `rawWeight`、熟重 `cookedWeight` 单位为 **千克 (kg)**
> - 食材实际用量 `actualQty` 单位为 **克 (g)**
--- ---
## 业务背景 ## 业务背景
配比秤新增「采样模式」与「制作模式」,两者通过**同一个提交接口** `POST /constitute/save``cookMode` 字段区分: 配比秤「采样」复用 **味养创新(nut_innovation** 的人工创新实验流程,不再直接落 `nut_food` 主表。
| cookMode | 模式 | 说明 | ### 完整流程(3 步)
|:--------:|------|------|
| 2 | 采样模式 | **从零建菜**:称量食材 → 新增菜品(生成构成/营养/宝塔)→ 进入「待审核」→ 后台审核通过后上架 |
| 1 | 制作模式 | **基于已有菜品**:称量食材 → 记录熟重+构成快照(历史参考,不覆盖正式构成)→ 标记菜品为「配比秤制作」 |
### 制作方式枚举 ```
① 提交采样(设备端) → 落库味养创新实验,状态 = 1 实验中
② 提交评测(味养创新后台)→ 计算营养 + 完善检验信息,状态 = 2 已评测
③ 转化餐品(味养创新后台)→ 走 nut_food 新建餐品流程关联,状态 = 9 已转化
```
| 值 | 名称 | 菜品对象 | 数据来源 data_source | ### 设备端职责边界
|:--:|------|---------|---------------------|
| 1 | 制作模式 | 已有菜品(foodId 必填) | 3(配比秤制作) |
| 2 | 采样模式 | 新增菜品(foodName 必填) | 2(配比秤采样) |
### 采样菜品审核状态枚举 配比秤设备端只负责 **① 提交采样**(称量食材 + 熟重 → 写味养创新实验)。后续的 **② 提交评测**、**③ 转化餐品** 走味养创新已有的后台接口(`/nut/innovation`),不在设备端范围内。
| 状态码 | 状态名 | 说明 | ### 采样交互流程
|:------:|--------|------|
| 1 | 待审核 | 采样提交后的初始状态,后台可审核 | ```
| 2 | 已通过 | 审核通过,菜品上架生效 | 搜菜品(food-search
| 3 | 已驳回 | 审核驳回,菜品保持下架,可重新采样同名菜品 |
├─ 有目标餐品 → 查菜品构成(food/{foodId}/composition)回显 → 可增删构成
└─ 无目标餐品 → 食材模糊搜索(mater-search)自己添加食材/调料
提交采样(constitute/save)→ 落库味养创新
```
### 食材分类枚举 ### 食材分类枚举
@@ -52,18 +55,101 @@
| 序号 | 接口 | 路径 | 用途 | | 序号 | 接口 | 路径 | 用途 |
|:----:|------|------|------| |:----:|------|------|------|
| 一 | 食材模糊搜索 | `POST /neglect/ratio-scale/mater-search` | 称量前检索食材(设备对应食堂食材库) | | 一 | 搜菜品 | `POST /neglect/ratio-scale/food-search` | 按名称模糊查上架菜品 |
| 二 | 菜品 | `POST /neglect/ratio-scale/food-search` | 制作模式选已有菜品(按名称模糊查上架菜品) | | 二 | 菜品构成 | `GET /neglect/ratio-scale/food/{foodId}/composition` | 按菜品id查主辅料+调料,回显构成 |
| 三 | 查菜品构成 | `GET /neglect/ratio-scale/food/{foodId}/composition` | 制作模式查看已有菜品的主辅料+调料构成 | | 三 | 食材模糊搜索 | `POST /neglect/ratio-scale/mater-search` | 采样称量前检索食材 |
| 四 | 提交构成 | `POST /neglect/ratio-scale/constitute/save` | 统一提交,cookMode 区分制作/采样 | | 四 | 提交采样 | `POST /neglect/ratio-scale/constitute/save` | **落库味养创新实验** |
| 五 | 采样历史 | `POST /neglect/ratio-scale/sample/history` | 查询本食堂采样提交历史(默认当天 | | 五 | 已提交采样列表 | `POST /neglect/ratio-scale/sample-list` | 按日期查已提交采样,默认当天 |
| 六 | 制作历史 | `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/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)
"ingredients": [
{
"materId": "1979825467306471454", // 食材id
"materName": "樱桃萝卜", // 食材名称
"useWeight": 300.0, // 用量(g)
"isMain": 1, // 1=主材 / 2=辅材 / 3=调料
"isMainName": "主材"
}
]
}
}
```
### 逻辑说明
- 数据源:`nut_food_composition`(按 `food_id` 查全部构成)
- 食材名称从 `nut_mater_base` **批量反查**(无 N+1
- `foodWeight``nut_food_cook.output_weight` 反查
---
## 三、食材模糊搜索
``` ```
POST /neglect/ratio-scale/mater-search POST /neglect/ratio-scale/mater-search
@@ -105,19 +191,6 @@ X-DEVICE-CODE: <设备编码>
} }
``` ```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| 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` - 数据源:`nut_mater_base`
@@ -127,92 +200,7 @@ X-DEVICE-CODE: <设备编码>
--- ---
## 二、搜菜品 ## 四、提交采样(落库味养创新)
```
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 POST /neglect/ratio-scale/constitute/save
@@ -220,17 +208,16 @@ Content-Type: application/json
X-DEVICE-CODE: <设备编码> X-DEVICE-CODE: <设备编码>
``` ```
**统一提交接口**,通过 `cookMode` 区分采样/制作 > ⚠️ **接口变更标注**:原提交接口含 `cookMode`(区分制作/采样),现制作模式已废弃,本接口统一为「提交采样」,入参去掉 `cookMode`、`foodId` 字段
后厨称量食材 + 熟重后提交,后端落库到 **味养创新实验**`nut_innovation`,人工创新,状态 = 实验中)。
### 请求体 ### 请求体
```json ```json
{ {
"cookMode": 2, // 必填 — 1=制作模式 / 2=采样模式 "foodName": "清炒西兰花", // 必填 — 菜品名称
"foodId": 2091839370283118593, // 制作模式必填 — 已有菜品id
"foodName": "清炒西兰花", // 采样模式必填 — 菜品名称
"foodWeight": 500, // 必填 — 熟重(g),必须 > 0 "foodWeight": 500, // 必填 — 熟重(g),必须 > 0
"mealType": 2, // 选填 — 餐次: 1=早餐/2=午餐/3=晚餐/4=加餐
"items": [ // 必填 — 食材构成列表,至少1条 "items": [ // 必填 — 食材构成列表,至少1条
{ {
"materId": 20001, // 必填 — 食材idnut_mater_base.id "materId": 20001, // 必填 — 食材idnut_mater_base.id
@@ -256,29 +243,24 @@ X-DEVICE-CODE: <设备编码>
} }
``` ```
### 处理流程(采样模式 cookMode=2 ### 处理流程
``` ```
1. 校验食材构成非空 + materId 非空且唯一 1. 校验食材构成非空 + materId 非空且唯一
2. 校验菜品名称在同食堂下唯一(排除已驳回菜品,允许驳回后重新采样同名 2. 反查食材名称(nut_mater_base + 一级分类名称(nut_mater_class
3. 生成菜品编号(FOOD + 5位序号) 3. 构建味养创新实验 DTO
4. 保存菜品 nut_fooddata_source=2(配比秤采样) / audit_status=1(待审核) / status=0(下架) - foodName → food_name
5. 保存熟重 nut_food_cookoutput_weight = foodWeight - foodWeight food_weight(熟重)
6. 保存食材构成 nut_food_compositionmater_id + use_weight + is_main - items → 食材构成(mater_id + mater_name + mater_cat + amount_g + is_main
7. 根据构成自动计算营养指标 nut_food_nutri + 膳食宝塔 nut_food_pagoda - canteenId → 从 X-DEVICE-CODE 解析
8. 发起审核记录 nut_food_audit_recordaudit_status=1(待审核) - startDate → 回填当天(实验开始日期)
- source / deviceCode → 来源标识(2) + 设备编码
- innovGoal → 默认值「配比秤采样」
4. 调用味养创新 manualAdd,落库 nut_innovationstatus=1 实验中)+ nut_innovation_ing(食材构成)
5. 自动初始化营养:按食材构成 + 熟重计算每100g熟重营养,写入草稿评测事件 nut_innovation_evalstatus=0+ 营养维度 nut_innovation_eval_nutri7 项:热量/蛋白/碳水/脂肪/维C/膳食纤维/钠,mineral 留空)
``` ```
### 处理流程(制作模式 cookMode=1 > 注:设备端不传 `foodCategory`(餐品分类),落库为空,后期在味养创新后台维护;`innovGoal`(创新目标)后端填默认值「配比秤采样」,后续可改。
```
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. 不覆盖菜品的正式构成/营养/宝塔(历史参考)
```
### 异常场景 ### 异常场景
@@ -287,54 +269,37 @@ X-DEVICE-CODE: <设备编码>
| 食材构成列表为空 | `请完善菜品构成信息` | | 食材构成列表为空 | `请完善菜品构成信息` |
| 食材id为空 | `菜品食材不能为空` | | 食材id为空 | `菜品食材不能为空` |
| 食材id重复 | `菜品食材不能重复,重复食材id:xxx` | | 食材id重复 | `菜品食材不能重复,重复食材id:xxx` |
| 制作方式非法 | `不支持的制作方式:xxx` |
| 采样:同食堂已存在同名菜品(非驳回) | `该食堂下已存在同名餐品` |
| 采样:食材id无效 | `存在无效的食材id` |
| 制作:菜品不存在 | `菜品不存在` |
| 熟重/用量 ≤ 0 | 参数校验:`熟重必须大于0` / `食材用量必须大于0` | | 熟重/用量 ≤ 0 | 参数校验:`熟重必须大于0` / `食材用量必须大于0` |
### 涉及数据表 ### 涉及数据表
**采样模式(cookMode=2**
| 表名 | 操作 | 说明 | | 表名 | 操作 | 说明 |
|------|:--:|------| |------|:--:|------|
| `nut_food` | 新增 | 采样菜品主表(采样来源 + 待审核 + 下架 | | `nut_innovation` | 新增 | 味养创新实验(status=1 实验中,含熟重 food_weight |
| `nut_food_cook` | 新增 | 保存熟重 | | `nut_innovation_ing` | 新增 | 食材构成(mater_id + mater_name + mater_cat + amount_g + is_main |
| `nut_food_composition` | 新增 | 食材构成 | | `nut_innovation_eval` | 新增 | 草稿评测事件(status=0,采样时初始化) |
| `nut_food_nutri` | 新增 | 每100g营养指标(自动计算 | | `nut_innovation_eval_nutri` | 新增 | 营养维度(7 项自动计算值,mineral 留空 |
| `nut_food_pagoda` | 新增 | 膳食宝塔(自动计算 | | `nut_mater_base` | 查询 | 反查食材名称 + 营养数据(计算营养 |
| `nut_food_audit_record` | 新增 | 待审核记录 | | `nut_mater_class` | 查询 | 反查一级分类名称 |
**制作模式(cookMode=1**
| 表名 | 操作 | 说明 |
|------|:--:|------|
| `nut_food` | 更新 | 标记 data_source=3(配比秤制作) |
| `nut_food_matching_use` | 新增 | 制作记录(熟重记录) |
| `nut_food_matching_use_item` | 新增 | 制作明细(构成快照) |
--- ---
## 五、采样历史 ## 五、已提交采样列表
``` ```
POST /neglect/ratio-scale/sample/history POST /neglect/ratio-scale/sample-list
Content-Type: application/json Content-Type: application/json
X-DEVICE-CODE: <设备编码> X-DEVICE-CODE: <设备编码>
``` ```
查询本食堂(由 `X-DEVICE-CODE` 解析)的采样提交历史,默认查询当天 查询本食堂已提交的采样记录(味养创新实验 `source=2`),分页返回、按提交时间倒序,供硬件端回看
### 请求体 ### 请求体
```json ```json
{ {
"pageNum": 1, // 必填 — 页码 "pageNum": 1, // 必填 — 页码
"pageSize": 10, // 必填 — 每页条数 "pageSize": 20 // 必填 — 每页条数
"keyword": "西兰花", // 选填 — 菜品名称模糊搜索
"auditStatus": 1, // 选填 — 审核状态筛选: 1=待审核 / 2=已通过 / 3=已驳回
"sampleDate": "2026-09-09" // 选填 — 采样日期,不传默认当天
} }
``` ```
@@ -346,14 +311,13 @@ X-DEVICE-CODE: <设备编码>
"msg": "操作成功", "msg": "操作成功",
"data": [ "data": [
{ {
"foodId": "2097486806812893186", // 菜品id "innovId": "2097486806812893186", // 味养创新实验id
"foodCode": "FOOD00030", // 菜品编号 "innovNo": "INN-H-2026-001", // 实验编号
"foodName": "清炒西兰花", // 品名称 "foodName": "清炒西兰花", // 品名称
"foodWeight": 500.0, // 熟重(g) "foodWeight": 500.0, // 熟重(g)
"auditStatus": 1, // 审核状态: 1=待审核 / 2=已通过 / 3=已驳回 "status": 1, // 状态: 1实验中/2已评测/9已转化
"auditStatusName": "待审核", // 审核状态中文 "statusName": "实验中", // 状态中文
"auditRecordId": "2097486809375612929", // 审核记录id "createTime": "2026-09-11 10:30:00" // 提交时间
"createTime": "2026-09-09 08:46:39" // 提交时间
} }
], ],
"total": 1 "total": 1
@@ -362,250 +326,26 @@ X-DEVICE-CODE: <设备编码>
### 逻辑说明 ### 逻辑说明
- 数据源:`nut_food``data_source=2` 配比秤采样) - 数据源:`nut_innovation`
- `canteenId` `X-DEVICE-CODE` 解析终端后自动限定 - 过滤:`canteen_id` = 当前食堂 + `source` = 2(配比秤采样,后台手动为 1
- 排序:`create_time DESC` - 排序:`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 (食材库) nut_mater_base (食材库)
采样称量 mater_id + use_weight(g) │ 称量 mater_id + use_weight(g) + 熟重 food_weight(g)
POST /constitute/save (cookMode=2) POST /constitute/save
├── 生成 nut_food (采样来源 + 待审核 + 下架) ├── 生成 nut_innovation (味养创新实验,status=1 实验中,含 food_weight)
├── 生成 nut_food_cook (熟重 = foodWeight) ├── 生成 nut_innovation_ing (食材构成:mater_id + mater_name + mater_cat + amount_g + is_main)
── 生成 nut_food_composition (食材构成) ── 自动算营养 → nut_innovation_eval(草稿) + nut_innovation_eval_nutri(7项)
├── 自动计算 nut_food_nutri (每100g营养) + nut_food_pagoda (膳食宝塔)
└── 生成 nut_food_audit_record (待审核)
```
## 数据链路图(制作模式 cookMode=1
``` 后续(味养创新后台,非设备端):
nut_food (已有菜品) ├── 提交评测 → 复核营养 + 补味道/口感/色泽,status=2 已评测
└── 转化餐品 → nut_food(新建餐品流程),convert_food_id 关联,status=9 已转化
│ 制作称量 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 = 已完成(每次提交都置此状态,允许继续烹制累加)
``` ```
@@ -0,0 +1,116 @@
# 配比秤烹饪时长 - 设备端对接文档
> 本次变更:配比秤新增「烹饪时长」采集与计算能力。设备端只需**传烹饪开始时间**和**调料放置时间**,时长由后端自动计算落库,设备端不需要自己算。
>
> 部署依赖:数据库迁移 `V21__nut_innovation_cook_duration.sql` 需先于服务发布执行。
---
## 通用说明
- 时间格式统一为 `yyyy-MM-dd HH:mm:ss`
- 时长单位:**分钟**,由后端计算,规则:`时长 = 接口调用时间 - 烹饪开始时间`,**向上取整,正数至少 1 分钟**(如 90 秒 → 2 分钟,30 秒 → 1 分钟)
- 烹饪开始时间为空、或晚于当前时间(无效)时,后端**跳过时长计算**,接口正常成功
---
## 1. 提交采样(新增入参字段)
**POST** `/nutrition/neglect/ratio-scale/constitute/save`
### 请求参数(仅列出新增字段,其余字段不变)
| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| cookStart | string | 建议 | **新增**。烹饪开始时间,用于与提交时间计算烹饪时长(分钟) |
| items[].putTime | string | - | **新增**。调料放置时间,仅调料项(`isMain=3`)传,主材/辅材不传 |
### 完整请求示例
```json
{
"foodName": "低钠宫保鸡丁",
"foodWeight": 1250.5,
"cookStart": "2026-09-21 10:30:00",
"items": [
{ "materId": 1001, "useWeight": 500, "isMain": 1 },
{ "materId": 1002, "useWeight": 200, "isMain": 2 },
{ "materId": 1003, "useWeight": 15, "isMain": 3, "putTime": "2026-09-21 10:32:10" },
{ "materId": 1004, "useWeight": 8, "isMain": 3, "putTime": "2026-09-21 10:35:40" }
]
}
```
### 后端行为
- 烹饪开始时间落库 `nut_innovation.cook_start`,时长落库 `nut_innovation.cook_duration`,调料放置时间落库 `nut_innovation_ing.put_time`
- 响应结构不变(`Result<Void>`
---
## 2. 烹饪完成(入参字段无变化,传值要求强化)
**POST** `/nutrition/neglect/ratio-scale/cook-orders/complete`
### 相关字段
| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| cookOrderId | long | ✓ | 烹制单 id(原有) |
| cookStart | string | **强烈建议** | 烹饪开始时间(字段原有)。**本次开始烹饪时务必回传**,否则不计算时长 |
| seasonings[].putTime | string | - | 调料放置时间(字段原有),继续按原逻辑传 |
### 后端行为(新增)
- 每次完成按 `本次 cookStart → 接口调用时间` 计算时长,**更新菜品烹饪工艺表 `nut_food_cook.cook_duration`**
- **多次烹饪时长不累计、各算各的**:同一烹制单多次提交时,每次都用本次的时长**覆盖**菜品烹饪时长
- 同时写一条餐品操作记录(后台可见):操作标题「配比秤烹饪餐品计算」,记录时长从 X 分钟更新为 Y 分钟
- `cookStart` 为空时跳过以上更新,其余原有逻辑(流水、生熟比、累加)不受影响
---
## 3. 已提交采样列表(响应新增字段)
**POST** `/nutrition/neglect/ratio-scale/sample-list`
### 响应 `data.list[]` 新增字段
| 字段 | 类型 | 说明 |
|------|------|------|
| cookStart | string | **新增**。烹饪开始时间;提交时未传则为 null |
| cookDuration | int | **新增**。烹饪时长(分钟);提交时未传 cookStart 则为 null |
### 响应示例
```json
{
"code": "00000",
"msg": "操作成功",
"data": {
"list": [
{
"innovId": 1930123456789,
"innovNo": "INN-H-2026-041",
"foodName": "低钠宫保鸡丁",
"foodWeight": 1250.5,
"cookStart": "2026-09-21 10:30:00",
"cookDuration": 12,
"status": 1,
"statusName": "实验中",
"createTime": "2026-09-21 10:42:03"
}
],
"total": 1
}
}
```
---
## 设备端改动清单
| 接口 | 改动 |
|------|------|
| constitute/save | 请求新增 `cookStart` + 调料项 `putTime`(开始烹饪时记录时间,放调料时记录时间) |
| cook-orders/complete | 字段无变化,确保每次提交回传 `cookStart``seasonings[].putTime` 继续传 |
| sample-list | 响应新增 `cookDuration`,列表可展示「烹饪时长(分钟)」 |