优化
This commit is contained in:
@@ -35,7 +35,7 @@ class MerchantRegistrationServiceTest {
|
||||
void registeredAuthAccountUsesTheOwnerIdentityReturnedByIamOnboarding() {
|
||||
when(encoder.encode("Password123")).thenReturn("encoded-password");
|
||||
when(onboarding.register(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED")));
|
||||
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED", "PENDING")));
|
||||
|
||||
var result = service.register(new MerchantRegisterRequest(
|
||||
"registered_owner", "Password123", "注册人", "测试企业", "张三",
|
||||
@@ -56,13 +56,14 @@ class MerchantRegistrationServiceTest {
|
||||
assertEquals("ACTIVE", storedAccount.getValue().getAccountStatus());
|
||||
assertEquals("encoded-password", storedAccount.getValue().getPasswordHash());
|
||||
assertEquals("LOGIN_AND_COMPLETE_QUALIFICATIONS", result.nextStep());
|
||||
assertEquals("PENDING", result.dimensionInitializationStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedAuthInsertCompensatesTheIamRegistration() {
|
||||
when(encoder.encode("Password123")).thenReturn("encoded-password");
|
||||
when(onboarding.register(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED")));
|
||||
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED", "PENDING")));
|
||||
when(onboarding.compensate(eq("internal-token"), any())).thenReturn(ApiResponse.success(null));
|
||||
doThrow(new IllegalStateException("auth database unavailable")).when(accounts).insert(any());
|
||||
|
||||
@@ -82,7 +83,7 @@ class MerchantRegistrationServiceTest {
|
||||
void rolledBackAuthTransactionCompensatesAfterTheLocalTransactionCompletes() {
|
||||
when(encoder.encode("Password123")).thenReturn("encoded-password");
|
||||
when(onboarding.register(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED")));
|
||||
new IamOnboardingClient.OnboardingResponse(10001L, 30001L, "PROFILE_REQUIRED", "PENDING")));
|
||||
when(onboarding.compensate(eq("internal-token"), any())).thenReturn(ApiResponse.success(null));
|
||||
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
|
||||
@@ -7,8 +7,10 @@ spring:
|
||||
baiyue:
|
||||
security-event:
|
||||
event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}
|
||||
customer-dimension-initialization:
|
||||
fixed-delay-ms: ${CUSTOMER_DIMENSION_INITIALIZATION_FIXED_DELAY_MS:5000}
|
||||
max-attempts: ${CUSTOMER_DIMENSION_INITIALIZATION_MAX_ATTEMPTS:12}
|
||||
notification:
|
||||
scheduler-enabled: ${NOTIFICATION_SCHEDULER_ENABLED:false}
|
||||
dispatch-timeout-seconds: ${NOTIFICATION_DISPATCH_TIMEOUT_SECONDS:300}
|
||||
mq-consumer-enabled: ${NOTIFICATION_MQ_CONSUMER_ENABLED:false}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ spring.datasource:
|
||||
username: ${IAM_DB_USERNAME:root}
|
||||
password: ${IAM_DB_PASSWORD:123456}
|
||||
baiyue:
|
||||
customer-dimension-initialization:
|
||||
fixed-delay-ms: ${CUSTOMER_DIMENSION_INITIALIZATION_FIXED_DELAY_MS:5000}
|
||||
max-attempts: ${CUSTOMER_DIMENSION_INITIALIZATION_MAX_ATTEMPTS:12}
|
||||
notification:
|
||||
scheduler-enabled: ${NOTIFICATION_SCHEDULER_ENABLED:false}
|
||||
dispatch-timeout-seconds: ${NOTIFICATION_DISPATCH_TIMEOUT_SECONDS:300}
|
||||
|
||||
@@ -14892,6 +14892,21 @@ BEGIN
|
||||
'baiyue_firstleg','baiyue_file','baiyue_audit','baiyue_event_bridge','baiyue_finance'))<>9 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '统一迁移失败:九个业务数据库未完整创建';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema='baiyue_iam' AND table_name='iam_tenant'
|
||||
AND column_name='dimension_initialization_status') <> 1
|
||||
OR (SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema='baiyue_iam'
|
||||
AND table_name='iam_customer_dimension_initialization_task') <> 1
|
||||
OR (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema='baiyue_iam'
|
||||
AND table_name='iam_customer_dimension_initialization_task'
|
||||
AND column_name IN ('tenant_id','initialization_key','action','status','attempt_count',
|
||||
'next_retry_at','lease_owner','lease_until','last_error_code','last_error_message',
|
||||
'task_version')) <> 11 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '统一迁移失败:IAM客户默认尺寸后台初始化状态或可靠任务结构不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema='baiyue_firstleg' AND table_name='fl_carrier_schedule_query'
|
||||
AND column_name IN ('trigger_source','latest_standard_node','latest_node_observed_at',
|
||||
@@ -16579,17 +16594,17 @@ CREATE TABLE IF NOT EXISTS iam_customer_dimension_initialization_task (
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '稳定初始化幂等键:iam-customer-create:{tenantId}',
|
||||
action VARCHAR(16) NOT NULL DEFAULT 'INITIALIZE' COMMENT '目标动作:INITIALIZE初始化、COMPENSATE补偿',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '投递状态:PENDING、SENDING、READY、FAILED、CANCELLED',
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
lease_owner VARCHAR(128) NULL,
|
||||
lease_until DATETIME(3) NULL,
|
||||
last_error_code VARCHAR(32) NULL,
|
||||
last_error_message VARCHAR(1000) NULL,
|
||||
remote_config_id BIGINT NULL,
|
||||
task_version INT NOT NULL DEFAULT 0,
|
||||
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),
|
||||
completed_at DATETIME(3) NULL,
|
||||
attempt_count INT NOT NULL DEFAULT 0 COMMENT '当前动作已失败尝试次数',
|
||||
next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次允许投递时间',
|
||||
lease_owner VARCHAR(128) NULL COMMENT '当前任务租约持有者',
|
||||
lease_until DATETIME(3) NULL COMMENT '当前任务租约截止时间',
|
||||
last_error_code VARCHAR(32) NULL COMMENT '最后下游错误码',
|
||||
last_error_message VARCHAR(1000) NULL COMMENT '最后诊断错误,不保存敏感凭证',
|
||||
remote_config_id BIGINT NULL COMMENT 'Firstleg返回的首条默认尺寸配置ID',
|
||||
task_version INT NOT NULL DEFAULT 0 COMMENT '任务乐观版本;切换动作或认领时递增',
|
||||
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 '任务更新时间',
|
||||
completed_at DATETIME(3) NULL COMMENT 'READY或CANCELLED完成时间',
|
||||
PRIMARY KEY (tenant_id),
|
||||
KEY idx_customer_dimension_task_dispatch (status,next_retry_at,lease_until),
|
||||
KEY idx_customer_dimension_task_diagnosis (status,updated_at)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 目的与口径
|
||||
|
||||
本基线覆盖后端服务之间的 OpenFeign、`RestClient`、`WebClient` 与 JDK `HttpClient` 调用。它用于识别同步依赖方向、在线请求深度、后台中继、超时、重试、熔断、幂等、错误映射和失败传播,不改变 Finance 或四期业务状态机。
|
||||
本基线覆盖当前官方 22 个 Maven 模块、15 个可执行服务之间的 OpenFeign、`RestClient`、`WebClient` 与 JDK `HttpClient` 调用。海外安全 Inbox/投影属于 Lastleg 内部子域,不再作为独立服务节点。基线用于识别同步依赖方向、在线请求深度、后台中继、超时、重试、熔断、幂等、错误映射和失败传播,不改变 Finance 或四期业务状态机。
|
||||
|
||||
“存在双向服务边”不等于“一次在线请求会递归调用”。每条边必须继续区分:
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|---|---|---:|---|
|
||||
| Auth | IAM | 2 | 登录授权、注册,`REQUEST` |
|
||||
| IAM | Auth | 1 | 账号开通/管理,`REQUEST` |
|
||||
| IAM | Firstleg | 1 | 客户尺寸规则,`REQUEST` |
|
||||
| IAM | Firstleg | 1 | 客户尺寸默认规则可靠任务投递,`BACKGROUND` |
|
||||
| IAM | Warehouse | 1 | 海外仓储费协作,`REQUEST` |
|
||||
| IAM | Event Bridge | 1 | 安全事件中继,`BACKGROUND` |
|
||||
| Order | Firstleg | 3 | 计价/统一订单查询为 `REQUEST`,入库协作为 Outbox `BACKGROUND` |
|
||||
@@ -45,10 +45,10 @@
|
||||
| 调用方 | 被调用方 | 实现 | 上下文与当前策略 |
|
||||
|---|---|---|---|
|
||||
| Gateway Overseas | Auth | `HttpPdaSessionValidationClient` | `INFRASTRUCTURE`;总超时 2 秒,异常由网关失败关闭 |
|
||||
| Gateway Overseas | Overseas Security | `HttpOverseasSecurityValidationClient` | `INFRASTRUCTURE`;安全投影实时校验,异常失败关闭,不进入在线业务图 |
|
||||
| Gateway Overseas | Lastleg Security 子域 | `HttpOverseasSecurityValidationClient` | `INFRASTRUCTURE`;Lastleg 内部安全投影实时校验,异常失败关闭,不进入在线业务图 |
|
||||
| Lastleg | Warehouse Overseas | 两个 `RestClient` Gateway | `REQUEST`;分别使用 3 秒连接、5 秒读取超时,配置可覆盖,不自动重试 |
|
||||
| Lastleg | File | 两个照片 `RestClient` Gateway | `REQUEST`;分别使用 3 秒连接、30 秒读取超时,配置可覆盖;校验响应 code 且失败关闭,不自动重试 |
|
||||
| Event Bridge | Firstleg / Lastleg / Overseas Security | 三个 JDK `HttpClient` | `BACKGROUND`;连接 3 秒、请求 10 秒,持久化租约和退避由 Event Bridge 专项治理;Security 投影不进入在线业务图 |
|
||||
| Event Bridge | Firstleg / Lastleg(业务事件与安全投影两个入口) | 三个 JDK `HttpClient` | `BACKGROUND`;连接 3 秒、请求 10 秒,持久化租约和退避由 Event Bridge 专项治理;Lastleg Security 投影不进入在线业务图 |
|
||||
| 启用 Common Audit 的服务 | Audit | `HttpAuditSink` | `INFRASTRUCTURE`;当前捕获异常后仅告警,可能让调用方误以为审计已成功,是高优先级失败传播缺口 |
|
||||
| Integration | 外部 OCR/船司 | 六个 JDK `HttpClient` | `EXTERNAL`;OCR 默认 10/60 秒,船司默认 10/30 秒,业务重试必须服从供应商和调度契约 |
|
||||
|
||||
@@ -67,13 +67,13 @@
|
||||
|
||||
### 在线 REQUEST 图
|
||||
|
||||
在线图只包含用户或设备请求线程中实际执行的服务边。Event Bridge、Outbox、船节点定时任务、导出 Provider 和海外 Security 校验均不计入在线深度。
|
||||
在线图只包含用户或设备请求线程中实际执行的服务边。Event Bridge、Outbox、船节点定时任务、导出 Provider 和 Gateway→Lastleg Security 基础设施校验均不计入在线深度。
|
||||
|
||||
当前在线强连通分量为:
|
||||
|
||||
`Auth ↔ IAM`;`Firstleg ↔ Order`
|
||||
|
||||
`IAM → Firstleg` 默认尺寸初始化已退出请求线程,合同现冻结 21 条在线服务边;新增在线边或新增强连通分量直接失败。File 不再属于在线强连通分量;Finance、Lastleg 只指向上述分量或叶子服务,不属于在线环;Gateway Overseas→Security 是 `INFRASTRUCTURE`,不是 REQUEST 边。
|
||||
`IAM → Firstleg` 默认尺寸初始化已退出请求线程,合同现冻结 21 条在线服务边;新增在线边或新增强连通分量直接失败。File 不再属于在线强连通分量;Finance、Lastleg 只指向上述分量或叶子服务,不属于在线环;Gateway Overseas→Lastleg Security 是 `INFRASTRUCTURE`,不是 REQUEST 边。
|
||||
|
||||
### 后台 BACKGROUND 图
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
`Event Bridge ↔ Firstleg ↔ Finance ↔ Lastleg`
|
||||
|
||||
File 的五个导出 Provider、Order→Firstleg 入库 Outbox、IAM 默认尺寸可靠任务、IAM 安全 Outbox、Firstleg/Lastleg 跨区 Outbox、Firstleg→Finance 费用 Outbox及 Event Bridge 三个消费者均只在后台图中计算。Event Bridge→Overseas Security 是单向后台叶子边,不形成在线业务环。
|
||||
File 的五个导出 Provider、Order→Firstleg 入库 Outbox、IAM 默认尺寸可靠任务、IAM 安全 Outbox、Firstleg/Lastleg 跨区 Outbox、Firstleg→Finance 费用 Outbox及 Event Bridge 三个消费者均只在后台图中计算。Event Bridge→Lastleg Security 与 Event Bridge→Lastleg 业务事件在服务图上收敛为同一条后台服务边,不形成新的在线业务边或独立安全节点。
|
||||
|
||||
### 全图
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ public class CustomerDimensionInitializationRelay {
|
||||
String value = failure.getMessage();
|
||||
if (value == null || value.isBlank()) value = failure.getClass().getSimpleName();
|
||||
value = value.replaceAll("[\\r\\n\\t]+", " ");
|
||||
value = value.replaceAll("(?i)Bearer\\s+[^\\s,;]+", "Bearer ***")
|
||||
.replaceAll("(?i)(token|password)=([^\\s&,;]+)", "$1=***");
|
||||
return value.length() > 1000 ? value.substring(0, 1000) : value;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ spring:
|
||||
baiyue:
|
||||
security-event:
|
||||
event-bridge-url: ${SECURITY_EVENT_BRIDGE_URL:http://event-bridge-overseas:9312}
|
||||
customer-dimension-initialization:
|
||||
fixed-delay-ms: ${CUSTOMER_DIMENSION_INITIALIZATION_FIXED_DELAY_MS:5000}
|
||||
max-attempts: ${CUSTOMER_DIMENSION_INITIALIZATION_MAX_ATTEMPTS:12}
|
||||
notification:
|
||||
scheduler-enabled: ${NOTIFICATION_SCHEDULER_ENABLED:false}
|
||||
dispatch-timeout-seconds: ${NOTIFICATION_DISPATCH_TIMEOUT_SECONDS:300}
|
||||
@@ -47,4 +50,3 @@ logging:
|
||||
max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}"
|
||||
total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}"
|
||||
clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}"
|
||||
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomerDimensionInitializationSqlContractTest {
|
||||
@Test
|
||||
void latestIamSqlContainsStatusAndDurableTask() throws Exception {
|
||||
String sql = new ClassPathResource("db/iam.sql").getContentAsString(StandardCharsets.UTF_8);
|
||||
assertContract(sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void idempotentStandaloneAndUnifiedSqlContainTheSameTaskContract() throws Exception {
|
||||
String standalone = new ClassPathResource("db/customer-dimension-initialization-task.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
Path unifiedPath = Files.exists(direct) ? direct
|
||||
: Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
String unified = Files.readString(unifiedPath, StandardCharsets.UTF_8);
|
||||
|
||||
assertContract(standalone);
|
||||
assertContract(unified);
|
||||
assertThat(standalone).contains("information_schema.columns", "CREATE TABLE IF NOT EXISTS");
|
||||
assertThat(unified).contains("information_schema.columns", "CREATE TABLE IF NOT EXISTS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void schedulerRetryBudgetHasSafeOverridableDefaultsInAllDomesticProfiles() throws Exception {
|
||||
String application = new ClassPathResource("application.yml").getContentAsString(StandardCharsets.UTF_8);
|
||||
assertSchedulerConfig(application);
|
||||
for (String profile : new String[] {"local", "cn"}) {
|
||||
Path direct = Path.of("config", "nacos", profile, "iam-service.yml");
|
||||
Path config = Files.exists(direct) ? direct
|
||||
: Path.of("..", "config", "nacos", profile, "iam-service.yml");
|
||||
assertSchedulerConfig(Files.readString(config, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertContract(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("dimension_initialization_status")
|
||||
.contains("iam_customer_dimension_initialization_task")
|
||||
.contains("initialization_key")
|
||||
.contains("attempt_count")
|
||||
.contains("next_retry_at")
|
||||
.contains("lease_until")
|
||||
.contains("last_error_code")
|
||||
.contains("task_version");
|
||||
}
|
||||
|
||||
private static void assertSchedulerConfig(String yaml) {
|
||||
assertThat(yaml)
|
||||
.contains("customer-dimension-initialization:")
|
||||
.contains("${CUSTOMER_DIMENSION_INITIALIZATION_FIXED_DELAY_MS:5000}")
|
||||
.contains("${CUSTOMER_DIMENSION_INITIALIZATION_MAX_ATTEMPTS:12}");
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -47,7 +48,7 @@ class CustomerDimensionInitializationRelayTest {
|
||||
relay.deliver(10001L);
|
||||
|
||||
verify(repository).markReady(task, workerId(), 91L);
|
||||
verify(repository, never()).recordFailure(any(), anyString(), anyLong(), any(), anyString(),
|
||||
verify(repository, never()).recordFailure(any(), anyString(), anyInt(), any(), anyString(),
|
||||
anyString(), any(Boolean.class));
|
||||
}
|
||||
|
||||
@@ -60,7 +61,7 @@ class CustomerDimensionInitializationRelayTest {
|
||||
relay.deliver(10001L);
|
||||
|
||||
ArgumentCaptor<Boolean> exhausted = ArgumentCaptor.forClass(Boolean.class);
|
||||
verify(repository).recordFailure(any(), anyString(), anyLong(), any(), anyString(),
|
||||
verify(repository).recordFailure(any(), anyString(), anyInt(), any(), anyString(),
|
||||
anyString(), exhausted.capture());
|
||||
assertFalse(exhausted.getValue());
|
||||
}
|
||||
@@ -75,7 +76,7 @@ class CustomerDimensionInitializationRelayTest {
|
||||
relay.deliver(10001L);
|
||||
|
||||
ArgumentCaptor<Boolean> exhausted = ArgumentCaptor.forClass(Boolean.class);
|
||||
verify(repository).recordFailure(any(), anyString(), anyLong(), any(), anyString(),
|
||||
verify(repository).recordFailure(any(), anyString(), anyInt(), any(), anyString(),
|
||||
anyString(), exhausted.capture());
|
||||
assertTrue(exhausted.getValue());
|
||||
verify(repository, never()).markReady(any(), anyString(), any());
|
||||
|
||||
@@ -74,6 +74,8 @@ class MerchantOnboardingServiceTest {
|
||||
"INITIAL_AUTHORIZATION");
|
||||
|
||||
assertEquals("PROFILE_REQUIRED", result.tenantStatus());
|
||||
assertEquals("PENDING", result.dimensionInitializationStatus());
|
||||
assertEquals("PENDING", tenant.getValue().getDimensionInitializationStatus());
|
||||
assertEquals(result.tenantId(), tenant.getValue().getId());
|
||||
assertEquals("测试企业", tenant.getValue().getLegalName());
|
||||
assertEquals("张三", tenant.getValue().getLegalRepresentative());
|
||||
@@ -122,7 +124,7 @@ class MerchantOnboardingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientRegistrationFailsWhenFourDefaultBoxDimensionsCannotBeInitialized() {
|
||||
void clientRegistrationFailsWhenDurableInitializationIntentCannotBePersisted() {
|
||||
IamRole ownerRole = new IamRole();
|
||||
ownerRole.setId(4000100L);
|
||||
ownerRole.setTenantId(0L);
|
||||
@@ -199,4 +201,3 @@ class MerchantOnboardingServiceTest {
|
||||
verify(tenants, never()).deleteById((java.io.Serializable) any());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -313,6 +313,7 @@ class TenantServiceTest {
|
||||
9001L);
|
||||
|
||||
assertEquals("PROFILE_REQUIRED", created.tenant().status());
|
||||
assertEquals("PENDING", created.tenant().dimensionInitializationStatus());
|
||||
assertEquals("Bob", created.tenant().legalRepresentative());
|
||||
assertEquals("基础版", created.tenant().servicePackage());
|
||||
assertEquals("13800138000", created.administratorUsername());
|
||||
@@ -326,7 +327,7 @@ class TenantServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRegistrationFailsWhenFourDefaultBoxDimensionsCannotBeInitialized() {
|
||||
void platformRegistrationFailsWhenDurableInitializationIntentCannotBePersisted() {
|
||||
doThrow(new BizException(ResultCode.SERVICE_UNAVAILABLE, "初始化失败"))
|
||||
.when(dimensionRuleInitialization).initialize(any(Long.class), any(Long.class));
|
||||
|
||||
@@ -491,12 +492,26 @@ class TenantServiceTest {
|
||||
assertEquals(true, withQualification.eligible());
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeCustomerCannotOrderUntilDefaultDimensionsAreReady() {
|
||||
IamTenant tenant = merchant(10001L, "ACTIVE");
|
||||
tenant.setDimensionInitializationStatus("FAILED");
|
||||
when(repository.findTenant(10001L)).thenReturn(tenant);
|
||||
|
||||
var result = service.orderEligibility(10001L);
|
||||
|
||||
assertEquals(false, result.eligible());
|
||||
assertEquals("Customer default dimensions are not ready", result.reason());
|
||||
verify(profileRepository, never()).findQualifications(10001L);
|
||||
}
|
||||
|
||||
private IamTenant merchant(long id, String status) {
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(id);
|
||||
tenant.setTenantCode("TEN" + id);
|
||||
tenant.setTenantType("MERCHANT");
|
||||
tenant.setStatus(status);
|
||||
tenant.setDimensionInitializationStatus("READY");
|
||||
tenant.setTenantName("Example Merchant");
|
||||
tenant.setLegalName("Example Merchant Ltd");
|
||||
tenant.setUnifiedSocialCreditCode("91330100EXAMPLE001");
|
||||
@@ -529,4 +544,3 @@ class TenantServiceTest {
|
||||
return review;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 1. 范围与生成方式
|
||||
|
||||
本基线只分析 `lastleg-service` 同一部署单元,不拆微服务,不修改业务代码、POM、数据库、OpenAPI、权限、接口或跨服务契约。统计时工作区同时存在 RestClient 超时专项的在途修改;本专项不修改 `client/`、`config/*RestClient*` 或各业务 HTTP Gateway。
|
||||
本基线只分析 `lastleg-service` 同一部署单元,不拆微服务,不修改业务流程、数据库、OpenAPI、权限、接口或跨服务契约。统计时工作区同时存在其他专项的在途修改;本专项不修改 `client/`、`config/*RestClient*` 或各业务 HTTP Gateway。模块内 POM 只增加由根 POM 统一版本管理的 ArchUnit 测试依赖。
|
||||
|
||||
可重复命令:
|
||||
|
||||
@@ -16,15 +16,15 @@ node lastleg-service/scripts/analyze-lastleg-dependencies.mjs --source-set all -
|
||||
node lastleg-service/scripts/analyze-lastleg-dependencies.mjs --source-set main --format dot --graph slice
|
||||
```
|
||||
|
||||
分析器解析显式内部 `import`、静态 `import` 及同包类型引用,输出类依赖、物理包图、逻辑切片图、Tarjan SCC、Entity/DTO/Model 泄漏与 Mapper 泄漏。`test` 单独输出测试包节点及测试类之间的边;当前测试主要引用生产类型,因此 test-only 图有 67 个包节点但没有测试类之间的内部边。`all` 把 `src/main/java` 和 `src/test/java` 合并解析,是观察测试对生产类型耦合的主图。Spring 扫描字符串、反射、MyBatis SQL 表引用和跨服务运行时调用不属于 Java 类型图。
|
||||
分析器解析显式内部 `import`、静态 `import` 及同包类型引用,输出类依赖、物理包图、逻辑切片图、Tarjan SCC、Entity/DTO/Model 泄漏与 Mapper 泄漏。`test` 单独输出测试包节点及测试类之间的边;当前测试主要引用生产类型,因此 test-only 图有 68 个包节点但没有测试类之间的内部边。`all` 把 `src/main/java` 和 `src/test/java` 合并解析,是观察测试对生产类型耦合的主图。Spring 扫描字符串、反射、MyBatis SQL 表引用和跨服务运行时调用不属于 Java 类型图。
|
||||
|
||||
## 2. 当前规模与 SCC
|
||||
|
||||
| 源集 | Java 文件 | 内部类依赖 | 物理包 | 跨切片类依赖 | 模型泄漏 | Mapper 泄漏 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| main | 347 | 672 | 157 | 137 | 60 | 38 |
|
||||
| test only | 98 | 0 | 67 | 0 | 0 | 0 |
|
||||
| main + test | 445 | 1,195 | 161 | 256 | 108 | 76 |
|
||||
| main | 353 | 679 | 160 | 138 | 60 | 38 |
|
||||
| test only | 103 | 0 | 68 | 0 | 0 | 0 |
|
||||
| main + test | 456 | 1,206 | 164 | 257 | 108 | 76 |
|
||||
|
||||
主源码物理包图没有多节点 SCC。主源码逻辑切片图存在一个四节点 SCC:
|
||||
|
||||
@@ -57,7 +57,8 @@ com.baiyue.lastleg.transfer.service
|
||||
| 换标/转运/废弃 `relabel-transfer-discard` | `relabel/`、`transfer/`、`discard/`,其 PDA 子包保持本域归属 | 61 | 31/0 | 直接使用尾程、库存和一件代发持久化模型;业务事务边界复杂 |
|
||||
| 一件代发 `dropship` | `dropship/` | 12 | 4/11 | 与尾程消费及废弃库存双向耦合,位于主 SCC |
|
||||
| 采购/卡车/散板 `procurement-trucking-looseboard` | `procurement/`、`trucking/`、`looseboard/` | 35 | 11/0 | 依赖尾程核心,采购另读取报价目录;仍由四期业务专项维护 |
|
||||
| 海外主数据 `overseas-master-data` | `masterdata/quote`、`exchange`、`vehicle` | 26 | 3/1 | 不在任何 SCC;仅出向共享权限常量,入向来自采购对报价目录的使用 |
|
||||
| 海外主数据 `overseas-master-data` | `masterdata/quote`、`exchange`、`vehicle` | 28 | 3/1 | 不在任何 SCC;仅出向共享权限常量,入向来自采购对报价目录的使用 |
|
||||
| 海外安全投影 `security-projection` | `security/controller`、`security/dto`、`security/service` | 6 | 1/0 | 独立于业务切片与主 SCC;唯一出向为内部 Controller 复用根 `InternalServiceTokenGuard`,无业务切片入向依赖 |
|
||||
|
||||
另有 6 个 `shared-technical` 类:应用启动类、Event Bridge Client、RestClient 配置、内部令牌保护和海外权限常量。该分类不授权移动这些共享类。
|
||||
|
||||
@@ -86,12 +87,13 @@ com.baiyue.lastleg.transfer.service
|
||||
| relabel-transfer-discard | inventory | 6 |
|
||||
| relabel-transfer-discard | shared-technical | 3 |
|
||||
| relabel-transfer-discard | tailleg-handoff | 14 |
|
||||
| security-projection | shared-technical | 1 |
|
||||
| tailleg-handoff | dropship | 2 |
|
||||
| tailleg-handoff | inbound-pda | 6 |
|
||||
| tailleg-handoff | inventory | 2 |
|
||||
| tailleg-handoff | shared-technical | 2 |
|
||||
|
||||
当前 25 条切片边冻结在 `src/test/resources/architecture/lastleg-slice-dependencies.json`。合同允许删除历史边,不允许增加新边;海外主数据继续锁定为只允许出向 `shared-technical`,只允许采购/卡车/散板切片入向。
|
||||
当前 26 条切片边冻结在 `src/test/resources/architecture/lastleg-slice-dependencies.json`。合同允许删除历史边,不允许增加新边;海外主数据继续锁定为只允许出向 `shared-technical`,只允许采购/卡车/散板切片入向;安全投影只允许出向 `shared-technical` 且禁止任何业务切片入向依赖。
|
||||
|
||||
## 5. Entity/DTO/Mapper 泄漏基线
|
||||
|
||||
@@ -175,8 +177,9 @@ Web Adapter 可继续依赖 Spring Web、common-security 和 `OverseasPermission
|
||||
## 9. 当前只读合同
|
||||
|
||||
- `scripts/analyze-lastleg-dependencies.mjs`:生成 JSON、Markdown、逻辑切片 DOT、main/test/all 物理包 DOT 和 Tarjan SCC。
|
||||
- `src/test/resources/architecture/lastleg-slice-dependencies.json`:冻结 25 条 main 切片边;历史边可减少,不可增加。
|
||||
- `scripts/analyze-lastleg-dependencies.test.mjs`:校验基线、main/test/all 包图输出,以及车辆候选的 5 类、零入向、唯一权限出向、零 Client/RestClient 和不进入 SCC。
|
||||
- `src/test/resources/architecture/lastleg-slice-dependencies.json`:冻结 26 条 main 切片边;历史边可减少,不可增加,并锁定安全投影 6 个类的位置、单一出向与零入向。
|
||||
- `scripts/analyze-lastleg-dependencies.test.mjs`:校验基线、main/test/all 包图输出、车辆切片边界及安全投影隔离。
|
||||
- `src/test/java/com/baiyue/lastleg/security/SecurityProjectionArchitectureTest.java`:用 ArchUnit 锁定投影内部只依赖本子域和根安全技术类、业务包不得反向依赖投影、DTO/Service 不依赖内部 Web Adapter。
|
||||
|
||||
最小验证:
|
||||
|
||||
@@ -197,3 +200,11 @@ node lastleg-service/scripts/analyze-lastleg-dependencies.mjs --source-set all -
|
||||
- 验证结果:Node 合同 4/4;`mvn -pl lastleg-service -am test-compile` 成功;车辆定向测试 5/5;`mvn -pl lastleg-service -am test` 中 lastleg-service 370 个测试,0 失败、0 错误、2 个环境验收跳过,构建成功。
|
||||
|
||||
首切片完成后停止,不进入第二切片。
|
||||
|
||||
## 11. 海外安全投影合入后的边界
|
||||
|
||||
- 用户确认安全投影保留在 `lastleg-service` 部署单元内,不拆新微服务;逻辑上独立为 `security-projection` 切片。
|
||||
- Controller 仅映射 `/internal/lastleg/security/v1/events` 与 `/validate`,使用 `@Hidden` 并在进入投影或校验服务前调用根 `InternalServiceTokenGuard`。
|
||||
- MySQL 表固定使用 Lastleg 前缀 `ll_security_event_inbox`、`ll_security_projection`;没有 `overseas_security_*` 表或独立数据库归属。
|
||||
- `loading`、`shipping`、`inboundplan` 及其他业务切片不得直接依赖 `security/controller|dto|service`;当前 Java 类型图零入向,Node 与 ArchUnit 双重锁定。
|
||||
- 安全投影不进入主逻辑 SCC,唯一出向为内部 Controller 复用共享令牌守卫,不注册为 loading/shipping/inbound 的模型、Mapper 或应用服务。
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ node scripts/create-pending-review-orders.mjs
|
||||
| Admin/Client 本地测试与类型检查 | `scripts/frontend-full-regression.ps1` |
|
||||
| PDA 本地页面和状态测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
|
||||
| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` |
|
||||
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、海外 Gateway 覆盖、PDA 注册页面、Maven 模块、海外安全服务部署映射、关键入口与排除目录 |
|
||||
| Java 质量聚合摘要 | `node --test scripts/java-quality-summary.test.mjs`、`node scripts/java-quality-summary.mjs --output target/java-quality-summary.json`;聚合 Maven/服务拓扑、OpenAPI 覆盖、JaCoCo 和 SpotBugs 报告,默认只报告存量缺口,CI 仅用显式 `--require-spotbugs` 检查报告完整性且当前允许失败 |
|
||||
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、海外 Gateway 覆盖、PDA 注册页面、Maven 模块、Lastleg 海外安全投影映射、关键入口与排除目录 |
|
||||
| Java 质量聚合摘要 | `node --test scripts/java-quality-summary.test.mjs`、`node scripts/java-quality-summary.mjs --output target/java-quality-summary.json`;聚合 Maven/服务拓扑、OpenAPI 覆盖、JaCoCo 和 SpotBugs 报告,默认只报告存量缺口,CI 用显式 `--require-jacoco --require-spotbugs` 检查报告完整性且当前允许失败 |
|
||||
| 单服务部署失败回滚合同 | `scripts/deploy-built-service-rollback.test.mjs`;模拟成功、健康失败和启动失败,确认仅恢复同服务先前健康镜像且部署任务仍失败 |
|
||||
|
||||
Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块边界,不访问测试环境:
|
||||
|
||||
@@ -130,6 +130,19 @@ test('every reviewed synchronous client has a complete and exclusive executable
|
||||
}
|
||||
})
|
||||
|
||||
test('IAM customer dimension Feign is reachable only from the background relay', async () => {
|
||||
const references = []
|
||||
for (const file of await javaFiles(path.join(root, 'iam', 'src', 'main', 'java'))) {
|
||||
const source = await readFile(file, 'utf8')
|
||||
if (source.includes('CustomerDimensionRuleClient')) {
|
||||
references.push(path.relative(root, file).replaceAll('\\', '/'))
|
||||
}
|
||||
}
|
||||
assert.deepEqual(references.sort(), [
|
||||
'iam/src/main/java/com/baiyue/iam/client/CustomerDimensionRuleClient.java',
|
||||
'iam/src/main/java/com/baiyue/iam/service/CustomerDimensionInitializationRelay.java'
|
||||
])
|
||||
})
|
||||
|
||||
test('all four Lastleg RestClient gateways use explicit bounded transports', async () => {
|
||||
const clients = new Map([
|
||||
@@ -242,4 +255,3 @@ test('shared Feign timeout defaults and long-running export overrides do not dri
|
||||
const fileConfig = await readFile(path.join(root, 'file', 'src', 'main', 'resources', 'application.yml'), 'utf8')
|
||||
assert.equal((fileConfig.match(/read-timeout:\s*15000/g) ?? []).length, 5)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user