---
schema: "hl-changelog/v2"
ticket: "5446"
title: "HOLD 通知管理端状态查询与受控重试"
consumer: "admin"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "Pi"
frontend_ref: "v2.1@49f38c7cfb4d5859a13c6ab1a7fea540cb524de5"
target_release: ""
verified_at: "2026-08-04"
status_note: "后端 PR #5447 已合并 dev-v3（merge 15d79a8d4）；测试部署 task ba1b84af 成功；网关验证 5 项通过（参数校验/missing/stale 拒绝）；管理后台已由 Pi 领取并开始适配。"
updated_at: "2026-08-04"
base: "dev-v3"
generated: "2026-08-03T23:19:24+08:00"
---


# HOLD 通知管理端状态查询与受控重试

## 关联

- Issue: [#5446](https://git.1814.love:8443/wx/HL/issues/5446)
- Backend tracking: [#5366 B10](https://git.1814.love:8443/wx/HL/issues/5366)
- PR: [#5447](https://git.1814.love:8443/wx/HL/pulls/5447)（merge commit 15d79a8d4）


### 联系人

- **后端负责人**: @wx
## 变更接口

| 方法 | 路径 | 来源 |
|---|---|---|
| `GET` | `/admin/fleet/assignments/hold-notification/status` | `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/controller/AssignmentHoldNotificationAdminController.java` |
| `POST` | `/admin/fleet/assignments/hold-notification/retry` | 同上 |

路径前缀 `/admin/fleet/**` 已由网关登录/角色校验与 `FleetAdminRoleGuardInterceptor`（VEHICLE_MANAGER / SUPER_ADMIN）收口，无需新增网关规则。

## 1. 状态查询 `GET /admin/fleet/assignments/hold-notification/status`

请求参数（query）：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `assignmentGroupId` | `String(Long)` | 是 | 派车组 ID |
| `notificationGeneration` | `String(Long)` | 是 | HOLD 通知代际 |
| `attemptId` | `String` | 否 | 调用方生成的 attempt 标识（幂等/审计定位），≤64 字符 |

响应 `data`：

| 字段 | JSON 类型 | 可空 | 说明 |
|---|---|---|---|
| `assignmentGroupId` | `String(Long)` | 否 | 回显 |
| `notificationGeneration` | `String(Long)` | 否 | 回显 |
| `assignmentStatus` | `String` | 是 | 派车状态；HOLDING=仍在待确认，其余=已离开 holding |
| `deliveryStatus` | `String` | 否 | `NOT_FOUND/PENDING/SENT/FAILED/AMBIGUOUS/INVALIDATED`；`SENT` 只表示系统发送成功，不表示通道送达回执 |
| `messageLogId` | `String(Long)` | 是 | 本地通知日志 ID |
| `outboxEventId` | `String(Long)` | 是 | 可靠通知 Outbox 事件 ID |
| `outboxStatus` | `String` | 是 | `PENDING/PROCESSING/QUARANTINED/SUCCESS` |
| `retryCount` | `Integer` | 是 | Outbox 累计处理重试次数 |
| `sentAt` | `String(date-time)` | 是 | 有可信发送成功事实时返回 |
| `dispatchAttemptedAt` | `String(date-time)` | 是 | 最近一次向通知中心发起分发的时间 |
| `lastError` | `String` | 是 | 最近失败/对账留痕（脱敏） |
| `canRetry` | `Boolean` | 否 | 是否允许管理端重试：仅明确失败且未发送成功且未取消时为 `true` |
| `lastReplayAttemptId` | `String` | 是 | 上次管理端重试 attemptId |
| `lastReplayReason` | `String` | 是 | 上次管理端重试原因 |
| `lastReplayedBy` | `String(Long)` | 是 | 上次重试操作人 |
| `lastReplayedAt` | `String(date-time)` | 是 | 上次重试时间 |
| `attemptStatus` | `String` | 是 | 本次请求 attempt 处理状态：`ACCEPTED/SUCCEEDED/REJECTED/AMBIGUOUS`（持久幂等：同一 attemptId 重复提交直接返回已记录结果） |
| `attemptResultNote` | `String` | 是 | 本次 attempt 处理结果摘要（脱敏） |

错误码：400 参数校验 / `100001` 派车组不存在 / `100003` 通知代际已过期 / 401 未登录。

## 2. 受控重试 `POST /admin/fleet/assignments/hold-notification/retry`

请求体：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `assignmentGroupId` | `String(Long)` | 是 | 派车组 ID |
| `notificationGeneration` | `String(Long)` | 是 | HOLD 通知代际 |
| `attemptId` | `String` | 是 | 本次重试 attempt 标识（幂等键组成部分 + 审计），≤64 字符 |
| `reason` | `String` | 是 | 核查失败原因后的重试说明，≤200 字 |

行为约束：

- 幂等键 = `assignmentGroupId:notificationGeneration:attemptId`，持久化于 `fleet_hold_notification_retry_attempt` 唯一索引，重复提交（含 Redis 防重窗口过期后）幂等返回已记录结果，不重复发送、不重复写 Outbox；
- 仅允许明确失败（`FAILED`，Outbox `QUARANTINED` 或 `PENDING`+错误）的 HOLD 通知重试；`QUARANTINED` 重置失败预算，`PENDING`+错误立即重试；
- `UNKNOWN/AMBIGUOUS`（dispatching）先按通知中心供应商发送日志对账：存在真实外部成功则补记发送事实并返回 `SENT`，仍无法判定则拒绝重试（fail closed）；
- stale（代际过期）/missing（派车组或日志不存在）/wrong identity 一律拒绝；
- 重试复用原 Outbox 事件与供应商幂等键，不新建事件、不直接外呼，不改变派车状态；
- 操作人、原因、attemptId 记入 Outbox 审计列（`last_replay_*`）。

响应：同状态查询 `data`（重试后最新事实）。

错误码：400 参数校验 / `100001` 派车组或通知日志不存在 / `100003` 通知代际已过期或已取消 / `100503` 资源竞争（并发重试）/ 401 未登录。

## 契约影响文件

- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/controller/AssignmentHoldNotificationAdminController.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/service/AssignmentHoldNotificationAdminService.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/vo/AssignmentHoldNotificationStatusReqVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/vo/AssignmentHoldNotificationStatusRespVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/vo/AssignmentHoldNotificationRetryReqVO.java`
- `hl-fleet-service/src/main/java/db/migration/V20260803_008__add_outbox_hold_replay_attempt_id.java`

## 前端/调用方动作

管理后台新增适配：派车详情/司机通知维度展示 `deliveryStatus` 与 `canRetry`；`canRetry=true` 时提供重试按钮，重试需携带调用方生成的 `attemptId` 与原因；`AMBIGUOUS` 不提供重试入口（等待自动对账）。所有 Long ID 按 String 消费。

## 验证证据

- 定向测试：`AssignmentHoldNotificationAdminServiceTest` 24 项 / `AssignmentHoldNotificationAdminControllerTest` 5 项 / `HoldNotificationRetryAttemptMapperTest` 2 / `WechatMessageLogMapperTest` 2 / `AssignmentInsuranceOutboxMapperTest` 18 / 迁移可重入测试 2（Fleet reactor verify 3047 项 0 failures，1 项 Docker 环境型 error 与本任务无关）
- Spotless: check 通过
- 网关验证：经 api.test.1814.love:9443 实测 5 项（status 缺参 400 / 组不存在 100001 / 过期代际 100003 / retry 缺 attemptId 400 / retry 过期代际 100003），证据 sha256 c8759216
- 兼容性结论：纯新增端点，无既有字段或行为变更
