36 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7741 | 订单用餐信息与用餐模版接口(查询、生成、整单保存、重置、查模版、存模版、套模版) | admin | lc(GIT) | 新增接口 | deployed | verified | verified | mmg | 061b728af421904d6f275292273814c206d374f2 | 2026-09-20 | 新增 7 个接口:订单详情「用餐」页签的用餐信息查询 / 按产品快照生成 / 整单一次保存 / 重置,以及用餐模版的查询 / 保存 / 套用。前端需按本文契约对接;人数与桌数二选一、类别字典过滤 SELF 两点见「四、契约约束」。前端已交付(mmg@061b728a):订单详情「用餐」页签(自动生成/整单保存全集语义/人数桌数互锁/金额同公式/589613 静默重拉/只读态)+存为模版/套用模版弹窗,类别下拉过滤 SELF;3 新 spec 18/18,scoped checkpoint 全绿。 | 2026-09-18 | 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<OrderMealInfoListRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| 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 |
请求示例
GET /v3/admin/order/meal-info/list?orderId=2100864597505286145
Authorization: Bearer <admin token>
响应示例
TEST 实测返回(节选 1 行):
{
"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,其余订单字段照常返回。按团期编号查不到团期时返回空列表。
错误响应
{ "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<OrderMealInfoListRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data | Object | 生成后按该订单查询的结果,字段同 1 号接口出参 |
请求示例
{ "orderId": "2100864614022451201" }
响应示例
TEST 实测(订单 B 生成时返回,节选 1 行):第 1 天午餐 = 含(营地),生成的行餐厅、餐食、付款方式为空,数值全为 0:
{
"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 为空。
错误响应
{ "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<OrderMealInfoListRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data | Object | 保存后按该订单查询的结果,字段同 1 号接口出参;新增行是新生成的 mealInfoId,dayNumber 为 null |
请求示例
示例(金额取自 TEST 实测数据;节选:改一行按人、改一行按桌、新增一行,没传上来的行被删除):
{
"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": "签单"
}
]
}
响应示例
{
"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。
错误响应
{ "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<OrderMealInfoListRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data | Object | 重置后按该订单查询的结果,字段同 1 号接口出参;行ID全部是新的 |
请求示例
{ "orderId": "2100864597505286145" }
响应示例
{
"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=[]。
错误响应
{ "code": 589607, "message": "订单已完成或已取消,不能修改用餐信息", "success": false, "data": null }
业务边界
- 删除和重新生成在一个事务里;返回日期早于出发日期 589612,此时不删也不生成。
- 不受 2 号接口「已有行就不再生成」的限制。
- 订单不存在、无操作权限按订单模块现有错误返回;已完成或已取消 589607。
5. 查询用餐模版 GET /v3/admin/order/meal-template/list
VO: MealTemplateListReqVO → List<MealTemplateRespVO>
使用场景
「用餐」页签选择模版时,列出模版及其内容。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| templateId | Query | String | 否 | — | 模版ID精确匹配;传了忽略 templateName |
| templateName | Query | String | 否 | ≤500 | 模版名精确匹配;两个都不传返回全部模版 |
出参 Result<List<MealTemplateRespVO>>
| 字段 | 类型 | 说明 |
|---|---|---|
| [].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 |
请求示例
GET /v3/admin/order/meal-template/list?templateId=2100883046935748610
Authorization: Bearer <admin token>
响应示例
TEST 实测返回(节选 1 行):
{
"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": "签单"
}
]
}
]
}
空数据 / 降级响应
{ "code": 200, "message": "成功", "success": true, "data": [] }
错误响应
{ "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<MealTemplateRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data | Object | 新模版,结构同 5 号接口的一个元素(含新生成的 templateId) |
请求示例
{
"templateName": "HL7741-TEST-含营地",
"mealInfoIds": ["2100883161750687746", "2100883161754882049", "2100883192117448705", "2100883161759076354"]
}
响应示例
{
"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,不建模版;部分取不到时只存取到的行。
错误响应
{ "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<OrderMealInfoListRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data | Object | 套用后按该订单查询的结果,字段同 1 号接口出参 |
请求示例
{ "orderId": "2100864597505286145", "templateId": "2100883046935748610" }
响应示例
TEST 实测:模版第 1 日午餐覆盖到出发日那天已有的午餐行(同一行ID),模版第 2 日早餐订单上没有,新建一行:
{
"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不存在或模版没有行时什么都不改,返回订单当前的行。
错误响应
{ "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 |
需要前端配合的两点
- 人数与桌数二选一:人数不为 0 时,桌数不能点击并设成 0;桌数不为 0 时,人数、免人数不能点击并设成 0。后端只按「桌数是否为 0」选公式。
- 类别下拉过滤 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、wx/HL#7953
- 关联 PR: wx/HL#7945、wx/HL#7954
- 依赖接口:餐食下拉
GET /admin/dish/items/list(#7737)、餐厅选项GET /admin/resource-options/restaurants
关联 / 联系人
链接
联系人
- 后端负责人: @lc