From 23984a3651c93c89584b422b3fdddf1a9770fc40 Mon Sep 17 00:00:00 2001 From: huangping Date: Wed, 27 May 2026 08:36:53 +0800 Subject: [PATCH] feat: add failure reason persistence and batch replay endpoints to callback inbox Added failureReason field to CallbackInboxStatusPatchRequest so Ops can categorize failure causes. Added POST /batch-replay for mass reprocess and GET /stats/backlog for backlog monitoring. Co-authored-by: Sisyphus --- .../api/callback/CallbackInboxController.java | 26 ++++++++++++++++ .../api/service/CallbackInboxService.java | 30 +++++++++++++++++++ .../dto/CallbackInboxStatusPatchRequest.java | 9 ++++++ 3 files changed, 65 insertions(+) diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/callback/CallbackInboxController.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/callback/CallbackInboxController.java index 42de0ba..53eced5 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/callback/CallbackInboxController.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/callback/CallbackInboxController.java @@ -27,6 +27,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; @RestController @RequestMapping("/api/v1/callback-inbox") @@ -103,4 +105,28 @@ public class CallbackInboxController { public CallbackWebhookDeliveryStatusResponse getWebhookDelivery(@PathVariable("id") long id) { return callbackInboxService.getWebhookDeliveryStatus(id); } + + /** M5-F07:批量重处理 — 接收 ID 列表,逐条触发 DEAD 重放。 */ + @PostMapping("/batch-replay") + public ResponseEntity> batchReplay(@RequestBody List ids) { + int success = 0; + int failed = 0; + java.util.List errors = new java.util.ArrayList<>(); + for (Long id : ids) { + try { + callbackInboxService.replayWebhookDelivery(id); + success++; + } catch (Exception e) { + failed++; + errors.add("ID " + id + ": " + e.getMessage()); + } + } + return ResponseEntity.ok(java.util.Map.of("success", success, "failed", failed, "errors", errors)); + } + + /** M5-F08:死信与积压监控摘要。 */ + @GetMapping("/stats/backlog") + public ResponseEntity> backlogStats() { + return ResponseEntity.ok(callbackInboxService.getBacklogStats()); + } } diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/CallbackInboxService.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/CallbackInboxService.java index 8b5dd8c..0bf61c9 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/CallbackInboxService.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/CallbackInboxService.java @@ -133,6 +133,9 @@ public class CallbackInboxService { row.setStatus(to.name()); row.setProcessedAt(now); row.setProcessedByUserId(currentActorId()); + if (request.getFailureReason() != null) { + row.setFailureReason(request.getFailureReason()); + } row.setUpdatedAt(now); inboxMapper.updateById(row); auditService.record( @@ -306,6 +309,33 @@ public class CallbackInboxService { return r; } + public Map getBacklogStats() { + long totalPending = inboxMapper.selectCount( + Wrappers.lambdaQuery(PlatformCallbackInbox.class) + .eq(PlatformCallbackInbox::getStatus, CallbackInboxStatus.PENDING)); + + long totalFailed = inboxMapper.selectCount( + Wrappers.lambdaQuery(PlatformCallbackInbox.class) + .eq(PlatformCallbackInbox::getStatus, CallbackInboxStatus.FAILED)); + + var oldestPending = inboxMapper.selectOne( + Wrappers.lambdaQuery(PlatformCallbackInbox.class) + .eq(PlatformCallbackInbox::getStatus, CallbackInboxStatus.PENDING) + .orderByAsc(PlatformCallbackInbox::getReceivedAt) + .last("LIMIT 1")); + + double oldestHours = 0; + if (oldestPending != null && oldestPending.getReceivedAt() != null) { + oldestHours = java.time.Duration.between( + oldestPending.getReceivedAt(), OffsetDateTime.now()).toMinutes() / 60.0; + } + + return java.util.Map.of( + "totalPending", totalPending, + "totalFailed", totalFailed, + "oldestPendingHours", Math.round(oldestHours * 10.0) / 10.0); + } + private static String currentActorId() { var a = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); if (a == null || !a.isAuthenticated()) { diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/CallbackInboxStatusPatchRequest.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/CallbackInboxStatusPatchRequest.java index eb24560..afddfb7 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/CallbackInboxStatusPatchRequest.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/CallbackInboxStatusPatchRequest.java @@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotBlank; public class CallbackInboxStatusPatchRequest { @NotBlank private String status; + private String failureReason; public String getStatus() { return status; @@ -13,4 +14,12 @@ public class CallbackInboxStatusPatchRequest { public void setStatus(String status) { this.status = status; } + + public String getFailureReason() { + return failureReason; + } + + public void setFailureReason(String failureReason) { + this.failureReason = failureReason; + } }