diff --git a/changelogs-v2/2026-07/68_4933_车务派单可靠通知取消重派与发送状态-修改接口-管理后台.md b/changelogs-v2/2026-07/68_4933_车务派单可靠通知取消重派与发送状态-修改接口-管理后台.md new file mode 100644 index 0000000..4c67916 --- /dev/null +++ b/changelogs-v2/2026-07/68_4933_车务派单可靠通知取消重派与发送状态-修改接口-管理后台.md @@ -0,0 +1,370 @@ +# 【前端对接·管理后台】车务派单可靠通知、取消后重派与发送状态契约 + +> Issue: [wx/HL#4933](https://git.1814.love:8443/wx/HL/issues/4933) +> +> PR: [wx/HL#5073](https://git.1814.love:8443/wx/HL/pulls/5073)、[wx/HL#5084](https://git.1814.love:8443/wx/HL/pulls/5084) +> +> 服务: `hl-fleet-service` / `hl-user-service` / `hl-order-service-v3` / `hl-gateway` +> +> 日期: 2026-07-19 +> +> 影响范围: 派单/改派弹窗、派单详情操作记录、通知发送日志、订单详情推送记录、取消后重新派车 + +## 一、前端结论 + +- `holdMode=1` 的创建派单和改派现在会冻结本次通知模板与正文,并由后端异步执行可靠短信发送。 +- 创建 HOLD 成功只表示派单和通知意图已落库;首次响应中的 `holdSentAt` 固定为 `null`。只有供应商真实受理后,派单详情的 `currentAssignment.holdSentAt` 才会回显发送时间。 +- 通知日志 `status` 已从旧的少量状态扩展为 `0~6`。前端必须展示“投递中、结果不确定、授权撤销”,不得把它们归并成发送成功或失败。 +- 取消派单成功后,后端会可靠地把当前生效用车需求重新打开,允许再次派车;该过程为最终一致。前端刷新看板和详情,并以最新 `canAssign`/当前需求状态决定是否开放重派,不调用内部重开接口。 +- 订单详情推送记录的归一化状态枚举已调整,前端需要同步新枚举。 +- `/internal/**`、`/v3/internal/**` 均为服务间接口,经网关调用返回业务码 `403`;任何 Web/小程序代码都不得调用。 + +## 二、前端可调用接口 + +| 接口 | 方法 | 路径 | 本轮变化 | +|---|---|---|---| +| 创建派单 | POST | `/admin/fleet/assignments` | 新增 `messageTemplateId/customBody`;明确 `holdSentAt` 语义 | +| 修改派单 | POST | `/admin/fleet/assignments/{assignmentId}/change` | HOLD 改派新增 `messageTemplateId/customBody` | +| 派单看板详情 | GET | `/admin/fleet/board/orders/{orderId}` | 回显真实 `holdSentAt`;操作记录补齐取消/退保完整时间线 | +| 派单看板汇总 | GET | `/admin/fleet/board/summary` | 取消后刷新当前状态与能力字段 | +| 派单看板列表 | GET | `/admin/fleet/board/orders` | 取消后刷新当前状态与能力字段 | +| 通知发送日志 | GET | `/admin/notification/logs` | 状态扩展为 `0~6`,新增可靠投递审计字段 | +| 通知发送统计 | GET | `/admin/notification/logs/stats` | 新增跳过、投递中、不确定、撤销等统计 | +| 人工核对可靠短信 | PUT | `/admin/notification/logs/{id}/resolve-reliable` | 新增,仅专用权限可用 | +| 订单详情推送记录 | GET | `/v3/admin/order/{id}/push-records` | 归一化状态枚举调整 | + +## 三、创建/修改 HOLD 派单 + +### 3.1 请求字段 + +两个写接口新增相同的可选字段: + +| 字段 | 类型 | 规则 | +|---|---|---| +| `messageTemplateId` | string | HOLD 通知模板 ID;可空,空时使用 `hold_notify` 默认模板;`holdMode=0` 时忽略 | +| `customBody` | string | 本次通知自定义正文;可空,最大 4000 字符;只冻结本次内容,不回写模板 | + +所有雪花 ID 继续按字符串传递和保存,禁止转为 JavaScript `Number`。 + +创建 HOLD 请求示例: + +```http +POST /admin/fleet/assignments +Content-Type: application/json +Authorization: Bearer +``` + +```json +{ + "orderId": "2074746808742928386", + "requirementId": "2075001000000000001", + "vehicleId": "2076001000000000001", + "driverId": "2077001000000000001", + "startDate": "2026-07-20", + "endDate": "2026-07-22", + "headcount": 4, + "holdMode": 1, + "messageTemplateId": "20260706000101", + "customBody": "王师傅您好,26-7218 团 7 月 20 日待确认。", + "fromEntry": "from-board", + "requestId": "hold-2074746808742928386-001" +} +``` + +修改为 HOLD 请求示例: + +```http +POST /admin/fleet/assignments/2078001000000000001/change +Content-Type: application/json +Authorization: Bearer +``` + +```json +{ + "effectiveDate": "2026-07-21", + "newVehicleId": "2076001000000000002", + "newDriverId": "2077001000000000002", + "holdMode": 1, + "messageTemplateId": "20260706000101", + "customBody": "李师傅您好,本团 7 月 21 日起调整由您服务,请确认。", + "reason": "原司机临时无法执行", + "requestId": "change-2078001000000000001-001" +} +``` + +### 3.2 创建响应与 `holdSentAt` + +HOLD 创建成功响应示例: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "id": "2078001000000000001", + "assignmentGroupId": "2078001000000000001", + "assignmentSlotId": "2078001000000000001", + "assignmentStatus": "holding", + "stageCode": "holding_wait_driver", + "stageLabel": "排车中·等待司机确认", + "currentStep": 3, + "skippedStepCodes": [], + "protocolPrice": "1300.00", + "holdSentAt": null, + "confirmedAt": null, + "sideEffects": null, + "dailyDifferences": [] + } +} +``` + +前端处理规则: + +1. `code=200` 且 `assignmentStatus=holding` 后立即关闭重复提交入口,并刷新详情。 +2. `holdSentAt=null` 不是接口失败,也不能显示“短信已发送”;应显示“通知处理中/等待发送结果”。 +3. 后续读取 `GET /admin/fleet/board/orders/{orderId}`,仅当 `currentAssignment.holdSentAt` 非空时显示真实发送时间。 +4. 模板缺失、供应商失败或结果不确定时,派单仍保持 `holding`,前端通过通知日志查看真实状态,不自行改派单状态。 + +## 四、通知发送日志状态 + +### 4.1 状态枚举 + +`GET /admin/notification/logs` 的请求筛选参数和响应字段 `status` 统一使用: + +| status | 含义 | 前端展示建议 | +|---:|---|---| +| 0 | 发送成功,供应商明确受理 | 成功 | +| 1 | 明确失败 | 失败 | +| 2 | 无收件人 | 已跳过·无收件人 | +| 3 | 无模板 | 已跳过·无模板 | +| 4 | 投递中 | 投递中 | +| 5 | 结果不确定 | 待核对 | +| 6 | 授权撤销 | 已撤销 | + +前端不得把 `4/5/6` 计入成功或失败。状态 `5` 也不能自动重发,避免供应商实际已发送时重复通知司机。 + +单条日志新增字段: + +```json +{ + "id": 2080001000000000001, + "eventCode": "FLEET_DISPATCH_CREATED", + "channel": "SMS", + "bizId": "2078001000000000001", + "bizType": "FLEET_ASSIGNMENT_HOLD", + "status": 5, + "latestProviderAttemptAt": "2026-06-19T10:00:00", + "providerSentAt": null, + "resultTime": null, + "manualResolvedAt": null, + "manualResolvedBy": null, + "manualResolutionReason": null +} +``` + +新增统计字段: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "totalToday": 20, + "successToday": 12, + "failToday": 2, + "skippedToday": 3, + "dispatchingToday": 1, + "unknownToday": 1, + "canceledToday": 1, + "terminalAttemptToday": 14, + "successRate": 85.71, + "channelStats": [] + } +} +``` + +`successRate` 的分母是 `terminalAttemptToday = successToday + failToday`,前端不要再用 `totalToday` 自行计算。 + +## 五、人工核对结果不确定短信 + +该入口只处理超过供应商 29 天查询窗口、仍为 `status=5` 的车务可靠短信,并要求 `NOTIFICATION_RELIABLE_RESOLVE` 专用权限。当前后端只授予 `SUPER_ADMIN`;普通管理员即使手工构造请求也会被拒绝。 + +确认已发送: + +```http +PUT /admin/notification/logs/2080001000000000001/resolve-reliable +Content-Type: application/json +Authorization: Bearer +``` + +```json +{ + "resolution": "SUCCESS", + "reason": "阿里云控制台发送记录核对,工单 SMS-20260719-001", + "externalMessageId": "SMS-20260719-001", + "providerSentAt": "2026-06-19T10:00:30" +} +``` + +确认未发送: + +```json +{ + "resolution": "NOT_SENT", + "reason": "阿里云控制台未查到对应发送记录", + "externalMessageId": null, + "providerSentAt": null +} +``` + +成功响应: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": null +} +``` + +处理规则: + +- `SUCCESS` 必须传 `externalMessageId` 和 `providerSentAt`;事实时间必须位于最近一次供应商尝试时间前后 5 分钟内。 +- `NOT_SENT` 不得传 `providerSentAt`。 +- 请求返回 `100001` 表示参数或证据时间不合法;返回 `100003` 表示无权限、日志不符合人工核对条件或状态已变化。 +- 操作成功后刷新当前日志行和统计;不要在前端直接篡改状态。 + +## 六、订单详情推送记录状态 + +`GET /v3/admin/order/{id}/push-records` 的 `records[].status` 改为: + +| status | 含义 | +|---|---| +| `SENT` | 供应商明确受理 | +| `FAILED` | 明确失败 | +| `SKIPPED_NO_RECIPIENT` | 无收件人 | +| `SKIPPED_NO_TEMPLATE` | 无模板 | +| `DISPATCHING` | 投递中 | +| `UNKNOWN` | 结果不确定 | +| `CANCELED` | 授权已撤销 | +| `UNRECOGNIZED` | 未识别的存量状态 | + +响应示例: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "total": 1, + "records": [ + { + "id": 2080001000000000001, + "eventCode": "FLEET_DISPATCH_CREATED", + "channel": "SMS", + "channelName": "短信", + "kind": "sms", + "target": "王师傅", + "status": "UNKNOWN", + "statusName": "结果不确定", + "rawStatus": 5, + "failReason": null, + "bizId": "2078001000000000001", + "bizType": "FLEET_ASSIGNMENT_HOLD", + "sentAt": "2026-07-19T10:00:00" + } + ], + "summary": { + "all": 1, + "sms": 1, + "miniapp": 0, + "officialAccount": 0, + "inapp": 0, + "internal": 0, + "wework": 0, + "other": 0, + "failed": 0 + } + } +} +``` + +`summary.failed` 只统计 `rawStatus=1`,不包含 `UNKNOWN/DISPATCHING/CANCELED`。 + +## 七、取消后重新派车 + +前端仍调用既有接口取消: + +```http +DELETE /admin/fleet/assignments/{assignmentId} +``` + +成功后的正确流程: + +1. 接受取消响应中的 `assignmentStatus=canceled`。 +2. 重新请求 `/admin/fleet/board/summary`、`/admin/fleet/board/orders` 和 `/admin/fleet/board/orders/{orderId}`。 +3. 后端完成需求重开后,当前订单重新出现可派状态;按钮只看最新响应的 `canAssign`,不要本地强制改为可派。 +4. 如果首次刷新仍未开放重派,保持处理中并短暂重试刷新;不要调用 `/v3/internal/order/**`,也不要让用户重复取消。 +5. 重新派车成功后再次刷新服务端状态,不能沿用已取消派单的 `assignmentId`。 + +派单详情 `operationLog.records[]` 会保留不可变取消时间线,新增/强化的 `opType` 包括: + +- `cancel_requested` +- `driver_notification_recorded` +- `cancel_evidence_recorded` +- `insurance_refund_pending` +- `insurance_refund_succeeded` +- `insurance_refund_failed` +- `cancel_completed` +- `cancel_restored` +- `cancel_failed` + +前端优先展示后端返回的 `opTypeLabel`、`operationStatusLabel` 和 `summary`,不要另维护中文文案。`operationStatus` 允许 `pending/succeeded/failed`。 + +## 八、网关 internal 边界 + +下列路径全部禁止客户端调用: + +```text +/internal +/internal/** +/v3/internal +/v3/internal/** +``` + +网关按项目协议返回 HTTP 200,但响应体为: + +```json +{ + "code": 403, + "message": "接口不可访问", + "success": false, + "data": null +} +``` + +请前端全仓检查是否仍有 `/v3/internal/mp/**` 等历史调用;如存在,不要自行改成另一个 internal 地址,应反馈后端补正式 BFF/admin 契约。 + +## 九、前端待处理清单 + +- [ ] 派单/改派弹窗在 HOLD 模式支持 `messageTemplateId/customBody`,DIRECT 模式不提交或忽略这两个字段。 +- [ ] HOLD 创建成功时把 `holdSentAt=null` 展示为处理中,不显示“已发送”。 +- [ ] 通知日志筛选、标签和统计适配 `0~6` 状态及新增字段。 +- [ ] 仅对具备专用权限的账号展示“人工核对可靠短信”入口,并实现 `SUCCESS/NOT_SENT` 两种表单校验。 +- [ ] 订单详情推送记录适配新的归一化状态枚举。 +- [ ] 取消派单后刷新服务端状态,以 `canAssign` 控制重新派车入口。 +- [ ] 确认前端不存在任何 `/internal/**` 或 `/v3/internal/**` 调用。 +- [ ] 所有雪花 ID 保持字符串。 + +## 十、后端交付与测试环境状态 + +- 后端 PR #5073、#5084 已合并到 `dev-v3`。 +- `hl-order-service-v3`、`hl-fleet-service`、`hl-gateway` 已按顺序部署 TEST,双实例健康;当前 OpenAPI 已公开本文全部管理端接口。 +- 已用真实测试订单完成 DIRECT、取消、需求重开、再次 DIRECT、司机同步和退保时间线验收。 +- TEST 当前 `hold_notify` 短信模板仍是占位配置,真实 HOLD 短信会失败关闭,`holdSentAt` 保持 `null`;这是环境配置阻塞,不应由前端伪造成发送成功。 +- 本文件只做契约交接,不修改 `hl-ui`。 +