# I8 设计 — Webhook 平台投递 DEAD 重放(MVP) > **角色**:解决方案架构;指导前后端实现与 Runbook。 > **前置**:I7 已落地 `webhook_platform_delivery`(`PENDING`/`SENT`/`DEAD`)与调度器;平台 `platform_callback_inbox.webhook_receipt_id` 与 Webhook 收据主键对齐(`PlatformCallbackRequestPlanner` 写入 `webhookReceiptId`)。 --- ## 1. 问题与目标 | 维度 | 说明 | |------|------| | **问题** | 出库 `DEAD` 后仅能通过查库/SQL 手工改状态,无受控运维入口,易误操作。 | | **目标** | Ops 在 **Callback 详情** 一键将对应 **`DEAD`** 投递重新入队(`PENDING`),由现有调度器按 `max-attempts` 再次尝试 `POST /internal/v1/callback-events`。 | | **非目标** | 修改比特 Callback 契约;不实现全量 DLQ 控制台;不在浏览器直连 Webhook 服务。 | --- ## 2. 信任边界与 API 分层 ```mermaid flowchart LR UI[delivery-platform-ui OPS JWT] API[delivery-platform-api] WH[license-webhook-ingress] UI -->|Bearer JWT| API API -->|X-Webhook-Ops-Token + receiptId| WH WH -->|调度| API ``` 1. **浏览器只调平台** `POST /api/v1/callback-inbox/{id}/replay-webhook-delivery`,沿用 `CallbackInboxController` 的 `@PreAuthorize("hasAnyRole('OPS','SYS_ADMIN')")`。 2. **平台服务器到 Webhook** 使用 **独立共享密钥** `X-Webhook-Ops-Token`(与 `X-Platform-Internal-Token` 分离:前者保护 Webhook 运维面,后者保护平台内部 ingest)。 3. **Webhook** 暴露 `POST /internal/v1/platform-deliveries/by-receipt/{receiptId}/replay`,由 **Servlet Filter** 校验 Ops Token(模块无 Spring Security 依赖,与现有 `/webhook/bitanswer/callback` 并存)。 --- ## 3. 业务规则 | 规则 | 说明 | |------|------| | **关联键** | 使用平台收件箱的 `webhook_receipt_id`(字符串数字)对应 `webhook_platform_delivery.receipt_id`(唯一)。缺失则 **400**,提示未关联 Webhook 出库记录。 | | **仅 DEAD 可重放** | Webhook 侧若行不存在 → **404**;若状态为 `PENDING`/`SENT` → **409**,避免误重置进行中或已成功任务。 | | **重放语义** | `status=PENDING`,`attempts=0`,`last_error=NULL`,`next_retry_at=NULL`,`updated_at=now`(重新给满 `max-attempts` 次数)。 | | **平台幂等** | 重放仅重新 HTTP 投递;若平台 Inbox 已存在同 `(sourceSystem, externalMessageId)`,内部 ingest 返回 duplicate,**不新建 Inbox**(与现有幂等一致)。 | --- ## 4. 配置项 | 组件 | 属性 / 环境变量 | 说明 | |------|-----------------|------| | Webhook | `craftlabs.webhook.ops-token` / `LICENSE_WEBHOOK_OPS_TOKEN` | 非空才启用 `/internal/**` 鉴权;空则 **503** 所有内部运维路径(避免误暴露)。 | | Platform | `craftlabs.webhook.base-url` / `LICENSE_WEBHOOK_BASE_URL` | Webhook 根协议+主机+端口。 | | Platform | `craftlabs.webhook.ops-token` | 与 Webhook 相同密钥,仅驻内存。 | **Runbook**:在 [RUNBOOK.md](../../../services/RUNBOOK.md) §10.5 旁补充:重放前确认平台已恢复;同 receipt **不要并发多次重放**(单机调度即可)。 --- ## 5. 前端 - **Callback 详情**:展示 `webhookReceiptId`;若存在则显示 **「重新入队出库(DEAD→待投递)」**,调平台 POST;成功/失败用现有 `apiErrorMessage`。 - **可见性**:路由已为 OPS/SYS_ADMIN,与 I7 一致。 --- ## 6. 契约 - 更新 `contracts/openapi/delivery-platform-api.json`:`POST /api/v1/callback-inbox/{id}/replay-webhook-delivery` 与响应 DTO(如 `status`、`receiptId`)。 - Webhook 内部路由 **不入** 对外 OpenAPI。 --- ## 7. 测试与验收 | 层级 | 验收 | |------|------| | Webhook | 单测:`replayDeadByReceiptId` 对 DEAD/非 DEAD/缺行行为;集成或 MockMvc:401/503 无 token。 | | Platform | 单测或 Mock:`CallbackInboxService` 在缺 receipt、Webhook 409/404 时映射 HTTP。 | | 手工 | DEAD 一行 → 详情点重放 → 行变 `PENDING` → 调度成功后 `SENT`。 | --- ## 8. 修订记录 | 日期 | 说明 | |------|------| | 2026-04-06 | 初版:I8 DEAD 重放架构(平台代理 + Webhook 内部 API)。 |