diff --git a/common-event-contract/src/main/java/com/baiyue/common/event/BusinessFeeSourceChangedV1.java b/common-event-contract/src/main/java/com/baiyue/common/event/BusinessFeeSourceChangedV1.java
new file mode 100644
index 000000000..8ae7a67e5
--- /dev/null
+++ b/common-event-contract/src/main/java/com/baiyue/common/event/BusinessFeeSourceChangedV1.java
@@ -0,0 +1,52 @@
+package com.baiyue.common.event;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 跨区域业务费用来源的完整不可变快照。
+ *
+ *
海外业务与国内 Finance 不做同步回查,因此事件必须携带可独立校验、入池的完整快照。
+ * 标识统一使用字符串,避免跨语言大整数精度损失。
+ */
+public record BusinessFeeSourceChangedV1(
+ String action,
+ String sourceSystem,
+ String sourceType,
+ String sourceFeeId,
+ int sourceVersion,
+ String customerId,
+ String customerName,
+ String sourceNo,
+ String businessType,
+ String businessId,
+ String businessNo,
+ String orderId,
+ String inboundId,
+ String inboundCode,
+ String containerId,
+ String containerBookingId,
+ String containerNo,
+ String warehouseId,
+ String feeItemCode,
+ String feeItemName,
+ String direction,
+ String supplierId,
+ String supplierName,
+ String supplierCategory,
+ String settlementMode,
+ Instant paidAt,
+ String paymentMethod,
+ String paymentReferenceNo,
+ List paymentVoucherFileIds,
+ LocalDate businessDate,
+ String currency,
+ BigDecimal totalAmount,
+ String reviewStatus,
+ String publicationStatus,
+ String sourceStatus,
+ String sourceHash
+) {
+}
diff --git a/common-event-contract/src/main/java/com/baiyue/common/event/EventTypes.java b/common-event-contract/src/main/java/com/baiyue/common/event/EventTypes.java
index eac3a335a..c992eea78 100644
--- a/common-event-contract/src/main/java/com/baiyue/common/event/EventTypes.java
+++ b/common-event-contract/src/main/java/com/baiyue/common/event/EventTypes.java
@@ -14,6 +14,7 @@ public final class EventTypes {
"FirstLegContainerShippingNodeRecorded";
public static final String OVERSEAS_CONTAINER_UNLOADED = "OverseasContainerUnloaded";
public static final String OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED = "OverseasInboundOrderDetainedChanged";
+ public static final String BUSINESS_FEE_SOURCE_CHANGED = "BusinessFeeSourceChanged";
public static final String IAM_USER_STATUS_CHANGED = "IamUserStatusChanged";
public static final String IAM_TENANT_STATUS_CHANGED = "IamTenantStatusChanged";
public static final String IAM_PERMISSION_VERSION_CHANGED = "IamPermissionVersionChanged";
diff --git a/config/nacos/cn/event-bridge-service.yml b/config/nacos/cn/event-bridge-service.yml
index 5b81ac297..d14d59c6d 100644
--- a/config/nacos/cn/event-bridge-service.yml
+++ b/config/nacos/cn/event-bridge-service.yml
@@ -11,3 +11,4 @@ baiyue:
event-dispatcher:
consumer-url: ${EVENT_CONSUMER_URL:http://firstleg:9202/internal/firstleg/v1/container-events}
firstleg-consumer-url: ${FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL:http://firstleg:9202/internal/firstleg/v1/container-events}
+ finance-consumer-url: ${FINANCE_FEE_EVENT_CONSUMER_URL:http://finance:9208/internal/finance/v1/business-fee-source-events}
diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql
index 4165d7614..8ca5fa993 100644
--- a/deploy/sql/头程数据库统一增量-MySQL8.sql
+++ b/deploy/sql/头程数据库统一增量-MySQL8.sql
@@ -3294,8 +3294,8 @@ CREATE TABLE IF NOT EXISTS fl_order_declaration_snapshot (
cargoes_json JSON NOT NULL COMMENT '客户申报货物行完整快照,不覆盖实测事实',
changes_json JSON NULL COMMENT '本版本申报字段变化摘要',
occurred_at DATETIME(3) NOT NULL COMMENT 'Order事件发生时间',
- created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
- updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
+ created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+ updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (tenant_id, order_id),
UNIQUE KEY uk_order_declaration_event (tenant_id, event_id),
KEY idx_order_declaration_version (tenant_id, order_id, order_version)
@@ -3307,7 +3307,7 @@ CREATE TABLE IF NOT EXISTS fl_order_declaration_event (
order_id BIGINT UNSIGNED NOT NULL COMMENT 'Order订单ID',
order_version INT NOT NULL COMMENT '事件携带的Order聚合版本',
request_hash CHAR(64) NOT NULL COMMENT '完整事件SHA-256,用于异参重放拒绝',
- consumed_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+ consumed_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '消费时间',
PRIMARY KEY (tenant_id, event_id),
KEY idx_order_declaration_event_order (tenant_id, order_id, order_version)
) ENGINE=InnoDB COMMENT='Order客户申报事件永久幂等账本';
diff --git a/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/FinanceEventClient.java b/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/FinanceEventClient.java
new file mode 100644
index 000000000..a8d4e7107
--- /dev/null
+++ b/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/FinanceEventClient.java
@@ -0,0 +1,7 @@
+package com.baiyue.eventbridge.service;
+
+import com.baiyue.eventbridge.domain.BridgeInbox;
+
+public interface FinanceEventClient {
+ String deliver(BridgeInbox event) throws Exception;
+}
diff --git a/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpFinanceEventClient.java b/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpFinanceEventClient.java
new file mode 100644
index 000000000..fcdd42872
--- /dev/null
+++ b/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpFinanceEventClient.java
@@ -0,0 +1,56 @@
+package com.baiyue.eventbridge.service;
+
+import com.baiyue.common.event.EventEnvelope;
+import com.baiyue.common.event.RegionCode;
+import com.baiyue.eventbridge.config.EventDispatcherProperties;
+import com.baiyue.eventbridge.domain.BridgeInbox;
+import com.baiyue.eventbridge.trace.EventBridgeHttpTracePropagation;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.Resource;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+@Component
+public class HttpFinanceEventClient implements FinanceEventClient {
+ @Resource private ObjectMapper objectMapper;
+ @Resource private EventDispatcherProperties properties;
+ @Resource private EventBridgeHttpTracePropagation tracePropagation;
+ @Value("${baiyue.event-dispatcher.finance-consumer-url:http://127.0.0.1:9208/internal/finance/v1/business-fee-source-events}")
+ private URI consumerUrl;
+ @Value("${baiyue.security.internal-service-token:}") private String serviceToken;
+ private HttpClient httpClient;
+
+ @PostConstruct
+ void initializeHttpClient() {
+ httpClient = HttpClient.newBuilder().connectTimeout(properties.connectTimeout()).build();
+ }
+
+ @Override
+ public String deliver(BridgeInbox row) throws Exception {
+ EventEnvelope envelope = new EventEnvelope(row.getEventId(), row.getEventType(), row.getEventVersion(),
+ row.getOccurredAt(), RegionCode.valueOf(row.getSourceRegion()), RegionCode.valueOf(row.getTargetRegion()),
+ row.getCustomerId(), row.getAggregateType(), row.getAggregateId(), row.getAggregateVersion(),
+ row.getHandoverVersion(), row.getTraceId(), row.getCorrelationId(), row.getCausationId(),
+ objectMapper.readTree(row.getPayloadJson()), row.getPayloadHash());
+ HttpRequest request = tracePropagation.apply(HttpRequest.newBuilder(consumerUrl)
+ .timeout(properties.requestTimeout()).header("Content-Type", "application/json")
+ .header("X-Internal-Service-Token", serviceToken))
+ .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(envelope))).build();
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ JsonNode body;
+ try { body = objectMapper.readTree(response.body()); }
+ catch (Exception ignored) { body = null; }
+ if (response.statusCode() / 100 != 2 || body == null || !"200".equals(body.path("code").asText())) {
+ throw new IllegalStateException("Finance消费失败: HTTP " + response.statusCode()
+ + ", code=" + (body == null ? "missing" : body.path("code").asText("missing")));
+ }
+ return String.valueOf(response.statusCode());
+ }
+}
diff --git a/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/OverseasEventDispatcher.java b/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/OverseasEventDispatcher.java
index b20be547f..e8b23334e 100644
--- a/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/OverseasEventDispatcher.java
+++ b/event-bridge-service/src/main/java/com/baiyue/eventbridge/service/OverseasEventDispatcher.java
@@ -27,6 +27,7 @@ public class OverseasEventDispatcher {
@Resource private BridgeDeliveryAttemptMapper attemptMapper;
@Resource private LastlegEventClient eventClient;
@Resource private FirstlegEventClient firstlegEventClient;
+ @Resource private FinanceEventClient financeEventClient;
@Resource private SecurityProjectionEventClient securityProjectionEventClient;
@Resource private EventDispatcherProperties properties;
@Resource private EventBridgeMetrics metrics;
@@ -89,7 +90,8 @@ public class OverseasEventDispatcher {
throw new IllegalArgumentException("事件类型不能为空,无法选择下游消费者");
}
return switch (event.getTargetRegion()) {
- case "CN" -> firstlegEventClient.deliver(event);
+ case "CN" -> EventTypes.BUSINESS_FEE_SOURCE_CHANGED.equals(eventType)
+ ? financeEventClient.deliver(event) : firstlegEventClient.deliver(event);
case "OVERSEAS" -> SECURITY_EVENT_TYPES.contains(eventType)
? securityProjectionEventClient.deliver(event) : eventClient.deliver(event);
default -> throw new IllegalArgumentException("不支持的事件目标区域: " + event.getTargetRegion());
diff --git a/event-bridge-service/src/main/resources/application.yml b/event-bridge-service/src/main/resources/application.yml
index 8b2c46a43..cb4c431be 100644
--- a/event-bridge-service/src/main/resources/application.yml
+++ b/event-bridge-service/src/main/resources/application.yml
@@ -33,6 +33,7 @@ baiyue:
event-dispatcher:
consumer-url: ${EVENT_CONSUMER_URL:${LASTLEG_CONSUMER_URL:http://127.0.0.1:9303/internal/lastleg/events}}
firstleg-consumer-url: ${FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL:http://127.0.0.1:9202/internal/firstleg/v1/container-events}
+ finance-consumer-url: ${FINANCE_FEE_EVENT_CONSUMER_URL:http://127.0.0.1:9208/internal/finance/v1/business-fee-source-events}
security-consumer-url: ${LASTLEG_SECURITY_EVENT_CONSUMER_URL:http://127.0.0.1:9303/internal/lastleg/security/v1/events}
fixed-delay: ${EVENT_DISPATCH_FIXED_DELAY:3000}
batch-size: ${EVENT_DISPATCH_BATCH_SIZE:50}
diff --git a/finance/pom.xml b/finance/pom.xml
index 08d3df433..8da2c4701 100644
--- a/finance/pom.xml
+++ b/finance/pom.xml
@@ -11,6 +11,7 @@
finance-service
+ com.baiyue.sharedcommon-event-contract${project.version}
com.baiyue.sharedbaiyue-common-web${project.version}
com.baiyue.sharedbaiyue-common-openapi${project.version}
com.baiyue.sharedbaiyue-common-security${project.version}
diff --git a/finance/src/main/java/com/baiyue/finance/controller/InternalBusinessFeeSourceController.java b/finance/src/main/java/com/baiyue/finance/controller/InternalBusinessFeeSourceController.java
new file mode 100644
index 000000000..88f9dc71e
--- /dev/null
+++ b/finance/src/main/java/com/baiyue/finance/controller/InternalBusinessFeeSourceController.java
@@ -0,0 +1,38 @@
+package com.baiyue.finance.controller;
+
+import com.baiyue.common.core.api.ApiResponse;
+import com.baiyue.common.event.EventEnvelope;
+import com.baiyue.finance.service.BusinessFeeSourceEventService;
+import com.baiyue.finance.service.FinanceEventException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Map;
+
+/** Receives full immutable fee snapshots from cross-region business producers. */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/internal/finance/v1/business-fee-source-events")
+public class InternalBusinessFeeSourceController {
+ private final BusinessFeeSourceEventService service;
+ @Value("${baiyue.security.internal-service-token}") private String expectedToken;
+
+ @PostMapping
+ public ApiResponse