# 吐盘机(营养秤)接口文档 > Controller:`NutPickupScaleController` > 模块前缀:`/neglect/pickup` > Nacos 白名单:`/nutrition/neglect/**`(无需 Sa-Token) > 设备上下文:请求头 `X-DEVICE-CODE` > 生成日期:2026-06-29 > 对照老版:`Downloads/营养秤_API_DOC.md`(v1.0.0,2026-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`: ```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` | 字段 | 类型 | 说明 | |------|------|------| | `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` --- ### 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` | 字段 | 类型 | 说明 | |------|------|------| | `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`,字段同 P-03 --- #### 接口 P-05 — 菜品详情 **POST** `/neglect/pickup/food/detail` **入参(Body JSON)** — `IdDTO` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `id` | Long | 是 | 菜品 id(foodId) | **入参示例** ```json { "id": 2001 } ``` **响应 `data`** — `NutTerminalAppFoodVO`,字段同 P-03 --- #### 接口 P-06 — 按多个菜品名称查询(顺序一致) **POST** `/neglect/pickup/food/by-names` 开餐页面批量按名称查询菜品,返回顺序与传入 `nameList` 一致。 **入参(Body JSON)** — `NutPickupFoodNamesQuery` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `nameList` | List\ | 是 | 菜品名称列表,至少 1 条 | **入参示例** ```json { "nameList": ["宫保鸡丁", "西红柿炒鸡蛋", "米饭"] } ``` **响应 `data`** — `List`,字段同 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`),仅以 `code` 判断成功 **响应示例** ```json { "code": "00000", "msg": "success", "data": null, "total": 0 } ``` --- #### 接口 P-08 — 加菜(追加制作量) **POST** `/neglect/pickup/meal/add` 开餐过程中追加制作量,累加到该菜品当前制作总量。 **入参 / 响应** — 同 P-07(`NutTerminalAppStartMealDTO` / `Result`) --- ### 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\ | 本餐菜品明细列表,见下表 | `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)` 写入 Redis,TTL 60s,BI/监控屏读取后自动刷新展示,无心跳即过期降级。 **入参(Body JSON)** — `NutTerminalAppScaleWeightDTO` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `foodId` | Long | 是 | 菜品 id | | `foodWeight` | Integer | 是 | 当前菜品重量(克),不能为负 | > 设备编码由 `X-DEVICE-CODE` 头携带。 **入参示例** ```json { "foodId": 2001, "foodWeight": 425 } ``` **响应 `data`** — 无(`Result`) --- ### 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` | 字段 | 类型 | 说明 | |------|------|------| | `materId` | Long | 食材 id | | `materName` | String | 食材名称 | | `traceCode` | String | 溯源码 | | `totalWeight` | BigDecimal | 总库存量(kg) | | `vegTypeList` | List\ | 净菜类型列表 | `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` | 字段 | 类型 | 说明 | |------|------|------| | `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 | 是 | 食材 id,FK→`nut_mater_base.id` | | `ingredientName` | String | 是 | 食材名称 | | `vegTypeId` | Long | 否 | 净菜类型 id,FK→`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`) --- ### 4.7 采样-分类 / 菜品搜索 --- #### 接口 P-19 — 采样菜品分类列表 **GET** `/neglect/pickup/sampling/categories` 无入参。返回采样页面用的餐品分类列表。 **响应 `data`** — `List` | 字段 | 类型 | 说明 | |------|------|------| | `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 | 否 | 分类 id,null 或 0 表示全部 | | `foodName` | String | 否 | 菜品名称,支持模糊搜索 | **入参示例** ```json { "pageNum": 1, "pageSize": 20, "categoryId": 2, "foodName": "鸡" } ``` **响应 `data`** — `List` | 字段 | 类型 | 说明 | |------|------|------| | `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 | 是 | 菜品 id(form 字段) | | `foodName` | String | 是 | 菜品名称(form 字段) | | `version` | String | 是 | 模型版本号(form 字段) | | `foodVector` | String | 是 | 菜品特征向量(JSON 数组字符串,form 字段) | | `foodPics` | MultipartFile[] | 是 | 菜品图片文件列表(file part) | **响应 `data`** — `List`,上传成功的图片 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 | 否 | 分类 id,null 或 0 表示全部 | | `foodName` | String | 否 | 菜品名称,支持模糊搜索 | **入参示例** ```json { "pageNum": 1, "pageSize": 10, "foodName": "鸡" } ``` **响应 `data`** — `List` | 字段 | 类型 | 说明 | |------|------|------| | `foodId` | String | 菜品 id | | `foodName` | String | 菜品名称 | | `foodImg` | String | 菜品图片 URL | | `className` | String | 分类名称 | | `mainIngredientName` | String | 主要食材名称 | | `calorie` | BigDecimal | 能量(kcal/100g) | | `totalCount` | Long | 采样总条数 | | `previewUrls` | List\ | 已采集图片缩略图(最多 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` | 字段 | 类型 | 说明 | |------|------|------| | `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,新版统一为 Long;JSON 序列化时仍以 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-会员),用于设备端差异化提示。