2026-09-29 18:47:37 +08:00
|
|
|
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,
|
2026-10-03 12:38:04 +08:00
|
|
|
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)),
|
2026-09-29 18:47:37 +08:00
|
|
|
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<OutboxOperations.Entry> 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<OutboxOperations.RetryResult> 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));
|
|
|
|
|
}
|
|
|
|
|
}
|