--- schema: "hl-changelog/v2" ticket: "8230" title: "团期订单支持用餐:生成、保存、重置、查询、套用模版可只传团期ID" consumer: "admin" author: "lc(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "not_required" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "已部署 TEST 并经 Gateway 实测通过。用餐的查询、生成、保存、重置、套用模版 5 个接口加入团期维度:orderId 与 groupBatchId 必须且只能传一个,都传或都不传返回 400。只传 groupBatchId 时读写的是团期自己的用餐行(行上团期ID有值、订单ID为空);按订单读写的行此后团期ID为空,按团期查不到子订单的行。团期的出发/返回日期取团期出团日期 depart_date / 结束日期 end_date。前端结论(2026-09-23):现有子订单用餐页(MealTab)5 个调用点全只传 orderId,查询恒有 orderId 空值守卫,零 batchNo/groupBatchId 读写、零 589601 分支——「挂团订单保存勿再带 groupBatchId」「只传 batchNo 400」等破坏点前端零命中,基本行为零改动。团期维度用餐属新增能力,前端无团期用餐页面,建页为新产品需求待拍板,非本次同步范围。" updated_at: "2026-09-23" base: "dev-v3" --- # order-v3: 团期订单支持用餐,5 个用餐接口可只传团期ID > **服务**: hl-order-service-v3 > **PR**: #8251 > **Issue**: #8230 > **日期**: 2026-09-23 > **影响范围**: 管理后台用餐信息的查询、生成、保存、重置与套用模版(订单、团期两种维度) --- ## ⚠️ 关键变化 - 🔁 **二选一(破坏性)**:查询、生成、保存、重置、套用模版这 5 个接口,`orderId` 与 `groupBatchId` **必须且只能传一个**。都传返回 `400`「订单ID和团期ID只能传一个」;都不传返回 `400`「订单ID和团期ID必须传一个」。查询接口的 `batchNo` 不算数,**只传 `batchNo` 视为两个都没传**,同样 400。 - 🔁 **订单行不再带团期**:按订单生成、保存、重置、套用出来的行,行上 `groupBatchId`、`batchNo` 一律为空。以前挂团订单的行会顺带写团期ID / 团期编号,现在不写。 - 🔁 **保存不再用 `orderId` + `groupBatchId` 一起传**:以前保存时可以同时传两者并校验订单是否属于该团期(589601),现在同时传直接 400,589601 不再出现。 - 🆕 **团期维度**:只传 `groupBatchId` 时读写的是**团期自己的用餐行**,行上 `groupBatchId` 有值、`orderId` 为空;按团期查询**只返回团期自己的行,不含团里子订单的行**。 - ℹ️ **3.1 用 `orderId` + `batchNo` 组合此后查不到数据**:订单行上已不存团期编号,这个组合固定返回空列表。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|---|---|---|---|---| | 1 | 查询用餐信息列表(3.1) | GET | `/v3/admin/order/meal-info/list` | 修改 | `orderId` / `groupBatchId` 二选一;只传团期ID只返回团期自己的行,顶层汇总按团期填 | | 2 | 生成用餐信息(3.2) | POST | `/v3/admin/order/meal-info/generate` | 修改 | 新增 `groupBatchId`,二选一;团期按团期产品当前行程生成 | | 3 | 保存用餐信息(整单,3.3) | POST | `/v3/admin/order/meal-info/save` | 修改 | `orderId` 不再必填,二选一;只传团期ID保存团期行 | | 4 | 重置用餐信息(3.4) | POST | `/v3/admin/order/meal-info/reset` | 修改 | 新增 `groupBatchId`,二选一;团期删掉自己的行后重新生成 | | 5 | 套用用餐模版(4.3) | POST | `/v3/admin/order/meal-template/apply` | 修改 | 新增 `groupBatchId`,二选一;同一个模版可套到团期 | 出参字段一个都没增减。模版查询、存为模版、删除模版不变(模版不分订单、团期)。没有新增错误码。 --- ## 三、接口详情 ### 1. 查询用餐信息列表 `GET /v3/admin/order/meal-info/list` **VO**: `OrderMealInfoListReqVO` → `OrderMealInfoListRespVO` #### 使用场景 按订单或按团期取用餐行及顶层汇总。两种维度用同一个接口,由传 `orderId` 还是 `groupBatchId` 区分。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Query | String | 二选一 | 正数 | 按订单查。**与 `groupBatchId` 必须且只能传一个** | | groupBatchId | Query | String | 二选一 | 正数 | 按团期查,只返回团期自己的行 | | batchNo | Query | String | 否 | — | 不参与二选一;只传它按都没传处理(400);与 `orderId` 组合固定查不到数据 | | 其余筛选 | Query | — | 否 | — | `mealType`、`mealDateStart/End`、`dayNumber`、`settleType`、`keyword`、`limit` 不变 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | orderId | String | 按订单查为订单ID;按团期查为 `null` | | groupBatchId / batchNo | String | 按订单查:**仍回显订单所属团期**(不挂团为 `null`);按团期查:该团期 | | departDate / returnDate | String | 按订单查:订单出发/返回日期;按团期查:团期出团日期 `depart_date` / 结束日期 `end_date` | | personCount | Integer | 按订单查:订单人数;按团期查:团期报名人数 | | orderEditable | Boolean | 按团期查恒为 `true` | | generated | Boolean | 按团期查:该团期有没有自己的行 | | items[].orderId / items[].groupBatchId | String | 订单行:`orderId` 有值、`groupBatchId` 为 `null`;团期行:`groupBatchId` 有值、`orderId` 为 `null` | | items[].batchNo | String | 新写入的行一律为 `null` | #### 请求示例 ```http GET /v3/admin/order/meal-info/list?groupBatchId=2101996448340238338 ``` #### 响应示例 ```json { "code": 200, "success": true, "data": { "orderId": null, "groupBatchId": "2101996448340238338", "batchNo": "Q202611042099750379129368577", "departDate": "2026-11-04", "returnDate": "2026-11-08", "personCount": 4, "orderEditable": true, "generated": true, "totalAmount": 0.00, "items": [ { "mealInfoId": "…", "orderId": null, "groupBatchId": "2101996448340238338", "batchNo": null, "mealType": "LUNCH", "mealDate": "2026-11-04", "dayNumber": 1, "amount": 0.00 } ] } } ``` 按订单查(订单挂在该团期下)时顶层 `groupBatchId` 仍是 `"2101996448340238338"`,但 `items[].groupBatchId` 为 `null`。 #### 空数据 / 降级响应 - 团期还没有自己的行:`items: []`、`generated: false`,顶层汇总照常填。 - 团期里子订单有用餐行、团期自己没有:按团期查仍是 `items: []`。 - `orderId` + `batchNo`:`items: []`。 #### 错误响应 ```json { "code": 400, "message": "订单ID和团期ID只能传一个", "success": false, "data": null } ``` 都不传(含只传 `batchNo`):`400`「订单ID和团期ID必须传一个」。按团期查没有团期查看权限:`589507`。订单维度的错误码不变。 #### 业务边界 - 二选一在入参校验阶段判断,先于任何权限、存在性判断。 - 按团期查只按团期ID取行,不含团里子订单的行;按订单查只返回该订单的行。 - 顶层 `groupBatchId` 是订单汇总回显,不代表行上存了团期ID。 ### 2. 生成用餐信息 `POST /v3/admin/order/meal-info/generate` **VO**: `OrderMealInfoGenerateReqVO` → `OrderMealInfoListRespVO` #### 使用场景 订单或团期还没有用餐行时,按产品行程生成空行。团期传 `groupBatchId`。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Body | String | 二选一 | 正数 | 按订单产品快照生成(不变) | | groupBatchId | Body | String | 二选一 | 正数 | **新增**。按团期产品当前行程生成团期行 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | 全部字段 | — | 与查询接口同维度的返回一致(团期维度见接口 1) | | items[].dayNumber | Integer | 团期:第 d 天用餐日期 = `depart_date` + d − 1,天数到 `end_date` | #### 请求示例 ```json { "groupBatchId": "2101996448340238338" } ``` #### 响应示例 ```json { "code": 200, "success": true, "data": { "orderId": null, "groupBatchId": "2101996448340238338", "departDate": "2026-11-04", "returnDate": "2026-11-08", "personCount": 4, "generated": true, "items": [ { "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "LUNCH", "mealDate": "2026-11-04", "dayNumber": 1, "amount": 0.00 }, { "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "DINNER", "mealDate": "2026-11-04", "dayNumber": 1, "amount": 0.00 } ] } } ``` #### 空数据 / 降级响应 - 团期已有自己的行:不再生成,直接返回现有行(幂等)。 - 团期 `depart_date` 或 `end_date` 为空:不生成,`code` 200,`message` 带 589602 文案。 - 行程里含(酒店)/ 自理的餐、行程没有的天不生成;团期产品取不到行程时生成 0 行。 #### 错误响应 ```json { "code": 400, "message": "订单ID和团期ID必须传一个", "success": false, "data": null } ``` 都传:`400`「订单ID和团期ID只能传一个」。团期:无团期管理权限 `589507`、团期不存在 `589500`、`end_date` 早于 `depart_date` `589612`。 #### 业务边界 - 团期生成的每天每餐取团期产品**当前**行程,不是某张子订单下单时的快照。 - 团期不按团期状态拦截。 - 按订单生成的行 `groupBatchId` 为空。 ### 3. 保存用餐信息(整单) `POST /v3/admin/order/meal-info/save` **VO**: `OrderMealInfoBatchSaveReqVO` → `OrderMealInfoListRespVO` #### 使用场景 一次提交某张订单或某个团期保存后应有的全部行:带行ID覆盖、不带新增、原来有而这次没传的软删。团期**只传 `groupBatchId`**,不要同时传 `orderId`。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Body | String | 二选一 | 正数 | **不再必填**。按订单保存 | | groupBatchId | Body | String | 二选一 | 正数 | **含义变化**:以前是「订单所属团期一致校验」,现在只传它表示按团期保存 | | items[] | Body | Array | 是 | — | 保存后应有的全部行,字段不变;空数组表示一行都不留 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | 全部字段 | — | 与查询接口同维度的返回一致 | | items[].dayNumber | Integer | 团期行 = 用餐日期 − `depart_date` + 1;`depart_date` 为空时为 `null` | #### 请求示例 ```json { "groupBatchId": "2101996448340238338", "items": [ { "mealType": "LUNCH", "mealDate": "2026-11-05", "restaurantName": "…", "dishName": "…", "unitPrice": 45.00, "tableCount": 0, "personCount": 4, "freePersonCount": 0, "freeAmount": 0, "otherCost": 0, "amount": 180.00, "settleType": "sign", "settleTypeName": "签单" } ] } ``` #### 响应示例 ```json { "code": 200, "success": true, "data": { "orderId": null, "groupBatchId": "2101996448340238338", "departDate": "2026-11-04", "totalAmount": 180.00, "items": [ { "mealInfoId": "…", "orderId": null, "groupBatchId": "2101996448340238338", "batchNo": null, "mealType": "LUNCH", "mealDate": "2026-11-05", "dayNumber": 2, "amount": 180.00 } ] } } ``` #### 空数据 / 降级响应 - 团期传 `items: []`:该团期自己的行全部软删,返回 `items: []`,不影响子订单的行。 #### 错误响应 ```json { "code": 400, "message": "订单ID和团期ID只能传一个", "success": false, "data": null } ``` 都不传:`400`「订单ID和团期ID必须传一个」。团期:无团期管理权限 `589507`、团期不存在 `589500`、带的行ID不是本团期自己的行 `589611`、行查不到或已删 `589606`、金额对不上 `589613`。`589601` 不再返回。 #### 业务边界 - 团期保存只动团期自己的行,软删范围也只在团期自己的行里。 - 按订单保存新增的行 `groupBatchId` 为空。 - 任一行校验失败整批不写。 ### 4. 重置用餐信息 `POST /v3/admin/order/meal-info/reset` **VO**: `OrderMealInfoResetReqVO` → `OrderMealInfoListRespVO` #### 使用场景 删掉某张订单或某个团期自己的全部行,再按生成规则重新生成空行。团期传 `groupBatchId`。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Body | String | 二选一 | 正数 | 按订单重置(不变) | | groupBatchId | Body | String | 二选一 | 正数 | **新增**。按团期重置 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | 全部字段 | — | 与接口 2 生成的返回一致 | #### 请求示例 ```json { "groupBatchId": "2101996448340238338" } ``` #### 响应示例 ```json { "code": 200, "success": true, "data": { "orderId": null, "groupBatchId": "2101996448340238338", "generated": true, "items": [ { "mealInfoId": "…(新行ID)", "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "LUNCH", "mealDate": "2026-11-04", "dayNumber": 1, "amount": 0.00 } ] } } ``` #### 空数据 / 降级响应 - 团期 `depart_date` 或 `end_date` 为空:原有团期行照样删掉、不生成,`code` 200,`message` 带 589602 文案。 #### 错误响应 ```json { "code": 589612, "message": "返回日期早于出发日期,无法生成用餐信息", "success": false, "data": null } ``` `end_date` 早于 `depart_date` 时原有行不删。二选一 400、`589507`、`589500` 同接口 2。 #### 业务边界 - 团期重置只删团期自己的行,子订单的行不动;删和生成一起成功或一起失败。 - 重置后行ID全部换新。 ### 5. 套用用餐模版 `POST /v3/admin/order/meal-template/apply` **VO**: `MealTemplateApplyReqVO` → `OrderMealInfoListRespVO` #### 使用场景 把一个模版按出发日铺开,接在现有行后面返回(只读,不写库),确认后再用接口 3 保存。模版不分订单、团期,同一个模版两边都能套。套到团期传 `groupBatchId`。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Body | String | 二选一 | 正数 | **不再必填**。套到订单 | | groupBatchId | Body | String | 二选一 | 正数 | **新增**。套到团期 | | templateId | Body | String | 是 | — | 模版ID(不变) | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | items[] | Array | 前段是现有行(团期:团期自己的行),后段是模版行(`mealInfoId` 为 `null`) | | items[].mealDate | String | 团期:模版第 d 日 = `depart_date` + d − 1 | | items[].personCount | Integer | 团期按人的模版行 = 团期报名人数;按桌为 0 | | items[].orderId / groupBatchId | String | 套到团期:模版行 `groupBatchId` 有值、`orderId` 为 `null`;套到订单:模版行 `groupBatchId` 为 `null` | | 其余字段 | — | 与 #8125 相同 | #### 请求示例 ```json { "groupBatchId": "2101996448340238338", "templateId": "2102035945748684802" } ``` #### 响应示例 ```json { "code": 200, "success": true, "data": { "orderId": null, "groupBatchId": "2101996448340238338", "departDate": "2026-11-04", "personCount": 4, "items": [ { "mealInfoId": "…", "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "LUNCH", "mealDate": "2026-11-05", "dayNumber": 2 }, { "mealInfoId": null, "orderId": null, "groupBatchId": "2101996448340238338", "mealType": "DINNER", "mealDate": "2026-11-06", "dayNumber": 3, "personCount": 4, "priceUnit": "person" } ] } } ``` #### 空数据 / 降级响应 - 团期 `depart_date` 为空:只返回团期现有行,`code` 200,`message` 带 589602 文案。 - 模版没有行:只返回现有行。 #### 错误响应 ```json { "code": 400, "message": "订单ID和团期ID只能传一个", "success": false, "data": null } ``` 都不传:`400`「订单ID和团期ID必须传一个」。团期:无团期管理权限 `589507`、团期不存在 `589500`。 #### 业务边界 - 套用不写库;保存时把返回的 `items` 原样用接口 3 提交,**套到团期的就只传 `groupBatchId` 提交**。 - 团期套用的现有行只取团期自己的行,不含子订单的行。 --- ## 四、契约约束与正确调用方式 | 场景 | payload | |------|---------| | ✅ 团期生成 / 重置 | `{ "groupBatchId": "…" }` | | ✅ 团期保存 | `{ "groupBatchId": "…", "items": [...] }` | | ✅ 团期套用 | `{ "groupBatchId": "…", "templateId": "…" }` | | ✅ 团期查询 | `?groupBatchId=…` | | ✅ 订单维度 | 只传 `orderId`,其余同改动前 | | ❌ 两个都传 | `{ "orderId": "…", "groupBatchId": "…" }` → 400「订单ID和团期ID只能传一个」 | | ❌ 两个都不传 | `{}` / 只传 `batchNo` → 400「订单ID和团期ID必须传一个」 | - 挂团订单保存时**不要再带 `groupBatchId`**(以前可带作一致校验,现在会 400)。 - 团期的出发 / 返回日期、第几天都以后端返回为准(取团期 `depart_date` / `end_date`)。 --- ## 五、数据库行为 - 只传 `groupBatchId` 写入的行:团期ID有值,订单ID、团期编号为空。 - 只传 `orderId` 写入的行:订单ID有值,团期ID、团期编号为空。 - 团期保存、重置的软删范围只在团期自己的行;子订单的行不受影响。 - 套用模版不写库。存量数据无需迁移。 --- ## 六、边界行为 - 二选一 400 先于权限、存在性判断,库里零写入。 - 团期写(生成 / 保存 / 重置 / 套用)要团期管理权限,查询要团期查看权限,不足 `589507`;团期不存在 `589500`;不按团期状态拦截。 - 团期 `depart_date` / `end_date` 为空:生成、重置、套用返回 `code` 200,`message` 为「订单出发或返回日期为空,无法生成用餐信息」(沿用 589602 文案),查询照常。 - `orderId` + `batchNo` 查询:固定空列表。 --- ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改动前 | 改动后 | |---|---|---| | 3.2 / 3.4 / 4.3 入参 `groupBatchId` | 无 | 新增,与 `orderId` 二选一 | | 3.3 / 4.3 入参 `orderId` | 必填 | 与 `groupBatchId` 二选一 | | 3.3 入参 `groupBatchId` | 与订单所属团期一致校验(589601) | 只传它表示按团期保存;与 `orderId` 同传 400 | | 订单行 `items[].groupBatchId` / `batchNo` | 挂团订单有值 | `null` | ### 行为级对比 | 行为 | 改动前 | 改动后 | |---|---|---| | 3.1 只传团期ID | 连同团里子订单的行一起返回,汇总为 null | 只返回团期自己的行,汇总按团期填 | | 3.1 都不传 | 查没挂订单也没挂团期的行 | 400 | | 3.1 `orderId` + `batchNo` | 按订单 + 团期编号筛 | 空列表 | | 3.3 都传 | 按订单保存 | 400 | ## 六.7、影响评估 - **是否破坏向后兼容**: 是。都传、都不传、只传 `batchNo` 由成功变为 400;订单行不再带团期ID - **前端是否必须同步上线**: 是。挂团订单保存不能再带 `groupBatchId`;团期用餐走只传 `groupBatchId` - **前端 workaround 清理点**: 挂团订单保存时附带 `groupBatchId` 的处理;依赖订单行 `groupBatchId` / `batchNo` 判断归属的处理 ## 七、不影响范围 - **仅影响**: 上述 5 个用餐接口 - **零影响**: 模版查询、存为模版、删除模版;金额公式;订单维度的权限与错误码(589601 除外) --- ## 八、测试环境已验证 部署提交 `b86e635c7`(含合并提交 `6ae15b3ed`),Deploy Panel 任务 `23cffa4a`;经 Gateway(`https://api.test.1814.love`)用真实 TEST 身份(SUPER_ADMIN)由固定脚本实测 73 项全部通过。团期 `2101996448340238338`(团期编号 `Q202611042099750379129368577`,出团 2026-11-04、结束 2026-11-08、报名 4 人),子订单 `2101996448319266818`: ``` 只传 groupBatchId 调 3.2 生成 → 12 行,与团期产品当前行程逐天逐餐一致;第 1 天(自理/含营地/含特色餐)只有午、晚 ✓ 生成的行 → groupBatchId=团期、orderId=null、batchNo=null;mealDate=2026-11-04+d−1 ✓ 再调 3.2 → 行数、行ID不变(幂等)✓ 改一行后调 3.4 重置 → 原行全部软删,重新生成与首次逐行一致 ✓ 同一个模版只传 groupBatchId / 只传 orderId 调 4.3 → 两边都能套;团期模版行第 d 日落 2026-11-04+d−1、按人行人数=4、orderId=null ✓ 只传 groupBatchId 调 3.3 保存 2 行 → groupBatchId 有值、orderId=null;dayNumber:11-05→2、11-06→3 ✓ 再保存改一行、少传一行 → 改的变了(105.00),少传的软删 ✓ 子订单按订单保存 1 行后 3.1 按订单查回 → 行上 groupBatchId=null;顶层仍回显所属团期 ✓ 只传 groupBatchId 调 3.1 → 查不到子订单那行,只有团期保存的行 ✓ 3.1/3.2/3.3/3.4/4.3 都传 → 400「订单ID和团期ID只能传一个」(5/5)✓ 3.1/3.2/3.3/3.4/4.3 都不传、3.1 只传 batchNo → 400「订单ID和团期ID必须传一个」(6/6),零写入 ✓ 只传 groupBatchId 调 3.1 → departDate=2026-11-04、returnDate=2026-11-08,与团期详情一致 ✓ 团期不存在 / 带子订单行ID / 带不存在行ID / 金额不符 → 589500 / 589611 / 589606 / 589613,零写入 ✓ ``` --- ## 十、相关文档 - 关联 Issue: [wx/HL#8230](https://git.1814.love:8443/wx/HL/issues/8230) - 关联 PR: [wx/HL#8251](https://git.1814.love:8443/wx/HL/pulls/8251) - 前置变更: [#8125 套用模版改为在现有行后追加](21_8125_用餐第几天按订单出发日算与套用模版改为在现有行后追加-修改接口-管理后台.md) ## 关联 / 联系人 ### 链接 - **Issue**: [#8230](https://git.1814.love:8443/wx/HL/issues/8230) - **PR**: [wx/HL#8251](https://git.1814.love:8443/wx/HL/pulls/8251) - **Merge commit**: [6ae15b3ed](https://git.1814.love:8443/wx/HL/commit/6ae15b3edaa58cea64c07fce4056fef8a02b551c) ### 联系人 - 后端: @lc