比较提交

...

2 次代码提交

作者 SHA1 备注 提交日期
yaosutu
f6fb5e16d8 docs(changelog): 补 Front Matter 必填 key frontend_owner/frontend_ref(#5599)
一些检查失败了
changelog-filename-gate / validate (push) Failing after 1s
2026-08-06 15:08:59 +08:00
yaosutu
82191f0931 docs(changelog): 核单餐食餐厅下拉值独立存储(#5599)
管理后台核单餐食 保存/修改/回显 接口新增 restaurantId/restaurantName
两字段(成对、非必填),与手动录入 mealName 完全独立不联动(PR #5600)。
2026-08-06 15:08:33 +08:00

查看文件

@ -0,0 +1,319 @@
---
schema: "hl-changelog/v2"
ticket: "5599"
title: "核单餐食餐厅下拉值独立存储"
consumer: "admin"
author: "yst"
change_type: "修改接口"
backend_status: "merged"
gateway_status: "pending"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
verified_at: ""
target_release: ""
updated_at: "2026-08-06"
base: "dev-v3"
status_note: "PR #5600 已合并 dev-v3,尚未部署测试服;restaurantId 关联资源服务餐厅下拉接口 PR #5583"
---
# 核单餐食餐厅下拉值独立存储(不覆盖餐食名称)
- **变更类型**:修改接口
- **端类型**:管理后台
- **服务**hl-order-service-v3
- **日期**2026-08-06
## 1. 接口背景
核单「餐食」Tab 原来只有手动录入的 `mealName`(餐食名称)一个文本字段承载餐食/餐厅信息。业务上需要把「从餐厅资源库选择的餐厅」作为独立结构化数据存下来,便于后续按餐厅维度统计与对账,且**不能覆盖**运营手动填写的餐食名称。
本次变更:餐食费用的「新增 / 修改 / 回显」三个接口同时新增 `restaurantId` + `restaurantName` 两个字段,与 `mealName` 完全独立、互不联动。
## 2. 变更清单
| # | 接口 | 变更 | 说明 |
|---|------|------|------|
| 1 | POST /v3/admin/order/{orderId}/settlement/meals | 入参 +2 字段 | 新增 restaurantId / restaurantName成对、非必填 |
| 2 | PUT /v3/admin/order/{orderId}/settlement/meals/{settlementId} | 入参 +2 字段 | 新增 restaurantId / restaurantName成对、非必填 |
| 3 | GET /v3/admin/order/{orderId}/settlement/meals | 出参 +2 字段 | 回显新增 restaurantId / restaurantName |
无删除字段、无改名字段、无枚举值变化。
## 3. 接口详情
### 3.1 新增餐食费用
- 方法/路径:`POST /v3/admin/order/{orderId}/settlement/meals`
- 接口名:新增餐食费用
- 认证:管理后台 JWT网关统一鉴权
- 幂等性:非幂等(重复提交会产生多条餐食记录)
- 限流:走网关默认限流,无接口级特殊限流
### 3.2 修改餐食费用
- 方法/路径:`PUT /v3/admin/order/{orderId}/settlement/meals/{settlementId}`
- 接口名:修改餐食费用
- 认证:管理后台 JWT网关统一鉴权
- 幂等性:幂等(相同 body 重复 PUT 结果一致)
- 限流:走网关默认限流
### 3.3 查询餐食费用(回显)
- 方法/路径:`GET /v3/admin/order/{orderId}/settlement/meals`
- 接口名:查询餐食费用
- 认证:管理后台 JWT网关统一鉴权
- 幂等性:只读接口
- 限流:走网关默认限流
## 4. 接口入参
### 4.1 路径参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| orderId | path | Long | 是 | 订单ID三个接口均有 |
| settlementId | path | Long | 是 | 餐食费用记录ID仅 PUT 修改接口) |
无 Query 参数。
### 4.2 请求体字段POST / PUT 共用同一请求结构)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| mealType | String | 是 | 餐型BREAKFAST / LUNCH / DINNER |
| mealDate | Dateyyyy-MM-dd | 否 | 发生日期 |
| mealName | String | 是 | 餐食名称,手动录入,最长 200 字符;**不受餐厅选择影响、不被覆盖** |
| quantity | Integer | 是 | 数量,1-10000 |
| unitPrice | Number | 是 | 单价,≥ 0,最多 8 位整数 + 2 位小数;unitPrice × quantity ≤ 99999999.99 |
| paymentMethod | String | 是 | 付款类型CASH_PAID / COMPANY_PAID / SIGNED |
| sourceType | String | 是 | 来源类型MEAL_ASSIGNMENT / MANUAL / SYSTEM |
| settlementConfirmStatus | String | 是 | 确认状态UNCONFIRMED / CONFIRMED |
| voucherUrls | String[] | 否 | 凭证 URL 列表,最多 9 个,仅支持 http/https,单个最长 1024 字符 |
| restaurantId | Long | 否 | **本次新增**。餐厅资源ID,与 restaurantName 成对出现;不选餐厅则两者都不传 |
| restaurantName | String | 否 | **本次新增**。餐厅名称快照,与 restaurantId 成对出现,最长 200 字符 |
| remark | String | 否 | 备注,最长 512 字符 |
**成对校验(关键业务规则)**`restaurantId``restaurantName` 必须**同时传或同时不传**restaurantName 为空白字符串视为未传)。只传其一 → 400,message 为「restaurantId 与 restaurantName 必须成对出现」或「餐食费用字段超出允许范围」(错误码 584094,MEAL_EXPENSE_REQUEST_INVALID。两字段与手动录入的 `mealName` **完全独立、不联动**——选了餐厅也不会改动/覆盖 mealName。
## 5. 出参字段
- POST / PUT 响应:`Result<SettlementMealRespVO>`(单条记录)
- GET 响应:`Result<List<SettlementMealRespVO>>`(列表,元素结构相同)
SettlementMealRespVO 字段表:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 餐食费用ID,雪花ID序列化为字符串 |
| mealType | String | 餐型 |
| mealDate | Dateyyyy-MM-dd | 发生日期 |
| mealName | String | 餐食名称 |
| restaurantId | String \| null | **本次新增**。餐厅资源ID,雪花ID序列化为字符串防 JS 精度丢失);未选餐厅为 null |
| restaurantName | String \| null | **本次新增**。餐厅名称快照(选中时记录,餐厅改名/删除不影响已存核单记录);未选餐厅为 null |
| quantity | Integer | 数量 |
| unitPrice | String | 单价BigDecimal 序列化为字符串) |
| actualAmount | String | 实际金额 = unitPrice × quantityBigDecimal 序列化为字符串) |
| paymentMethod | String | 付款类型 |
| sourceType | String | 来源类型 |
| sourceTypeName | String | 来源类型名称 |
| voucherUrls | String[] | 凭证 URL 列表 |
| settlementConfirmStatus | String | 确认状态 |
| settlementConfirmStatusName | String | 确认状态名称 |
| remark | String | 备注 |
外层为统一响应结构:`{ "code": 200, "message": "...", "data": ..., "traceId": ..., "success": ... }`;业务失败时 code 为错误码、data 为 null。
## 6. 枚举 / 数据字典
本次**无新增、无变更**枚举。以下现有枚举取值保持不变:
| 字段 | 取值 | 说明 |
|---|---|---|
| mealType | BREAKFAST / LUNCH / DINNER | 早餐 / 午餐 / 晚餐 |
| paymentMethod | CASH_PAID / COMPANY_PAID / SIGNED | 现金垫付 / 公司支付 / 签单 |
| sourceType | MEAL_ASSIGNMENT / MANUAL / SYSTEM | 餐食安排 / 手动录入 / 系统生成 |
| settlementConfirmStatus | UNCONFIRMED / CONFIRMED | 未确认 / 已确认 |
`restaurantId` 的取值来源:资源服务「餐厅资源下拉选项」接口 `GET /admin/resource-options/restaurants`PR #5583 已上线)返回的 `resourceId`。餐厅选项是业务数据,不是枚举/字典。
## 7. 错误码
| HTTP | 错误码 | message | 触发条件 |
|---|---|---|---|
| 400 | 584094 | 餐食费用字段超出允许范围 | restaurantId 与 restaurantName 只传其一不成对,service 层抛 MEAL_EXPENSE_REQUEST_INVALID |
| 400 | (参数校验失败) | restaurantId 与 restaurantName 必须成对出现 | 同上场景,bean validation 层先行拦截时的提示文案 |
| 400 | (参数校验失败) | mealType 必须是 BREAKFAST / LUNCH / DINNER 之一 等 | 既有字段校验失败(本次不变) |
| 401 | - | 未登录 / token 失效 | 未携带或携带无效的管理后台 JWT |
## 8. 示例
### 8.1 典型成功:选择餐厅保存 + 回显
请求:
```http
POST /v3/admin/order/1951234567890123456/settlement/meals
Authorization: Bearer {adminToken}
Content-Type: application/json
```
```json
{
"mealType": "LUNCH",
"mealDate": "2026-08-10",
"mealName": "团队桌餐10人标",
"quantity": 10,
"unitPrice": 68.00,
"paymentMethod": "COMPANY_PAID",
"sourceType": "MANUAL",
"settlementConfirmStatus": "UNCONFIRMED",
"voucherUrls": ["https://oss.example.com/voucher/a1.jpg"],
"restaurantId": 1889900112233445566,
"restaurantName": "海拉尔XX手把肉餐厅",
"remark": "导游现场确认"
}
```
响应200
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "1952345678901234567",
"mealType": "LUNCH",
"mealDate": "2026-08-10",
"mealName": "团队桌餐10人标",
"restaurantId": "1889900112233445566",
"restaurantName": "海拉尔XX手把肉餐厅",
"quantity": 10,
"unitPrice": "68.00",
"actualAmount": "680.00",
"paymentMethod": "COMPANY_PAID",
"sourceType": "MANUAL",
"sourceTypeName": "手动录入",
"voucherUrls": ["https://oss.example.com/voucher/a1.jpg"],
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": "导游现场确认"
}
}
```
回显:`GET /v3/admin/order/1951234567890123456/settlement/meals` 返回 `data` 为列表,元素结构同上(含 restaurantId / restaurantName
### 8.2 边界:不选餐厅,纯手动录入(两字段都不传)
请求:
```http
POST /v3/admin/order/1951234567890123456/settlement/meals
Authorization: Bearer {adminToken}
Content-Type: application/json
```
```json
{
"mealType": "BREAKFAST",
"mealDate": "2026-08-11",
"mealName": "酒店自助早餐",
"quantity": 10,
"unitPrice": 0,
"paymentMethod": "SIGNED",
"sourceType": "MANUAL",
"settlementConfirmStatus": "CONFIRMED"
}
```
响应200`data``restaurantId: null``restaurantName: null`,其余字段正常填充。存量历史餐食记录回显同样两字段为 null。
### 8.3 业务失败:只传 restaurantId、缺 restaurantName不成对
请求:
```json
{
"mealType": "DINNER",
"mealName": "涮羊肉",
"quantity": 10,
"unitPrice": 88.00,
"paymentMethod": "CASH_PAID",
"sourceType": "MANUAL",
"settlementConfirmStatus": "UNCONFIRMED",
"restaurantId": 1889900112233445566
}
```
响应400
```json
{
"code": 584094,
"message": "餐食费用字段超出允许范围",
"data": null
}
```
(若 bean validation 层先行拦截,message 为「restaurantId 与 restaurantName 必须成对出现」。反向场景——只传 restaurantName 不传 restaurantId——同样 400。
## 9. 业务边界
**适用**
- 核单「餐食」Tab 新增 / 编辑费用时,从餐厅资源下拉中选择餐厅,结构化保存 restaurantId + restaurantName
- 运营纯手动录入餐食(不关联餐厅):两字段都不传即可,行为与本次变更前完全一致
**不适用**
- restaurantId 只接受「餐厅资源下拉选项」接口返回的 resourceId,不接受其他类型资源ID
- 已提交核单(终态)订单不可再改餐食费用(核单提交守卫为既有逻辑,本次不变)
**特殊边界**
- **快照语义**restaurantName 是选中那一刻的名称快照。之后餐厅在资源库改名 / 删除,**不影响**已存核单记录的 restaurantName
- **与 mealName 完全独立**:选了餐厅也不会改动 / 覆盖 mealName;mealName 仍是必填手动录入字段
- 存量数据:历史餐食费用记录两字段均为 null,回显正常、无需迁移
## 10. 修改前后对比
### 字段级对比
| 接口 | 维度 | 修改前 | 修改后 |
|---|---|---|---|
| POST / PUT 保存 | 入参 | 无餐厅字段 | +restaurantIdLong,可选,成对+restaurantNameString,最长 200,可选,成对 |
| GET 回显 | 出参 | 无餐厅字段 | +restaurantIdString,可 null+restaurantNameString,可 null |
| mealName | 语义 | 餐食/餐厅信息只能挤在这一个文本字段里 | 仍为必填手动录入;餐厅信息改由独立字段承载,不再占用 mealName |
### 行为级对比
| 场景 | 修改前 | 修改后 |
|---|---|---|
| 保存餐食费用 | 想记餐厅只能写进 mealName 文本 | 可结构化传 restaurantId + restaurantName,mealName 不被覆盖 |
| 回显 | 无法区分「餐食名称」和「餐厅」 | 两类信息独立字段回显,前端可分开展示 |
| 入参校验 | 无成对概念 | restaurantId / restaurantName 不成对 → 400 |
## 11. 影响评估 / 回滚
- **破坏性兼容**:无。两个字段均为可选新增;存量请求不传两字段时行为完全不变;存量数据两列为 NULL
- **前端同步上线**:不强制同步。前端未接新字段前,保存 / 回显行为与之前一致(回显多两个 null 字段,不读即可);前端接新字段需等后端部署后联调
- **后端部署状态**PR #5600 已合并 dev-v3,**尚未部署测试服**(见 Front Matter backend_status / status_note
- **回滚方案**:后端回滚 = 下线两字段即可。回滚时前端需同步停止传这两字段;存量数据两列本就为 NULL 或历史快照值,无数据迁移、无回滚 SQL
## 12. 注意事项
1. `restaurantId` 出参是 **String**(雪花 ID 序列化),前端不要用 Number 解析,防 JS 精度丢失
2. `unitPrice` / `actualAmount` 出参同样是 StringBigDecimal 序列化),金额展示直接渲染字符串即可
3. 编辑时「清掉已选餐厅」= 两字段都不传(或都传 null,保持成对空态
4. restaurantName 是快照不是实时关联,餐厅后续改名 / 删除不影响历史核单记录显示,属预期
5. 餐厅下拉数据源接口 `GET /admin/resource-options/restaurants` 属资源服务PR #5583),不在本变更范围内
6. 本变更只涉及接口契约;后端代码已合并 dev-v3,测试服部署时间以后端通知为准
## 13. 关联 / 联系人
- Issuehttps://git.1814.love:8443/wx/HL/issues/5599
- PRhttps://git.1814.love:8443/wx/HL/pulls/5600
- Commithttps://git.1814.love:8443/wx/HL/commit/9884585a292924c1a0a7a5fec6b7dc8ab87aa59d
- 关联依赖:餐厅资源下拉接口 PR #5583(资源服务,已上线)
- 后端负责人yst腰苏图