hl-api-changelog/changelogs-v2/2026-07/23_5185_Step2票种规格默认值-修改接口-管理后台.md
yaosutu 8288bfcc7f
所有检测均成功
changelog-filename-gate / validate (push) Successful in 1s
补充Step2票种规格字典与默认值说明
2026-07-23 16:43:36 +08:00

426 行
14 KiB
Markdown

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

# 【🔧 修改接口·管理后台】Step2 票种规格默认值(#5185
> **PR**: #5191 | **更新时间**: 2026-07-23
## 1. 接口背景
Step2 门票/游玩项目明细原先可能返回或保存空的 `specName`,前端无法稳定展示票种/规格。现在查询和保存统一补齐“成人票”默认值,同时保留已有的非空规格。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | Step2 查询门票核单明细 | GET | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | 无明确规格的明细统一返回 `specName=成人票` |
| 2 | Step2 保存门票核单明细 | PUT | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | `specName``null`、空串或纯空白时按“成人票”保存 |
## 3. 接口详情
### 3.1 Step2 查询门票核单明细
- **使用场景**:进入或刷新核单 Step2 时查询门票/游玩项目明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:是,只读查询。
- **限流**:无接口专属限流约定。
- **默认语义**:自动生成且无明确规格、或已有明细规格为空时,`specName` 返回“成人票”。
- **保留语义**:已有非空规格原样返回,例如“骑马体验”。
### 3.2 Step2 保存门票核单明细
- **使用场景**:全量保存 Step2 门票/游玩项目明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:业务数据为全量替换语义;重复提交相同明细得到相同业务内容,行 ID 可能重新生成。
- **限流**:无接口专属限流约定。
- **默认语义**`items[].specName``null``""` 或纯空白时,保存并回读为“成人票”。
- **保留语义**:非空规格原样保存,例如“骑马体验”不会被替换。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long / String | 是 | 订单 ID,必须大于 `0`;19 位 ID 建议按字符串拼入路径 |
GET 无 Query 参数、无请求体。
### 4.2 PUT 请求体
推荐使用对象形式;接口同时兼容直接提交明细数组。
```json
{
"items": []
}
```
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `items` | Array | 是 | 门票/游玩项目明细,全量替换 | 可为空数组;空数组表示清空已保存草稿 |
### 4.3 `items[]` 字段
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `id` | Long / String | 否 | 已存在行 ID;新增或自动生成行可为空 | 19 位 ID 建议使用字符串 |
| `sourceType` | String | 是 | 来源类型 | `SCENIC_ASSIGNMENT``ACTIVITY_ASSIGNMENT``CUSTOM_ASSIGNMENT` |
| `sourceTypeName` | String | 否 | 来源类型中文名 | 最长 32 字符 |
| `scenicAssignmentId` | Long / String | 否 | 来源记录 ID;手动补充行为空 | 19 位 ID 建议使用字符串 |
| `dayNumber` | Integer | 否 | 行程第几天,从 `1` 开始;保存时按 `dayDate` 计算 | 无需前端计算 |
| `dayDate` | String | 是 | 项目日期 | `yyyy-MM-dd`,不得早于订单出发日期 |
| `scenicName` | String | 是 | 景区或游玩项目名称 | 非空,最长 200 字符 |
| `specName` | String | 否 | 票种/规格名称 | 最长 128 字符;`null`、空串、纯空白统一为“成人票” |
| `ticketCount` | Integer | 是 | 实际购票数量 | 套餐含项目可填 `0` |
| `ticketUnitPrice` | Decimal | 否 | 参考单价,单位元 | 自费项目可填;包价项目可为 `null` |
| `sellPrice` | Decimal | 否 | 客户成交单价,单位元 | 大于等于 `0` |
| `totalAmount` | Decimal | 否 | 客户成交小计,单位元 | 大于等于 `0`;为空时按 `sellPrice × ticketCount` 计算 |
| `plannedCost` | Decimal | 是 | 计划成本,单位元 | 大于等于 `0` |
| `actualCost` | Decimal | 是 | 实际成本,单位元 | 大于等于 `0` |
| `paymentMethod` | String | 否 | 付款方式 | `SIGNED``COMPANY_PAID``CASH_PAID`;为空时为 `COMPANY_PAID` |
| `paymentMethodName` | String | 否 | 付款方式中文名 | 最长 32 字符 |
| `voucherUrls` | Array\<String> | 否 | 凭证图片 URL 列表 | 可为空数组 |
| `remark` | String | 否 | 备注 | 最长 500 字符 |
## 5. 出参(响应)
### 5.1 统一响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码,成功为 `200` |
| `message` | String | 响应消息,成功为“成功” |
| `data` | Object / Array | GET 为明细数组,PUT 为保存结果对象 |
| `traceId` | String / null | 链路追踪 ID |
| `success` | Boolean | `code=200` 时为 `true` |
### 5.2 GET `data[]`
| 字段 | 类型 | 可为空 | 说明 |
|------|------|--------|------|
| `id` | String / null | 是 | 已保存的 19 位行 ID 按字符串返回;未保存的自动生成行可为 `null` |
| `sourceType` | String | 否 | 来源类型,取值见 §6.2 |
| `sourceTypeName` | String | 是 | 来源类型中文名 |
| `scenicAssignmentId` | String / null | 是 | 19 位来源记录 ID 按字符串返回;手动补充行为空 |
| `dayNumber` | Integer | 是 | 根据项目日期与订单行程计算的天序 |
| `dayDate` | String | 否 | 项目日期,格式为 `yyyy-MM-dd` |
| `scenicName` | String | 否 | 景区或游玩项目名称 |
| `specName` | String | 否 | 无明确规格时返回“成人票”;已有非空规格原样返回 |
| `ticketCount` | Integer | 否 | 实际购票数量 |
| `ticketUnitPrice` | Decimal | 是 | 参考单价,单位元 |
| `sellPrice` | Decimal | 是 | 客户成交单价,单位元 |
| `totalAmount` | Decimal | 是 | 客户成交小计,单位元 |
| `plannedCost` | Decimal | 否 | 计划成本,单位元 |
| `actualCost` | Decimal | 否 | 实际成本,单位元 |
| `paymentMethod` | String | 是 | 付款方式,取值见 §6.3 |
| `paymentMethodName` | String | 是 | 付款方式中文名 |
| `voucherUrls` | Array\<String> | 是 | 凭证图片 URL 列表 |
| `remark` | String | 是 | 备注 |
### 5.3 PUT `data`
| 字段 | 类型 | 说明 |
|------|------|------|
| `addedIds` | Array\<String> | 本次新增行 ID 列表 |
| `updatedIds` | Array\<String> | 本次更新行 ID 列表 |
| `deletedIds` | Array\<String> | 本次删除行 ID 列表 |
| `totalActualCost` | String | 保存后实际成本合计,单位元 |
## 6. 枚举 / 数据字典
### 6.1 `specName`(数据字典 `settlement_ticket_spec`
**所属字段**`items[].specName` | **类型**String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `成人票` | 成人票 | 当前默认票种/规格;字典接口返回的 `dictValue` |
加载选项使用:
```http
GET /admin/dict/data/settlement_ticket_spec
```
展示使用字典项 `dictLabel`,提交使用 `dictValue`。本次没有新增 `specCode` 字段。
### 6.2 `sourceType`
**所属字段**`items[].sourceType` | **类型**String | **必填**:是
| 值 | 中文 | 说明 |
|----|------|------|
| `SCENIC_ASSIGNMENT` | 景区 | 来源于景区项目 |
| `ACTIVITY_ASSIGNMENT` | 游玩项目 | 来源于游玩项目 |
| `CUSTOM_ASSIGNMENT` | 手动补充 | 核单时手动新增 |
### 6.3 `paymentMethod`
**所属字段**`items[].paymentMethod` | **类型**String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `SIGNED` | 签单 | 供应商签单 |
| `COMPANY_PAID` | 公司付款 | 未传付款方式时的默认值 |
| `CASH_PAID` | 现付 | 现场付款,可附凭证 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询或保存成功 |
| `400` | 请求参数校验失败 | 必填字段为空、枚举值不合法、金额为负数、字段超长或日期格式错误 |
| `401` | 未认证或认证失效 | 未携带有效管理后台 JWT |
| `584011` | 当前核单状态不允许录门票核单 | PUT 时订单核单状态不是“待核单”或“核单中” |
| `584017` | 订单缺出发日期 | PUT 时无法根据 `dayDate` 计算 `dayNumber` |
| `584018` | 项目日期早于订单出发日期 | PUT 的 `items[].dayDate` 早于订单出发日期 |
| `500` | 系统异常 | 查询或保存过程发生未预期异常 |
## 8. 示例
### 8.1 典型成功:查询自动生成明细
**请求**
```http
GET /v3/admin/order/2079454953641836546/settlement/step2
Authorization: Bearer <admin-token>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": [
{
"id": null,
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区",
"scenicAssignmentId": "2079454953641837001",
"dayNumber": 1,
"dayDate": "2026-07-21",
"scenicName": "示例景区",
"specName": "成人票",
"ticketCount": 2,
"ticketUnitPrice": 100.00,
"sellPrice": 120.00,
"totalAmount": 240.00,
"plannedCost": 200.00,
"actualCost": 200.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"remark": null
}
],
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
### 8.2 典型成功:提交字典选中的“成人票”
**请求**
```http
PUT /v3/admin/order/2079454953641836546/settlement/step2
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"items": [
{
"id": null,
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区",
"scenicAssignmentId": "2079454953641837001",
"dayNumber": 1,
"dayDate": "2026-07-21",
"scenicName": "示例景区",
"specName": "成人票",
"ticketCount": 2,
"ticketUnitPrice": 100.00,
"sellPrice": 120.00,
"totalAmount": 240.00,
"plannedCost": 200.00,
"actualCost": 200.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"remark": null
}
]
}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"addedIds": ["2079454953641840001"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "200.00"
},
"traceId": "b2c3d4e5-f6a7-8901",
"success": true
}
```
### 8.3 边界情况:空规格归一为“成人票”
**保存请求**
```http
PUT /v3/admin/order/2079454953641836546/settlement/step2
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"items": [
{
"sourceType": "ACTIVITY_ASSIGNMENT",
"sourceTypeName": "游玩项目",
"scenicAssignmentId": "2079454953641837002",
"dayDate": "2026-07-22",
"scenicName": "示例游玩项目",
"specName": null,
"ticketCount": 2,
"ticketUnitPrice": null,
"sellPrice": 0,
"totalAmount": 0,
"plannedCost": 0,
"actualCost": 0,
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"remark": null
}
]
}
```
**保存响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"addedIds": ["2079454953641840002"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "0"
},
"traceId": "c3d4e5f6-a7b8-9012",
"success": true
}
```
随后 GET 回读时,该行的关键字段为:
```json
{
"scenicName": "示例游玩项目",
"specName": "成人票"
}
```
### 8.4 业务失败:非法来源类型
**请求**
```http
PUT /v3/admin/order/2079454953641836546/settlement/step2
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"items": [
{
"sourceType": "UNKNOWN",
"dayDate": "2026-07-21",
"scenicName": "示例项目",
"specName": "成人票",
"ticketCount": 1,
"plannedCost": 0,
"actualCost": 0
}
]
}
```
**响应**
```json
{
"code": 400,
"message": "sourceType 必须是 SCENIC_ASSIGNMENT / ACTIVITY_ASSIGNMENT / CUSTOM_ASSIGNMENT 之一",
"data": null,
"traceId": "d4e5f6a7-b8c9-0123",
"success": false
}
```
## 9. 业务边界
- GET自动生成明细没有明确规格时,返回 `specName=成人票`
- GET历史已保存明细的 `specName``null`、空串或纯空白时,也返回“成人票”。
- GET/PUT已有非空规格保持不变,例如“骑马体验”不会被覆盖。
- PUT`specName` 最大 128 字符;空值会归一为“成人票”而不是报错。
- PUT`items` 为全量数据;遗漏的旧明细不会继续保留。
- PUT仅订单核单状态为“待核单”或“核单中”时允许保存。
- 前端从 `settlement_ticket_spec` 字典读取选项,展示 `dictLabel`,提交 `dictValue``specName`
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `items[].specName` | String,可返回或保存为空 | String;查询和保存的空值统一为“成人票” |
| `items[].specCode` | 不存在 | 仍不存在,本次未新增 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 自动生成明细没有明确规格 | `specName` 可能为空或与项目名重复 | `specName=成人票` |
| 保存 `specName=null`、空串或纯空白 | 可能按空值保存和回显 | 保存、回读均为“成人票” |
| 保存非空自定义规格 | 原样保存 | 仍原样保存 |
| 接口数量 | GET、PUT 两个既有接口 | 不变,没有新增 Step2 接口 |
## 11. 影响评估
- **是否破坏向后兼容**:否,接口路径、请求结构和响应字段均未改变;只收紧了空规格的返回语义。
- **前端是否必须同步上线**:否;前端可逐步接入字典下拉,未接入时也会收到稳定的“成人票”默认值。
## 12. 注意事项
- 前端如有 `specName || "成人票"` 的临时兜底,可在确认接口已覆盖当前环境后移除。
- 不要新增或提交 `specCode`;当前契约只使用 `specName`
- 选择字典项后提交 `dictValue`,不要提交 `dictLabel` 以外的展示元数据或 `dictDataId`
- 自定义非空规格可以继续提交,接口不会强制替换为字典当前默认项。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5185](https://git.1814.love:8443/wx/HL/issues/5185)
- **PR**: [#5191](https://git.1814.love:8443/wx/HL/pulls/5191)
- **Merge commit**: [03ab19b46](https://git.1814.love:8443/wx/HL/commit/03ab19b463be4b00f92848cb348ce6158918e041)
### 13.2 联系人
- **后端负责人**: @yst