From 524837ec209f59f9124e21f469d11e5c0a6d8850 Mon Sep 17 00:00:00 2001 From: mazengfei <331023091@qq.com> Date: Tue, 18 Aug 2026 09:12:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(face):=20=E6=94=AF=E6=8C=81=E6=8C=89userFa?= =?UTF-8?q?ceId=E6=9F=A5=E8=AF=A2=E5=8D=95=E6=9D=A1=E4=BA=BA=E8=84=B8?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FaceDao新增queryByUserFaceId接口,实现按userFaceId精确查询 - FaceApi新增queryByUserFaceId方法,封装Dao查询逻辑 - LoginByFaceActivity插入人脸时,先按userFaceId查询,存在则更新避免重复 - NetViewModelV2同步人脸数据时,优先用userFaceId判重,减少重复入库 - 修正FaceEntity中userId字段数据库列名错误(user_id替代user_idd) - 增加注释说明及代码格式优化,提升代码可读性和健壮性 --- .../activity/LoginByFaceActivity.kt | 12 +- .../platecabinet/viewmodel/NetViewModelV2.kt | 16 +- app/吐盘机接口文档.md | 1272 +++++++++++++++++ .../com/sw/plate/utils/arcface/FaceApi.java | 11 + .../utils/arcface/facedb/dao/FaceDao.java | 6 + .../arcface/facedb/entity/FaceEntity.java | 2 +- 6 files changed, 1312 insertions(+), 7 deletions(-) create mode 100644 app/吐盘机接口文档.md diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt index 9b1889b..bcef431 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt @@ -1018,7 +1018,17 @@ class LoginByFaceActivity : BaseActivity(), loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------") val faceDao = FaceDatabase.getInstance(this).faceDao() val faceEntity = faceModel.toFaceEntity() - val faceId = faceDao.insert(faceEntity) + // 先查后插:按 userFaceId 判重,避免采集入库与增量同步各插一条 + val userFaceId = faceModel.userFaceId + val existing = if (!userFaceId.isNullOrEmpty()) faceDao.queryByUserFaceId(userFaceId) else null + val faceId = if (existing != null) { + // 已存在记录,更新特征等字段并复用主键,避免重复 + faceEntity.faceId = existing.faceId + faceDao.updateFaceEntity(faceEntity) + existing.faceId + } else { + faceDao.insert(faceEntity) + } faceEntity.faceId = faceId loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------") recognizeViewModel.refreshFaceList() diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/NetViewModelV2.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/NetViewModelV2.kt index a23d938..334b807 100644 --- a/app/src/main/java/com/sw/platecabinet/viewmodel/NetViewModelV2.kt +++ b/app/src/main/java/com/sw/platecabinet/viewmodel/NetViewModelV2.kt @@ -232,12 +232,18 @@ class NetViewModelV2 : ViewModel() { faceApi.deleteByUserFaceId(userFaceId) } } else { - // 获取该用户所有已存特征,判断当前特征是否已存在 + // 优先按 userFaceId 精确判重,避免特征字段不一致导致重复入库 + val userFaceId = model.userFaceId + if (!userFaceId.isNullOrEmpty() && faceApi.queryByUserFaceId(userFaceId) != null) { + // 本地已存在该 userFaceId,跳过插入 + return@forEach + } + // userFaceId 为空时,回退到特征比较去重(修正字段来源,保证与入库一致) + val featureStr = model.resolveFeatureStr() val existList = faceApi.queryAllByUserName(model.userId) - val featureMatches = - existList.map { Base64.encode(it.featureData) == model.faceFeatureStr } - Timber.d("userId=${model.userId}, 已存记录数=${existList.size}, 各条特征是否与服务器一致=$featureMatches") - val alreadyExists = featureMatches.any { it } + val alreadyExists = existList.any { entity -> + featureStr != null && Base64.encode(entity.featureData) == featureStr + } if (!alreadyExists) { faceApi.insert(model.toFaceEntity()) } diff --git a/app/吐盘机接口文档.md b/app/吐盘机接口文档.md new file mode 100644 index 0000000..76be037 --- /dev/null +++ b/app/吐盘机接口文档.md @@ -0,0 +1,1272 @@ +# 吐盘机(营养秤)接口文档 + +> 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-会员),用于设备端差异化提示。 diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java index b8fd28a..319d033 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceApi.java @@ -98,6 +98,17 @@ public class FaceApi { getFaceDao().deleteFaceByUserFaceId(userFaceId); } + + /** + * 按 userFaceId 查询单条人脸记录 + */ + public FaceEntity queryByUserFaceId(String userFaceId) { + if (userFaceId == null || userFaceId.isEmpty()) { + return null; + } + return getFaceDao().queryByUserFaceId(userFaceId); + } + public Long insert(FaceEntity entity) { if (entity == null) { return 0L; diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java index c02ee40..31a837f 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java @@ -61,6 +61,12 @@ public interface FaceDao { @Query("DELETE from face WHERE user_face_id = :userFaceId") int deleteFaceByUserFaceId(String userFaceId); + /** + * 按 userFaceId 查询单条人脸记录 + */ + @Query("SELECT * FROM face WHERE user_face_id = :userFaceId LIMIT 1") + FaceEntity queryByUserFaceId(String userFaceId); + /** * 删除所有已注册的人脸 * diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java index 3dc95f5..1a952f2 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/entity/FaceEntity.java @@ -58,7 +58,7 @@ public class FaceEntity implements Parcelable { @Ignore private int trackId;//人脸追踪ID - @ColumnInfo(name = "user_idd") + @ColumnInfo(name = "user_id") private String userId; @ColumnInfo(name = "user_face_id") private String userFaceId;