diff --git a/changelogs-v2/2026-07/23_5185_Step2票种规格默认值-修改接口-管理后台.md b/changelogs-v2/2026-07/23_5185_Step2票种规格默认值-修改接口-管理后台.md new file mode 100644 index 0000000..d0ad7e5 --- /dev/null +++ b/changelogs-v2/2026-07/23_5185_Step2票种规格默认值-修改接口-管理后台.md @@ -0,0 +1,425 @@ +# 【🔧 修改接口·管理后台】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\ | 否 | 凭证图片 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\ | 是 | 凭证图片 URL 列表 | +| `remark` | String | 是 | 备注 | + +### 5.3 PUT `data` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `addedIds` | Array\ | 本次新增行 ID 列表 | +| `updatedIds` | Array\ | 本次更新行 ID 列表 | +| `deletedIds` | Array\ | 本次删除行 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 +``` + +无请求体。 + +**响应**: + +```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 +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 +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 +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 diff --git a/changelogs/2026-07/23_5185_Step2票种规格字典-修改接口-管理后台.md b/changelogs/2026-07/23_5185_Step2票种规格字典-修改接口-管理后台.md new file mode 100644 index 0000000..a2238a6 --- /dev/null +++ b/changelogs/2026-07/23_5185_Step2票种规格字典-修改接口-管理后台.md @@ -0,0 +1,219 @@ +# 【🔧 修改接口·管理后台】Step2 票种规格字典(#5185) + +> **PR**: #5191 | **更新时间**: 2026-07-23 + +## 1. 接口背景 + +Step2 门票/游玩项目需要统一的票种/规格选项。管理后台现在可以按字典类型加载启用选项,首个可用选项为“成人票”,避免前端写死选项文案。 + +## 2. 变更清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 按类型查询字典数据 | GET | `/admin/dict/data/settlement_ticket_spec` | 修改接口 | 新增 `settlement_ticket_spec` 字典类型及启用项“成人票” | + +## 3. 接口详情 + +### 3.1 按类型查询 Step2 票种规格 + +- **使用场景**:加载 Step2 门票/游玩项目的票种/规格下拉选项。 +- **认证**:需要管理后台 JWT。 +- **幂等性**:是,只读查询。 +- **限流**:无接口专属限流约定。 +- **排序**:按 `sortOrder` 升序,同一排序号再按 `dictDataId` 升序。 +- **过滤**:仅返回 `status=ACTIVE` 的字典项。 + +## 4. 接口入参 + +### 4.1 路径参数 + +| 字段 | 类型 | 必填 | 固定值 | 说明 | +|------|------|------|--------|------| +| `dictType` | String | 是 | `settlement_ticket_spec` | Step2 票种/规格字典类型编码 | + +### 4.2 Query 参数与请求体 + +无 Query 参数,无请求体。 + +## 5. 出参(响应) + +### 5.1 统一响应字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `code` | Integer | 业务状态码,成功为 `200` | +| `message` | String | 响应消息,成功为“成功” | +| `data` | Array | 启用的字典项列表;无匹配项时为 `[]` | +| `traceId` | String / null | 链路追踪 ID | +| `success` | Boolean | `code=200` 时为 `true` | + +### 5.2 `data[]` 字典项字段 + +| 字段 | 类型 | 可为空 | 说明 | +|------|------|--------|------| +| `dictDataId` | Long | 否 | 字典数据 ID | +| `dictType` | String | 否 | 字典类型编码,本接口固定为 `settlement_ticket_spec` | +| `dictLabel` | String | 否 | 展示文案 | +| `dictValue` | String | 否 | 提交值;选择后写入 Step2 `items[].specName` | +| `icon` | String | 是 | 图标,本字典项当前为 `null` | +| `color` | String | 是 | 展示色值,本字典项当前为 `null` | +| `sortOrder` | Integer | 否 | 排序号,越小越靠前 | +| `status` | String | 否 | 字典项状态 | +| `remark` | String | 是 | 字典项说明 | +| `createdAt` | String | 是 | 创建时间,格式为 `yyyy-MM-dd HH:mm:ss` | +| `updatedAt` | String | 是 | 更新时间,格式为 `yyyy-MM-dd HH:mm:ss` | + +## 6. 枚举 / 数据字典 + +### 6.1 `settlement_ticket_spec` + +**展示字段**:`dictLabel` | **提交字段**:`dictValue` | **提交目标**:Step2 `items[].specName` + +| `dictValue` | `dictLabel` | `sortOrder` | `status` | 说明 | +|-------------|-------------|-------------|----------|------| +| `成人票` | 成人票 | `10` | `ACTIVE` | Step2 默认票种/规格 | + +### 6.2 `status` + +| 值 | 中文 | 是否由本接口返回 | +|----|------|------------------| +| `ACTIVE` | 启用 | 是 | +| `INACTIVE` | 禁用 | 否;查询接口会过滤禁用项 | + +## 7. 错误码 + +| code | 含义 | 触发场景 | +|------|------|----------| +| `200` | 成功 | 查询成功;没有匹配项时 `data=[]` | +| `401` | 未认证或认证失效 | 未携带有效管理后台 JWT | +| `500` | 系统异常 | 查询过程发生未预期异常 | + +## 8. 示例 + +### 8.1 典型成功 + +**请求**: + +```http +GET /admin/dict/data/settlement_ticket_spec +Authorization: Bearer +``` + +无请求体。 + +**响应**: + +```json +{ + "code": 200, + "message": "成功", + "data": [ + { + "dictDataId": 101411, + "dictType": "settlement_ticket_spec", + "dictLabel": "成人票", + "dictValue": "成人票", + "icon": null, + "color": null, + "sortOrder": 10, + "status": "ACTIVE", + "remark": "Step2 默认票种/规格", + "createdAt": "2026-07-23 10:00:00", + "updatedAt": "2026-07-23 10:00:00" + } + ], + "traceId": "a1b2c3d4-e5f6-7890", + "success": true +} +``` + +### 8.2 边界情况:不存在的字典类型 + +**请求**: + +```http +GET /admin/dict/data/not_exists +Authorization: Bearer +``` + +无请求体。 + +**响应**: + +```json +{ + "code": 200, + "message": "成功", + "data": [], + "traceId": "b2c3d4e5-f6a7-8901", + "success": true +} +``` + +### 8.3 业务失败:未认证 + +**请求**: + +```http +GET /admin/dict/data/settlement_ticket_spec +``` + +无请求体,且未携带 `Authorization`。 + +**响应**: + +```json +{ + "code": 401, + "message": "未认证或登录已失效", + "data": null, + "traceId": "c3d4e5f6-a7b8-9012", + "success": false +} +``` + +## 9. 业务边界 + +- 字典接口只返回启用项;禁用项不会出现在下拉列表中。 +- 前端展示 `dictLabel`,并将选中项的 `dictValue` 原样提交到 Step2 `items[].specName`。 +- 当前首个字典值为“成人票”;后续新增启用项时,接口会按排序规则一并返回。 +- 字典为单层平铺列表,不包含父子层级。 + +## 10. 修改前后对比 + +### 10.1 字段级对比 + +| 项目 | 改前 | 改后 | +|------|------|------| +| 响应结构 | `Result>` | 不变 | +| Step2 票种规格字典类型 | 无 `settlement_ticket_spec` 可用项 | 返回启用项“成人票” | + +### 10.2 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 加载 Step2 票种/规格选项 | 无专用字典数据 | 可查询 `settlement_ticket_spec` | +| 前端选项值 | 需要自行维护 | 使用接口返回的 `dictValue` | + +## 11. 影响评估 + +- **是否破坏向后兼容**:否,接口路径和响应结构未改变。 +- **前端是否必须同步上线**:否;接入后可使用动态字典,旧逻辑不会因本次新增字典项而报错。 + +## 12. 注意事项 + +- 不要把“成人票”选项数组硬编码在前端;应按需调用本接口。 +- 展示使用 `dictLabel`,保存使用 `dictValue`,不要提交 `dictDataId`。 +- Step2 仍使用原有 `specName` 字段,没有新增 `specCode`。 + +## 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