diff --git a/changelogs-v2/2026-09/18_7741_订单用餐信息与用餐模版接口-新增接口-管理后台.md b/changelogs-v2/2026-09/18_7741_订单用餐信息与用餐模版接口-新增接口-管理后台.md new file mode 100644 index 00000000..9ffc12ed --- /dev/null +++ b/changelogs-v2/2026-09/18_7741_订单用餐信息与用餐模版接口-新增接口-管理后台.md @@ -0,0 +1,831 @@ +--- +schema: "hl-changelog/v2" +ticket: "7741" +title: "订单用餐信息与用餐模版接口(查询、生成、整单保存、重置、查模版、存模版、套模版)" +consumer: "admin" +author: "lc(GIT)" +change_type: "新增接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "新增 7 个接口:订单详情「用餐」页签的用餐信息查询 / 按产品快照生成 / 整单一次保存 / 重置,以及用餐模版的查询 / 保存 / 套用。前端需按本文契约对接;人数与桌数二选一、类别字典过滤 SELF 两点见「四、契约约束」。" +updated_at: "2026-09-18" +base: "dev-v3" +--- + +# order-v3: 订单用餐信息与用餐模版接口 + +> **服务**: hl-order-service-v3(订单库新建两张表,随服务部署由 Flyway 创建) +> **PR**: #7945(主体)、#7954(3.1 团期编号回显补充) +> **Issue**: #7741、#7953 +> **日期**: 2026-09-18 +> **影响范围**: 管理后台订单详情「用餐」页签(新接口,老接口不变) + +--- + +## ⚠️ 关键变化 + +🔴 **新增 7 个接口,路径前缀 `/v3/admin/order/meal-info` 与 `/v3/admin/order/meal-template`。** + +🔴 **订单用餐的增、删、改只有一个接口:3 号「整单保存」。** 页面上加行、改值、删行都不调后端,点「保存」时把这张订单页面上的全部行一次提交:带 `mealInfoId` 的整行覆盖,不带的新增,原来有而这次没传上来的行被删除。 + +🔴 **金额由前端按公式算好传上来,后端逐行重算核对,对不上返回 589613,整批不保存。** 桌数为 0 按人算,不为 0 按桌算(公式见 3 号接口)。 + +🔴 **类别字典 `meal_type` 在 TEST 有 BREAKFAST / LUNCH / DINNER / SELF 四个值,接口只接受前三个,页面下拉请过滤掉 SELF。** + +--- + +## 一、背景 + +订单详情新增「用餐」页签:按订单产品快照里每天早午晚餐的安排,自动生成需要安排的用餐行;运营逐行选餐厅、餐食,填桌数或人数,保存金额;排好的几天可以存成「用餐模版」,以后套到别的订单上。 + +生成规则(快照里行程天的早 / 午 / 晚餐取值,字典 `meal_option`): + +| 取值 | 中文 | 是否生成用餐行 | +|------|------|----------------| +| `HOTEL` | 含(酒店) | 不生成 | +| `SELF` | 自理 | 不生成 | +| `CAMP` | 含(营地) | 生成这一天这一餐的一行空行 | +| `SPECIAL` | 含(特色餐) | 生成这一天这一餐的一行空行 | +| `REGULAR` | 含(正餐) | 生成这一天这一餐的一行空行 | + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 查询用餐信息列表 | GET | `/v3/admin/order/meal-info/list` | 新增 | 页签查询行和合计 | +| 2 | 生成用餐信息 | POST | `/v3/admin/order/meal-info/generate` | 新增 | 按产品快照生成空行,已有行不重复生成 | +| 3 | 保存用餐信息(整单一次保存) | POST | `/v3/admin/order/meal-info/save` | 新增 | 增、改、删一次提交 | +| 4 | 重置用餐信息 | POST | `/v3/admin/order/meal-info/reset` | 新增 | 删掉全部行后按快照重新生成空行 | +| 5 | 查询用餐模版 | GET | `/v3/admin/order/meal-template/list` | 新增 | 按模版ID或模版名查,按模版分组 | +| 6 | 保存为用餐模版 | POST | `/v3/admin/order/meal-template/save` | 新增 | 选几行存成一个新模版 | +| 7 | 套用用餐模版 | POST | `/v3/admin/order/meal-template/apply` | 新增 | 把模版套到一张订单上 | + +--- + +## 三、接口详情 + +### 1. 查询用餐信息列表 `GET /v3/admin/order/meal-info/list` + +**VO**: `OrderMealInfoListReqVO` → `OrderMealInfoListRespVO` + +#### 使用场景 + +订单详情「用餐」页签打开时传当前订单的 `orderId`,取这张订单的全部用餐行、合计金额和订单信息;`generated=false` 且 `orderEditable=true` 时再调 2 号接口生成。按团期查询时传 `groupBatchId` 或 `batchNo`。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Query | String | 否 | 正数 | 订单ID | +| groupBatchId | Query | String | 否 | 正数 | 团期ID | +| batchNo | Query | String | 否 | ≤64 | 团期编号,精确匹配 | +| mealType | Query | String | 否 | BREAKFAST / LUNCH / DINNER | 类别 | +| mealDateStart | Query | String | 否 | yyyy-MM-dd | 用餐日期起(含) | +| mealDateEnd | Query | String | 否 | yyyy-MM-dd,≥ mealDateStart | 用餐日期止(含) | +| dayNumber | Query | Integer | 否 | ≥1 | 第几天 | +| settleType | Query | String | 否 | cash / sign / company | 付款方式 | +| keyword | Query | String | 否 | ≤64 | 模糊匹配餐厅名称、餐食名称;`%`、`_` 按字面量匹配 | +| limit | Query | Integer | 否 | 1–500,默认 200 | 最大返回条数 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| orderId | String | 入参订单ID;没传为 null | +| groupBatchId | String | 订单所属团期ID;订单没挂团期或没传订单ID时为入参团期ID;都没有为 null | +| batchNo | String | 与 `groupBatchId` 对应的团期编号;没有为 null | +| departDate | String | 订单出发日期 yyyy-MM-dd;没传订单ID为 null | +| returnDate | String | 订单返回日期;没传订单ID为 null | +| personCount | Integer | 订单人数:成人 + 儿童 + 幼童 + 婴儿;没传订单ID为 null | +| orderEditable | Boolean | 订单是否可改:已完成、已取消为 false;没传订单ID为 null | +| generated | Boolean | 该订单是否已有用餐行(不受筛选条件影响);没传订单ID为 null | +| totalAmount | Number | 本次返回各行金额之和 | +| items[].mealInfoId | String | 用餐信息ID | +| items[].orderId | String | 订单ID,可为 null | +| items[].groupBatchId | String | 团期ID,可为 null | +| items[].batchNo | String | 团期编号,可为 null | +| items[].restaurantId | String | 餐厅ID,可为 null | +| items[].restaurantName | String | 餐厅名称,可为 null | +| items[].dishId | String | 餐食ID,没选餐食为 null | +| items[].dishCode | String | 餐食编码,没选餐食为 null | +| items[].dishName | String | 餐食名称,没选餐食为 null | +| items[].mealType | String | 类别 BREAKFAST / LUNCH / DINNER | +| items[].mealDate | String | 用餐日期 yyyy-MM-dd | +| items[].dayNumber | Integer | 第几天;手动添加的行为 null | +| items[].unitPrice | Number | 单价(按人时是每人单价,按桌时是每桌单价) | +| items[].tableCount | Integer | 桌数;0 表示按人算 | +| items[].personCount | Integer | 人数 | +| items[].freePersonCount | Integer | 免人数 | +| items[].freeAmount | Number | 免金额 | +| items[].otherCost | Number | 其他成本 | +| items[].amount | Number | 金额 | +| items[].settleType | String | 付款方式编码 cash / sign / company,可为 null | +| items[].settleTypeName | String | 付款方式中文,可为 null | +| items[].createTime | String | 创建时间 yyyy-MM-dd HH:mm:ss | + +#### 请求示例 + +```http +GET /v3/admin/order/meal-info/list?orderId=2100864597505286145 +Authorization: Bearer +``` + +#### 响应示例 + +TEST 实测返回(节选 1 行): + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "orderId": "2100864597505286145", + "groupBatchId": null, + "batchNo": null, + "departDate": "2026-10-23", + "returnDate": "2026-10-25", + "personCount": 2, + "orderEditable": true, + "generated": true, + "totalAmount": 60.00, + "items": [ + { + "mealInfoId": "2100883161750687746", + "orderId": "2100864597505286145", + "groupBatchId": null, + "batchNo": null, + "restaurantId": "2023382108491247617", + "restaurantName": "七间房全羊馆", + "dishId": "2100864423122898946", + "dishCode": "HL7741A", + "dishName": "HL7741-TEST-餐食A", + "mealType": "LUNCH", + "mealDate": "2026-10-20", + "dayNumber": 1, + "unitPrice": 30.00, + "tableCount": 0, + "personCount": 0, + "freePersonCount": 0, + "freeAmount": 0.00, + "otherCost": 0.00, + "amount": 0.00, + "settleType": "sign", + "settleTypeName": "签单", + "createTime": "2026-09-18 17:42:33" + } + ] + } +} +``` + +#### 空数据 / 降级响应 + +订单还没有用餐行时 `generated=false`、`items=[]`、`totalAmount=0`,其余订单字段照常返回。按团期编号查不到团期时返回空列表。 + +#### 错误响应 + +```json +{ "code": 400, "message": "用餐日期起不能晚于用餐日期止", "success": false, "data": null } +``` + +#### 业务边界 + +- 只查未删除的行,各条件之间是「并且」;排序:用餐日期升序 → 早、午、晚 → 创建时间升序。 +- 传 `orderId`:订单不存在按订单模块现有错误返回;房务角色返回 581045;需对该订单有查看权限。 +- 不传 `orderId`、传团期ID或团期编号:需要团期查看权限(不足 589507),范围是挂在该团期上的行。 +- 三个都不传:房务角色返回 581045;只查「没挂订单也没挂团期」的行。 + +### 2. 生成用餐信息 `POST /v3/admin/order/meal-info/generate` + +**VO**: `OrderMealInfoGenerateReqVO` → `OrderMealInfoListRespVO` + +#### 使用场景 + +1 号接口返回 `generated=false` 且订单可改时调用,按订单产品快照生成空行(规则见「一、背景」表)。生成不读模版,要模版的调 7 号接口。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Body | String | ✅ | 正数 | 订单ID | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | Object | 生成后按该订单查询的结果,字段同 1 号接口出参 | + +#### 请求示例 + +```json +{ "orderId": "2100864614022451201" } +``` + +#### 响应示例 + +TEST 实测(订单 B 生成时返回,节选 1 行):第 1 天午餐 = 含(营地),生成的行餐厅、餐食、付款方式为空,数值全为 0: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "orderId": "2100864614022451201", + "departDate": "2026-10-20", + "returnDate": "2026-10-22", + "personCount": 2, + "orderEditable": true, + "generated": true, + "totalAmount": 0.00, + "items": [ + { + "mealInfoId": "2100882946096353281", + "orderId": "2100864614022451201", + "restaurantId": null, + "restaurantName": null, + "dishId": null, + "dishCode": null, + "dishName": null, + "mealType": "LUNCH", + "mealDate": "2026-10-20", + "dayNumber": 1, + "unitPrice": 0.00, + "tableCount": 0, + "personCount": 0, + "freePersonCount": 0, + "freeAmount": 0.00, + "otherCost": 0.00, + "amount": 0.00, + "settleType": null, + "settleTypeName": null, + "createTime": "2026-09-18 17:41:42" + } + ] + } +} +``` + +#### 空数据 / 降级响应 + +订单出发或返回日期为空时不生成,**返回成功**(`code` 200),`message` 为「订单出发或返回日期为空,无法生成用餐信息」(错误码 589602 的文案,只作提示),`data.items` 为 `[]`。快照里没有需要生成的餐时也返回成功、`items` 为空。 + +#### 错误响应 + +```json +{ "code": 589607, "message": "订单已完成或已取消,不能修改用餐信息", "success": false, "data": null } +``` + +#### 业务边界 + +- 用餐日期 = 出发日期 + d − 1,第几天 = d;天数按出发到返回日期算,快照里超出的天不生成。 +- 幂等:该订单已有未删除的行时不再生成,直接返回现有列表。 +- 订单不存在、对订单无操作权限按订单模块现有错误返回;已完成或已取消 589607;返回日期早于出发日期 589612。 + +### 3. 保存用餐信息(整单一次保存) `POST /v3/admin/order/meal-info/save` + +**VO**: `OrderMealInfoBatchSaveReqVO` → `OrderMealInfoListRespVO` + +#### 使用场景 + +「用餐」页签点「保存」,把这张订单页面上的全部行一次提交。页面上的「添加」「删除」和行内改值都不调后端。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Body | String | ✅ | 正数 | 订单ID | +| groupBatchId | Body | String | 否 | 正数 | 传了须与订单所属团期一致,否则 589601 | +| items | Body | Array | ✅ | 可为空数组 | 这张订单保存后应有的全部行;空数组表示一行都不留 | +| items[].mealInfoId | Body | String | 否 | — | 传了更新这一行,不传新增一行 | +| items[].restaurantId | Body | String | 否 | — | 餐厅ID(来自餐厅选项接口),原样保存 | +| items[].restaurantName | Body | String | 否 | ≤128 | 餐厅名称,原样保存 | +| items[].dishId | Body | String | 否 | — | 餐食ID(来自餐食下拉),没选餐食不传 | +| items[].dishCode | Body | String | 否 | ≤20 | 餐食编码,原样保存 | +| items[].dishName | Body | String | 否 | 去首尾空格后 ≤500 | 餐食名称,原样保存 | +| items[].mealType | Body | String | 否 | BREAKFAST / LUNCH / DINNER,默认 LUNCH | 类别 | +| items[].mealDate | Body | String | ✅ | yyyy-MM-dd | 用餐日期 | +| items[].unitPrice | Body | Number | 否 | 0–99999999.99,两位小数,默认 0 | 单价 | +| items[].tableCount | Body | Integer | 否 | 0–999,默认 0 | 桌数;0 按人算,不为 0 按桌算 | +| items[].personCount | Body | Integer | 否 | 0–9999,默认 0 | 人数 | +| items[].freePersonCount | Body | Integer | 否 | ≥0,默认 0 | 免人数;不校验与人数的大小关系 | +| items[].freeAmount | Body | Number | 否 | 0–99999999.99,两位小数,默认 0 | 免金额 | +| items[].otherCost | Body | Number | 否 | 0–99999999.99,两位小数,默认 0 | 其他成本 | +| items[].amount | Body | Number | 否 | 0–99999999.99,两位小数,默认 0 | 金额,前端按公式算好传上来 | +| items[].settleType | Body | String | 否 | cash / sign / company | 付款方式编码 | +| items[].settleTypeName | Body | String | 否 | ≤32 | 付款方式中文,原样保存 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | Object | 保存后按该订单查询的结果,字段同 1 号接口出参;新增行是新生成的 `mealInfoId`,`dayNumber` 为 null | + +#### 请求示例 + +示例(金额取自 TEST 实测数据;节选:改一行按人、改一行按桌、新增一行,没传上来的行被删除): + +```json +{ + "orderId": "2100864597505286145", + "items": [ + { + "mealInfoId": "2100883161750687746", + "restaurantId": "2023382108491247617", "restaurantName": "七间房全羊馆", + "dishId": "2100864423122898946", "dishCode": "HL7741A", "dishName": "HL7741-TEST-餐食A", + "mealType": "LUNCH", "mealDate": "2026-10-20", + "unitPrice": 30, "tableCount": 0, "personCount": 4, "freePersonCount": 1, + "freeAmount": 0, "otherCost": 10, "amount": 100.00, + "settleType": "sign", "settleTypeName": "签单" + }, + { + "mealInfoId": "2100883161754882049", + "restaurantId": "2023382108491247617", "restaurantName": "七间房全羊馆", + "dishId": "2100864435710009346", "dishCode": "HL7741B", "dishName": "HL7741-TEST-餐食B", + "mealType": "DINNER", "mealDate": "2026-10-20", + "unitPrice": 800, "tableCount": 2, "personCount": 0, "freePersonCount": 0, + "freeAmount": 100, "otherCost": 50, "amount": 1550.00, + "settleType": "cash", "settleTypeName": "现付" + }, + { + "restaurantId": "2023382108491247617", "restaurantName": "七间房全羊馆", + "dishId": "2100864423122898946", "dishCode": "HL7741A", "dishName": "HL7741-TEST-餐食A", + "mealType": "BREAKFAST", "mealDate": "2026-10-21", + "unitPrice": 30, "personCount": 2, "amount": 60.00, + "settleType": "sign", "settleTypeName": "签单" + } + ] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "orderId": "2100864597505286145", + "generated": true, + "orderEditable": true, + "totalAmount": 1710.00, + "items": [ + { "mealInfoId": "2100883161750687746", "mealType": "LUNCH", "mealDate": "2026-10-20", "dayNumber": 1, "tableCount": 0, "personCount": 4, "freePersonCount": 1, "amount": 100.00 }, + { "mealInfoId": "2100883161754882049", "mealType": "DINNER", "mealDate": "2026-10-20", "dayNumber": 1, "tableCount": 2, "personCount": 0, "freePersonCount": 0, "amount": 1550.00 }, + { "mealInfoId": "2100883192117448705", "mealType": "BREAKFAST", "mealDate": "2026-10-21", "dayNumber": null, "tableCount": 0, "personCount": 2, "freePersonCount": 0, "amount": 60.00 } + ] + } +} +``` + +(`items` 每行实际返回 1 号接口出参的全部字段,此处省略餐厅、餐食等字段。) + +#### 空数据 / 降级响应 + +`items` 传空数组表示这张订单一行都不留:原有行全部删除,返回 `items=[]`、`totalAmount=0`、`generated=false`。 + +#### 错误响应 + +```json +{ "code": 589613, "message": "金额与明细对不上,请刷新后重试", "success": false, "data": null } +``` + +#### 业务边界 + +- 金额公式:桌数为 0 时 **金额 = 单价 ×(人数 − 免人数)− 免金额 + 其他成本**;桌数不为 0 时 **金额 = 单价 × 桌数 − 免金额 + 其他成本**;算出负数存 0,四舍五入到两位小数。后端按同一公式逐行核对,任何一行对不上返回 589613,整批不保存。 +- 带 `mealInfoId` 的行整行覆盖:可选字段不传按清空处理(数值取 0,类别取 LUNCH);`dayNumber` 保持原值。 +- `mealInfoId` 必须是这张订单未删除的行:查不到或已删除 589606;原来挂的不是这张订单 589611。 +- 餐厅、餐食、单价、付款方式按页面传的原样保存,不回查资源服务;保存前餐食被下架或删除也照常保存。 +- 订单已完成或已取消 589607;`groupBatchId` 与订单所属团期不一致 589601;整批一个事务,任何错误都不写入。 + +### 4. 重置用餐信息 `POST /v3/admin/order/meal-info/reset` + +**VO**: `OrderMealInfoResetReqVO` → `OrderMealInfoListRespVO` + +#### 使用场景 + +把这张订单的用餐行全部删掉(含人工改过的、手动加的),再按产品快照重新生成一套空行,结果与第一次调 2 号接口一致。要模版的,重置后再调 7 号接口。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Body | String | ✅ | 正数 | 订单ID | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | Object | 重置后按该订单查询的结果,字段同 1 号接口出参;行ID全部是新的 | + +#### 请求示例 + +```json +{ "orderId": "2100864597505286145" } +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "orderId": "2100864597505286145", + "generated": true, + "totalAmount": 0.00, + "items": [ + { "mealInfoId": "2100883161750687746", "mealType": "LUNCH", "mealDate": "2026-10-20", "dayNumber": 1, "dishName": null, "restaurantName": null, "unitPrice": 0.00, "tableCount": 0, "personCount": 0, "amount": 0.00 } + ] + } +} +``` + +(TEST 实测,节选 1 行与部分字段,字段同 1 号接口。) + +#### 空数据 / 降级响应 + +订单出发或返回日期为空时:原有行照样删除、不生成新行,**返回成功**,`message` 为 589602 的提示文案,`items=[]`。 + +#### 错误响应 + +```json +{ "code": 589607, "message": "订单已完成或已取消,不能修改用餐信息", "success": false, "data": null } +``` + +#### 业务边界 + +- 删除和重新生成在一个事务里;返回日期早于出发日期 589612,此时不删也不生成。 +- 不受 2 号接口「已有行就不再生成」的限制。 +- 订单不存在、无操作权限按订单模块现有错误返回;已完成或已取消 589607。 + +### 5. 查询用餐模版 `GET /v3/admin/order/meal-template/list` + +**VO**: `MealTemplateListReqVO` → `List` + +#### 使用场景 + +「用餐」页签选择模版时,列出模版及其内容。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| templateId | Query | String | 否 | — | 模版ID精确匹配;传了忽略 templateName | +| templateName | Query | String | 否 | ≤500 | 模版名精确匹配;两个都不传返回全部模版 | + +#### 出参 `Result>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| [].templateId | String | 模版ID;同一个模版的行共用 | +| [].templateName | String | 模版名;允许重名,重名的是各自独立的模版 | +| [].items[].id | String | 模版行ID | +| [].items[].dayNumber | Integer | 第几日,从 1 开始 | +| [].items[].mealType | String | 餐次 BREAKFAST / LUNCH / DINNER | +| [].items[].restaurantId | String | 餐厅ID,可为 null | +| [].items[].restaurantName | String | 餐厅名称,可为 null | +| [].items[].dishId | String | 餐食ID,可为 null | +| [].items[].dishCode | String | 餐食编码,可为 null | +| [].items[].dishName | String | 餐食名称,可为 null | +| [].items[].unitPrice | Number | 价钱 | +| [].items[].settleType | String | 付款方式编码,可为 null | +| [].items[].settleTypeName | String | 付款方式中文,可为 null | + +#### 请求示例 + +```http +GET /v3/admin/order/meal-template/list?templateId=2100883046935748610 +Authorization: Bearer +``` + +#### 响应示例 + +TEST 实测返回(节选 1 行): + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": [ + { + "templateId": "2100883046935748610", + "templateName": "HL7741-TEST-含营地", + "items": [ + { + "id": "2100883046935748611", + "dayNumber": 1, + "mealType": "LUNCH", + "restaurantId": "2023382108491247617", + "restaurantName": "七间房全羊馆", + "dishId": "2100864423122898946", + "dishCode": "HL7741A", + "dishName": "HL7741-TEST-餐食A", + "unitPrice": 30.00, + "settleType": "sign", + "settleTypeName": "签单" + } + ] + } + ] +} +``` + +#### 空数据 / 降级响应 + +```json +{ "code": 200, "message": "成功", "success": true, "data": [] } +``` + +#### 错误响应 + +```json +{ "code": 581045, "message": "房务角色无权查看订单详情,房务仅可配房", "success": false, "data": null } +``` + +#### 业务边界 + +- 按模版ID分组,模版之间按保存先后排序;同一个模版内按第几日升序、早午晚排序。 +- 模版里存的是快照:餐食或餐厅后来被下架、删除,照常返回存下的名称和价钱。 +- 模版不存用餐日期、桌数、人数、免人数、免金额、其他成本、金额。 + +### 6. 保存为用餐模版 `POST /v3/admin/order/meal-template/save` + +**VO**: `MealTemplateSaveReqVO` → `MealTemplateRespVO` + +#### 使用场景 + +在「用餐」页签把已排好的几行存成模版,模版名自己取。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| templateName | Body | String | ✅ | 去首尾空格后 1–500 | 模版名;允许和已有模版重名 | +| mealInfoIds | Body | Array<String> | ✅ | 非空,自动去重 | 要存进模版的用餐信息行ID | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | Object | 新模版,结构同 5 号接口的一个元素(含新生成的 `templateId`) | + +#### 请求示例 + +```json +{ + "templateName": "HL7741-TEST-含营地", + "mealInfoIds": ["2100883161750687746", "2100883161754882049", "2100883192117448705", "2100883161759076354"] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "templateId": "2100883046935748610", + "templateName": "HL7741-TEST-含营地", + "items": [ + { "id": "2100883046935748611", "dayNumber": 1, "mealType": "LUNCH", "dishName": "HL7741-TEST-餐食A", "unitPrice": 30.00, "settleType": "sign", "settleTypeName": "签单" }, + { "id": "2100883047007051777", "dayNumber": 1, "mealType": "DINNER", "dishName": "HL7741-TEST-餐食B", "unitPrice": 800.00, "settleType": "cash", "settleTypeName": "现付" }, + { "id": "2100883047032217601", "dayNumber": 2, "mealType": "BREAKFAST", "dishName": "HL7741-TEST-餐食A", "unitPrice": 30.00, "settleType": "sign", "settleTypeName": "签单" }, + { "id": "2100883047007051778", "dayNumber": 2, "mealType": "LUNCH", "dishName": "HL7741-TEST-餐食A", "unitPrice": 30.00, "settleType": "sign", "settleTypeName": "签单" } + ] + } +} +``` + +(请求为示例;响应行取自 TEST 实测模版,节选字段,实际返回 5 号接口 `items[]` 的全部字段。第 2 日早餐是手动加的行,按最早日期补成第 2 日。) + +#### 空数据 / 降级响应 + +`mealInfoIds` 一行都取不到(都已删除或不存在)时返回 589606,不建模版;部分取不到时只存取到的行。 + +#### 错误响应 + +```json +{ "code": 400, "message": "用餐信息行ID不能为空; 模版名不能为空", "success": false, "data": null } +``` + +#### 业务边界 + +- 每次保存都新建一个模版(新的 `templateId`),不覆盖任何已有模版,名字一样也一样。 +- 第几日:行上有第几天直接用;没有(手动加的行)时以这批行里最早的用餐日期为第 1 日,按实际相差天数补。 +- 同一日同一餐出现多条返回 400,提示「第 d 日某餐重复」。 +- 存餐厅、餐食、餐次、第几日、价钱、付款方式(编码和中文);房务角色 581045。 + +### 7. 套用用餐模版 `POST /v3/admin/order/meal-template/apply` + +**VO**: `MealTemplateApplyReqVO` → `OrderMealInfoListRespVO` + +#### 使用场景 + +「用餐」页签选一个模版套到当前订单上。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Body | String | ✅ | 正数 | 订单ID | +| templateId | Body | String | ✅ | — | 模版ID | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | Object | 套用后按该订单查询的结果,字段同 1 号接口出参 | + +#### 请求示例 + +```json +{ "orderId": "2100864597505286145", "templateId": "2100883046935748610" } +``` + +#### 响应示例 + +TEST 实测:模版第 1 日午餐覆盖到出发日那天已有的午餐行(同一行ID),模版第 2 日早餐订单上没有,新建一行: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "orderId": "2100864597505286145", + "generated": true, + "items": [ + { "mealInfoId": "2100883161750687746", "mealType": "LUNCH", "mealDate": "2026-10-20", "dayNumber": 1, "dishName": "HL7741-TEST-餐食A", "unitPrice": 30.00, "settleType": "sign", "tableCount": 0, "personCount": 0, "amount": 0.00 }, + { "mealInfoId": "2100883192117448705", "mealType": "BREAKFAST", "mealDate": "2026-10-21", "dayNumber": 2, "dishName": "HL7741-TEST-餐食A", "unitPrice": 30.00, "settleType": "sign", "tableCount": 0, "personCount": 0, "amount": 0.00 }, + { "mealInfoId": "2100883161759076355", "mealType": "DINNER", "mealDate": "2026-10-21", "dayNumber": 2, "restaurantName": "HL7741-TEST-原行", "dishName": null, "unitPrice": 20.00, "settleType": null, "tableCount": 0, "personCount": 3, "amount": 60.00 } + ] + } +} +``` + +(TEST 实测,节选字段与行:模版没有第 2 日晚餐,订单原来的那一行不动。) + +#### 空数据 / 降级响应 + +订单出发日期为空时不套用,**返回成功**,`message` 为 589602 的提示文案,行不变。模版ID不存在或模版没有行时什么都不改,返回订单当前的行。 + +#### 错误响应 + +```json +{ "code": 589607, "message": "订单已完成或已取消,不能修改用餐信息", "success": false, "data": null } +``` + +#### 业务边界 + +- 订单出发日是第 1 日,模版第 d 日落到出发日 + d − 1。 +- 模版第 d 日某餐有安排:订单上已有那天那一餐的行,覆盖餐厅(ID、名称)、餐食(ID、编码、名称)、价钱、付款方式(编码、中文),金额按新价钱用同一公式重算;没有那一行就新建一行,桌数、人数、免人数、免金额、其他成本为 0。 +- 模版里没有的第 d 日、或第 d 日没有的那一餐,订单原来的行不动。 +- 模版里的餐食、餐厅被下架或删除,仍用模版里存的名称和价钱。订单已完成或已取消 589607。 + +--- + +## 四、契约约束与正确调用方式 + +### ✅ 正确 / ❌ 错误 payload 对照 + +| 场景 | payload | 结果 | +|------|---------|------| +| ✅ 按人算 | `{ "unitPrice": 30, "tableCount": 0, "personCount": 4, "freePersonCount": 1, "otherCost": 10, "amount": 100.00 }` | 保存成功 | +| ✅ 按桌算 | `{ "unitPrice": 800, "tableCount": 2, "personCount": 0, "freePersonCount": 0, "freeAmount": 100, "otherCost": 50, "amount": 1550.00 }` | 保存成功 | +| ✅ 算出负数 | `{ "unitPrice": 30, "personCount": 2, "freeAmount": 100, "amount": 0 }` | 保存成功,金额 0 | +| ❌ 金额对不上 | `{ "unitPrice": 30, "personCount": 4, "freePersonCount": 1, "otherCost": 10, "amount": 99.99 }` | 589613,整批不保存 | +| ❌ 类别传 SELF | `{ "mealType": "SELF", "mealDate": "2026-10-20" }` | 400 | + +### 需要前端配合的两点 + +1. **人数与桌数二选一**:人数不为 0 时,桌数不能点击并设成 0;桌数不为 0 时,人数、免人数不能点击并设成 0。后端只按「桌数是否为 0」选公式。 +2. **类别下拉过滤 SELF**:字典 `meal_type` 的 SELF 值不要出现在类别下拉里。 + +### 保存时的整单语义 + +3 号接口的 `items` 必须是页面上这张订单的**全部**行。只提交改动过的行会把其余行删掉。 + +--- + +## 五、数据库行为 + +| 前端操作 | 结果 | +|----------|------| +| 2 号生成 | 该订单没有用餐行时写入一批空行;已有行时不写 | +| 3 号保存 | 带ID的行整行覆盖,不带ID的新增,没传上来的原有行软删除;任一行失败整批不写 | +| 4 号重置 | 软删除该订单全部用餐行,再写入一批新空行;一个事务 | +| 6 号存模版 | 新增一组模版行(新模版ID),不改任何已有模版 | +| 7 号套模版 | 覆盖对应日期对应餐次的行、新建缺少的行;其余行不动 | + +被删除的行不会再出现在任何查询结果里。订单后来改出发日期或人数,已生成的用餐行不会跟着变(需要时调 4 号重置)。 + +--- + +## 六、边界行为 + +- 未登录 → 401(网关拦截)。 +- 房务角色调 1、5、6 号及写接口 → 581045。 +- 订单不存在 → 订单模块现有「订单不存在」错误。 +- 订单已完成或已取消 → 2、3、4、7 号返回 589607,不写入;1 号照常可查,`orderEditable=false`。 +- 餐食、餐厅被下架或删除 → 已保存的用餐行与模版照常显示存下的名称和价钱。 + +--- + +## 六.5、枚举 / 数据字典 + +### mealType(类别 / 餐次) + +**所属字段**: `mealType`(3、5、6、7 号入参或出参) | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `BREAKFAST` | 早餐 | — | +| `LUNCH` | 午餐 | 3 号接口不传时的默认值 | +| `DINNER` | 晚餐 | — | + +### settleType(付款方式,字典 `resource_settle_type`) + +**所属字段**: `settleType` / `settleTypeName` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `cash` | 现付 | — | +| `sign` | 签单 | — | +| `company` | 公司付款 | — | + +### 错误码(hl-order-service-v3,段位 589600–589699) + +| code | message | 触发接口 | +|------|---------|----------| +| 589601 | 订单不属于该团期 | 3 | +| 589602 | 订单出发或返回日期为空,无法生成用餐信息 | 2、4、7(只作提示:`code` 200,文案放在 `message`) | +| 589606 | 用餐信息不存在 | 3、6 | +| 589607 | 订单已完成或已取消,不能修改用餐信息 | 2、3、4、7 | +| 589611 | 已挂订单或团期的用餐信息不能改挂或清空 | 3 | +| 589612 | 返回日期早于出发日期,无法生成用餐信息 | 2、4 | +| 589613 | 金额与明细对不上,请刷新后重试 | 3 | + +--- + +## 七、不影响范围 + +- **仅影响**:订单详情「用餐」页签(全新接口)。 +- **零影响**: + - 订单、团期、核单等现有接口的入参出参; + - 餐食管理、餐厅选项接口(本单只读用它们的数据); + - 历史订单:不会自动生成用餐行,打开页签时才按 2 号接口生成。 + +--- + +## 八、测试环境已验证 + +部署 hl-order-service-v3(dev-v3,最终部署提交 6a121038b,含 #7945 与 #7954),经 Gateway 用管理员身份逐项实测,假数据名称均带 `HL7741-TEST`: + +``` +POST /v3/admin/order/meal-info/generate → 只为含营地/特色餐/正餐生成 6 行空行,含酒店、自理不生成;再调不重复 ✓ +POST /v3/admin/order/meal-info/save → 新增+改+少传一行:新增有、改的变、少传的软删;按人 100.00、按桌 1550.00、负数存 0、免人数>人数照存 ✓ +POST /v3/admin/order/meal-info/save → 金额写错 → 589613,库里行不变 ✓;餐食下架/删除后照存页面值 ✓ +POST /v3/admin/order/meal-info/save → 行ID已删除/不存在 → 589606;别的订单的行 → 589611,两单行不变 ✓ +POST /v3/admin/order/meal-template/save → 模版只存模版名/第几日/餐次/餐厅/餐食/价钱/付款方式;同名再存是新模版,老模版不变 ✓ +POST /v3/admin/order/meal-info/reset → 原有行(含改过的、手动加的)全部软删,重新生成与首次一致 ✓ +POST /v3/admin/order/meal-template/apply → 第 1 日落出发日;已有行覆盖、缺的新建、模版缺的不动;餐食下架/删除后仍用模版名称和价钱 ✓ +POST /v3/admin/order/{id}/adjustment/submit(改出发日期)→ 已生成的用餐行不变 ✓ +已取消 / 已完成订单调 2、3、4、7 号 → 589607,零写入 ✓ +GET /v3/admin/order/meal-info/list(没挂团期的订单 + 团期ID / 团期编号)→ 回显同一团期的ID和编号 ✓ +``` + +验证订单:`2100864597505286145`(HL7741-TEST-orderA)、`2100864614022451201`(HL7741-TEST-orderB,已取消)。 + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7741](https://git.1814.love:8443/wx/HL/issues/7741)、[wx/HL#7953](https://git.1814.love:8443/wx/HL/issues/7953) +- 关联 PR: [wx/HL#7945](https://git.1814.love:8443/wx/HL/pulls/7945)、[wx/HL#7954](https://git.1814.love:8443/wx/HL/pulls/7954) +- 依赖接口:餐食下拉 `GET /admin/dish/items/list`(#7737)、餐厅选项 `GET /admin/resource-options/restaurants` + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7741](https://git.1814.love:8443/wx/HL/issues/7741)、[#7953](https://git.1814.love:8443/wx/HL/issues/7953) +- **PR**: [#7945](https://git.1814.love:8443/wx/HL/pulls/7945)、[#7954](https://git.1814.love:8443/wx/HL/pulls/7954) +- **Merge commit**: [f284bad1](https://git.1814.love:8443/wx/HL/commit/f284bad157f0b7da9f10d36e7a5f0ad5a0d1978b)(#7945)、[6a121038](https://git.1814.love:8443/wx/HL/commit/6a121038b5ae4b5fac8af9ee7b89482f860e810e)(#7954) + +### 联系人 + +- **后端负责人**: @lc