Files
SmartPlateCabinet/app/吐盘机接口文档.md
T
mazengfei 524837ec20 feat(face): 支持按userFaceId查询单条人脸记录
- FaceDao新增queryByUserFaceId接口,实现按userFaceId精确查询
- FaceApi新增queryByUserFaceId方法,封装Dao查询逻辑
- LoginByFaceActivity插入人脸时,先按userFaceId查询,存在则更新避免重复
- NetViewModelV2同步人脸数据时,优先用userFaceId判重,减少重复入库
- 修正FaceEntity中userId字段数据库列名错误(user_id替代user_idd)
- 增加注释说明及代码格式优化,提升代码可读性和健壮性
2026-08-18 09:12:40 +08:00

1273 lines
42 KiB
Markdown
Raw 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.
# 吐盘机(营养秤)接口文档
> Controller`NutPickupScaleController`
> 模块前缀:`/neglect/pickup`
> Nacos 白名单:`/nutrition/neglect/**`(无需 Sa-Token
> 设备上下文:请求头 `X-DEVICE-CODE`
> 生成日期:2026-06-29
> 对照老版:`Downloads/营养秤_API_DOC.md`v1.0.02026-05-26
---
## 一、服务器环境
| 环境 | 地址 | 端口 |
|------|------|------|
| 本地 | `http://localhost` | `24810` |
| 测试 | `https://dev.yixiong-tech.com` | `8083` |
| 生产 | `https://api.dm.yixiong-tech.com` | `8443` |
接口完整地址 = `{baseUrl}/neglect/pickup/{sub-path}`
---
## 二、请求头(全局)
| Header | 是否必填 | 说明 |
|--------|---------|------|
| `Content-Type` | 是 | `application/json`(上传接口 `multipart/form-data` |
| `X-DEVICE-CODE` | 是 | 设备唯一编码,由 `GlobalData.deviceId` 注入;服务端据此解析食堂、餐次等上下文 |
| `X-Access-Token` | 否 | JWT Token,吐盘机白名单接口不强制校验 |
| `authorization` | 否 | 固定授权码 `57ee87183f2a4fa59683ec9ef41c8f5d` |
---
## 三、通用响应格式
所有接口统一返回 `Result<T>`
```json
{
"code": "00000",
"msg": "success",
"data": { },
"total": 0
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | String | `"00000"` 表示成功,其它为业务/系统错误码 |
| `msg` | String | 响应描述 |
| `data` | T | 业务数据,类型随接口变化 |
| `total` | int | 分页总条数,不分页接口为 `0` |
> Long 类型 ID 在 JSON 中序列化为 String,避免 JS 精度丢失。
---
## 四、接口详情
### 4.1 人脸数据同步
---
#### 接口 P-01 — 获取全量人脸数据
**POST** `/neglect/pickup/face/page`
设备开机或重置本地特征库时调用,分页拉取全量人脸特征。
**入参(Body JSON**`BasePageQuery`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | Long | 是 | 页码,从 1 开始 |
| `pageSize` | Long | 是 | 每页条数,建议 100 |
| `column` | String | 否 | 排序字段 |
| `order` | String | 否 | 排序方向(asc/desc |
**入参示例**
```json
{
"pageNum": 1,
"pageSize": 100
}
```
**响应 `data`**`List<NutTerminalAppFaceVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `userFaceId` | String | 人脸记录 id |
| `userId` | String | 用户 id |
| `faceFeature` | String | 人脸特征码(Base64 字符串) |
| `faceUpdateTimestamp` | Long | 人脸更新时间戳(毫秒) |
| `cardNo` | String | 会员卡号;非会员或未发卡为空 |
| `member` | Boolean | 是否会员 |
| `faceDeleted` | Boolean | 是否已删除(true 表示用户/人脸已注销,APP 端需清理本地缓存) |
| `personType` | String | 人员类型:1-员工,2-工作人员,3-普通用户,4-外部人员,5-会员 |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": [
{
"userFaceId": "1024",
"userId": "88001",
"faceFeature": "ArcFaceBase64...",
"faceUpdateTimestamp": 1748000000000,
"cardNo": "M2024001",
"member": true,
"faceDeleted": false,
"personType": "5"
}
],
"total": 128
}
```
---
#### 接口 P-02 — 获取增量人脸数据
**POST** `/neglect/pickup/face/increment`
按时间戳增量同步,仅返回 `update_time` 严格大于 `timestamp` 的记录(含已删除记录,APP 端据此清理本地缓存)。
**入参(Body JSON**`NutTerminalAppFaceIncrementQuery`(继承 `BasePageQuery`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | Long | 是 | 页码 |
| `pageSize` | Long | 是 | 每页条数 |
| `column` | String | 否 | 排序字段(继承) |
| `order` | String | 否 | 排序方向(继承) |
| `timestamp` | Long | 是 | 上次同步时间戳(毫秒),首次同步传 0 |
**入参示例**
```json
{
"pageNum": 1,
"pageSize": 100,
"timestamp": 1748000000000
}
```
**响应 `data`** — 同 P-01`List<NutTerminalAppFaceVO>`
---
### 4.2 菜品查询
---
#### 接口 P-03 — 按名称模糊搜索菜品
**POST** `/neglect/pickup/food/search`
按菜品名称关键词分页模糊搜索。结果按当前食堂 + 当餐次过滤。
**入参(Body JSON**`NutTerminalAppFoodSearchQuery`(继承 `BasePageQuery`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | Long | 是 | 页码 |
| `pageSize` | Long | 是 | 每页条数 |
| `name` | String | 否 | 菜品名称关键词,不传则返回当餐全部菜品 |
**入参示例**
```json
{
"pageNum": 1,
"pageSize": 20,
"name": "宫保鸡丁"
}
```
**响应 `data`**`List<NutTerminalAppFoodVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `cookOrderId` | String | 按单烹制 id(就餐时需回传给下单接口) |
| `foodId` | String | 菜品 id |
| `foodName` | String | 菜品名称 |
| `calorie` | BigDecimal | 能量(kcal/100g |
| `protein` | BigDecimal | 蛋白质(g/100g |
| `fat` | BigDecimal | 脂肪(g/100g |
| `carbohydrate` | BigDecimal | 碳水化合物(g/100g |
| `price` | BigDecimal | 称重基础价(元/100g),对应 `nut_food.weight_base_price` |
| `specPrice` | BigDecimal | 默认规格售价(元) |
| `vipPrice` | BigDecimal | VIP 售卖价;新系统暂无该概念,固定返回 `null` |
| `specWeight` | BigDecimal | 默认规格重量(g |
| `stapleFood` | BigDecimal | 主食占比(g/100g):grain + potato |
| `fruitsVegetables` | BigDecimal | 果蔬占比(g/100g):vegetable + fruits |
| `meatEggs` | BigDecimal | 肉蛋占比(g/100g):livestock + birds + aquatic + egg |
| `recommendCalorie` | BigDecimal | 推荐能量;菜品层面无意义,固定 `0` |
| `foodMaterialId` | String | 餐品营养表 id(默认主料 mater_id |
| `specId` | String | 默认规格 id |
| `foodImg` | String | 菜品图片 URL(首张图片型 media) |
| `tablewareStatus` | Boolean | 是否含餐具;当前未建模,固定 `false` |
| `tablewareWeight` | Integer | 餐具重量(g);当前未建模,固定 `0` |
| `foodLabel` | String | 菜品标签(`tags` 字段) |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": [
{
"cookOrderId": "CO20260629-001",
"foodId": "2001",
"foodName": "宫保鸡丁",
"calorie": 195.5,
"protein": 8.2,
"fat": 12.1,
"carbohydrate": 9.3,
"price": 0.28,
"specPrice": 6.00,
"vipPrice": null,
"specWeight": 200,
"stapleFood": 9.3,
"fruitsVegetables": 2.0,
"meatEggs": 8.2,
"recommendCalorie": 0,
"foodMaterialId": "3001",
"specId": "2001-1",
"foodImg": "https://cdn.yixiong.com/food/2001.jpg",
"tablewareStatus": false,
"tablewareWeight": 0,
"foodLabel": "招牌,微辣"
}
],
"total": 1
}
```
---
#### 接口 P-04 — 当餐菜品列表
**POST** `/neglect/pickup/food/current-meal`
返回当前食堂 + 当前餐次下所有可供应菜品。无入参。
**入参示例**
```json
{ }
```
**响应 `data`**`List<NutTerminalAppFoodVO>`,字段同 P-03
---
#### 接口 P-05 — 菜品详情
**POST** `/neglect/pickup/food/detail`
**入参(Body JSON**`IdDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | 是 | 菜品 idfoodId |
**入参示例**
```json
{ "id": 2001 }
```
**响应 `data`**`NutTerminalAppFoodVO`,字段同 P-03
---
#### 接口 P-06 — 按多个菜品名称查询(顺序一致)
**POST** `/neglect/pickup/food/by-names`
开餐页面批量按名称查询菜品,返回顺序与传入 `nameList` 一致。
**入参(Body JSON**`NutPickupFoodNamesQuery`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `nameList` | List\<String\> | 是 | 菜品名称列表,至少 1 条 |
**入参示例**
```json
{
"nameList": ["宫保鸡丁", "西红柿炒鸡蛋", "米饭"]
}
```
**响应 `data`**`List<NutTerminalAppFoodVO>`,字段同 P-03
---
### 4.3 开餐 / 加菜
---
#### 接口 P-07 — 开餐(记录初始制作量)
**POST** `/neglect/pickup/meal/start`
档口开餐时记录菜品初始制作量,作为余量监控的起始锚点。
**入参(Body JSON**`NutTerminalAppStartMealDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `foodId` | Long | 是 | 菜品 id |
| `foodName` | String | 否 | 菜品名称(冗余) |
| `foodWeight` | BigDecimal | 是 | 食物重量(克) |
| `powerOnTime` | LocalDateTime | 否 | 开机时间(设备上报),格式 `yyyy-MM-dd HH:mm:ss` |
> 设备编码、食堂 id 等终端上下文由 `X-DEVICE-CODE` 头解析,无需 DTO 携带。
**入参示例**
```json
{
"foodId": 2001,
"foodName": "宫保鸡丁",
"foodWeight": 5000,
"powerOnTime": "2026-06-29 10:30:00"
}
```
**响应 `data`** — 无(`Result<Void>`),仅以 `code` 判断成功
**响应示例**
```json
{ "code": "00000", "msg": "success", "data": null, "total": 0 }
```
---
#### 接口 P-08 — 加菜(追加制作量)
**POST** `/neglect/pickup/meal/add`
开餐过程中追加制作量,累加到该菜品当前制作总量。
**入参 / 响应** — 同 P-07`NutTerminalAppStartMealDTO` / `Result<Void>`
---
### 4.4 就餐记录
---
#### 接口 P-09a — 取餐盘(记录开始就餐时刻)
**POST** `/neglect/pickup/plate-pickup`
记录用户"开始就餐"的时刻(取餐盘时间)。调用一次 = upsert 到 `nut_pickup_order``plate_pickup_time` 字段。
**使用约束**
- 本接口是 **P-09 提交单道菜就餐记录** 的**强前置**:未取盘直接打菜会返回错误 `msg: "请先取餐盘再提交就餐记录"`
- upsert 语义:当日 + 当前餐次 + 当前食堂已有 `pickup_method='plate_only'``plate_pickup_time` 非空的主单 → 刷新时间戳,共用一条主单;否则新建 `pickup_method='plate_only'` 的独立主单(无子表、无占位流水,不进入支付链路)。
- 不限制设备(取盘机 vs 营养秤可以是不同设备),按 `userId` 关联。
**入参(Body JSON**`IdDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | 是 | 用户 id(餐盘号) |
**入参示例**
```json
{ "id": 88001 }
```
**响应 `data`**`String`,主单 recordNo`Result.saveSuccess` 包装,新建或刷新)
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": "M-20260709-0001",
"total": 0
}
```
> 设备编码、食堂 id、餐线 id、餐次等终端上下文由请求头 `X-DEVICE-CODE` 解析,无需 DTO 携带。
---
#### 接口 P-09 — 提交单道菜就餐记录
**POST** `/neglect/pickup/eat`
调用一次 = 一道菜的就餐数据;同一用户同一餐次多次调用按主单聚合:
- 主单查找规则:`(userId, eatDay=今日, mealType=当前餐次, pickup_end_time IS NULL)`
- 命中则只追加子表 + 更新主单合计;未命中则新建主单 + 1 条子表
**强前置校验(取盘校验)**
本接口被调用前,业务端必须保证当前用户当日在当前食堂、当前餐次存在一条 `pickup_method='plate_only'``plate_pickup_time` 非空的主单(即已先调用 **P-09a 取餐盘**)。否则直接抛业务异常:
```
msg: "请先取餐盘再提交就餐记录"
```
> 该校验由 [`NutPickupScaleAppServiceImpl#submitEat`](../../platform-nutrition/nutrition-service/src/main/java/com/platform/nutrition/service/impl/NutPickupScaleAppServiceImpl.java) 在主单查找之前完成,过滤条件严格按 `(userId, canteenId, eatDay, mealType, pickup_method='plate_only', plate_pickup_time IS NOT NULL)` 命中,避免跨餐次/跨食堂误判。
**入参(Body JSON**`NutTerminalAppEatDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `userId` | Long | 是 | 用户 id(餐盘号 = userId |
| `foodId` | Long | 是 | 菜品 id |
| `foodName` | String | 是 | 菜品名称(冗余存储,便于历史回溯) |
| `specId` | Long | 否 | 菜品规格 id;无规格传 0 |
| `foodWeight` | BigDecimal | 是 | 菜品总重量(克) |
| `eatWeight` | BigDecimal | 是 | 实际食用重量(克),可为 0 |
| `member` | Boolean | 否 | 是否会员(仅作冗余记录用;新表 `person_type``nut_user` 实际身份决定) |
| `cookOrderId` | Long | 否 | 按单烹制 id(终端菜品列表返回的 cookOrderId,就餐时原样回传) |
**入参示例**
```json
{
"userId": 88001,
"foodId": 2001,
"foodName": "宫保鸡丁",
"specId": 0,
"foodWeight": 200,
"eatWeight": 150,
"member": true,
"cookOrderId": 1000123
}
```
**响应 `data`**`String`,主单 id`Result.saveSuccess` 包装)
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": "1000456",
"total": 0
}
```
---
#### 接口 P-10 — 按用户 id(餐盘号)查本餐就餐数据
**POST** `/neglect/pickup/plate-food`
按餐盘号(= userId)查询本餐次已就餐明细,实时聚合主单 + 子表 + 营养现算。
**入参(Body JSON**`IdDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | 是 | 用户 id(餐盘号) |
**入参示例**
```json
{ "id": 88001 }
```
**响应 `data`**`NutTerminalAppPlateFoodVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | String | 用户姓名 |
| `calorie` | BigDecimal | 本餐热量(kcal |
| `grain` | BigDecimal | 谷类摄入量(g |
| `fruitsVegetables` | BigDecimal | 果蔬摄入量(g |
| `other` | BigDecimal | 肉蛋豆摄入量(g |
| `eatWeightSum` | Integer | 食用总重量(g |
| `incomeSum` | BigDecimal | 菜品总收入价格(元) |
| `discountSum` | BigDecimal | 优惠总价格(元);当前固定 `0` |
| `recommendCalorie` | BigDecimal | 推荐能量(kcal);当前固定 `0` |
| `maxCalorie` | BigDecimal | 能量上限(kcal);当前固定 `0` |
| `minCalorie` | BigDecimal | 能量下限(kcal);当前固定 `0` |
| `grainRecommend` / `grainNearExcess` / `grainExcess` | String | 谷类推荐/即将超量/超量描述(当前固定空) |
| `fruitsVegetablesRecommend` / `fruitsVegetablesNearExcess` / `fruitsVegetablesExcess` | String | 果蔬推荐/即将超量/超量描述(当前固定空) |
| `otherRecommend` / `otherNearExcess` / `otherExcess` | String | 肉蛋豆推荐/即将超量/超量描述(当前固定空) |
| `message` | String | 提示信息(暂未启用) |
| `list` | List\<Food\> | 本餐菜品明细列表,见下表 |
`NutTerminalAppPlateFoodVO.Food` 子对象:
| 字段 | 类型 | 说明 |
|------|------|------|
| `foodId` | String | 菜品 id |
| `foodName` | String | 菜品名称 |
| `foodMaterialId` | String | 菜品营养表 id(默认主料 mater_id |
| `specId` | String | 餐品规格 id;无规格为 0 |
| `eatWeight` | Integer | 食用重量(g |
| `deviceWeight` | Integer | 记录 eatWeight 时秤的当前重量(g |
| `initWeight` | Integer | 记录前秤重量(g= realWeight + eatWeight |
| `eatNum` | Integer | 份数 |
| `price` | BigDecimal | 菜品单价(元) |
| `discount` | BigDecimal | 优惠价格(元);当前固定 `0` |
| `income` | BigDecimal | 菜品收入(元) |
| `isSyncData` | Boolean | 是否已同步标识;新接口固定 `true` |
| `orderFrom` | Integer | 来源:1-营养秤,2-档口机;当前固定 `1` |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": {
"name": "张三",
"calorie": 293.25,
"grain": 75.0,
"fruitsVegetables": 30.0,
"other": 124.5,
"eatWeightSum": 229,
"incomeSum": 6.50,
"discountSum": 0,
"recommendCalorie": 0,
"maxCalorie": 0,
"minCalorie": 0,
"grainRecommend": "",
"grainNearExcess": "",
"grainExcess": "",
"fruitsVegetablesRecommend": "",
"fruitsVegetablesNearExcess": "",
"fruitsVegetablesExcess": "",
"otherRecommend": "",
"otherNearExcess": "",
"otherExcess": "",
"message": "",
"list": [
{
"foodId": "2001",
"foodName": "宫保鸡丁",
"foodMaterialId": "3001",
"specId": "0",
"eatWeight": 150,
"deviceWeight": 350,
"initWeight": 500,
"eatNum": 1,
"price": 6.50,
"discount": 0,
"income": 6.50,
"isSyncData": true,
"orderFrom": 1
}
]
},
"total": 0
}
```
---
#### 接口 P-11 — 推送菜品实时重量
**POST** `/neglect/pickup/weight`
设备在称重过程中持续推送当前菜品重量。服务端按 `(deviceCode, foodId)` 写入 RedisTTL 60s,BI/监控屏读取后自动刷新展示,无心跳即过期降级。
**入参(Body JSON**`NutTerminalAppScaleWeightDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `foodId` | Long | 是 | 菜品 id |
| `foodWeight` | Integer | 是 | 当前菜品重量(克),不能为负 |
> 设备编码由 `X-DEVICE-CODE` 头携带。
**入参示例**
```json
{
"foodId": 2001,
"foodWeight": 425
}
```
**响应 `data`** — 无(`Result<Void>`
---
### 4.5 公共能力
---
#### 接口 P-12 — 用户当日就餐营养
**POST** `/neglect/pickup/user/current-food`
按用户 id 累加当日全部已就餐主单(不仅本餐次),按子表 `food_weight × 营养项/100g` 现算。
**入参(Body JSON**`IdDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | 是 | 用户 id |
**入参示例**
```json
{ "id": 88001 }
```
**响应 `data`**`NutTerminalAppUserNutritionVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | String | 用户姓名 |
| `calorie` | BigDecimal | 已摄入热量(kcal |
| `fruitsVegetables` | BigDecimal | 果蔬摄入量(g |
| `meatEggs` | BigDecimal | 肉蛋摄入量(g |
| `stapleFood` | BigDecimal | 主食摄入量(g |
| `maxCalorie` | BigDecimal | 能量上限(kcal);当前固定 `0` |
| `minCalorie` | BigDecimal | 能量下限(kcal);当前固定 `0` |
| `recommendCalorie` | BigDecimal | 推荐能量(kcal);当前固定 `0` |
| `stapleFoodRecommend` / `stapleFoodNearExcess` / `stapleFoodExcess` | String | 主食推荐/即将超量/超量描述(当前固定空) |
| `fruitsVegetablesRecommend` / `fruitsVegetablesNearExcess` / `fruitsVegetablesExcess` | String | 果蔬推荐/即将超量/超量描述(当前固定空) |
| `meatEggsRecommend` / `meatEggsNearExcess` / `meatEggsExcess` | String | 肉蛋推荐/即将超量/超量描述(当前固定空) |
| `message` | String | 提示信息(暂未启用) |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": {
"name": "张三",
"calorie": 588.5,
"fruitsVegetables": 60.0,
"meatEggs": 124.5,
"stapleFood": 150.0,
"maxCalorie": 0,
"minCalorie": 0,
"recommendCalorie": 0,
"stapleFoodRecommend": "",
"stapleFoodNearExcess": "",
"stapleFoodExcess": "",
"fruitsVegetablesRecommend": "",
"fruitsVegetablesNearExcess": "",
"fruitsVegetablesExcess": "",
"meatEggsRecommend": "",
"meatEggsNearExcess": "",
"meatEggsExcess": "",
"message": ""
},
"total": 0
}
```
---
#### 接口 P-13 — 虹软 SDK 设备配置
**GET** `/neglect/pickup/device/config`
无入参(设备编码由 `X-DEVICE-CODE` 头传递)。终端 APP 启动时调一次缓存到本地,不需要持续轮询。
**响应 `data`**`NutTerminalAppDeviceConfigVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `arcsoftAppId` | String | 虹软 ArcFace SDK App Id |
| `arcsoftSdkKey` | String | 虹软 ArcFace SDK Key |
| `arcsoftActiveKey` | String | 虹软 ArcFace 激活码 |
| `payType` | Integer | 档口秤结算方式:1-独立结算,2-联合结算;吐盘机无意义 |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": {
"arcsoftAppId": "8cXr...",
"arcsoftSdkKey": "FpHn...",
"arcsoftActiveKey": "3aEb...",
"payType": null
},
"total": 0
}
```
---
#### 接口 P-14 — 当前餐次信息
**GET** `/neglect/pickup/meal/current`
无入参。餐次时段当前由 `MealPeriodHelper` 硬编码(公司全局统一),不在任何区间时返回最近的下一餐。
**响应 `data`**`NutTerminalAppMealInfoVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `mealName` | String | 餐次名称(早餐/午餐/晚餐/加餐) |
| `mealTimeStart` | LocalDateTime | 餐次开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
| `mealTimeEnd` | LocalDateTime | 餐次结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": {
"mealName": "午餐",
"mealTimeStart": "2026-06-29 11:30:00",
"mealTimeEnd": "2026-06-29 13:00:00"
},
"total": 0
}
```
---
#### 接口 P-15 — 会员折扣率
**POST** `/neglect/pickup/member/discount`
按用户 id 返回折扣系数(如 `0.9` 表示九折)。
**入参(Body JSON**`IdDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | 是 | 用户 id |
**入参示例**
```json
{ "id": 88001 }
```
**响应 `data`**`BigDecimal`
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": 0.90,
"total": 0
}
```
---
### 4.6 食材库存 / 净菜加工
---
#### 接口 P-16 — 食材库存搜索
**POST** `/neglect/pickup/ingredient/search`
按食材名称模糊搜索,按溯源码分组返回。
**入参(Body JSON**`NutIngredientSearchQuery`(继承 `BasePageQuery`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | Long | 是 | 页码 |
| `pageSize` | Long | 是 | 每页条数 |
| `keyword` | String | 否 | 食材名称(模糊) |
**入参示例**
```json
{
"pageNum": 1,
"pageSize": 20,
"keyword": "白菜"
}
```
**响应 `data`**`List<NutIngredientInventoryVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `materId` | Long | 食材 id |
| `materName` | String | 食材名称 |
| `traceCode` | String | 溯源码 |
| `totalWeight` | BigDecimal | 总库存量(kg |
| `vegTypeList` | List\<NutMaterVegTypeOptionVO\> | 净菜类型列表 |
`NutMaterVegTypeOptionVO` 子对象:
| 字段 | 类型 | 说明 |
|------|------|------|
| `vegTypeId` | Long | 净菜类型 id |
| `vegTypeName` | String | 净菜类型名称(如 洗净米、肉片、豆腐块) |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": [
{
"materId": 1001,
"materName": "大白菜",
"traceCode": "HV2026060001",
"totalWeight": 125.50,
"vegTypeList": [
{ "vegTypeId": 11, "vegTypeName": "洗净叶菜" },
{ "vegTypeId": 12, "vegTypeName": "菜段" }
]
}
],
"total": 1
}
```
---
#### 接口 P-17 — 加工步骤字典
**GET** `/neglect/pickup/dict/process-step`
无入参。返回净菜加工步骤字典(清洗/去皮/切割/分拣/综合加工)。
**响应 `data`**`List<NutDictItemVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Long | 主键 id |
| `dictType` | String | 所属字典类型编码 |
| `dictCode` | String | 字典值(存入业务表的实际值) |
| `dictLabel` | String | 显示名称 |
| `sortOrder` | Integer | 排序 |
| `isBuiltin` | Short | 是否系统内置(1-是,0-否) |
| `status` | Short | 状态(1-启用,0-禁用) |
| `remark` | String | 备注说明 |
| `updateBy` | Long | 修改人 id |
| `updateByName` | String | 修改人姓名 |
| `createTime` | LocalDateTime | 创建时间 |
| `updateTime` | LocalDateTime | 修改时间 |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": [
{ "id": 501, "dictType": "PROCESS_STEP", "dictCode": "WASH", "dictLabel": "清洗", "sortOrder": 1, "isBuiltin": 1, "status": 1, "remark": "", "updateBy": 1, "updateByName": "system", "createTime": "2026-05-20 09:00:00", "updateTime": "2026-05-20 09:00:00" },
{ "id": 502, "dictType": "PROCESS_STEP", "dictCode": "PEEL", "dictLabel": "去皮", "sortOrder": 2, "isBuiltin": 1, "status": 1, "remark": "", "updateBy": 1, "updateByName": "system", "createTime": "2026-05-20 09:00:00", "updateTime": "2026-05-20 09:00:00" },
{ "id": 503, "dictType": "PROCESS_STEP", "dictCode": "CUT", "dictLabel": "切割", "sortOrder": 3, "isBuiltin": 1, "status": 1, "remark": "", "updateBy": 1, "updateByName": "system", "createTime": "2026-05-20 09:00:00", "updateTime": "2026-05-20 09:00:00" },
{ "id": 504, "dictType": "PROCESS_STEP", "dictCode": "SORT", "dictLabel": "分拣", "sortOrder": 4, "isBuiltin": 1, "status": 1, "remark": "", "updateBy": 1, "updateByName": "system", "createTime": "2026-05-20 09:00:00", "updateTime": "2026-05-20 09:00:00" },
{ "id": 505, "dictType": "PROCESS_STEP", "dictCode": "MIXED", "dictLabel": "综合加工", "sortOrder": 5, "isBuiltin": 1, "status": 1, "remark": "", "updateBy": 1, "updateByName": "system", "createTime": "2026-05-20 09:00:00", "updateTime": "2026-05-20 09:00:00" }
],
"total": 0
}
```
---
#### 接口 P-18 — 净菜加工新增
**POST** `/neglect/pickup/clean-process/add`
提交一条净菜加工记录,关联毛菜批次与食材溯源。
**入参(Body JSON**`NutSupCleanProcessDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | 否 | 主键 id(修改时必填) |
| `batchNo` | String | 否 | 加工批次号,如 `PROC-001`,全局唯一 |
| `traceCode` | String | 是 | 溯源码(来源 `nut_farm_harvest.trace_code` |
| `materialId` | Long | 是 | 食材 idFK→`nut_mater_base.id` |
| `ingredientName` | String | 是 | 食材名称 |
| `vegTypeId` | Long | 否 | 净菜类型 idFK→`nut_mater_veg_type.id` |
| `requiredCleanWeight` | BigDecimal | 否 | 净菜需求重量(kg),来源毛菜清洗任务的 `required_weight` |
| `rawBatchNo` | String | 否 | 关联毛菜秤批次号 `LC-xxx`(毛菜秤提交时自动写入) |
| `processStep` | String | 是 | 加工步骤:清洗/去皮/切割/分拣/综合加工 |
| `rawWeight` | BigDecimal | 是 | 毛菜重量(kg |
| `cleanWeight` | BigDecimal | 否 | 净菜重量(kg),加工完成后填写 |
| `yieldRate` | BigDecimal | 否 | 出净率(%),= `clean_weight / raw_weight * 100`,应用层计算回填 |
| `operator` | String | 否 | 加工人员姓名,不传时从登录态自动取 |
| `startTime` | LocalDateTime | 是 | 加工开始时间 |
| `endTime` | LocalDateTime | 否 | 加工结束时间 |
| `qcResult` | Short | 否 | 质检结果(0-待检,1-合格,2-不合格),不传默认 0 |
| `inboundStatus` | Short | 否 | 入库状态(0-未入库,1-已入库) |
| `cameraCode` | String | 否 | 关联摄像头编号 |
| `videoUrl` | String | 否 | 加工监控视频地址 |
| `opType` | Short | 否 | 操作类型(1-设备自动,2-人工录入) |
| `deviceType` | String | 否 | 设备类型(字典值),如 `CLEAN_PROCESS_SCALE` |
| `deviceCode` | String | 否 | 设备编码 |
| `remark` | String | 否 | 备注 |
**入参示例**
```json
{
"traceCode": "HV2026060001",
"materialId": 1001,
"ingredientName": "大白菜",
"vegTypeId": 11,
"processStep": "清洗",
"rawWeight": 50.0,
"cleanWeight": 45.0,
"startTime": "2026-06-29 14:00:00",
"endTime": "2026-06-29 14:20:00",
"deviceType": "CLEAN_PROCESS_SCALE",
"deviceCode": "CPS-001",
"opType": 1
}
```
**响应 `data`** — 无(`Result<Void>`
---
### 4.7 采样-分类 / 菜品搜索
---
#### 接口 P-19 — 采样菜品分类列表
**GET** `/neglect/pickup/sampling/categories`
无入参。返回采样页面用的餐品分类列表。
**响应 `data`**`List<NutFoodClassSimpleVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `classId` | String | 分类 id |
| `className` | String | 分类名称(主食/荤菜/素菜/汤品/饮品) |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": [
{ "classId": "1", "className": "主食" },
{ "classId": "2", "className": "荤菜" },
{ "classId": "3", "className": "素菜" },
{ "classId": "4", "className": "汤品" },
{ "classId": "5", "className": "饮品" }
],
"total": 0
}
```
---
#### 接口 P-20 — 采样菜品搜索
**POST** `/neglect/pickup/sampling/food/search`
按分类 + 名称模糊搜索,从按单烹制按食堂 + 日期 + 餐次过滤。
**入参(Body JSON**`NutSamplingFoodSearchQuery`(继承 `BasePageQuery`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | Long | 是 | 页码 |
| `pageSize` | Long | 是 | 每页条数 |
| `categoryId` | Long | 否 | 分类 idnull 或 0 表示全部 |
| `foodName` | String | 否 | 菜品名称,支持模糊搜索 |
**入参示例**
```json
{
"pageNum": 1,
"pageSize": 20,
"categoryId": 2,
"foodName": "鸡"
}
```
**响应 `data`**`List<NutSamplingFoodVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `cookOrderId` | String | 按单烹制 id |
| `foodId` | String | 菜品 id |
| `foodName` | String | 菜品名称 |
| `foodImg` | String | 菜品图片 URL |
| `classId` | String | 分类 id |
| `className` | String | 分类名称 |
| `mainIngredientName` | String | 主要食材名称 |
| `calorie` | BigDecimal | 能量(kcal/100g |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": [
{
"cookOrderId": "CO20260629-001",
"foodId": "2001",
"foodName": "宫保鸡丁",
"foodImg": "https://cdn.yixiong.com/food/2001.jpg",
"classId": "2",
"className": "荤菜",
"mainIngredientName": "鸡胸肉",
"calorie": 195.5
}
],
"total": 1
}
```
---
### 4.8 采样-保存采集数据
---
#### 接口 P-21 — 保存采集图片及向量
**POST** `/neglect/pickup/sampling/save``multipart/form-data`
提交一道菜的采样图片及特征向量,返回上传成功的图片 URL 列表。
**入参(multipart 表单字段)**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `foodId` | Long | 是 | 菜品 idform 字段) |
| `foodName` | String | 是 | 菜品名称(form 字段) |
| `version` | String | 是 | 模型版本号(form 字段) |
| `foodVector` | String | 是 | 菜品特征向量(JSON 数组字符串,form 字段) |
| `foodPics` | MultipartFile[] | 是 | 菜品图片文件列表(file part |
**响应 `data`**`List<String>`,上传成功的图片 URL 列表
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": [
"https://cdn.yixiong.com/sample/2001_v1_1.jpg",
"https://cdn.yixiong.com/sample/2001_v1_2.jpg",
"https://cdn.yixiong.com/sample/2001_v1_3.jpg"
],
"total": 0
}
```
---
### 4.9 采样历史
---
#### 接口 P-22 — 采样历史分页(按菜品聚合)
**POST** `/neglect/pickup/sampling/history/page`
按菜品聚合返回采样历史,每条含 3 张缩略图。
**入参(Body JSON**`NutSamplingHistoryQuery`(继承 `BasePageQuery`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pageNum` | Long | 是 | 页码 |
| `pageSize` | Long | 是 | 每页条数 |
| `categoryId` | Long | 否 | 分类 idnull 或 0 表示全部 |
| `foodName` | String | 否 | 菜品名称,支持模糊搜索 |
**入参示例**
```json
{
"pageNum": 1,
"pageSize": 10,
"foodName": "鸡"
}
```
**响应 `data`**`List<NutSamplingHistoryVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `foodId` | String | 菜品 id |
| `foodName` | String | 菜品名称 |
| `foodImg` | String | 菜品图片 URL |
| `className` | String | 分类名称 |
| `mainIngredientName` | String | 主要食材名称 |
| `calorie` | BigDecimal | 能量(kcal/100g |
| `totalCount` | Long | 采样总条数 |
| `previewUrls` | List\<String\> | 已采集图片缩略图(最多 3 张) |
| `lastSampleTime` | String | 最近采样时间 |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": [
{
"foodId": "2001",
"foodName": "宫保鸡丁",
"foodImg": "https://cdn.yixiong.com/food/2001.jpg",
"className": "荤菜",
"mainIngredientName": "鸡胸肉",
"calorie": 195.5,
"totalCount": 12,
"previewUrls": [
"https://cdn.yixiong.com/sample/2001_v1_1.jpg",
"https://cdn.yixiong.com/sample/2001_v1_2.jpg",
"https://cdn.yixiong.com/sample/2001_v1_3.jpg"
],
"lastSampleTime": "2026-06-28 17:45:00"
}
],
"total": 1
}
```
---
#### 接口 P-23 — 菜品采样全部图片
**POST** `/neglect/pickup/sampling/history/detail`
按菜品 id 查询采样全部图片(最多 50 条)。
**入参(Body JSON**`IdDTO`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | 是 | 菜品 id |
**入参示例**
```json
{ "id": 2001 }
```
**响应 `data`**`List<NutSamplingDetailVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 采集图片 id |
| `foodPic` | String | 采集图片 URL |
| `createTime` | String | 采样时间 |
**响应示例**
```json
{
"code": "00000",
"msg": "success",
"data": [
{ "id": "9001", "foodPic": "https://cdn.yixiong.com/sample/2001_v1_1.jpg", "createTime": "2026-06-28 17:45:00" },
{ "id": "9002", "foodPic": "https://cdn.yixiong.com/sample/2001_v1_2.jpg", "createTime": "2026-06-28 17:45:01" }
],
"total": 0
}
```
---
## 五、接口速查表
| 接口号 | 接口名称 | 方法 | 路径 |
|--------|----------|------|------|
| P-01 | 全量人脸数据 | POST | `/neglect/pickup/face/page` |
| P-02 | 增量人脸数据 | POST | `/neglect/pickup/face/increment` |
| P-03 | 模糊搜索菜品 | POST | `/neglect/pickup/food/search` |
| P-04 | 当餐菜品列表 | POST | `/neglect/pickup/food/current-meal` |
| P-05 | 菜品详情 | POST | `/neglect/pickup/food/detail` |
| P-06 | 按多名菜品查询 | POST | `/neglect/pickup/food/by-names` |
| P-07 | 开餐(初始制作量) | POST | `/neglect/pickup/meal/start` |
| P-08 | 加菜(追加制作量) | POST | `/neglect/pickup/meal/add` |
| P-09a | 取餐盘(记录开始就餐时刻) | POST | `/neglect/pickup/plate-pickup` |
| P-09 | 提交单道菜就餐记录 | POST | `/neglect/pickup/eat` |
| P-10 | 餐盘号查本餐就餐数据 | POST | `/neglect/pickup/plate-food` |
| P-11 | 推送菜品实时重量 | POST | `/neglect/pickup/weight` |
| P-12 | 用户当日就餐营养 | POST | `/neglect/pickup/user/current-food` |
| P-13 | 虹软 SDK 设备配置 | GET | `/neglect/pickup/device/config` |
| P-14 | 当前餐次信息 | GET | `/neglect/pickup/meal/current` |
| P-15 | 会员折扣率 | POST | `/neglect/pickup/member/discount` |
| P-16 | 食材库存搜索 | POST | `/neglect/pickup/ingredient/search` |
| P-17 | 加工步骤字典 | GET | `/neglect/pickup/dict/process-step` |
| P-18 | 净菜加工新增 | POST | `/neglect/pickup/clean-process/add` |
| P-19 | 采样菜品分类列表 | GET | `/neglect/pickup/sampling/categories` |
| P-20 | 采样菜品搜索 | POST | `/neglect/pickup/sampling/food/search` |
| P-21 | 保存采集图片及向量 | POST | `/neglect/pickup/sampling/save` |
| P-22 | 采样历史分页 | POST | `/neglect/pickup/sampling/history/page` |
| P-23 | 菜品采样全部图片 | POST | `/neglect/pickup/sampling/history/detail` |
---
## 六、版本差异(对照老版 `营养秤_API_DOC.md` v1.0.0 / 2026-05-26
### 6.1 全局差异
| 维度 | 老版 | 新版 | 影响 |
|------|------|------|------|
| 路径前缀 | `/terminal/neglect/...`(混杂 `common/app``nutrition/scale/app``boothMachine/app` | 统一 `/neglect/pickup/...` | 全部接口路径变更,APP 端需更新 Retrofit baseUrl 映射 |
| 响应包装 | `{ "code": "00000", "msg": "...", "data": {} }` | `{ "code": "00000", "msg": "...", "data": {}, "total": 0 }` | 新增 `total` 字段(分页接口返回总条数,非分页为 0) |
| 鉴权 | 全局硬编码 `X-Access-Token` + `authorization` | Nacos 白名单 `/nutrition/neglect/**`,不强制校验 Sa-Token | APP 端可继续保留请求头,但服务端不再据此鉴权 |
| 设备上下文 | DTO 内冗余携带 `deviceId` | 统一由 `X-DEVICE-CODE` 请求头解析 | DTO 字段精简(详见各接口差异) |
| HTTP 方法 | 大量使用 GET + Query String | 项目规范强制 POST + `@RequestBody`,仅查询类无入参接口保留 GET | 多个老 GET 接口改为 POST + body(详见下表) |
### 6.2 逐接口差异
| 老接口号 | 老路径 / 方法 | 新路径 / 方法 | 入参变化 | 响应变化 |
|---------|--------------|--------------|---------|---------|
| 2.1 | POST `/terminal/neglect/common/app/faceFeature/list` | POST `/neglect/pickup/face/page` | 入参从 `{pageNum, pageSize}` 升级为 `BasePageQuery`(含 `column`/`order` 排序字段),`pageNum/pageSize` 类型由 Integer 改为 Long | 字段精简:移除 `faceFeatureString``faceFeatureStr` 备用字段,新增 `personType` 字段 |
| 2.2 | POST `/terminal/neglect/common/app/faceFeature/increment/list` | POST `/neglect/pickup/face/increment` | 入参 `pageNum/pageSize` 由 Long 保留为 Long`timestamp` 含义不变 | 同 2.1 |
| 2.3 | POST `/terminal/neglect/nutrition/scale/app/startMealService` | 拆为两个接口:POST `/neglect/pickup/meal/start`type=1+ POST `/neglect/pickup/meal/add`type=2 | **拆分**:老接口靠 `type` 字段区分 `0-就餐/1-初始制作量/2-后期加菜`,新版按业务拆为独立 endpoint;就餐数据走 P-09 `/eat`DTO 字段精简,移除 `deviceId``specId``foodMaterialId``type``currentScaleWeight`,新增 `powerOnTime` | 响应仍为空对象 |
| 2.4 | POST `/terminal/neglect/nutrition/scale/app/userEatFood` | POST `/neglect/pickup/eat` | 字段类型:`userId/foodId/specId` 由 String 改为 Long`foodWeight/eatWeight` 由 String 改为 BigDecimal;移除 `deviceId``foodMaterialId`;新增 `cookOrderId`(按单烹制回传) | 响应 `data` 由空 Object 改为 String(主单 id |
| 2.5 | GET `/terminal/neglect/boothMachine/app/getUserCurrentFoodDetails?id=xxx` | POST `/neglect/pickup/user/current-food` | **方法由 GET 改为 POST**;入参从 Query `id` 改为 `IdDTO` body | 字段语义不变;`maxCalorie/minCalorie/recommendCalorie` 当前固定 0;各 `*Recommend/*NearExcess/*Excess` 描述字段当前固定空字符串 |
| 2.6 | GET `/terminal/neglect/nutrition/scale/app/getFoodInfoByPlateNumber?userId=xxx` | POST `/neglect/pickup/plate-food` | **方法由 GET 改为 POST**;入参从 Query `userId` 改为 `IdDTO` body`id` 即 userId/餐盘号) | 子对象 `Food` 字段类型对齐:`eatWeight/deviceWeight/initWeight/eatNum` 改为 Integer`price/discount/income` 改为 BigDecimal`isSyncData` 固定 true、`orderFrom` 固定 1 |
| 2.7 | POST `/terminal/neglect/boothMachine/app/getFoodsByFoodNameList` | POST `/neglect/pickup/food/search` | 入参从 `{name, pageNum, pageSize}`String)改为 `NutTerminalAppFoodSearchQuery`Long + `name`),`name` 改为非必填 | `FoodInfo` 字段语义变化:`price` 单位由「元/千克」改为「元/100g」;`vipPrice` 固定 null`recommendCalorie` 固定 0;新增 `cookOrderId``tablewareStatus` 固定 false、`tablewareWeight` 固定 0 |
| 2.8 | GET `/terminal/neglect/boothMachine/app/getFoodsByCurrentMeal` | POST `/neglect/pickup/food/current-meal` | **方法由 GET 改为 POST**;无入参 | 同 2.7 |
| 2.9 | GET `/terminal/neglect/common/app/getfoodInfoByFoodId?foodId=xxx` | POST `/neglect/pickup/food/detail` | **方法由 GET 改为 POST**;入参从 Query `foodId` 改为 `IdDTO` body | 同 2.7 |
| 2.10 | GET `/terminal/neglect/common/app/getYxEquipmentByEquipmentCode` | GET `/neglect/pickup/device/config` | 路径变更,方法保留 GET,无入参 | 新增 `payType` 字段(吐盘机无意义,仅档口秤用) |
| 2.11 | POST `/terminal/neglect/nutrition/scale/app/updateScaleRealTimeWeight` | POST `/neglect/pickup/weight` | DTO 字段精简:移除 `deviceId`(由请求头携带),保留 `foodId/foodWeight`;类型 `foodId` 由 String 改为 Long | 响应仍为空 |
| 2.12 | GET `/terminal/neglect/common/app/getCurrentPlaceMeal` | GET `/neglect/pickup/meal/current` | 路径变更,方法保留 GET | 字段类型由 String 改为 LocalDateTime(仍按 `yyyy-MM-dd HH:mm:ss` 序列化) |
| 2.13 | GET `/terminal/neglect/common/app/getMemberDiscount?userId=xxx` | POST `/neglect/pickup/member/discount` | **方法由 GET 改为 POST**;入参从 Query `userId` 改为 `IdDTO` body | 返回类型由 Double 改为 BigDecimal |
### 6.3 新版新增接口(老版无对应)
| 接口号 | 路径 | 用途 |
|--------|------|------|
| P-06 | `/neglect/pickup/food/by-names` | 按多个菜品名称查询(顺序一致),开餐页面批量查询 |
| P-08 | `/neglect/pickup/meal/add` | 加菜(从老 2.3 拆分独立) |
| P-16 | `/neglect/pickup/ingredient/search` | 食材库存搜索(按溯源码分组) |
| P-17 | `/neglect/pickup/dict/process-step` | 加工步骤字典 |
| P-18 | `/neglect/pickup/clean-process/add` | 净菜加工记录新增 |
| P-19 | `/neglect/pickup/sampling/categories` | 采样菜品分类列表 |
| P-20 | `/neglect/pickup/sampling/food/search` | 采样菜品搜索 |
| P-21 | `/neglect/pickup/sampling/save` | 保存采集图片及向量 |
| P-22 | `/neglect/pickup/sampling/history/page` | 采样历史分页 |
| P-23 | `/neglect/pickup/sampling/history/detail` | 菜品采样全部图片 |
### 6.4 字段类型迁移注意事项
1. **ID 字段统一 Long**:老版 `foodId/userId/specId/foodMaterialId` 全为 String,新版统一为 LongJSON 序列化时仍以 String 形式输出,避免 JS 精度丢失,但 APP 端反序列化目标类型需调整为 Long。
2. **重量字段统一 BigDecimal**:老版 `foodWeight/eatWeight` 为 String,新版改为 BigDecimal,杜绝字符串数字解析异常。
3. **价格单位变化**`FoodInfo.price` 由「元/千克」改为「元/100g」,APP 端计价公式需同步调整:`总价 = 重量(g) × price / 100`
4. **VIP 价格概念移除**`vipPrice` 固定返回 null,会员优惠改走 `member/discount` 接口拿折扣系数。
5. **`personType` 字段新增**:人脸 VO 新增 `personType`(1-员工,2-工作人员,3-普通用户,4-外部人员,5-会员),用于设备端差异化提示。