package com.baiyue.lastleg.controller; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.outbox.OutboxOperations; import com.baiyue.common.core.outbox.EventOperationsTokenGuard; import com.baiyue.common.event.*; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.web.bind.annotation.*; import java.util.Set; /** Internal-only operator recovery. It cannot mutate the original event or call a consumer directly. */ @RestController @RequestMapping("/internal/lastleg/event-operations") public class EventOperationsController { private final EventOperationsTokenGuard guard; private final OutboxOperations operations; public EventOperationsController(JdbcTemplate jdbc, PlatformTransactionManager transactions, ObjectMapper mapper, @Value("${baiyue.event-delivery.operations-token:}") String token) { guard = new EventOperationsTokenGuard(token); operations = new OutboxOperations(jdbc, transactions, new OutboxOperations.Layout("ll_event_outbox", "id", "status", Set.of(EventTypes.OVERSEAS_CONTAINER_UNLOADED, EventTypes.OVERSEAS_DELIVERY_COMPLETED, EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED, EventTypes.WALLET_COMMAND_REQUESTED, EventTypes.BUSINESS_FEE_SOURCE_CHANGED, EventTypes.COMPANY_VEHICLE_COST_SUBMITTED)), json -> { try { EventEnvelope envelope = mapper.readValue(json, EventEnvelope.class).requireValid(); if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(envelope.payload()), envelope.payloadHash())) { throw new IllegalArgumentException("Hash mismatch"); } return new OutboxOperations.Identity(envelope.eventId(), envelope.eventType(), envelope.payloadHash()); } catch (Exception invalid) { throw new BizException(ResultCode.CONFLICT, "原事件格式或摘要无效,禁止人工重试"); } }); } @GetMapping("/{entryId}") public ApiResponse inspect( @RequestHeader(value = "X-Event-Operations-Token", required = false) String token, @PathVariable String entryId) { guard.require(token); return ApiResponse.success(operations.inspect(entryId)); } @PostMapping("/{entryId}/retry") public ApiResponse retry( @RequestHeader(value = "X-Event-Operations-Token", required = false) String token, @PathVariable String entryId, @RequestBody OutboxOperations.RetryRequest request) { guard.require(token); return ApiResponse.success(operations.retry(entryId, request)); } }