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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-27 08:36:53 +08:00
parent 25395a648b
commit 23984a3651
3 changed files with 65 additions and 0 deletions
@@ -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<Map<String, Object>> batchReplay(@RequestBody List<Long> ids) {
int success = 0;
int failed = 0;
java.util.List<String> 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<Map<String, Object>> backlogStats() {
return ResponseEntity.ok(callbackInboxService.getBacklogStats());
}
}
@@ -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<String, Object> 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()) {
@@ -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;
}
}