Test #3
@@ -43,12 +43,11 @@ class FinanceContractTest {
|
||||
assertFalse(operations.contains("bl.source_id=f.source_fee_id"));
|
||||
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertTrue(unified.contains("CREATE DATABASE IF NOT EXISTS baiyue_finance"));
|
||||
assertTrue(unified.contains("assert_finance_permissions"));
|
||||
assertTrue(unified.contains("merchant:finance:statement:view"));
|
||||
int walletEventSchema = unified.indexOf("CREATE TABLE IF NOT EXISTS fin_wallet_event_outbox");
|
||||
int finalPass = unified.lastIndexOf("SELECT 'PASS' AS migration_status");
|
||||
int walletEventSchema = unified.indexOf("CREATE TABLE fin_wallet_event_outbox");
|
||||
int finalPass = unified.lastIndexOf("SELECT 'PASS' AS initialization_status");
|
||||
assertTrue(walletEventSchema >= 0);
|
||||
assertTrue(finalPass > walletEventSchema);
|
||||
assertTrue(unified.contains("CREATE PROCEDURE assert_wallet_event_schema()"));
|
||||
assertFalse(unified.contains("CREATE PROCEDURE assert_wallet_event_schema()"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,13 @@ class FinanceSettlementSchemaContractTest {
|
||||
.contains("idx_fin_settlement_customer_direction_status (tenant_id, direction, status)")
|
||||
.contains("total_amount DECIMAL(20,6)", "paid_amount DECIMAL(20,6)", "outstanding_amount DECIMAL(20,6)")
|
||||
.contains("updated_at DATETIME(3)");
|
||||
assertThat(unified)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fin_settlement (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fin_settlement_line (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fin_settlement_payment (")
|
||||
.contains("idx_fin_settlement_customer_direction_status (tenant_id, direction, status)")
|
||||
.contains("'fin_settlement', 'fin_settlement_line', 'fin_settlement_payment')) <> 16");
|
||||
String normalized = unified.toLowerCase().replaceAll("\\s+", "");
|
||||
assertThat(normalized)
|
||||
.contains("createtablefin_settlement(")
|
||||
.contains("createtablefin_settlement_line(")
|
||||
.contains("createtablefin_settlement_payment(")
|
||||
.contains("idx_fin_settlement_customer_direction_status(tenant_id,direction,status)");
|
||||
assertThat(unified).doesNotContain("ALTER TABLE fin_settlement", "assert_finance");
|
||||
}
|
||||
|
||||
private static Path resolveUnifiedSql() {
|
||||
|
||||
@@ -62,16 +62,14 @@ class FinanceSqlCommentContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationAddsBillLineAdjustmentReasonForNewAndExistingSchemas() throws Exception {
|
||||
void unifiedInitializationDefinesBillLineAdjustmentReasonDirectly() throws Exception {
|
||||
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
Path unified = Files.exists(direct) ? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
String sql = Files.readString(unified, StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("adjustment_reason VARCHAR(500) NULL COMMENT '最近一次账单调整行原因;完整历史保存在修订与审计记录'")
|
||||
.contains("table_schema='baiyue_finance' AND table_name='fin_bill_line' AND column_name='adjustment_reason'")
|
||||
.contains("ALTER TABLE baiyue_finance.fin_bill_line ADD COLUMN adjustment_reason")
|
||||
.contains("queue_column_comment('baiyue_finance', 'fin_bill_line', 'adjustment_reason'");
|
||||
.contains("adjustment_reason varchar(500) DEFAULT NULL COMMENT '最近一次账单调整行原因;完整历史保存在修订与审计记录'")
|
||||
.doesNotContain("ALTER TABLE baiyue_finance.fin_bill_line", "queue_column_comment");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,12 +95,17 @@ class FinanceSqlCommentContractTest {
|
||||
assertThat(baseline)
|
||||
.contains("updated_by_name VARCHAR(128) NULL COMMENT '最后修改操作人名称快照'")
|
||||
.contains("created_by_name VARCHAR(128) NULL COMMENT '登记操作人名称快照'");
|
||||
assertThat(unified)
|
||||
.contains("CALL finance_add_operator_snapshot_column('fin_bill', 'created_by_name', 'created_by')")
|
||||
.contains("CALL finance_add_operator_snapshot_column('fin_bill', 'updated_by_name', 'updated_by')")
|
||||
.contains("CALL finance_add_operator_snapshot_column('fin_receipt', 'created_by_name', 'created_by')")
|
||||
.contains("CALL finance_add_operator_snapshot_column('fin_cost', 'created_by_name', 'created_by')")
|
||||
.contains("CALL finance_add_operator_snapshot_column('fin_settlement_payment', 'created_by_name', 'created_by')")
|
||||
.contains("CALL finance_add_operator_snapshot_column('fin_bill_revision', 'created_by_name', 'created_by')");
|
||||
assertThat(tableBlock(unified, "fin_bill")).contains("created_by_name", "updated_by_name");
|
||||
assertThat(tableBlock(unified, "fin_receipt")).contains("created_by_name");
|
||||
assertThat(tableBlock(unified, "fin_cost")).contains("created_by_name");
|
||||
assertThat(tableBlock(unified, "fin_settlement_payment")).contains("created_by_name");
|
||||
assertThat(tableBlock(unified, "fin_bill_revision")).contains("created_by_name");
|
||||
assertThat(unified).doesNotContain("finance_add_operator_snapshot_column");
|
||||
}
|
||||
|
||||
private static String tableBlock(String sql, String table) {
|
||||
int start = sql.indexOf("CREATE TABLE " + table + " (");
|
||||
int end = sql.indexOf(';', start);
|
||||
return start < 0 || end < 0 ? "" : sql.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,14 @@ class FinanceSubjectSeedContractTest {
|
||||
String module = Files.readString(Path.of("src/main/resources/db/finance.sql"), StandardCharsets.UTF_8);
|
||||
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"), StandardCharsets.UTF_8);
|
||||
|
||||
for (String sql : new String[] {module, unified}) {
|
||||
assertThat(sql)
|
||||
.contains("INSERT INTO fin_subject")
|
||||
.contains("('PLATFORM_DEFAULT', '平台', 'ACTIVE', 0, 0, 0)")
|
||||
.contains("ON DUPLICATE KEY UPDATE")
|
||||
.contains("status = 'ACTIVE'");
|
||||
}
|
||||
assertThat(module)
|
||||
.contains("INSERT INTO fin_subject")
|
||||
.contains("('PLATFORM_DEFAULT', '平台', 'ACTIVE', 0, 0, 0)")
|
||||
.contains("ON DUPLICATE KEY UPDATE")
|
||||
.contains("status = 'ACTIVE'");
|
||||
assertThat(unified)
|
||||
.contains("INSERT INTO fin_subject (id, subject_code, subject_name, status")
|
||||
.contains("VALUES (1,'PLATFORM_DEFAULT','平台','ACTIVE',0,0,0")
|
||||
.doesNotContain("ON DUPLICATE KEY UPDATE");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,16 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FinanceWalletContractTest {
|
||||
@Test
|
||||
void unifiedMigrationDropsTheLegacyBalanceConstraintBeforeRewritingBalances() throws Exception {
|
||||
void unifiedInitializationDefinesTheFinalWalletBalanceConstraintWithoutRewritingData() throws Exception {
|
||||
Path migration = Path.of("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
if (!Files.exists(migration)) migration = Path.of("..").resolve(migration);
|
||||
String sql = Files.readString(migration, StandardCharsets.UTF_8);
|
||||
int procedure = sql.indexOf("CREATE PROCEDURE migrate_fin_wallet_credit()");
|
||||
int dropConstraint = sql.indexOf("ALTER TABLE fin_wallet_account DROP CHECK chk_fin_wallet_account_balance", procedure);
|
||||
int rewriteBalance = sql.indexOf("SET actual_balance=actual_balance-credit_used", procedure);
|
||||
|
||||
assertThat(procedure).isGreaterThanOrEqualTo(0);
|
||||
assertThat(sql.substring(procedure, rewriteBalance))
|
||||
.contains("CHECK_CLAUSE LIKE '%actual_balance%'", "v_legacy_wallet_constraint OR v_credit_expiry_missing");
|
||||
assertThat(dropConstraint).isBetween(procedure, rewriteBalance - 1);
|
||||
String account = tableBlock(sql, "fin_wallet_account");
|
||||
assertThat(account).contains(
|
||||
"credit_expires_on date DEFAULT NULL",
|
||||
"CONSTRAINT chk_fin_wallet_account_balance CHECK",
|
||||
"frozen_balance >= credit_frozen");
|
||||
assertThat(sql).doesNotContain("migrate_fin_wallet_credit", "SET actual_balance=actual_balance-credit_used");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,4 +66,10 @@ class FinanceWalletContractTest {
|
||||
.contains("/holds/{businessType}/{businessNo}/release")
|
||||
.contains("Idempotency-Key");
|
||||
}
|
||||
|
||||
private static String tableBlock(String sql, String table) {
|
||||
int start = sql.indexOf("CREATE TABLE " + table + " (");
|
||||
int end = sql.indexOf(';', start);
|
||||
return start < 0 || end < 0 ? "" : sql.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ class PdaConvergenceOpenApiContractTest {
|
||||
.contains("loadingPlanId:", "containerBookingId:")
|
||||
.doesNotContain("containerNo:");
|
||||
assertThat(batchItemRequest)
|
||||
.contains("PICK_PALLET", "palletNo:", "every pickItem", "No carton quantities")
|
||||
.contains("PICK_PALLET", "palletNo:", "every SKU line", "No carton quantities")
|
||||
.doesNotContain("releaseMode:", "reservationIds:", "locationIds:");
|
||||
String pickItem = yaml.substring(yaml.indexOf(" PickItem:"), yaml.indexOf(" PendingTask:"));
|
||||
assertThat(pickItem).contains("unitType:", "palletId:", "palletNo:", "indivisible:", "SEALED_PALLET");
|
||||
|
||||
+2
-3
@@ -25,9 +25,8 @@ class TruckAppointmentControllerContractTest {
|
||||
Path direct=Path.of("deploy","sql","头程数据库统一增量-MySQL8.sql");
|
||||
Path unifiedPath=Files.exists(direct)?direct:Path.of("..","deploy","sql","头程数据库统一增量-MySQL8.sql");
|
||||
String unifiedSql=Files.readString(unifiedPath,StandardCharsets.UTF_8);
|
||||
for(String sql:new String[]{moduleSql,unifiedSql}) {
|
||||
assertThat(sql).contains("'bill_of_lading_no'","'actual_arrived_warehouse_at'","'actual_loading_at'","'actual_departed_at'");
|
||||
}
|
||||
assertThat(moduleSql).contains("'bill_of_lading_no'","'actual_arrived_warehouse_at'","'actual_loading_at'","'actual_departed_at'");
|
||||
assertThat(unifiedSql).contains("bill_of_lading_no varchar(128)","actual_arrived_warehouse_at datetime(3)","actual_loading_at datetime(3)","actual_departed_at datetime(3)");
|
||||
}
|
||||
@Test void openApiDocumentsLifecycleIdempotencyAndActualTimes() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("/api/platform/v1/truck-appointments/{id}/{action}:","/api/platform/v1/truck-appointments/{id}/reschedule:","name: Idempotency-Key","required: true","actual event timestamps");}
|
||||
@Test void openApiDocumentsActionableDuplicateCreateConflict() throws Exception {String s=new ClassPathResource("openapi/truck-appointments-v1.yaml").getContentAsString(StandardCharsets.UTF_8);assertThat(s).contains("A0409","existing appointment number","continue editing","database booking-level unique key");}
|
||||
|
||||
+11
-6
@@ -17,17 +17,22 @@ class ColoadGroupSqlContractTest {
|
||||
String baseline = Files.readString(Path.of("src/main/resources/db/phase3-coload-group-schema.sql"));
|
||||
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
for (String source : new String[]{baseline, unified}) {
|
||||
String normalized = source
|
||||
.replace("CREATE TABLE IF NOT EXISTS ", "CREATE TABLE ")
|
||||
.replace("CREATE TABLE ", "CREATE TABLE IF NOT EXISTS ");
|
||||
assertThat(source)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_coload_group (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_coload_group_member (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_coload_group_history (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_coload_command (")
|
||||
.contains("ACTIVE','LOCKED','CONSUMED','CANCELLED")
|
||||
.contains("ACTIVE", "LOCKED", "CONSUMED", "CANCELLED")
|
||||
.contains("chk_coload_group_status")
|
||||
.contains("uk_coload_active_inbound")
|
||||
.contains("full_package_count")
|
||||
.contains("uk_coload_command")
|
||||
.contains("locked_plan_id");
|
||||
assertThat(tableDefinition(source, "fl_coload_group"))
|
||||
assertThat(normalized)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_coload_group (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_coload_group_member (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_coload_group_history (")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_coload_command (");
|
||||
assertThat(tableDefinition(normalized, "fl_coload_group"))
|
||||
.doesNotContain("chk_coload_group_destination")
|
||||
.doesNotContain("destination_type")
|
||||
.doesNotContain("destination_warehouse_id")
|
||||
|
||||
+4
-3
@@ -30,11 +30,12 @@ class LoadingEvidenceSqlContractTest {
|
||||
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
Path unified = Files.exists(direct) ? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
String sql = Files.readString(unified, StandardCharsets.UTF_8);
|
||||
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence")
|
||||
assertThat(sql).contains("CREATE TABLE fl_loading_cabinet_evidence")
|
||||
.contains("uk_cabinet_evidence_active")
|
||||
.contains("uk_cabinet_evidence_operation")
|
||||
.contains("normalize_loading_cabinet_evidence_collation")
|
||||
.doesNotContain("CREATE TABLE IF NOT EXISTS fl_loading_evidence")
|
||||
.contains("FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking (id)")
|
||||
.doesNotContain("CREATE TABLE fl_loading_evidence")
|
||||
.doesNotContain("normalize_loading_cabinet_evidence_collation")
|
||||
.doesNotContain("phase3_loading_evidence_add_column");
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -38,8 +38,13 @@ class PdaConvergenceSqlContractTest {
|
||||
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertThat(createBlock(unified, "fl_loading_pick_item_assignment"))
|
||||
.isEqualTo(createBlock(sql, "fl_loading_pick_item_assignment"));
|
||||
assertThat(unified)
|
||||
.contains("CREATE TABLE fl_loading_pick_item_assignment")
|
||||
.contains("assignment_type varchar(16) NOT NULL")
|
||||
.contains("PRIMARY KEY (pick_item_id)")
|
||||
.contains("KEY idx_loading_pick_item_assignee (assignee_id,updated_at)")
|
||||
.contains("CONSTRAINT fk_loading_pick_item_assignment_item")
|
||||
.contains("platform:firstleg:pda:assign");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+25
-46
@@ -108,49 +108,28 @@ class CarrierScheduleQuerySqlContractTest {
|
||||
@Test
|
||||
void unifiedDeploymentEntryContainsTheSameContract() throws Exception {
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
StandardCharsets.UTF_8).toLowerCase();
|
||||
assertThat(unified)
|
||||
.contains("fl_carrier_schedule_query")
|
||||
.contains("create table fl_carrier_schedule_query")
|
||||
.contains("uk_carrier_schedule_query_scope")
|
||||
.contains("id IN (5102184,5102186,5102188)")
|
||||
.contains("assert_carrier_schedule_query_schema")
|
||||
.contains("CREATE PROCEDURE migrate_carrier_schedule_retry_columns()")
|
||||
.contains("ADD COLUMN trigger_source VARCHAR(32) NOT NULL DEFAULT 'MANUAL'")
|
||||
.contains("ADD COLUMN latest_standard_node VARCHAR(32) NULL")
|
||||
.contains("ADD COLUMN latest_node_observed_at DATETIME(3) NULL")
|
||||
.contains("ADD COLUMN stopped_at DATETIME(3) NULL")
|
||||
.contains("ADD COLUMN stop_reason VARCHAR(500) NULL")
|
||||
.contains("ADD COLUMN consecutive_failures INT UNSIGNED NOT NULL DEFAULT 0")
|
||||
.contains("ADD COLUMN window_started_at DATETIME(3) NULL")
|
||||
.contains("ADD COLUMN attempt_count INT UNSIGNED NOT NULL DEFAULT 0")
|
||||
.contains("ADD COLUMN next_allowed_at DATETIME(3) NULL")
|
||||
.contains("ADD KEY idx_carrier_schedule_query_due (status,next_allowed_at,id)")
|
||||
.contains("DROP CHECK chk_carrier_schedule_query_access")
|
||||
.contains("DROP CHECK chk_carrier_schedule_query_carrier")
|
||||
.contains("DROP CHECK chk_carrier_schedule_query_status")
|
||||
.contains("DROP INDEX uk_carrier_schedule_query_scope")
|
||||
.contains("ADD UNIQUE KEY uk_carrier_schedule_query_scope")
|
||||
.contains("trigger_source varchar(32) not null default 'manual'")
|
||||
.contains("latest_standard_node varchar(32) default null")
|
||||
.contains("latest_node_observed_at datetime(3) default null")
|
||||
.contains("stopped_at datetime(3) default null")
|
||||
.contains("stop_reason varchar(500) default null")
|
||||
.contains("consecutive_failures int unsigned not null default '0'")
|
||||
.contains("window_started_at datetime(3) not null")
|
||||
.contains("attempt_count int unsigned not null default '0'")
|
||||
.contains("next_allowed_at datetime(3) default null")
|
||||
.contains("key idx_carrier_schedule_query_due (status,next_allowed_at,id)")
|
||||
.contains("unique key uk_carrier_schedule_query_scope")
|
||||
.contains("(customer_id,ocean_booking_id,container_scope_id)")
|
||||
.contains("'PUBLIC_FORM','PUBLIC_WEB_PROXY','PUBLIC_HTML','HCAPTCHA','OFFICIAL_API'")
|
||||
.contains("'MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'")
|
||||
.contains("'OFFICIAL_API','MANUAL'")
|
||||
.contains("status IN ('READY','MANUAL_REQUIRED','STOPPED')")
|
||||
.contains("ARRIVED_DESTINATION_PORT")
|
||||
.contains("EMPTY_RETURNED")
|
||||
.contains("SET @carrier_schedule_now = CONVERT_TZ(UTC_TIMESTAMP(3),'+00:00','+08:00')")
|
||||
.contains("SET @carrier_schedule_next = CASE")
|
||||
.contains("CREATE PROCEDURE ensure_carrier_schedule_retry_check()")
|
||||
.contains("CREATE PROCEDURE migrate_carrier_schedule_history_action_check()")
|
||||
.contains("DROP CHECK chk_carrier_schedule_history_action")
|
||||
.contains("'MANUAL_NODE','NODE_OBSERVED'")
|
||||
.contains("'EXCEPTION_OBSERVED','PROVIDER_QUERY','STOP'")
|
||||
.contains("ADD CONSTRAINT chk_carrier_schedule_query_attempt CHECK (attempt_count BETWEEN 0 AND 3)")
|
||||
.contains("统一迁移失败:船司查询固定槽位字段、索引或尝试次数约束不完整")
|
||||
.doesNotContain("DROP CHECK chk_carrier_schedule_query_attempt")
|
||||
.doesNotContain("\n+--")
|
||||
.contains("'PUBLIC_FORM','PUBLIC_WEB_PROXY','PUBLIC_HTML','HCAPTCHA','OFFICIAL_API'")
|
||||
.contains("船司查询任务角色授权未完整创建")
|
||||
.doesNotContain("'platform:firstleg:port-schedule-query:attempt','执行船司查询'");
|
||||
.contains("chk_carrier_schedule_query_attempt check ((attempt_count between 0 and 3))")
|
||||
.contains("arrived_destination_port")
|
||||
.contains("empty_returned")
|
||||
.contains("platform:firstleg:port-schedule-query:manual")
|
||||
.doesNotContain("migrate_carrier_schedule_retry_columns")
|
||||
.doesNotContain("drop check chk_carrier_schedule_query_attempt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -158,13 +137,13 @@ class CarrierScheduleQuerySqlContractTest {
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertThat(unified)
|
||||
.contains("CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_key'")
|
||||
.contains("CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_version'")
|
||||
.contains("CALL firstleg_finance_add_column('fl_inbound_outbox', 'aggregate_sequence'")
|
||||
.contains("CALL firstleg_finance_add_column('fl_inbound_outbox', 'payload_hash'")
|
||||
.contains("event_key varchar(160) DEFAULT NULL")
|
||||
.contains("event_version int unsigned NOT NULL DEFAULT '1'")
|
||||
.contains("aggregate_sequence int unsigned DEFAULT NULL")
|
||||
.contains("payload_hash varchar(71) DEFAULT NULL")
|
||||
.contains("uk_inbound_outbox_event_key")
|
||||
.contains("MODIFY COLUMN payload_hash VARCHAR(71)")
|
||||
.contains("MODIFY COLUMN payload_json LONGTEXT NOT NULL");
|
||||
.contains("payload_json longtext NOT NULL")
|
||||
.doesNotContain("firstleg_finance_add_column");
|
||||
}
|
||||
|
||||
private static String moduleSql() throws Exception {
|
||||
|
||||
+10
-10
@@ -108,16 +108,16 @@ class ContainerBookingMapperContractTest {
|
||||
@Test
|
||||
@DisplayName("初始化与统一增量锁定真实柜号权威、唯一和ISO结构约束")
|
||||
void databaseContractsFreezeAuthoritativeContainerNumber() throws Exception {
|
||||
for (String source : new String[] {
|
||||
Files.readString(Path.of("src/main/resources/db/phase3-smart-loading-schema.sql")),
|
||||
Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"))
|
||||
}) {
|
||||
assertThat(source)
|
||||
.contains("国内唯一权威真实集装箱号")
|
||||
.contains("提柜后可原子补录")
|
||||
.contains("UNIQUE KEY uk_container_booking_container_no (container_no)")
|
||||
.contains("CONSTRAINT chk_container_booking_iso6346")
|
||||
.contains("container_no REGEXP '^[A-Z]{3}[UJZ][0-9]{7}$'");
|
||||
String module = Files.readString(Path.of("src/main/resources/db/phase3-smart-loading-schema.sql"));
|
||||
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertThat(module.toLowerCase())
|
||||
.contains("国内唯一权威真实集装箱号")
|
||||
.contains("提柜后可原子补录");
|
||||
for (String source : new String[] {module, unified}) {
|
||||
assertThat(source.toLowerCase())
|
||||
.contains("unique key uk_container_booking_container_no (container_no)")
|
||||
.contains("constraint chk_container_booking_iso6346")
|
||||
.contains("^[a-z]{3}[ujz][0-9]{7}$");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-55
@@ -28,21 +28,23 @@ class CustomerDimensionConfigSqlContractTest {
|
||||
);
|
||||
|
||||
for (String sql : sqlEntrypoints) {
|
||||
assertThat(sql)
|
||||
String normalized = sql.toLowerCase().replace(", ", ",");
|
||||
assertThat(normalized)
|
||||
.contains("fl_customer_dimension_config")
|
||||
.contains("box_type VARCHAR(24) NOT NULL")
|
||||
.contains("inner_length_cm DECIMAL(9,3) NOT NULL")
|
||||
.contains("inner_width_cm DECIMAL(9,3) NOT NULL")
|
||||
.contains("inner_height_cm DECIMAL(9,3) NOT NULL")
|
||||
.contains("edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000")
|
||||
.contains("default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ACTUAL'")
|
||||
.contains("initialization_key VARCHAR(128) NOT NULL")
|
||||
.contains("created_by BIGINT UNSIGNED NOT NULL")
|
||||
.contains("updated_by BIGINT UNSIGNED NOT NULL")
|
||||
.contains("version INT NOT NULL DEFAULT 0")
|
||||
.contains("UNIQUE KEY uk_customer_dimension_config_tenant_box (tenant_id, box_type)")
|
||||
.contains("CHECK (box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE'))")
|
||||
.doesNotContain("UNIQUE KEY uk_customer_dimension_config_tenant (tenant_id)");
|
||||
.contains("box_type varchar(24) not null")
|
||||
.contains("inner_length_cm decimal(9,3) not null")
|
||||
.contains("inner_width_cm decimal(9,3) not null")
|
||||
.contains("inner_height_cm decimal(9,3) not null")
|
||||
.contains("edge_addition_cm decimal(6,3) not null default")
|
||||
.contains("default_calculation_mode varchar(16)")
|
||||
.contains("not null default 'actual'")
|
||||
.contains("initialization_key varchar(128) not null")
|
||||
.contains("created_by bigint unsigned not null")
|
||||
.contains("updated_by bigint unsigned not null")
|
||||
.contains("version int not null default")
|
||||
.contains("unique key uk_customer_dimension_config_tenant_box (tenant_id,box_type)")
|
||||
.contains("'small'", "'medium'", "'large'", "'extra_large'")
|
||||
.doesNotContain("unique key uk_customer_dimension_config_tenant (tenant_id)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,21 +106,19 @@ class CustomerDimensionConfigSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationKeepsPermissionGuardsAndAssertsFinalFourRowSchema() throws Exception {
|
||||
void unifiedInitializationKeepsPermissionsAndFinalFourRowSchema() throws Exception {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE PROCEDURE unified_guard_dimension_config_permissions()")
|
||||
.contains("(5102111, 0, 'PLATFORM', 'BUTTON', 5100010")
|
||||
.contains("r.role_code = 'PLATFORM_ADMIN'")
|
||||
.contains("r.role_code = 'ORDER_OPERATOR'")
|
||||
.contains("index_name = 'uk_customer_dimension_config_tenant_box'")
|
||||
.contains("客户四箱规尺寸配置字段不完整")
|
||||
.contains("客户默认计费规则初始化幂等字段未创建")
|
||||
.contains("index_name = 'idx_charge_rule_initialization'")
|
||||
.contains("客户默认计费规则初始化索引未完整创建")
|
||||
.contains("客户尺寸计算配置权限码、固定ID或父节点不正确");
|
||||
.contains("CREATE TABLE fl_customer_dimension_config")
|
||||
.contains("UNIQUE KEY uk_customer_dimension_config_tenant_box")
|
||||
.contains("5102111,0,'PLATFORM','BUTTON',5100010")
|
||||
.contains("firstleg:dimension-config:view")
|
||||
.contains("firstleg:dimension-config:edit")
|
||||
.contains("VALUES (4000001,5102111,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000003,5102111,0,CURRENT_TIMESTAMP(3))")
|
||||
.doesNotContain("unified_guard_dimension_config_permissions");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -129,43 +129,21 @@ class CustomerDimensionConfigSqlContractTest {
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(schema).contains("calculation_rule_version VARCHAR(64) NOT NULL");
|
||||
assertThat(unified)
|
||||
.contains("calculation_rule_version VARCHAR(64) NOT NULL")
|
||||
.contains("MODIFY COLUMN calculation_rule_version VARCHAR(64) NOT NULL");
|
||||
assertThat(unified).contains("calculation_rule_version varchar(64) NOT NULL")
|
||||
.doesNotContain("MODIFY COLUMN calculation_rule_version");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationUpgradesOnlyRecognizedLegacyTableAndBackfillsIdempotently() throws Exception {
|
||||
void unifiedInitializationCreatesOnlyTheFinalDimensionTableWithoutBackfill() throws Exception {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
int upgrade = sql.indexOf("CREATE PROCEDURE unified_upgrade_legacy_customer_dimension_config()");
|
||||
int create = sql.indexOf("CREATE TABLE IF NOT EXISTS baiyue_firstleg.fl_customer_dimension_config");
|
||||
int backfill = sql.indexOf("INSERT INTO baiyue_firstleg.fl_customer_dimension_config (", create);
|
||||
assertThat(upgrade).isGreaterThanOrEqualTo(0);
|
||||
assertThat(create).isGreaterThan(upgrade);
|
||||
assertThat(backfill).isGreaterThan(create);
|
||||
assertThat(sql)
|
||||
.contains("AND column_name = 'box_type') = 0")
|
||||
.contains("AND column_name IN ('id', 'tenant_id', 'edge_addition_cm', 'remark', 'initialization_key'")
|
||||
.contains("AND table_name = 'fl_customer_dimension_config') <> 11")
|
||||
.contains("AND index_name = 'uk_customer_dimension_config_tenant'")
|
||||
.contains("DROP TABLE baiyue_firstleg.fl_customer_dimension_config")
|
||||
.contains("客户尺寸配置表不是可识别的旧单行结构")
|
||||
.contains("SELECT id AS tenant_id, created_by")
|
||||
.contains("FROM baiyue_iam.iam_tenant")
|
||||
.contains("WHERE tenant_type = 'MERCHANT' AND deleted = 0 AND id > 0")
|
||||
.contains("CONCAT('unified-four-box-default-v1:', tenant.tenant_id)")
|
||||
.contains("ON DUPLICATE KEY UPDATE id = id")
|
||||
.contains("现有企业客户未完整持有固定四箱规尺寸配置");
|
||||
|
||||
String repeatSafeClause = sql.substring(backfill,
|
||||
sql.indexOf("-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql", backfill));
|
||||
assertThat(repeatSafeClause)
|
||||
.doesNotContain("edge_addition_cm = VALUES")
|
||||
.doesNotContain("inner_length_cm = VALUES")
|
||||
.doesNotContain("remark = VALUES")
|
||||
.doesNotContain("version = VALUES");
|
||||
.contains("CREATE TABLE fl_customer_dimension_config")
|
||||
.contains("COMMENT='客户固定四箱规尺寸配置'")
|
||||
.doesNotContain("unified_upgrade_legacy_customer_dimension_config")
|
||||
.doesNotContain("DROP TABLE baiyue_firstleg.fl_customer_dimension_config")
|
||||
.doesNotContain("ON DUPLICATE KEY UPDATE");
|
||||
}
|
||||
|
||||
private static String resource(String path) throws Exception {
|
||||
|
||||
+21
-24
@@ -49,34 +49,31 @@ class DropshipPalletizationSqlContractTest {
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
|
||||
|
||||
for (String sql : new String[]{inbound, unified}) {
|
||||
assertThat(sql)
|
||||
.contains("counted_cartons INT UNSIGNED NOT NULL DEFAULT 0")
|
||||
.contains("discrepancy_cartons INT UNSIGNED NOT NULL DEFAULT 0")
|
||||
.contains("pallet_status VARCHAR(16) NOT NULL COMMENT '托盘状态:OPEN组托中、SEALED已封托'")
|
||||
.contains("expected_sku_quantity INT UNSIGNED NOT NULL")
|
||||
.contains("actual_sku_quantity INT UNSIGNED NOT NULL")
|
||||
.contains("count_operation_id VARCHAR(128) NOT NULL")
|
||||
.contains("bind_operation_id VARCHAR(128) NULL");
|
||||
assertThat(sql.toLowerCase())
|
||||
.contains("counted_cartons int unsigned not null default")
|
||||
.contains("discrepancy_cartons int unsigned not null default")
|
||||
.contains("pallet_status varchar(16) not null comment '托盘状态:open组托中、sealed已封托'")
|
||||
.contains("expected_sku_quantity int unsigned not null")
|
||||
.contains("actual_sku_quantity int unsigned not null")
|
||||
.contains("count_operation_id varchar(128) not null")
|
||||
.contains("bind_operation_id varchar(128)");
|
||||
}
|
||||
for (String sql : new String[]{loading, unified}) {
|
||||
assertThat(sql)
|
||||
.contains("unit_type VARCHAR(24) NOT NULL DEFAULT 'PACKAGE'")
|
||||
.contains("pallet_id BIGINT UNSIGNED NULL")
|
||||
.contains("indivisible TINYINT(1) NOT NULL DEFAULT 0")
|
||||
.contains("active_assignment_key VARCHAR(96) GENERATED ALWAYS AS")
|
||||
.contains("UNIQUE KEY uk_loading_item_plan_assignment (loading_plan_id, active_assignment_key)")
|
||||
.doesNotContain("CREATE TABLE IF NOT EXISTS fl_loading_work_pallet (")
|
||||
.doesNotContain("CREATE TABLE IF NOT EXISTS fl_loading_work_pallet_carton (");
|
||||
String normalized = sql.toLowerCase().replace(", ", ",");
|
||||
assertThat(normalized)
|
||||
.contains("unit_type varchar(24) not null default 'package'")
|
||||
.contains("pallet_id bigint unsigned")
|
||||
.contains("indivisible tinyint(1) not null default")
|
||||
.contains("active_assignment_key varchar(96) generated always as")
|
||||
.contains("unique key uk_loading_item_plan_assignment (loading_plan_id,active_assignment_key)")
|
||||
.doesNotContain("create table fl_loading_work_pallet (")
|
||||
.doesNotContain("create table fl_loading_work_pallet_carton (");
|
||||
}
|
||||
assertThat(unified)
|
||||
.contains("CREATE PROCEDURE converge_loading_assignment_schema()")
|
||||
.contains("CREATE PROCEDURE converge_dropship_palletization_schema()")
|
||||
.contains("ALTER TABLE fl_dropship_pallet DROP CHECK chk_dropship_pallet_status")
|
||||
.contains("ALTER TABLE fl_dropship_pallet DROP CHECK chk_dropship_pallet_status;\n END IF;\n UPDATE fl_dropship_pallet")
|
||||
.contains("CHECK (pallet_status IN ('OPEN','SEALED'))")
|
||||
.contains("CHANGE COLUMN operation_id count_operation_id")
|
||||
.contains("DROP INDEX uk_loading_item_plan_detail")
|
||||
.contains("ADD UNIQUE KEY uk_loading_item_plan_assignment (loading_plan_id,active_assignment_key)");
|
||||
.contains("CONSTRAINT chk_dropship_pallet_status CHECK")
|
||||
.contains("UNIQUE KEY uk_loading_item_plan_assignment")
|
||||
.doesNotContain("converge_loading_assignment_schema")
|
||||
.doesNotContain("converge_dropship_palletization_schema");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+9
-2
@@ -11,9 +11,13 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class FeeItemMigrationSqlContractTest {
|
||||
@Test
|
||||
void temporarySeedTableIsNotReopenedWithinOneMysqlStatement() throws Exception {
|
||||
assertMysqlCompatible(resolve("deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertMysqlCompatible(resolve(
|
||||
"firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql"));
|
||||
String unified = Files.readString(resolve("deploy/sql/头程数据库统一增量-MySQL8.sql"), StandardCharsets.UTF_8);
|
||||
assertThat(unified)
|
||||
.contains("193000000001000111,0,'头程运费','INTERNATIONAL','FBA_HEAD_FREIGHT'")
|
||||
.contains("193000000001000112,0,'尾程派送费','INTERNATIONAL','PRIVATE_LAST_MILE_DELIVERY'")
|
||||
.doesNotContain("tmp_fee_item_seed");
|
||||
}
|
||||
|
||||
private static void assertMysqlCompatible(Path path) throws Exception {
|
||||
@@ -35,9 +39,12 @@ class FeeItemMigrationSqlContractTest {
|
||||
|
||||
@Test
|
||||
void inboundFeeSourceIndexUsesTheFinalVersionedDefinition() throws Exception {
|
||||
assertFinalSourceIndex(resolve("deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertFinalSourceIndex(resolve(
|
||||
"firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql"));
|
||||
String unified = Files.readString(resolve("deploy/sql/头程数据库统一增量-MySQL8.sql"), StandardCharsets.UTF_8);
|
||||
assertThat(unified)
|
||||
.contains("UNIQUE KEY uk_inbound_fee_source (tenant_id,source_type,source_fee_id,source_fee_version)")
|
||||
.doesNotContain("DROP INDEX uk_inbound_fee_source");
|
||||
}
|
||||
|
||||
private static void assertFinalSourceIndex(Path path) throws Exception {
|
||||
|
||||
+5
-5
@@ -17,11 +17,11 @@ class FulfillmentTypeSqlContractTest {
|
||||
|
||||
assertFulfillmentMigration(order, "baiyue_order.ord_order");
|
||||
assertFulfillmentMigration(inbound, "fl_inbound_order");
|
||||
assertFulfillmentMigration(unified, "baiyue_order.ord_order");
|
||||
assertFulfillmentMigration(unified, "fl_inbound_order");
|
||||
assertThat(unified)
|
||||
.contains("unified_assert_column('baiyue_order', 'ord_order', 'fulfillment_type')")
|
||||
.contains("unified_assert_column('baiyue_firstleg', 'fl_inbound_order', 'fulfillment_type')");
|
||||
assertThat(unified.toLowerCase())
|
||||
.contains("create table ord_order")
|
||||
.contains("create table fl_inbound_order")
|
||||
.contains("fulfillment_type varchar(32) not null default 'standard'")
|
||||
.doesNotContain("unified_assert_column");
|
||||
}
|
||||
|
||||
private static void assertFulfillmentMigration(String sql, String table) {
|
||||
|
||||
+9
-12
@@ -29,20 +29,17 @@ class InboundCartonLabelSqlContractTest {
|
||||
assertThat(versioningMigration).contains("phase2_carton_label_add_column('label_version'")
|
||||
.contains("phase2_carton_label_add_column('batch_status'")
|
||||
.contains("idx_carton_label_active (tenant_id, inbound_id, batch_status, label_version)");
|
||||
assertThat(unified).contains("CREATE TABLE IF NOT EXISTS fl_inbound_carton_label")
|
||||
.contains("units_per_carton INT UNSIGNED NULL")
|
||||
.contains("unified_carton_label_add_column('label_version'")
|
||||
.contains("unified_carton_label_add_column('batch_status'")
|
||||
.contains("unified_carton_label_add_column('cargo_fingerprint'")
|
||||
.contains("unified_carton_label_add_column('voided_at'")
|
||||
.contains("ALTER TABLE fl_inbound_carton_label DROP INDEX uk_carton_label_index")
|
||||
.contains("ADD UNIQUE KEY uk_carton_label_index (tenant_id,inbound_id,label_version,carton_index)")
|
||||
.contains("ADD KEY idx_carton_label_active (tenant_id,inbound_id,batch_status,label_version)")
|
||||
assertThat(unified).contains("CREATE TABLE fl_inbound_carton_label")
|
||||
.contains("units_per_carton int unsigned DEFAULT NULL")
|
||||
.contains("label_version int unsigned NOT NULL DEFAULT '1'")
|
||||
.contains("batch_status varchar(16) NOT NULL DEFAULT 'ACTIVE'")
|
||||
.contains("cargo_fingerprint char(64) DEFAULT NULL")
|
||||
.contains("voided_at datetime(3) DEFAULT NULL")
|
||||
.contains("UNIQUE KEY uk_carton_label_index (tenant_id,inbound_id,label_version,carton_index)")
|
||||
.contains("KEY idx_carton_label_active (tenant_id,inbound_id,batch_status,label_version)")
|
||||
.contains("uk_carton_label_box_code")
|
||||
.contains("uk_carton_label_index")
|
||||
.contains("稳定箱号映射表未创建")
|
||||
.contains("稳定箱码映射兼容字段未完整创建")
|
||||
.contains("箱唛有效批次索引未完整创建");
|
||||
.doesNotContain("unified_carton_label_add_column");
|
||||
}
|
||||
|
||||
private String resource(String path) throws Exception {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ class InboundStatusUnificationSqlContractTest {
|
||||
.doesNotContain("UPDATE fl_inbound_order")
|
||||
.doesNotContain("UPDATE fl_inbound_status_history");
|
||||
assertThat(unified)
|
||||
.contains("入库完成后的唯一状态为 WAIT_SPLIT")
|
||||
.contains("WAIT_SPLIT待分柜")
|
||||
.doesNotContain("历史 WAIT_SPLIT")
|
||||
.doesNotContain("SET process_status = 'WAIT_SPLIT'")
|
||||
.doesNotContain("SET from_status = 'WAIT_SPLIT'")
|
||||
|
||||
+13
-7
@@ -77,9 +77,12 @@ class InboundWorkflowSqlContractTest {
|
||||
.contains("uk_measure_box_code")
|
||||
.contains("information_schema.STATISTICS");
|
||||
assertThat(unified)
|
||||
.contains("CALL phase2_measure_add_column('fl_inbound_measurement_item', 'box_code'")
|
||||
.contains("CREATE TABLE fl_inbound_measurement_item")
|
||||
.contains("box_code varchar(128) DEFAULT NULL")
|
||||
.contains("machine_no varchar(64) DEFAULT NULL")
|
||||
.contains("device_measured_at datetime(3) DEFAULT NULL")
|
||||
.contains("uk_measure_box_code")
|
||||
.contains("单箱当前测量字段未完整创建");
|
||||
.doesNotContain("phase2_measure_add_column");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,10 +100,13 @@ class InboundWorkflowSqlContractTest {
|
||||
.doesNotContain("final_weight_kg")
|
||||
.doesNotContain("uk_pallet_allocation_pallet");
|
||||
assertThat(unified)
|
||||
.contains("CALL phase2_measure_add_column('fl_inbound_weight_result', 'pallet_id'")
|
||||
.contains("ALTER TABLE fl_inbound_weight_result DROP COLUMN final_weight_kg")
|
||||
.contains("ALTER TABLE fl_inbound_pallet_allocation DROP INDEX uk_pallet_allocation_pallet")
|
||||
.contains("称重托盘快照或毛重净重字段未正确收敛")
|
||||
.contains("同票托盘唯一约束仍阻止托盘主数据复用");
|
||||
.contains("CREATE TABLE fl_inbound_weight_result")
|
||||
.contains("pallet_id bigint unsigned NOT NULL")
|
||||
.contains("pallet_tare_weight_kg decimal(18,3) NOT NULL")
|
||||
.contains("gross_weight_kg decimal(18,3) DEFAULT NULL")
|
||||
.contains("net_weight_kg decimal(18,3) DEFAULT NULL")
|
||||
.contains("KEY idx_weight_inbound_pallet")
|
||||
.doesNotContain("final_weight_kg")
|
||||
.doesNotContain("uk_pallet_allocation_pallet");
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -19,18 +19,20 @@ class MojiaMeasurementReceiptSqlContractTest {
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
for (String sql : new String[]{schema, unified}) {
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt")
|
||||
.contains("box_code VARCHAR(128) NOT NULL")
|
||||
.contains("scanned_shipment_id VARCHAR(64) NULL")
|
||||
.contains("image_file_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED'")
|
||||
String normalized = sql.toLowerCase()
|
||||
.replace("create table if not exists ", "create table ")
|
||||
.replace("create table ", "create table if not exists ");
|
||||
assertThat(normalized)
|
||||
.contains("create table if not exists fl_mojia_measurement_receipt")
|
||||
.contains("box_code varchar(128) not null")
|
||||
.contains("scanned_shipment_id varchar(64)")
|
||||
.contains("image_file_id bigint unsigned not null")
|
||||
.contains("status varchar(24) not null default 'unmatched'")
|
||||
.contains("idx_mojia_receipt_status_time")
|
||||
.doesNotContain("picture_base64");
|
||||
}
|
||||
assertThat(unified)
|
||||
.contains("墨家未匹配测量收件箱字段未完整创建")
|
||||
.contains("箱级测量实扫Shipment ID字段未创建")
|
||||
.contains("测量计算规则版本字段长度不是64");
|
||||
.contains("COMMENT='墨家模拟或未匹配入库单的原始测量收件箱'")
|
||||
.contains("calculation_rule_version varchar(64) NOT NULL");
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -31,10 +31,11 @@ class PdaSplitSettlementSqlContractTest {
|
||||
.contains("phase3_split_add_column('fl_preload_plan', 'confirmed_by_name'")
|
||||
.contains("phase3_split_add_column('fl_preload_plan', 'confirmed_at'");
|
||||
assertThat(unifiedSql)
|
||||
.contains("CREATE PROCEDURE migrate_preload_confirmation_columns()")
|
||||
.contains("ADD COLUMN confirmed_by BIGINT UNSIGNED NULL")
|
||||
.contains("ADD COLUMN confirmed_by_name VARCHAR(64) NULL")
|
||||
.contains("ADD COLUMN confirmed_at DATETIME(3) NULL");
|
||||
.contains("CREATE TABLE fl_preload_plan")
|
||||
.contains("confirmed_by bigint unsigned DEFAULT NULL")
|
||||
.contains("confirmed_by_name varchar(64) DEFAULT NULL")
|
||||
.contains("confirmed_at datetime(3) DEFAULT NULL")
|
||||
.doesNotContain("migrate_preload_confirmation_columns");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,9 +189,8 @@ class PdaSplitSettlementSqlContractTest {
|
||||
.contains("UNIQUE KEY uk_pallet_allocation_weight (tenant_id, inbound_id, weight_result_id)")
|
||||
.doesNotContain("UNIQUE KEY uk_pallet_allocation_slot (tenant_id, block_id, slot_no)");
|
||||
assertThat(unifiedSql)
|
||||
.contains("'tenant_id,inbound_id,block_id,slot_no'")
|
||||
.contains("ADD UNIQUE KEY uk_pallet_allocation_slot (tenant_id,inbound_id,block_id,slot_no)")
|
||||
.contains("'tenant_id,inbound_id,weight_result_id'")
|
||||
.contains("ADD UNIQUE KEY uk_pallet_allocation_weight (tenant_id,inbound_id,weight_result_id)");
|
||||
.contains("UNIQUE KEY uk_pallet_allocation_slot (tenant_id,inbound_id,block_id,slot_no)")
|
||||
.contains("UNIQUE KEY uk_pallet_allocation_weight (tenant_id,inbound_id,weight_result_id)")
|
||||
.doesNotContain("ADD UNIQUE KEY uk_pallet_allocation_slot");
|
||||
}
|
||||
}
|
||||
|
||||
+12
-22
@@ -58,36 +58,26 @@ class Phase3BookingTruckPermissionSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedSqlLocksCanonicalIdsScopeParentsAndRoleGrants() throws Exception {
|
||||
void unifiedSqlCreatesCanonicalIdsScopeParentsAndRoleGrants() throws Exception {
|
||||
String sql = unifiedSql();
|
||||
assertThat(sql)
|
||||
.contains("UNIQUE KEY uk_permission_scope_code (tenant_id, app_code, permission_code)")
|
||||
.contains("CREATE PROCEDURE assert_ocean_booking_permission_ids_available()")
|
||||
.contains("CREATE PROCEDURE assert_truck_appointment_permission_ids_available()")
|
||||
.contains("CREATE PROCEDURE assert_phase3_service_center_schema()")
|
||||
.contains("CALL assert_phase3_service_center_schema();")
|
||||
.contains("DROP PROCEDURE IF EXISTS assert_phase3_service_center_schema;")
|
||||
.contains("三期订舱权限迁移失败:5102140-5102149 ID 或权限码被其他语义占用")
|
||||
.contains("三期拖车权限迁移失败:5102150-5102154 ID 或权限码被其他语义占用")
|
||||
String normalized = sql.replace(", ", ",");
|
||||
assertThat(normalized)
|
||||
.contains("UNIQUE KEY uk_permission_scope_code (tenant_id,app_code,permission_code)")
|
||||
.contains("(5102140,0,'PLATFORM','MENU',5102000,'platform:firstleg:ocean-booking:list'")
|
||||
.contains("(5102149,0,'PLATFORM','BUTTON',5102140,'platform:firstleg:ocean-booking:allocate'")
|
||||
.contains("(5102150,0,'PLATFORM','MENU',5102000,'platform:firstleg:truck-appointment:list'")
|
||||
.contains("(5102154,0,'PLATFORM','BUTTON',5102150,'platform:firstleg:truck-appointment:operate'")
|
||||
.contains("permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 6")
|
||||
.contains("p.permission_code LIKE 'platform:firstleg:ocean-booking:%') <> 12")
|
||||
.contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')")
|
||||
.doesNotContain("assert_phase3_booking_truck_permission_ids_available")
|
||||
.contains("VALUES (4000001,5102140,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000003,5102140,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000001,5102150,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000003,5102150,0,CURRENT_TIMESTAMP(3))")
|
||||
.doesNotContain("assert_ocean_booking_permission_ids_available")
|
||||
.doesNotContain("assert_truck_appointment_permission_ids_available")
|
||||
.doesNotContain("/trucking");
|
||||
|
||||
assertThat(count(sql, "(5102140,0,'PLATFORM','MENU',5102000,'platform:firstleg:ocean-booking:list'"))
|
||||
assertThat(count(normalized, "(5102140,0,'PLATFORM','MENU',5102000,'platform:firstleg:ocean-booking:list'"))
|
||||
.isEqualTo(1);
|
||||
assertThat(count(sql, "(5102150,0,'PLATFORM','MENU',5102000,'platform:firstleg:truck-appointment:list'"))
|
||||
.isEqualTo(1);
|
||||
assertThat(count(sql, "CREATE PROCEDURE assert_ocean_booking_permission_ids_available()"))
|
||||
.isEqualTo(1);
|
||||
assertThat(count(sql, "CREATE PROCEDURE assert_truck_appointment_permission_ids_available()"))
|
||||
.isEqualTo(1);
|
||||
assertThat(count(sql, "CREATE PROCEDURE assert_phase3_service_center_schema()"))
|
||||
assertThat(count(normalized, "(5102150,0,'PLATFORM','MENU',5102000,'platform:firstleg:truck-appointment:list'"))
|
||||
.isEqualTo(1);
|
||||
}
|
||||
|
||||
|
||||
+13
-3
@@ -17,10 +17,20 @@ class Phase3ChannelContainerMigrationContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationRestoresGuardsBeforeSurfacingFailures() throws Exception {
|
||||
void unifiedInitializationCreatesFinalGuardsAndColumnsWithoutMigrationWindow() throws Exception {
|
||||
String sql = unifiedMigration();
|
||||
assertSafeWindow(sql, true);
|
||||
assertNoDuplicateInboundColumns(sql);
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE fl_inbound_order")
|
||||
.contains("warehouse_id bigint unsigned NOT NULL")
|
||||
.contains("warehouse_name varchar(128) NOT NULL")
|
||||
.contains("channel_code varchar(64) NOT NULL")
|
||||
.contains("channel_name varchar(128) NOT NULL")
|
||||
.contains("CREATE TRIGGER trg_container_booking_before_update")
|
||||
.contains("装柜作业开始后仅允许修改集装箱号与放舱运输数据")
|
||||
.contains("CREATE TRIGGER trg_loading_pick_task_before_update")
|
||||
.contains("已完成PDA拣货任务不可修改")
|
||||
.doesNotContain("migrate_phase3_channel_containers")
|
||||
.doesNotContain("assert_phase3_channel_container_migration");
|
||||
}
|
||||
|
||||
private static void assertNoDuplicateInboundColumns(String sql) {
|
||||
|
||||
+57
-338
@@ -5,370 +5,89 @@ import org.junit.jupiter.api.Test;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class Phase3FinalUnifiedSqlContractTest {
|
||||
private static final Path UNIFIED = Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
private static final Pattern CREATE_TABLE = Pattern.compile(
|
||||
"(?i)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+(?:`[^`]+`\\.)?`?([a-zA-Z0-9_]+)`?\\s*\\(");
|
||||
private static final Pattern COLUMN = Pattern.compile(
|
||||
"(?i)^\\s*`?([a-zA-Z0-9_]+)`?\\s+(bigint|binary|bit|blob|boolean|char|date|datetime|decimal|double|enum|float|int|integer|json|longblob|longtext|mediumblob|mediumint|mediumtext|numeric|real|set|smallint|text|time|timestamp|tinyblob|tinyint|tinytext|varbinary|varchar|year)\\b");
|
||||
private static final Pattern CHINESE_COMMENT = Pattern.compile(
|
||||
"(?i)\\bCOMMENT\\s*=?\\s*'(?:(?:''|\\\\'|[^'])*[\\u3400-\\u9fff](?:''|\\\\'|[^'])*)'");
|
||||
|
||||
@Test
|
||||
void unifiedEntryOwnsPhase3OutboundCustomsPortAndEvidenceHardening() throws Exception {
|
||||
String sql = Files.readString(UNIFIED, StandardCharsets.UTF_8);
|
||||
void unifiedEntryCreatesTheFinalPhase3SchemaDirectly() throws Exception {
|
||||
String sql = sql();
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE fl_inbound_split_batch")
|
||||
.contains("CREATE TABLE fl_ocean_booking")
|
||||
.contains("CREATE TABLE fl_truck_appointment")
|
||||
.contains("CREATE TABLE fl_outbound_dispatch")
|
||||
.contains("CREATE TABLE fl_customs_container")
|
||||
.contains("CREATE TABLE fl_port_schedule")
|
||||
.contains("CREATE TABLE fl_carrier_schedule_query")
|
||||
.contains("CREATE TABLE fl_terminal_drayage")
|
||||
.contains("CREATE TABLE fl_loading_pick_assignment")
|
||||
.contains("CREATE TABLE fl_loading_pick_item_assignment")
|
||||
.contains("CREATE TABLE fl_loading_pick_review")
|
||||
.contains("CREATE TABLE fl_loading_pick_operation")
|
||||
.contains("CREATE TABLE fl_loading_cabinet_evidence")
|
||||
.contains("CREATE TABLE fl_container_fee_detail")
|
||||
.doesNotContain("phase3_order_add_column")
|
||||
.doesNotContain("assert_phase3_final_schema")
|
||||
.doesNotContain("-- 来源:");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedEntryKeepsFinalConstraintsAndImmutableTriggers() throws Exception {
|
||||
String sql = sql();
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_outbound_dispatch")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_container")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_customer_document")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_customer_document_inbound")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_completion_document")
|
||||
.contains("terminal_trucking_company_id BIGINT UNSIGNED NULL")
|
||||
.contains("phase3_container_supplier_snapshot_add_column")
|
||||
.doesNotContain("phase3_customs_supplier_snapshot_add_column","CREATE TABLE IF NOT EXISTS fl_customs_case")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_port_schedule")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_port_schedule_history")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_preload_loading_plan_link")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_split_detail")
|
||||
.contains("trg_inbound_split_detail_before_insert")
|
||||
.contains("trg_inbound_split_detail_before_update")
|
||||
.contains("trg_inbound_split_detail_before_delete")
|
||||
.contains("CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH', 'PICK_PALLET', 'COMPLETE_LOADING'))")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence")
|
||||
.contains("UNIQUE KEY uk_cabinet_evidence_active (container_booking_id,evidence_type,active_flag)")
|
||||
.contains("UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key)")
|
||||
.contains("normalize_loading_cabinet_evidence_collation")
|
||||
.contains("table_collation<>'utf8mb4_0900_ai_ci'")
|
||||
.contains("column_name IN ('container_booking_id','source_task_id','active_flag','operation_key','request_hash')")
|
||||
.doesNotContain("CREATE TABLE IF NOT EXISTS fl_loading_evidence", "phase3_loading_evidence_add_column")
|
||||
.contains("CALL assert_phase3_final_schema()")
|
||||
.contains("trg_port_schedule_history_before_update")
|
||||
.contains("trg_port_schedule_history_before_delete")
|
||||
.contains("trg_loading_pick_operation_before_delete");
|
||||
.contains("UNIQUE KEY uk_inbound_one_active_remainder")
|
||||
.contains("UNIQUE KEY uk_inbound_split_source_once")
|
||||
.contains("CREATE TRIGGER trg_inbound_split_detail_before_insert")
|
||||
.contains("CREATE TRIGGER trg_inbound_split_detail_before_update")
|
||||
.contains("CREATE TRIGGER trg_inbound_split_detail_before_delete")
|
||||
.contains("CREATE TRIGGER trg_port_schedule_history_before_update")
|
||||
.contains("CREATE TRIGGER trg_port_schedule_history_before_delete")
|
||||
.contains("CREATE TRIGGER trg_loading_pick_operation_before_delete")
|
||||
.doesNotContain("UNIQUE KEY uk_inbound_one_child")
|
||||
.doesNotContain("UNIQUE KEY uk_inbound_one_secondary");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedOceanBookingBlockIncludesExistingSchemaReconciliation() throws Exception {
|
||||
String sql = normalise(Files.readString(UNIFIED));
|
||||
assertThat(sql)
|
||||
.contains("DROP CHECK chk_ocean_booking_status")
|
||||
.contains("ADD CONSTRAINT chk_ocean_booking_status CHECK")
|
||||
.contains("DROP COLUMN failure_reason")
|
||||
.contains("(status IN ('DRAFT','RELEASED'))")
|
||||
.contains("platform:firstleg:ocean-booking:edit")
|
||||
.contains("platform:firstleg:ocean-booking:delete")
|
||||
.doesNotContain("reconcile_ocean_booking_legacy")
|
||||
.doesNotContain("UPDATE fl_ocean_booking SET status='CONFIRMED'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void permissionRangesDoNotOverlapAndBothPlatformRolesAreGranted() throws Exception {
|
||||
String sql = Files.readString(UNIFIED, StandardCharsets.UTF_8);
|
||||
|
||||
for (int id = 5102160; id <= 5102164; id++) assertThat(sql).contains(Integer.toString(id));
|
||||
for (int id : java.util.List.of(5102170,5102172)) assertThat(sql).contains(Integer.toString(id));
|
||||
assertThat(sql).contains("DELETE FROM iam_permission WHERE id IN(5102171,5102173,5102174,5102175)")
|
||||
.doesNotContain("(5102171,0,'PLATFORM','BUTTON'","(5102173,0,'PLATFORM','BUTTON'",
|
||||
"(5102174,0,'PLATFORM','BUTTON'","(5102175,0,'PLATFORM','BUTTON'");
|
||||
for (int id = 5102180; id <= 5102183; id++) assertThat(sql).contains(Integer.toString(id));
|
||||
for (int id = 5102190; id <= 5102193; id++) assertThat(sql).contains(Integer.toString(id));
|
||||
assertThat(sql)
|
||||
.contains("platform:firstleg:outbound-dispatch:%")
|
||||
.contains("platform:firstleg:customs:%")
|
||||
.contains("platform:firstleg:port-schedule:%")
|
||||
.contains("id BETWEEN 5102180 AND 5102183 AND permission_code LIKE 'platform:firstleg:port-schedule:%') <> 3")
|
||||
.contains("OR p.permission_code LIKE 'platform:firstleg:pda:%')) <> 28")
|
||||
.contains("platform:firstleg:pda:%")
|
||||
.contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')")
|
||||
.contains("角色授权未完整写入");
|
||||
}
|
||||
|
||||
@Test
|
||||
void phase3ModuleSqlBlocksAreContiguousAndSynchronised() throws Exception {
|
||||
String unified = normalise(Files.readString(resolveUnified()));
|
||||
assertBlock(unified, "-- 来源:order/src/main/resources/db/phase3-order-parent-child-schema.sql",
|
||||
"-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql",
|
||||
"order/src/main/resources/db/phase3-order-parent-child-schema.sql");
|
||||
assertBlock(unified, "-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql",
|
||||
"-- 来源:firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql",
|
||||
"firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql");
|
||||
assertBlock(unified, "-- 来源:firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql",
|
||||
"-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
|
||||
"firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql");
|
||||
assertBlock(unified, "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
|
||||
"-- 入库完成后的唯一状态为 WAIT_SPLIT。",
|
||||
"iam/src/main/resources/db/phase3-warehouse-roles.sql");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customsBaselineIsUniqueSynchronisedAndRunsBeforeFinalSchemaAssertion() throws Exception {
|
||||
String unified = normalise(Files.readString(resolveUnified()));
|
||||
String marker = "-- 来源:firstleg/src/main/resources/db/phase3-container-customs-schema.sql";
|
||||
String finalAssertion = "CREATE PROCEDURE assert_phase3_final_schema()";
|
||||
List<String> tables = List.of(
|
||||
"fl_customs_container",
|
||||
"fl_customs_customer_document",
|
||||
"fl_customs_customer_document_inbound",
|
||||
"fl_customs_customer_document_version",
|
||||
"fl_customs_frozen_document_inbound",
|
||||
"fl_customs_completion_document",
|
||||
"fl_customs_completion_document_version",
|
||||
"fl_customs_container_history",
|
||||
"fl_customs_command"
|
||||
);
|
||||
|
||||
assertBlock(unified, marker, "/* Phase 3: link inbound preload plans",
|
||||
"firstleg/src/main/resources/db/phase3-container-customs-schema.sql");
|
||||
assertThat(count(unified, marker)).isEqualTo(1);
|
||||
assertThat(unified.indexOf(marker)).isLessThan(unified.indexOf(finalAssertion));
|
||||
for (String table : tables) {
|
||||
String create = "CREATE TABLE IF NOT EXISTS " + table + " (";
|
||||
assertThat(count(unified, create)).as(table).isEqualTo(1);
|
||||
assertThat(unified.indexOf(create)).as(table + " before final assertion")
|
||||
.isLessThan(unified.indexOf(finalAssertion));
|
||||
void unifiedEntrySeedsCanonicalPermissionsAndBothPlatformRoles() throws Exception {
|
||||
String sql = sql();
|
||||
for (int id : new int[] {5102140, 5102149, 5102150, 5102154, 5102160, 5102164,
|
||||
5102170, 5102172, 5102180, 5102183, 5102190, 5102193}) {
|
||||
assertThat(sql).contains(Integer.toString(id));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void financeFeeSourceMigrationsAreContiguousAndSynchronised() throws Exception {
|
||||
String unified = normalise(Files.readString(resolveUnified()));
|
||||
assertBlock(unified, "-- 来源:firstleg/src/main/resources/db/finance-fee-source-migration.sql",
|
||||
"-- 来源:finance/src/main/resources/db/fee-source-event-migration.sql",
|
||||
"firstleg/src/main/resources/db/finance-fee-source-migration.sql");
|
||||
assertBlock(unified, "-- 来源:finance/src/main/resources/db/fee-source-event-migration.sql",
|
||||
"-- END FINANCE FEE SOURCE MODULE MIGRATIONS",
|
||||
"finance/src/main/resources/db/fee-source-event-migration.sql");
|
||||
|
||||
assertThat(unified)
|
||||
.contains("column_name IN ('source_type','source_no','direction','container_id','fee_item_code',")
|
||||
.contains("column_name IN ('payload_hash','result_resource_id','error_code')")
|
||||
.contains("column_name IN ('event_key', 'event_version', 'aggregate_sequence', 'payload_hash',")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_container_fee_detail");
|
||||
}
|
||||
|
||||
@Test
|
||||
void commentUpgradeIsIdempotentAndGuardsNineSchemaStructure() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE PROCEDURE queue_table_comment")
|
||||
.contains("CREATE PROCEDURE queue_column_comment")
|
||||
.contains("CREATE PROCEDURE apply_one_table_comments")
|
||||
.contains("CREATE PROCEDURE apply_queued_comments")
|
||||
.contains("NOT (BINARY v_current_comment=BINARY p_column_comment)")
|
||||
.contains("SET v_signature_before=SHA2")
|
||||
.contains("REPLACE(v_generation_expression,CONCAT(CHAR(92),CHAR(39)),CHAR(39))")
|
||||
.contains("BINARY target.signature_before")
|
||||
.contains("字段注释升级遇到未知数据类型")
|
||||
.contains("字段注释升级遇到未知EXTRA定义")
|
||||
.contains("字段注释升级遇到未知生成列结构")
|
||||
.contains("MySQL DDL 会隐式提交")
|
||||
.contains("不使用 GROUP_CONCAT")
|
||||
.contains("DECLARE table_cursor CURSOR FOR")
|
||||
.contains("CALL apply_one_table_comments(v_schema_name,v_table_name)")
|
||||
.contains("CALL apply_queued_comments();")
|
||||
.contains("PREPARE table_comment_batch_stmt FROM @table_comment_batch_sql")
|
||||
.contains("九个业务数据库未完整创建")
|
||||
.contains("存在缺失中文注释的业务表")
|
||||
.contains("存在缺失中文注释的业务字段")
|
||||
.doesNotContain("CREATE PROCEDURE upgrade_column_comment");
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(147);
|
||||
assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2374);
|
||||
assertThat(sql)
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_id'")
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_name'")
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_category'")
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_node_fact', 'container_booking_id'");
|
||||
assertThat(sql.indexOf("CALL apply_queued_comments();",
|
||||
sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS")))
|
||||
.isGreaterThan(sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS"));
|
||||
.contains("platform:firstleg:ocean-booking:list")
|
||||
.contains("platform:firstleg:truck-appointment:list")
|
||||
.contains("platform:firstleg:outbound-dispatch:list")
|
||||
.contains("platform:firstleg:customs:list")
|
||||
.contains("platform:firstleg:port-schedule:list")
|
||||
.contains("platform:firstleg:pda:view")
|
||||
.contains("VALUES (4000001,5102190,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000003,5102190,0,CURRENT_TIMESTAMP(3))");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedCreateDefinitionsHaveChineseTableAndColumnComments() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
List<String> issues = new ArrayList<>();
|
||||
Matcher tableMatcher = CREATE_TABLE.matcher(sql);
|
||||
int tableCount = 0;
|
||||
int columnCount = 0;
|
||||
while (tableMatcher.find()) {
|
||||
tableCount++;
|
||||
String table = tableMatcher.group(1);
|
||||
int open = tableMatcher.end() - 1;
|
||||
int close = matchingParen(sql, open);
|
||||
int semicolon = sql.indexOf(';', close);
|
||||
assertThat(semicolon).as("missing semicolon for %s", table).isGreaterThan(close);
|
||||
if (!CHINESE_COMMENT.matcher(sql.substring(close + 1, semicolon)).find()) {
|
||||
issues.add(table + ": table comment");
|
||||
}
|
||||
for (String definition : splitDefinitions(sql.substring(open + 1, close))) {
|
||||
Matcher columnMatcher = COLUMN.matcher(definition);
|
||||
if (!columnMatcher.find()) continue;
|
||||
columnCount++;
|
||||
if (!table.startsWith("fl_customs_") && !CHINESE_COMMENT.matcher(definition).find()) {
|
||||
issues.add(table + "." + columnMatcher.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThat(tableCount).isGreaterThan(100);
|
||||
assertThat(columnCount).isGreaterThan(2000);
|
||||
assertThat(issues).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void passIsUniqueAndOnlyTheFinalExecutableStatement() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
String pass = "SELECT 'PASS' AS migration_status";
|
||||
void initializationPassIsUniqueAndFinal() throws Exception {
|
||||
String sql = sql();
|
||||
String pass = "SELECT 'PASS' AS initialization_status, 'CN' AS region, 208 AS initialized_tables;";
|
||||
assertThat(count(sql, pass)).isEqualTo(1);
|
||||
int passIndex = sql.lastIndexOf(pass);
|
||||
assertThat(passIndex).isGreaterThan(sql.lastIndexOf("USE "))
|
||||
.isGreaterThan(sql.lastIndexOf("ALTER TABLE "))
|
||||
.isGreaterThan(sql.lastIndexOf("CREATE TABLE "))
|
||||
.isGreaterThan(sql.lastIndexOf("CALL "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedEntryUsesMysql8CompatibleConditionalDdl() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
|
||||
assertThat(sql.trim()).endsWith(pass);
|
||||
assertThat(sql)
|
||||
.doesNotContain("\n ADD COLUMN IF NOT EXISTS")
|
||||
.doesNotContain("\n DROP CHECK IF EXISTS")
|
||||
.doesNotContain("\n+--")
|
||||
.doesNotContain("\n---")
|
||||
.doesNotContain("\n>>>")
|
||||
.doesNotContain("\n<<<")
|
||||
.contains("CREATE PROCEDURE phase3_pda_add_column")
|
||||
.contains("CREATE PROCEDURE phase3_pda_replace_operation_check");
|
||||
.doesNotContain("SELECT 'PASS' AS migration_status")
|
||||
.doesNotContain("ON DUPLICATE KEY UPDATE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedPdaBlockDropsLegacyPerItemReleaseColumns() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
|
||||
assertThat(sql)
|
||||
.contains("column_name='release_location_ids'")
|
||||
.contains("ALTER TABLE fl_loading_pick_item DROP COLUMN release_location_ids")
|
||||
.contains("column_name='release_mode'")
|
||||
.contains("ALTER TABLE fl_loading_pick_item DROP COLUMN release_mode")
|
||||
.doesNotContain("'fl_loading_pick_item', 'release_mode'")
|
||||
.doesNotContain("'fl_loading_pick_item', 'release_location_ids'")
|
||||
.doesNotContain("CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_mode'")
|
||||
.doesNotContain("CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_location_ids'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacySplitIndexesAreOnlyDroppedAndNeverRecreated() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
assertThat(sql)
|
||||
.contains("DROP INDEX uk_inbound_one_child")
|
||||
.contains("DROP INDEX uk_inbound_one_secondary")
|
||||
.doesNotContain("UNIQUE KEY `uk_inbound_one_child`")
|
||||
.doesNotContain("UNIQUE KEY `uk_inbound_one_secondary`")
|
||||
.doesNotContain("UNIQUE KEY `uk_inbound_split_batch_source_item`");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedEntryEnforcesSingleRemainderWritesAndKeepsLegacyColumns() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
|
||||
assertThat(sql)
|
||||
.contains("'fl_inbound_order', 'active_remainder_root_id'")
|
||||
.contains("uk_inbound_one_active_remainder")
|
||||
.contains("uk_inbound_split_source_once")
|
||||
.contains("NEW.loaded_child_inbound_id IS NOT NULL")
|
||||
.contains("NEW.child_inbound_id = batch.remainder_child_inbound_id")
|
||||
.contains("source_order.package_count = NEW.loaded_package_count")
|
||||
.contains("根入仓单原始箱数与已完成分片及当前余量不守恒")
|
||||
.contains("'fl_inbound_split_batch', 'loaded_child_inbound_id'")
|
||||
.contains("fk_inbound_split_loaded_child");
|
||||
}
|
||||
|
||||
private static void assertBlock(String unified, String marker, String nextMarker, String modulePath)
|
||||
throws Exception {
|
||||
int start = unified.indexOf(marker);
|
||||
int next = unified.indexOf(nextMarker, start + marker.length());
|
||||
assertThat(start).as("missing marker %s", marker).isGreaterThanOrEqualTo(0);
|
||||
assertThat(next).as("missing next marker %s", nextMarker).isGreaterThan(start);
|
||||
String module = normalise(Files.readString(resolve(modulePath))).trim();
|
||||
String block = unified.substring(start + marker.length(), next).trim();
|
||||
assertThat(block).as("module block drift: %s", modulePath)
|
||||
.startsWith(module);
|
||||
}
|
||||
|
||||
private static Path resolveUnified() {
|
||||
return resolve("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
}
|
||||
|
||||
private static Path resolve(String path) {
|
||||
Path direct = Path.of(path);
|
||||
if (Files.exists(direct)) return direct;
|
||||
return Path.of("..", path).normalize();
|
||||
}
|
||||
|
||||
private static String normalise(String value) {
|
||||
return value.replace("\r\n", "\n").replace("\uFEFF", "");
|
||||
private static String sql() throws Exception {
|
||||
return Files.readString(UNIFIED, StandardCharsets.UTF_8)
|
||||
.replace("\r\n", "\n")
|
||||
.replace("\uFEFF", "");
|
||||
}
|
||||
|
||||
private static int count(String value, String token) {
|
||||
return value.split(java.util.regex.Pattern.quote(token), -1).length - 1;
|
||||
}
|
||||
|
||||
private static int matchingParen(String sql, int open) {
|
||||
int depth = 0;
|
||||
char quote = 0;
|
||||
for (int index = open; index < sql.length(); index++) {
|
||||
char current = sql.charAt(index);
|
||||
if (quote != 0) {
|
||||
if (current == '\\') index++;
|
||||
else if (current == quote) {
|
||||
if (index + 1 < sql.length() && sql.charAt(index + 1) == quote) index++;
|
||||
else quote = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (current == '\'' || current == '"' || current == '`') quote = current;
|
||||
else if (current == '(') depth++;
|
||||
else if (current == ')' && --depth == 0) return index;
|
||||
}
|
||||
throw new AssertionError("unclosed CREATE TABLE at " + open);
|
||||
}
|
||||
|
||||
private static List<String> splitDefinitions(String body) {
|
||||
List<String> definitions = new ArrayList<>();
|
||||
int start = 0;
|
||||
int depth = 0;
|
||||
char quote = 0;
|
||||
for (int index = 0; index < body.length(); index++) {
|
||||
char current = body.charAt(index);
|
||||
if (quote != 0) {
|
||||
if (current == '\\') index++;
|
||||
else if (current == quote) {
|
||||
if (index + 1 < body.length() && body.charAt(index + 1) == quote) index++;
|
||||
else quote = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (current == '\'' || current == '"' || current == '`') quote = current;
|
||||
else if (current == '(') depth++;
|
||||
else if (current == ')') depth--;
|
||||
else if (current == ',' && depth == 0) {
|
||||
definitions.add(body.substring(start, index));
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
definitions.add(body.substring(start));
|
||||
return definitions;
|
||||
}
|
||||
}
|
||||
|
||||
+16
-16
@@ -10,7 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ServiceCenterUnifiedSqlContractTest {
|
||||
@Test
|
||||
void unifiedMigrationContainsServiceCenterSchemaAndGrants() throws Exception {
|
||||
void unifiedInitializationContainsServiceCenterSchemaAndGrants() throws Exception {
|
||||
Path working = Path.of(System.getProperty("user.dir"));
|
||||
Path path = working.resolve("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
if (!Files.exists(path)) {
|
||||
@@ -19,24 +19,24 @@ class ServiceCenterUnifiedSqlContractTest {
|
||||
String sql = Files.readString(path, StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_party")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_status_history")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_operation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_truck_appointment")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_truck_appointment_history")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_truck_appointment_attachment")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_truck_appointment_operation")
|
||||
.contains("CREATE TABLE fl_ocean_booking")
|
||||
.contains("CREATE TABLE fl_ocean_booking_party")
|
||||
.contains("CREATE TABLE fl_ocean_booking_container_requirement")
|
||||
.contains("CREATE TABLE fl_ocean_booking_container_allocation")
|
||||
.contains("CREATE TABLE fl_ocean_booking_status_history")
|
||||
.contains("CREATE TABLE fl_ocean_booking_operation")
|
||||
.contains("CREATE TABLE fl_truck_appointment")
|
||||
.contains("CREATE TABLE fl_truck_appointment_history")
|
||||
.contains("CREATE TABLE fl_truck_appointment_attachment")
|
||||
.contains("CREATE TABLE fl_truck_appointment_operation")
|
||||
.contains("5102140")
|
||||
.contains("5102149")
|
||||
.contains("5102150")
|
||||
.contains("5102154")
|
||||
.contains("PLATFORM_ADMIN")
|
||||
.contains("ORDER_OPERATOR")
|
||||
.contains("统一迁移失败:订舱或拖车预约角色授权未完整写入")
|
||||
.contains("assert_phase3_service_center_schema()")
|
||||
.contains("CALL assert_phase3_service_center_schema();");
|
||||
.contains("VALUES (4000001,5102140,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000003,5102140,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000001,5102150,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000003,5102150,0,CURRENT_TIMESTAMP(3))")
|
||||
.doesNotContain("assert_phase3_service_center_schema");
|
||||
}
|
||||
}
|
||||
|
||||
+73
-377
@@ -7,406 +7,102 @@ import org.springframework.core.io.ClassPathResource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("智能装柜SQL契约")
|
||||
class SmartLoadingSqlContractTest {
|
||||
private static final Pattern PERMISSION_PATTERN = Pattern.compile("'((?:platform:firstleg:)[^']+)'");
|
||||
private static final Set<String> PHASE3_PERMISSIONS = Set.of(
|
||||
"platform:firstleg:container:list",
|
||||
"platform:firstleg:container:create", "platform:firstleg:container:edit",
|
||||
"platform:firstleg:smart-loading:list",
|
||||
"platform:firstleg:smart-loading:assign", "platform:firstleg:smart-loading:delete",
|
||||
"platform:firstleg:preload:list",
|
||||
"platform:firstleg:preload:edit",
|
||||
"platform:firstleg:preload:complete", "platform:firstleg:preload:confirm",
|
||||
"platform:firstleg:loading-list:list");
|
||||
|
||||
@Test
|
||||
@DisplayName("平台聚合与客户明细边界及原型快照字段完整")
|
||||
void shouldProvidePlatformAggregatesAndAccountScopedSnapshots() throws Exception {
|
||||
assertThat(migration())
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_document_sequence")
|
||||
void moduleMigrationStillProtectsExistingDatabaseUpgrades() throws Exception {
|
||||
String sql = moduleMigration();
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_container_booking")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_plan")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_plan_item")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_container_document")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_plan_history")
|
||||
.contains("account_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("customer_name VARCHAR(128) NOT NULL")
|
||||
.contains("warehouse_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("cabinet_no VARCHAR(64) NOT NULL")
|
||||
.contains("bill_of_lading_no VARCHAR(128)")
|
||||
.contains("destination_warehouse_name VARCHAR(128)")
|
||||
.contains("product_name_zh VARCHAR(255)")
|
||||
.contains("product_name_en VARCHAR(255)")
|
||||
.contains("product_image_file_id BIGINT UNSIGNED")
|
||||
.contains("pallet_count INT UNSIGNED")
|
||||
.contains("net_weight_kg DECIMAL(18,3)")
|
||||
.contains("dimensional_weight_kg DECIMAL(18,3)")
|
||||
.contains("product_quantity INT UNSIGNED")
|
||||
.contains("units_per_carton INT UNSIGNED")
|
||||
.contains("fba_tracking_no VARCHAR(128)")
|
||||
.contains("shipment_no VARCHAR(128)")
|
||||
.contains("shipping_mark VARCHAR(500)")
|
||||
.contains("internal_remark VARCHAR(1000)")
|
||||
.contains("customer_remark VARCHAR(1000)")
|
||||
.contains("warehouse_remark VARCHAR(1000)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("活动明细唯一锁、乐观锁和操作幂等字段完整")
|
||||
void shouldProvideConcurrencyAndIdempotencyBoundaries() throws Exception {
|
||||
assertContainerLifecycleSchema(migration());
|
||||
assertContainerLifecycleSchema(unifiedMigration());
|
||||
assertThat(migration())
|
||||
.contains("active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS")
|
||||
.contains("CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END")
|
||||
.contains("active_assignment_key VARCHAR(96) GENERATED ALWAYS AS")
|
||||
.contains("UNIQUE KEY uk_loading_item_plan_assignment (loading_plan_id, active_assignment_key)")
|
||||
.contains("KEY idx_loading_item_active_detail (account_id, active_inbound_detail_id)")
|
||||
.contains("UNIQUE KEY uk_container_booking_no (booking_no)")
|
||||
.contains("UNIQUE KEY uk_container_booking_cabinet (cabinet_no)")
|
||||
.contains("UNIQUE KEY uk_loading_history_operation (operation_id)")
|
||||
.contains("version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("存量货柜迁移补齐BaseMapper所需客户快照列")
|
||||
void shouldMigrateContainerCustomerSnapshotColumns() throws Exception {
|
||||
for (String sql : Set.of(migration(), unifiedMigration())) {
|
||||
assertThat(sql)
|
||||
.contains("ADD COLUMN customer_id BIGINT UNSIGNED NULL")
|
||||
.contains("ADD COLUMN customer_name VARCHAR(128) NULL")
|
||||
.contains("JOIN fl_ocean_booking ocean_booking ON ocean_booking.id=container_booking.ocean_booking_id")
|
||||
.contains("container_booking.customer_id=COALESCE(container_booking.customer_id,ocean_booking.customer_id)")
|
||||
.contains("container_booking.customer_name=COALESCE(container_booking.customer_name,ocean_booking.customer_name)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("同一入库明细的多条单箱测量记录只生成一个候选")
|
||||
void shouldAggregateCartonMeasurementRowsByInboundDetail() throws Exception {
|
||||
String mapper = Files.readString(Path.of(
|
||||
"src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java"), StandardCharsets.UTF_8);
|
||||
assertThat(mapper)
|
||||
.contains("SUM(package_count) package_count")
|
||||
.contains("GROUP BY tenant_id,measurement_id,inbound_detail_id")
|
||||
.contains("split_item.source_inbound_detail_id=i.inbound_detail_id")
|
||||
.doesNotContain("split_item.source_measurement_item_id=i.id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("全局候选分页具备平台维度和关联明细索引")
|
||||
void shouldProvideGlobalCandidatePagingIndexes() throws Exception {
|
||||
assertThat(new ClassPathResource("db/phase2-inbound-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8))
|
||||
.contains("idx_inbound_smart_loading (warehouse_id, process_status, channel_code, expected_arrival_at, id)")
|
||||
.contains("idx_measure_inbound_status_id (tenant_id, inbound_id, status, id)")
|
||||
.contains("idx_measure_item_candidate (tenant_id, measurement_id, inbound_detail_id)");
|
||||
assertThat(migration())
|
||||
.contains("idx_loading_item_candidate (account_id, inbound_id, inbound_detail_id, deleted)");
|
||||
String unified = unifiedMigration();
|
||||
assertThat(unified)
|
||||
.contains("CREATE PROCEDURE baiyue_firstleg.ensure_smart_loading_candidate_indexes()")
|
||||
.contains("ADD KEY idx_inbound_smart_loading")
|
||||
.contains("ADD KEY idx_measure_inbound_status_id")
|
||||
.contains("ADD KEY idx_measure_item_candidate")
|
||||
.contains("ADD KEY idx_loading_item_candidate")
|
||||
.contains("ADD KEY idx_inbound_split_candidate");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("可编辑备注使用计划先行与明细单表CAS并允许已拣明细仅改备注")
|
||||
void shouldUpdateEditableRemarksWithoutMultiTableTriggerConflict() throws Exception {
|
||||
String mapper = Files.readString(Path.of(
|
||||
"src/main/java/com/baiyue/firstleg/mapper/LoadingPlanItemMapper.java"), StandardCharsets.UTF_8);
|
||||
assertThat(mapper)
|
||||
.contains("UPDATE fl_loading_plan_item i")
|
||||
.contains("p.version=#{touchedPlanVersion}")
|
||||
.contains("p.plan_status IN ('EDITING','LOADING')")
|
||||
.contains("i.frozen_at IS NULL")
|
||||
.doesNotContain("UPDATE fl_loading_plan_item i JOIN fl_loading_plan p")
|
||||
.doesNotContain("p.version=p.version+1");
|
||||
assertEditableRemarksTriggerContract(migration());
|
||||
assertEditableRemarksTriggerContract(unifiedMigration());
|
||||
}
|
||||
|
||||
private static void assertContainerLifecycleSchema(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("source_type VARCHAR(24) NOT NULL DEFAULT 'SIMULATION'")
|
||||
.contains("record_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE'")
|
||||
.contains("merged_into_container_booking_id BIGINT UNSIGNED NULL")
|
||||
.contains("UNIQUE KEY uk_container_booking_container_no (container_no)")
|
||||
.contains("KEY idx_container_booking_merge (record_status,merged_into_container_booking_id)")
|
||||
.contains("CHECK (source_type IN ('SIMULATION','OCEAN_BOOKING'))")
|
||||
.contains("CHECK (record_status IN ('ACTIVE','MERGED'))")
|
||||
.contains("column_name='source_type'")
|
||||
.contains("column_name='record_status'")
|
||||
.contains("column_name='merged_into_container_booking_id'");
|
||||
}
|
||||
|
||||
private static void assertEditableRemarksTriggerContract(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("IF NOT (NEW.internal_remark <=> OLD.internal_remark)")
|
||||
.contains("OR NOT (NEW.warehouse_remark <=> OLD.warehouse_remark)")
|
||||
.contains("OR NOT (NEW.customer_service_remark <=> OLD.customer_service_remark) THEN")
|
||||
.contains("IF NEW.package_count <> OLD.package_count OR NEW.deleted <> OLD.deleted")
|
||||
.contains("已有PDA拣货记录的装柜明细修改备注时不得变更其他业务字段")
|
||||
.contains("ELSEIF NOT EXISTS (")
|
||||
.contains("IF OLD.frozen_at IS NOT NULL THEN")
|
||||
.contains("装柜完成后的明细永久冻结");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("货柜简单文件只保留目的港清关与DO,PU改为数字码")
|
||||
void shouldAllowExactlyTwoSimpleContainerDocumentTypes() throws Exception {
|
||||
assertThat(migration())
|
||||
.contains("'DESTINATION_CLEARANCE', 'DELIVERY_ORDER'")
|
||||
.contains("pickup_code VARCHAR(64) NULL")
|
||||
.contains("file_hash VARCHAR(64) NULL")
|
||||
.contains("completed_by BIGINT UNSIGNED NULL")
|
||||
.contains("completed_at DATETIME(3) NULL");
|
||||
assertThat(unifiedMigration())
|
||||
.contains("pickup_code VARCHAR(64) NULL")
|
||||
.contains("pickup_code_updated_by BIGINT UNSIGNED NULL")
|
||||
.contains("pickup_code_updated_by_name VARCHAR(64) NULL")
|
||||
.contains("pickup_code_updated_at DATETIME(3) NULL")
|
||||
.contains("chk_container_booking_pickup_code")
|
||||
.contains("CHECK (pickup_code IS NULL OR pickup_code REGEXP '^[0-9]{1,64}$')");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("平台菜单按钮权限使用统一前缀并授权运营角色")
|
||||
void shouldProvidePlatformPermissions() throws Exception {
|
||||
assertThat(migration())
|
||||
.contains("platform:firstleg:container:view")
|
||||
.doesNotContain("'platform:firstleg:smart-loading:push', '推送预装'")
|
||||
.contains("platform:firstleg:preload:confirm")
|
||||
.contains("platform:firstleg:loading-list:view")
|
||||
.contains("role.role_code IN ('PLATFORM_ADMIN', 'ORDER_OPERATOR')")
|
||||
.contains("智能装柜迁移失败:平台权限资源未完整创建")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_task")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_item")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_history")
|
||||
.contains("migrate_legacy_loading_pick_task")
|
||||
.contains("assert_phase3_smart_loading_schema")
|
||||
.contains("智能装柜迁移失败:平台角色授权未完整写入");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("模块脚本和统一脚本均校验11项权限与两个角色22项授权")
|
||||
void shouldAssertCompletePermissionGrants() throws Exception {
|
||||
assertPermissionContract(migration());
|
||||
assertPermissionContract(unifiedMigration());
|
||||
assertThat(unifiedMigration())
|
||||
.contains("CALL migrate_legacy_loading_pick_task()")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_loading_pick_task")
|
||||
.contains("5102121, 0, 'PLATFORM'")
|
||||
.contains("5102129, 0, 'PLATFORM'")
|
||||
.contains("三期入库装柜角色授权未完整写入")
|
||||
.contains("permission.permission_code IN (")
|
||||
.contains("role.role_code IN ('PLATFORM_ADMIN', 'ORDER_OPERATOR')")
|
||||
.contains("'firstleg:inbound:split', 'firstleg:inbound:preload'")
|
||||
.contains(")) <> 18 THEN");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("计划状态只允许编辑中、装柜中和已完成单向推进")
|
||||
void shouldEnforceOneWayStatusAndPermanentFreeze() throws Exception {
|
||||
assertThat(migration())
|
||||
.contains("CHECK (plan_status IN ('EDITING', 'LOADING', 'COMPLETED'))")
|
||||
.contains("OLD.plan_status = 'EDITING' AND NEW.plan_status IN ('EDITING', 'LOADING')")
|
||||
.contains("NEW.plan_status IN ('LOADING', 'COMPLETED')")
|
||||
.contains("OLD.plan_status = 'LOADING' AND NEW.plan_status = 'EDITING'")
|
||||
.doesNotContain("旧版本在推送预装时即写入 LOADING")
|
||||
.doesNotContain("修复旧版本“推送预装即拣货中”的状态污染")
|
||||
.contains("SET MESSAGE_TEXT = '装柜状态只允许单向推进'")
|
||||
.contains("IF OLD.plan_status = 'LOADING' AND NEW.plan_status = 'COMPLETED' THEN")
|
||||
.contains("SET frozen_at = COALESCE(frozen_at, NEW.confirmed_at, CURRENT_TIMESTAMP(3))")
|
||||
.contains("已有PDA拣货记录的装柜明细仅允许收敛部分拣货实际箱数")
|
||||
.contains("装柜完成后的明细永久冻结")
|
||||
.contains("current_plan_status IN ('LOADING', 'COMPLETED')")
|
||||
.contains("装柜作业开始后仅允许修改集装箱号与放舱运输数据")
|
||||
.doesNotContain("NOT (OLD.bill_of_lading_no <=> NEW.bill_of_lading_no)")
|
||||
.doesNotContain("NOT (OLD.vessel_name <=> NEW.vessel_name)")
|
||||
.doesNotContain("NOT (OLD.estimated_sailing_at <=> NEW.estimated_sailing_at)")
|
||||
.doesNotContain("preload_status")
|
||||
.doesNotContain("PRELOAD_PUSHED")
|
||||
.doesNotContain("PRELOADING")
|
||||
.doesNotContain("PRELOAD_COMPLETED")
|
||||
.doesNotContain("CONFIRMED")
|
||||
.doesNotContain("ROLLED_BACK")
|
||||
.doesNotContain("WITHDRAWN");
|
||||
assertLoadingCompletionTrigger(migration());
|
||||
assertLoadingCompletionTrigger(unifiedMigration());
|
||||
assertThat(unifiedMigration().indexOf("CALL phase3_add_container_channel_group()"))
|
||||
.isLessThan(unifiedMigration().indexOf("CREATE TRIGGER trg_container_booking_before_update"));
|
||||
}
|
||||
|
||||
private static void assertLoadingCompletionTrigger(String sql) {
|
||||
int triggerStart = sql.indexOf("CREATE TRIGGER trg_loading_plan_before_update");
|
||||
int triggerEnd = sql.indexOf("DROP TRIGGER IF EXISTS trg_loading_plan_after_update", triggerStart);
|
||||
assertThat(triggerStart).isGreaterThanOrEqualTo(0);
|
||||
assertThat(triggerEnd).isGreaterThan(triggerStart);
|
||||
String beforeUpdateTrigger = sql.substring(triggerStart, triggerEnd);
|
||||
assertThat(beforeUpdateTrigger)
|
||||
.contains("AND NOT EXISTS (")
|
||||
.contains("pick_task.loading_plan_id = OLD.id")
|
||||
.doesNotContain("pick_task.task_status")
|
||||
.contains("pick_item.item_status = 'PICKED'")
|
||||
.contains("pick_item.item_status = 'PARTIAL'")
|
||||
.contains("pick_item.picked_package_count > 0")
|
||||
.contains("pick_item.item_status = 'SKIPPED'")
|
||||
.contains("pick_item.picked_package_count = 0")
|
||||
.doesNotContain("pick_item.picked_package_count <> item.package_count");
|
||||
assertThat(org.springframework.util.StringUtils.countOccurrencesOf(sql,
|
||||
"pick_item.picked_package_count = item.package_count")).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PDA三表、唯一索引、向上取整和不可变审计契约完整")
|
||||
void shouldProvidePdaContract() throws Exception {
|
||||
assertThat(migration())
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_task")
|
||||
.contains("CALL migrate_legacy_loading_pick_task()")
|
||||
.contains("source_total_columns = 13 AND source_current_columns = 13")
|
||||
.contains("index_name = 'uk_loading_pick_task_booking'")
|
||||
.contains("RENAME TABLE fl_loading_pick_task TO fl_inbound_loading_pick_task")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_item")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_history")
|
||||
.contains("UNIQUE KEY uk_loading_pick_task_plan (loading_plan_id)")
|
||||
.contains("UNIQUE KEY uk_loading_pick_item_plan_item (loading_plan_item_id)")
|
||||
.contains("UNIQUE KEY uk_loading_pick_history_operation (operation_id)")
|
||||
.contains("上层拆分按业务规则向上取整")
|
||||
.contains("CONSTRAINT chk_loading_pick_item_planned CHECK (planned_package_count > 0)")
|
||||
.contains("CALL repair_phase3_loading_pick_task_foreign_keys()")
|
||||
.contains("GROUP_CONCAT(CONCAT('DROP FOREIGN KEY `'")
|
||||
.contains("item_fk_count <> 1 OR item_target_count <> 1")
|
||||
.contains("history_fk_count <> 1 OR history_target_count <> 1")
|
||||
.contains("FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id)")
|
||||
.contains("referenced_column_name = 'id'")
|
||||
.contains("柜次PDA明细任务外键不唯一或指向错误")
|
||||
.contains("柜次PDA历史任务外键不唯一或指向错误")
|
||||
.doesNotContain("DROP FOREIGN KEY fk_loading_pick_item_task")
|
||||
.doesNotContain("DROP FOREIGN KEY fk_loading_pick_history_task")
|
||||
.contains("trg_loading_pick_task_before_update")
|
||||
.contains("trg_loading_pick_item_before_update")
|
||||
.contains("trg_loading_pick_item_before_delete")
|
||||
.contains("trg_loading_pick_history_before_update")
|
||||
.contains("trg_loading_pick_history_before_delete")
|
||||
.contains("PDA唯一索引缺失")
|
||||
.contains(")) <> 16 THEN");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("统一脚本只定义并调用一次智能装柜断言")
|
||||
void shouldDefineSmartLoadingAssertionOnce() throws Exception {
|
||||
String unified = unifiedMigration();
|
||||
assertThat(count(unified, "CREATE PROCEDURE assert_phase3_smart_loading_schema()"))
|
||||
.isEqualTo(1);
|
||||
assertThat(count(unified, "CALL assert_phase3_smart_loading_schema();"))
|
||||
.isEqualTo(1);
|
||||
assertThat(count(unified, "SELECT 'PASS' AS migration_status"))
|
||||
.isEqualTo(1);
|
||||
int relocation = unified.indexOf("CALL unified_relocate_inbound_loading_permissions();");
|
||||
int assertion = unified.indexOf("CALL assert_phase3_smart_loading_schema();");
|
||||
assertThat(relocation).isGreaterThan(0);
|
||||
assertThat(assertion).isGreaterThan(relocation);
|
||||
assertThat(unified.substring(relocation, assertion))
|
||||
.contains("tmp_phase3_inbound_role_grant")
|
||||
.contains("DROP TEMPORARY TABLE IF EXISTS tmp_phase3_inbound_role_grant")
|
||||
.contains("5102121, 0, 'PLATFORM'")
|
||||
.contains("5102129, 0, 'PLATFORM'")
|
||||
.contains("role.role_code IN ('PLATFORM_ADMIN', 'ORDER_OPERATOR')");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("详细拣货和入库汇总拣货使用独立完整表型")
|
||||
void shouldKeepDetailedAndInboundPickTaskShapesSeparate() throws Exception {
|
||||
String unified = unifiedMigration();
|
||||
assertThat(unified)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_task (")
|
||||
.contains("loading_plan_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("container_booking_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("cabinet_no VARCHAR(64) NOT NULL")
|
||||
.contains("container_no VARCHAR(64) NULL")
|
||||
.contains("task_status VARCHAR(24) NOT NULL DEFAULT 'LOADING'")
|
||||
.contains("column_name = 'task_status' AND LOWER(column_type) = 'varchar(24)'")
|
||||
void unifiedInitializationCreatesSeparateLoadingAndInboundTaskShapes() throws Exception {
|
||||
String sql = unified();
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE fl_loading_pick_task")
|
||||
.contains("loading_plan_id bigint unsigned NOT NULL")
|
||||
.contains("container_booking_id bigint unsigned NOT NULL")
|
||||
.contains("task_status varchar(24) NOT NULL DEFAULT 'LOADING'")
|
||||
.contains("UNIQUE KEY uk_loading_pick_task_plan (loading_plan_id)")
|
||||
.contains("UNIQUE KEY uk_loading_pick_task_booking (container_booking_id)")
|
||||
.contains("KEY idx_loading_pick_container (container_no, task_status, started_at)")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_loading_pick_task (")
|
||||
.contains("tenant_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("inbound_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("expected_count INT UNSIGNED NULL")
|
||||
.contains("picked_count INT UNSIGNED NULL")
|
||||
.contains("UNIQUE KEY uk_loading_pick_tenant_inbound (tenant_id, inbound_id)")
|
||||
.contains("KEY idx_loading_pick_status (tenant_id, status, updated_at)");
|
||||
.contains("CREATE TABLE fl_inbound_loading_pick_task")
|
||||
.contains("tenant_id bigint unsigned NOT NULL")
|
||||
.contains("inbound_id bigint unsigned NOT NULL")
|
||||
.contains("UNIQUE KEY uk_loading_pick_tenant_inbound (tenant_id,inbound_id)")
|
||||
.contains("KEY idx_loading_pick_status (tenant_id,status,updated_at)")
|
||||
.doesNotContain("migrate_legacy_loading_pick_task");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("迁移拒绝缺列错类型和混合拣货表型")
|
||||
void shouldRejectMalformedLegacyPickTaskShapes() throws Exception {
|
||||
assertMalformedShapeGuards(migration());
|
||||
assertMalformedShapeGuards(unifiedMigration());
|
||||
void unifiedInitializationCreatesFinalConcurrencyAndCandidateIndexes() throws Exception {
|
||||
String sql = unified();
|
||||
assertThat(sql)
|
||||
.contains("active_inbound_detail_id bigint unsigned GENERATED ALWAYS AS")
|
||||
.contains("active_assignment_key varchar(96) GENERATED ALWAYS AS")
|
||||
.contains("UNIQUE KEY uk_loading_item_plan_assignment (loading_plan_id,active_assignment_key)")
|
||||
.contains("KEY idx_loading_item_active_detail (account_id,active_inbound_detail_id)")
|
||||
.contains("KEY idx_inbound_smart_loading (warehouse_id,process_status,channel_code,expected_arrival_at,id)")
|
||||
.contains("KEY idx_measure_inbound_status_id (tenant_id,inbound_id,status,id)")
|
||||
.contains("KEY idx_measure_item_candidate (tenant_id,measurement_id,inbound_detail_id)")
|
||||
.contains("KEY idx_loading_item_candidate (account_id,inbound_id,inbound_detail_id,deleted)")
|
||||
.contains("KEY idx_inbound_split_candidate")
|
||||
.doesNotContain("ensure_smart_loading_candidate_indexes");
|
||||
}
|
||||
|
||||
private static String migration() throws Exception {
|
||||
@Test
|
||||
void unifiedInitializationKeepsFinalLifecycleConstraintsAndTriggers() throws Exception {
|
||||
String sql = unified();
|
||||
assertThat(sql)
|
||||
.contains("source_type varchar(24) NOT NULL DEFAULT 'SIMULATION'")
|
||||
.contains("record_status varchar(16) NOT NULL DEFAULT 'ACTIVE'")
|
||||
.contains("merged_into_container_booking_id bigint unsigned DEFAULT NULL")
|
||||
.contains("UNIQUE KEY uk_container_booking_container_no (container_no)")
|
||||
.contains("KEY idx_container_booking_merge (record_status,merged_into_container_booking_id)")
|
||||
.contains("CREATE TRIGGER trg_loading_plan_before_update")
|
||||
.contains("装柜状态只允许单向推进")
|
||||
.contains("CREATE TRIGGER trg_loading_item_before_update")
|
||||
.contains("装柜完成后的明细永久冻结")
|
||||
.contains("CREATE TRIGGER trg_container_booking_before_update")
|
||||
.contains("装柜作业开始后仅允许修改集装箱号与放舱运输数据")
|
||||
.doesNotContain("assert_phase3_smart_loading_schema");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedInitializationSeedsSmartLoadingPermissionsForBothPlatformRoles() throws Exception {
|
||||
String sql = unified();
|
||||
assertThat(sql)
|
||||
.contains("(5102090,0,'PLATFORM','MENU',5102000,'platform:firstleg:container:list'")
|
||||
.contains("(5102094,0,'PLATFORM','MENU',5102000,'platform:firstleg:smart-loading:list'")
|
||||
.contains("(5102100,0,'PLATFORM','MENU',5102000,'platform:firstleg:preload:list'")
|
||||
.contains("(5102106,0,'PLATFORM','MENU',5102000,'platform:firstleg:loading-list:list'")
|
||||
.contains("platform:firstleg:container:list")
|
||||
.contains("platform:firstleg:smart-loading:assign")
|
||||
.contains("platform:firstleg:preload:confirm")
|
||||
.contains("platform:firstleg:loading-list:list")
|
||||
.contains("VALUES (4000001,5102106,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000003,5102106,0,CURRENT_TIMESTAMP(3))");
|
||||
}
|
||||
|
||||
private static String moduleMigration() throws Exception {
|
||||
return new ClassPathResource("db/phase3-smart-loading-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String unifiedMigration() throws Exception {
|
||||
Path working = Path.of("").toAbsolutePath();
|
||||
Path file = working.resolve("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
if (!Files.exists(file)) {
|
||||
file = working.resolve("../deploy/sql/头程数据库统一增量-MySQL8.sql").normalize();
|
||||
}
|
||||
return Files.readString(file, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void assertPermissionContract(String sql) {
|
||||
int message = sql.indexOf("智能装柜迁移失败:平台角色授权未完整写入");
|
||||
assertThat(message).isGreaterThan(0);
|
||||
|
||||
int grantStart = sql.lastIndexOf("INSERT IGNORE INTO iam_role_permission", message);
|
||||
int grantEnd = sql.indexOf(';', grantStart);
|
||||
assertThat(permissions(sql.substring(grantStart, grantEnd))).isEqualTo(PHASE3_PERMISSIONS);
|
||||
|
||||
int assertionStart = sql.lastIndexOf("IF (SELECT COUNT(DISTINCT CONCAT", message);
|
||||
String assertion = sql.substring(assertionStart, message);
|
||||
assertThat(permissions(assertion)).isEqualTo(PHASE3_PERMISSIONS);
|
||||
assertThat(assertion).contains("<> 22 THEN");
|
||||
}
|
||||
|
||||
private static Set<String> permissions(String sql) {
|
||||
Matcher matcher = PERMISSION_PATTERN.matcher(sql);
|
||||
return matcher.results().map(result -> result.group(1)).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static void assertMalformedShapeGuards(String sql) {
|
||||
assertThat(sql)
|
||||
.containsAnyOf("source_total_columns = 11", "source_total = 11")
|
||||
.containsAnyOf("source_smart_columns = 11", "source_smart = 11")
|
||||
.containsAnyOf("source_total_columns = 12", "source_total = 12")
|
||||
.containsAnyOf("source_inbound_columns = 12", "source_inbound = 12")
|
||||
.contains("column_name = 'version'")
|
||||
.contains("column_name = 'remark'")
|
||||
.contains("column_name = 'updated_by'")
|
||||
.contains("column_name = 'updated_by_name'")
|
||||
.contains("column_name = 'created_at'")
|
||||
.contains("column_name = 'updated_at'")
|
||||
.contains("LOWER(column_type) = 'int'")
|
||||
.contains("LOWER(column_type) = 'int unsigned'")
|
||||
.contains("LOWER(extra) LIKE '%on update current_timestamp%'")
|
||||
.contains("uk_loading_pick_tenant_inbound")
|
||||
.contains("idx_loading_pick_status")
|
||||
.contains("referenced_table_name = 'fl_inbound_order'")
|
||||
.contains("字段签名未知或混合");
|
||||
}
|
||||
|
||||
private static int count(String value, String token) {
|
||||
return (value.length() - value.replace(token, "").length()) / token.length();
|
||||
private static String unified() throws Exception {
|
||||
Path direct = Path.of("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
Path path = Files.exists(direct) ? direct : Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
return Files.readString(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-15
@@ -22,12 +22,12 @@ class TerminalDrayageSqlContractTest {
|
||||
assertThat(selected).isEqualTo(fields);
|
||||
if (name.equals("pendingMails")) assertThat(sql).contains("earlier.container_booking_id=m.container_booking_id", "earlier.id<m.id", "earlier.status<>'SENT'");
|
||||
}
|
||||
for (Path path : java.util.List.of(Path.of("src/main/resources/db/phase3-terminal-drayage-schema.sql"), Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"))) {
|
||||
assertThat(Files.readString(path)).contains(
|
||||
"CREATE TABLE IF NOT EXISTS fl_terminal_drayage_mail (",
|
||||
"CHECK(status IN ('PENDING','SENDING','SENT','FAILED'))",
|
||||
"('SELECT_FLEET','SEND_PICKUP_EMAIL','SCHEDULE_PICKUP','RECORD_EMPTY_RETURN')");
|
||||
}
|
||||
String baseline = Files.readString(Path.of("src/main/resources/db/phase3-terminal-drayage-schema.sql"));
|
||||
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertThat(baseline).contains("CREATE TABLE IF NOT EXISTS fl_terminal_drayage_mail (");
|
||||
for (String sql : java.util.List.of(baseline, unified)) assertThat(sql).contains(
|
||||
"PENDING","SENDING","SENT","FAILED",
|
||||
"SELECT_FLEET","SEND_PICKUP_EMAIL","SCHEDULE_PICKUP","RECORD_EMPTY_RETURN");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -35,10 +35,10 @@ class TerminalDrayageSqlContractTest {
|
||||
String baseline = Files.readString(Path.of("src/main/resources/db/phase3-terminal-drayage-schema.sql"));
|
||||
assertThat(baseline).doesNotContain("actual_pickup_at", "chk_terminal_drayage_times");
|
||||
String release = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertThat(release).contains("ALTER TABLE fl_terminal_drayage DROP COLUMN actual_pickup_at",
|
||||
"DROP CHECK chk_terminal_drayage_history_action",
|
||||
"ADD CONSTRAINT chk_terminal_drayage_history_action CHECK(action IN");
|
||||
assertThat(release).doesNotContain("chk_terminal_drayage_times");
|
||||
assertThat(release)
|
||||
.contains("CREATE TABLE fl_terminal_drayage")
|
||||
.contains("CONSTRAINT chk_terminal_drayage_history_action CHECK")
|
||||
.doesNotContain("actual_pickup_at", "chk_terminal_drayage_times", "ALTER TABLE fl_terminal_drayage");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,12 +62,13 @@ class TerminalDrayageSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationRepairsColumnsAfterAPartialDdlRun() throws Exception {
|
||||
void unifiedInitializationCreatesFinalColumnsWithoutRepairDdl() throws Exception {
|
||||
String sql = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertThat(sql).contains(
|
||||
"ALTER TABLE fl_terminal_drayage",
|
||||
"ALTER TABLE fl_terminal_drayage_history",
|
||||
"ALTER TABLE fl_terminal_drayage_operation",
|
||||
"MODIFY COLUMN estimated_empty_return_at DATETIME(3) NULL COMMENT '服务端按卸柜完成时间加72小时计算的预计还柜时间'");
|
||||
"CREATE TABLE fl_terminal_drayage",
|
||||
"CREATE TABLE fl_terminal_drayage_history",
|
||||
"CREATE TABLE fl_terminal_drayage_operation",
|
||||
"estimated_empty_return_at datetime(3) DEFAULT NULL COMMENT '服务端按卸柜完成时间加72小时计算的预计还柜时间'");
|
||||
assertThat(sql).doesNotContain("ALTER TABLE fl_terminal_drayage");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,23 +7,18 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class NotificationSqlContractTest {
|
||||
@Test void definesTablesIsolationIdempotencyAndPermissions() throws Exception {String sql;try(var in=getClass().getResourceAsStream("/db/notification-center.sql")){assertThat(in).isNotNull();sql=new String(in.readAllBytes(),StandardCharsets.UTF_8);}assertThat(sql).contains("iam_notification_template","iam_notification_task","iam_notification_recipient","iam_notification_inbox","iam_notification_event_inbox","uk_notification_task_no","uk_notification_task_idempotency","uk_notification_source_event","uk_notification_recipient","idx_notification_inbox_list","platform:notification:template:status","platform:notification:task:retry","platform:notification:read","platform:notification:read-all","merchant:notification:view","merchant:notification:read","merchant:notification:read-all").contains("'MENU',5106000,'platform:notification:template:list'","'MENU',5106000,'platform:notification:task:list'");}
|
||||
|
||||
@Test void unifiedMigrationContainsNotificationBaselineAndSevenIndexAssertion() throws Exception {
|
||||
@Test void cleanInitializationContainsNotificationSchemaAndPermissionSeeds() throws Exception {
|
||||
Path direct=Path.of("deploy","sql","头程数据库统一增量-MySQL8.sql");
|
||||
Path migration=Files.exists(direct)?direct:Path.of("..","deploy","sql","头程数据库统一增量-MySQL8.sql").normalize();
|
||||
String sql=Files.readString(migration,StandardCharsets.UTF_8);
|
||||
assertThat(sql).contains("iam_notification_template","iam_notification_task","iam_notification_recipient",
|
||||
assertThat(sql).contains("CREATE TABLE iam_notification_template","CREATE TABLE iam_notification_task","CREATE TABLE iam_notification_recipient",
|
||||
"iam_notification_inbox","iam_notification_event_inbox","uk_notification_template_code",
|
||||
"uk_notification_task_no","uk_notification_task_idempotency","uk_notification_source_event",
|
||||
"uk_notification_recipient","uk_notification_inbox_recipient","uk_notification_event",
|
||||
"assert_notification_center_schema","permission_code='merchant:notification:view'",
|
||||
"permission_code LIKE 'platform:notification%'");
|
||||
assertThat(sql).contains("COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema='baiyue_iam'");
|
||||
"'merchant:notification:view'", "'platform:notification:template:list'",
|
||||
"'platform:notification:task:list'");
|
||||
assertThat(sql)
|
||||
.contains(")) <> 7 THEN")
|
||||
.contains("permission_code='merchant:notification:view')) <> 13 THEN")
|
||||
.contains("permission_code LIKE 'platform:notification%') <> 12 THEN")
|
||||
.contains("'merchant:notification:view','merchant:notification:read','merchant:notification:read-all')) <> 3 THEN");
|
||||
assertThat(sql.indexOf("(5106014,0,'MERCHANT','BUTTON'"))
|
||||
.isLessThan(sql.indexOf("CALL baiyue_iam.assert_notification_center_schema();"));
|
||||
.contains("(5106014,0,'MERCHANT','BUTTON'")
|
||||
.doesNotContain("assert_notification_center_schema", "information_schema.statistics");
|
||||
}
|
||||
}
|
||||
|
||||
+7
-11
@@ -60,15 +60,9 @@ class AdminPrototypeMenuPermissionSqlContractTest {
|
||||
}
|
||||
assertThat(unified)
|
||||
.contains("platform:finance:receivable-statistics:export")
|
||||
.contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 34")
|
||||
.contains("(p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 35")
|
||||
.contains("Finance 导出和下载中心授权不完整")
|
||||
.contains("客户全局 Finance 权限模板不完整")
|
||||
.contains("客户全局管理员 Finance 模板授权不完整")
|
||||
.satisfies(content -> assertThat(content.indexOf("(5105050,0,'PLATFORM','BUTTON'"))
|
||||
.isLessThan(content.indexOf("CREATE PROCEDURE assert_finance_permissions()")))
|
||||
.satisfies(content -> assertThat(content.indexOf("CALL migrate_wallet_permissions();"))
|
||||
.isLessThan(content.indexOf("CALL assert_finance_permissions();")));
|
||||
.contains("(4000001,5105050,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000100,5305010,0,CURRENT_TIMESTAMP(3))")
|
||||
.doesNotContain("CREATE PROCEDURE assert_finance_permissions", "CALL migrate_wallet_permissions");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -80,8 +74,10 @@ class AdminPrototypeMenuPermissionSqlContractTest {
|
||||
.contains("'platform:overseas-storage-fee:create','platform:overseas-storage-fee:delete'");
|
||||
|
||||
String unified = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8);
|
||||
assertThat(unified).contains("<> 4")
|
||||
.contains("platform:overseas-storage-fee:list', 'platform:overseas-storage-fee:create'");
|
||||
assertThat(unified)
|
||||
.contains("'platform:overseas-storage-fee:list','海外仓阶梯费用','/customers/overseas-storage-fees','GET',20,'ACTIVE'")
|
||||
.contains("'platform:overseas-storage-fee:create','新增海外仓阶梯费用',NULL,NULL,10,'INACTIVE'")
|
||||
.contains("'platform:overseas-storage-fee:delete','删除海外仓阶梯费用',NULL,NULL,30,'INACTIVE'");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+5
-16
@@ -90,27 +90,16 @@ class ContainerWorkspacePermissionSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedIncrementContainsTheCompleteModuleSql() throws Exception {
|
||||
void cleanInitializationContainsTheCompletePermissionCatalog() throws Exception {
|
||||
Path unifiedPath = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
if (!Files.exists(unifiedPath)) {
|
||||
unifiedPath = Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
}
|
||||
String unified = Files.readString(unifiedPath, StandardCharsets.UTF_8);
|
||||
String startMarker = "-- 货柜双工作台权限开始(与 iam/db/container-workspace-permissions.sql 保持一致)";
|
||||
String endMarker = "-- 货柜双工作台权限结束,恢复头程数据库上下文";
|
||||
int start = unified.indexOf(startMarker);
|
||||
int bodyStart = unified.indexOf('\n', start) + 1;
|
||||
int end = unified.indexOf(endMarker, bodyStart);
|
||||
|
||||
assertThat(start).isGreaterThanOrEqualTo(0);
|
||||
assertThat(bodyStart).isGreaterThan(start);
|
||||
assertThat(end).isGreaterThan(bodyStart);
|
||||
assertThat(normalizeLineEndings(unified.substring(bodyStart, end)).trim())
|
||||
.isEqualTo(normalizeLineEndings(sql()).trim());
|
||||
}
|
||||
|
||||
private static String normalizeLineEndings(String value) {
|
||||
return value.replace("\r\n", "\n");
|
||||
assertThat(codes(unified)).containsAll(REQUIRED);
|
||||
assertThat(unified)
|
||||
.contains("(5102090,0,'PLATFORM','MENU'", "(5102205,0,'PLATFORM','BUTTON'")
|
||||
.doesNotContain("migrate_container_workspace_permissions", "ON DUPLICATE KEY UPDATE");
|
||||
}
|
||||
|
||||
private static String sql() throws Exception {
|
||||
|
||||
@@ -20,15 +20,24 @@ class CustomerCodeSchemaContractTest {
|
||||
}
|
||||
String unified = Files.readString(unifiedPath, StandardCharsets.UTF_8);
|
||||
|
||||
assertSequenceDefinition(baseline);
|
||||
assertSequenceDefinition(unified);
|
||||
assertMigrationSequenceDefinition(baseline);
|
||||
assertInitializationSequenceDefinition(unified);
|
||||
}
|
||||
|
||||
private static void assertSequenceDefinition(String sql) {
|
||||
private static void assertMigrationSequenceDefinition(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("tenant_code VARCHAR(8) NOT NULL")
|
||||
.contains("CREATE TABLE IF NOT EXISTS iam_number_sequence")
|
||||
.contains("PRIMARY KEY (sequence_key)")
|
||||
.contains("INSERT IGNORE INTO iam_number_sequence(sequence_key, next_value) VALUES ('CUSTOMER_CODE', 1)");
|
||||
}
|
||||
|
||||
private static void assertInitializationSequenceDefinition(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("tenant_code varchar(8) NOT NULL")
|
||||
.contains("CREATE TABLE iam_number_sequence")
|
||||
.contains("PRIMARY KEY (sequence_key)")
|
||||
.contains("INSERT INTO iam_number_sequence (sequence_key, next_value, updated_at) VALUES ('CUSTOMER_CODE',1")
|
||||
.doesNotContain("CREATE TABLE IF NOT EXISTS iam_number_sequence", "INSERT IGNORE INTO iam_number_sequence");
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -28,7 +28,9 @@ class CustomerDimensionInitializationSqlContractTest {
|
||||
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");
|
||||
assertThat(unified)
|
||||
.contains("CREATE TABLE iam_customer_dimension_initialization_task")
|
||||
.doesNotContain("information_schema.columns", "CREATE TABLE IF NOT EXISTS");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+17
-24
@@ -49,7 +49,7 @@ class CustomerProfileMigrationContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleCase03() throws IOException {
|
||||
void cleanInitializationContainsTheFinalCustomerProfileContract() throws IOException {
|
||||
Path repositoryRoot = Path.of(System.getProperty("maven.multiModuleProjectDirectory", ".."));
|
||||
Path migration = repositoryRoot.resolve("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
Path readme = repositoryRoot.resolve("deploy/sql/README.md");
|
||||
@@ -60,43 +60,36 @@ class CustomerProfileMigrationContractTest {
|
||||
String instructions = Files.readString(readme, StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("customer_nickname VARCHAR(32)")
|
||||
.contains("ALTER TABLE iam_tenant ADD COLUMN customer_nickname VARCHAR(32)")
|
||||
.contains("ALTER TABLE iam_tenant MODIFY COLUMN status")
|
||||
.contains("customer_nickname varchar(32)")
|
||||
.contains("DEFAULT 'PROFILE_REQUIRED'")
|
||||
.contains("CALL phase1_iam_add_column('iam_tenant', 'approved_at'")
|
||||
.contains("CALL phase1_iam_add_column('iam_tenant', 'approved_by'")
|
||||
.contains("CALL phase1_iam_add_column('iam_tenant', 'reject_reason'")
|
||||
.contains("CALL phase1_iam_add_column('iam_tenant_qualification', 'review_comment'")
|
||||
.contains("approved_at datetime(3)")
|
||||
.contains("approved_by bigint")
|
||||
.contains("reject_reason varchar(500)")
|
||||
.contains("review_comment varchar(500)")
|
||||
.contains("platform:customer:view")
|
||||
.contains("platform:customer:export")
|
||||
.contains("platform:download:list")
|
||||
.contains("'MERCHANT_OWNER'")
|
||||
.contains("owner_role.id, permission.id, 0")
|
||||
.contains("统一增量全部执行完成");
|
||||
.contains("SELECT 'PASS' AS initialization_status")
|
||||
.doesNotContain("ALTER TABLE iam_tenant", "phase1_iam_add_column");
|
||||
assertThat(instructions)
|
||||
.contains("phase1-platform-master-data-and-permission-management.sql")
|
||||
.contains("phase1-customer-profile.sql")
|
||||
.contains("phase1-onboarding.sql")
|
||||
.contains("`PROFILE_REQUIRED` 默认状态、审核结果字段、资质审核备注及成员审计字段")
|
||||
.contains("全局 `MERCHANT_OWNER` 角色模板")
|
||||
.contains("测试环境结构发布只执行全量固定入口");
|
||||
.contains("当前开发阶段只维护纯初始化")
|
||||
.contains("模块 `src/main/resources/db/` 用作结构来源和开发追溯")
|
||||
.contains("不再向历史迁移文件追加发布步骤");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationGrantsCustomerOperationsToPlatformAdmin() throws IOException {
|
||||
void cleanInitializationGrantsCustomerOperationsToPlatformAdmin() throws IOException {
|
||||
Path repositoryRoot = Path.of(System.getProperty("maven.multiModuleProjectDirectory", ".."));
|
||||
String sql = Files.readString(
|
||||
repositoryRoot.resolve("deploy/sql/头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
int grantStart = sql.indexOf("-- 新增平台操作权限默认只授予系统管理员。");
|
||||
int grantEnd = sql.indexOf("-- 已有客户运营角色继续具备", grantStart);
|
||||
|
||||
assertThat(grantStart).isGreaterThanOrEqualTo(0);
|
||||
assertThat(grantEnd).isGreaterThan(grantStart);
|
||||
assertThat(sql.substring(grantStart, grantEnd))
|
||||
assertThat(sql)
|
||||
.contains("'platform:customer:reset-password'")
|
||||
.contains("'platform:customer:delete'")
|
||||
.contains("'platform:customer:impersonate'");
|
||||
.contains("'platform:customer:impersonate'")
|
||||
.contains("(4000001,5100016,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000001,5100017,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000001,5100018,0,CURRENT_TIMESTAMP(3))");
|
||||
}
|
||||
}
|
||||
|
||||
+6
-18
@@ -46,28 +46,16 @@ class FinanceG1G2PermissionSqlContractTest {
|
||||
|
||||
@Test
|
||||
void unifiedSqlMirrorsCatalogAndGrantsTheFinanceCatalogToPlatformAdmin() throws Exception {
|
||||
String sql = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8)
|
||||
.replace("\r\n", "\n");
|
||||
String financeSection = sql.substring(sql.indexOf("-- Finance F1 人工财务权限"),
|
||||
sql.indexOf("-- Admin 原型菜单与财务五页权限补充"));
|
||||
String sql = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(financeSection)
|
||||
.contains("(5105024, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:review'")
|
||||
.contains("(5105025, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:edit'")
|
||||
.contains("NULL, 'POST', 150, 'ACTIVE', 1, 0")
|
||||
.contains("NULL, 'PUT', 160, 'ACTIVE', 1, 0")
|
||||
.contains("permission_code IN (\n 'platform:finance',")
|
||||
.contains("Finance G1-G4");
|
||||
assertThat(financeSection)
|
||||
.contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission")
|
||||
.contains("status='INACTIVE',deleted=1");
|
||||
assertThat(sql)
|
||||
.contains("(5105024,0,'PLATFORM','BUTTON',5105041,'platform:finance:fee-source:review'")
|
||||
.contains("(5105025,0,'PLATFORM','BUTTON',5105041,'platform:finance:fee-source:edit'")
|
||||
.contains("(4000001,5105024,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000001,5105025,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("platform:finance:settlement-ar:list")
|
||||
.contains("platform:finance:settlement-ap:list")
|
||||
.contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 34")
|
||||
.contains("rp.role_id=4000001")
|
||||
.contains("LIKE 'platform:finance:%')) <> 35")
|
||||
.contains("平台管理员 Finance 授权不完整");
|
||||
.doesNotContain("migrate_finance_g1_g4_permissions", "status='INACTIVE',deleted=1");
|
||||
}
|
||||
|
||||
private static Path resolveUnifiedSql() {
|
||||
|
||||
+4
-12
@@ -39,21 +39,13 @@ class FinanceG3G4PermissionSqlContractTest {
|
||||
@Test
|
||||
void unifiedSqlContainsCatalogAndGrantsItToPlatformAdmin() throws Exception {
|
||||
String sql = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8);
|
||||
String financeSection = sql.substring(sql.indexOf("-- Finance F1 人工财务权限"),
|
||||
sql.indexOf("-- Admin 原型菜单与财务五页权限补充"));
|
||||
for (String code : CODES) assertThat(financeSection).contains(code);
|
||||
assertThat(financeSection)
|
||||
.contains("migrate_finance_g1_g4_permissions")
|
||||
.contains("finance G1-G4 permission id/code conflict");
|
||||
assertThat(financeSection)
|
||||
.contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission")
|
||||
.contains("platform:finance:reconciliation:create");
|
||||
for (String code : CODES) assertThat(sql).contains(code);
|
||||
assertThat(sql)
|
||||
.contains("(4000001,5105026,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000001,5105035,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("platform:finance:settlement-ar:list")
|
||||
.contains("platform:finance:settlement-ap:list")
|
||||
.contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 34")
|
||||
.contains("rp.role_id=4000001")
|
||||
.contains("LIKE 'platform:finance:%')) <> 35");
|
||||
.doesNotContain("migrate_finance_g1_g4_permissions", "finance G1-G4 permission id/code conflict");
|
||||
}
|
||||
|
||||
private static Path resolveUnifiedSql() {
|
||||
|
||||
+5
-11
@@ -63,10 +63,11 @@ class InboundHoldPermissionSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedIncrementContainsTheSameMigrationBody() throws Exception {
|
||||
String marker = "DROP PROCEDURE IF EXISTS migrate_inbound_hold_permission;";
|
||||
|
||||
assertThat(migrationBody(unifiedSql(), marker)).isEqualTo(migrationBody(sql(), marker));
|
||||
void cleanInitializationContainsThePermissionAndDefaultGrant() throws Exception {
|
||||
assertThat(unifiedSql())
|
||||
.contains("(5102032,0,'PLATFORM','BUTTON',5102030,'platform:firstleg:inbound:hold'")
|
||||
.contains("(4000001,5102032,0,CURRENT_TIMESTAMP(3))")
|
||||
.doesNotContain("migrate_inbound_hold_permission", "ON DUPLICATE KEY UPDATE");
|
||||
}
|
||||
|
||||
private static String sql() throws Exception {
|
||||
@@ -80,11 +81,4 @@ class InboundHoldPermissionSqlContractTest {
|
||||
return Files.readString(path.normalize(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String migrationBody(String sql, String marker) {
|
||||
int start = sql.indexOf(marker);
|
||||
int end = sql.indexOf(marker, start + marker.length());
|
||||
assertThat(start).isGreaterThanOrEqualTo(0);
|
||||
assertThat(end).isGreaterThan(start);
|
||||
return sql.substring(start, end + marker.length()).replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
}
|
||||
|
||||
+18
-2
@@ -18,9 +18,17 @@ class InitialIdentitySeedSqlContractTest {
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String unified = Files.readString(unifiedSql(), StandardCharsets.UTF_8);
|
||||
assertPlatformIdentitySeed(identitySeed);
|
||||
assertPlatformIdentitySeed(unified);
|
||||
assertCleanPlatformIdentitySeed(unified);
|
||||
assertGlobalMerchantTemplate(templateSeed);
|
||||
assertGlobalMerchantTemplate(unified);
|
||||
assertCleanGlobalMerchantTemplate(unified);
|
||||
}
|
||||
|
||||
private static void assertCleanPlatformIdentitySeed(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("(0,'PLATFORM','虫虫出海平台'")
|
||||
.contains("(2000001,1000001,'INTERNAL','平台系统管理员'")
|
||||
.contains("(3000001,0,2000001,'EMPLOYEE'")
|
||||
.doesNotContain("merchant_admin", "IT_TEST", "一期演示企业", "ON DUPLICATE KEY UPDATE");
|
||||
}
|
||||
|
||||
private static void assertPlatformIdentitySeed(String sql) {
|
||||
@@ -47,6 +55,14 @@ class InitialIdentitySeedSqlContractTest {
|
||||
.doesNotContain("WHERE tenant_id = 10001 AND app_code = 'MERCHANT'");
|
||||
}
|
||||
|
||||
private static void assertCleanGlobalMerchantTemplate(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("(4000100,0,'MERCHANT','MERCHANT_OWNER'")
|
||||
.contains("(5300000,0,'MERCHANT','MENU'")
|
||||
.contains("(5301024,0,'MERCHANT','BUTTON'")
|
||||
.doesNotContain("WHERE tenant_id = 10001 AND app_code = 'MERCHANT'");
|
||||
}
|
||||
|
||||
private static Path unifiedSql() {
|
||||
Path path = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
return Files.exists(path) ? path : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
|
||||
@@ -13,7 +13,16 @@ class LocationPermissionSqlContractTest {
|
||||
@Test
|
||||
void acceptsKnownPollutionAndRestoresCanonicalPermissionIds() throws Exception {
|
||||
assertRepairContract(moduleSql());
|
||||
assertRepairContract(unifiedSql());
|
||||
assertFinalInitializationContract(unifiedSql());
|
||||
}
|
||||
|
||||
private static void assertFinalInitializationContract(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("(5103040,0,'PLATFORM','MENU',5103000,'platform:download:list'")
|
||||
.contains("(5103041,0,'PLATFORM','BUTTON',5103040,'platform:download:all'")
|
||||
.contains("(5103060,0,'PLATFORM','MENU',5103000,'platform:location:list'")
|
||||
.contains("(5103061,0,'PLATFORM','BUTTON',5103060,'platform:location:create'")
|
||||
.doesNotContain("tmp_location_role_grant", "库位权限迁移失败");
|
||||
}
|
||||
|
||||
private static void assertRepairContract(String sql) {
|
||||
|
||||
+30
-22
@@ -64,10 +64,10 @@ class OverseasPhase4PermissionSqlContractTest {
|
||||
Set<String> moduleCatalog = codes(new ClassPathResource("db/lastleg-permissions.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8));
|
||||
assertThat(codes(sql)).containsAll(moduleCatalog);
|
||||
assertThat(sql).contains("baiyue_iam.iam_permission", "baiyue_iam.iam_role_permission", "'lastleg:pda:operate'");
|
||||
assertPdaPickerGrant(sql);
|
||||
assertCustomAdminGrant(sql);
|
||||
assertCustomAdminNavigationGrant(sql);
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE iam_permission", "CREATE TABLE iam_role_permission", "'lastleg:pda:operate'")
|
||||
.contains("(4000005,5107015,0,CURRENT_TIMESTAMP(3))")
|
||||
.doesNotContain("assert_overseas_pda_picker_permission", "DELETE role_permission FROM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,25 +78,33 @@ class OverseasPhase4PermissionSqlContractTest {
|
||||
if (!Files.exists(path)) path = Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
String unifiedSql = Files.readString(path, StandardCharsets.UTF_8);
|
||||
|
||||
for (String sql : List.of(moduleSql, unifiedSql)) {
|
||||
String matrix = matrixBlock(sql);
|
||||
assertThat(matrix).contains("WAREHOUSE_OPERATOR", "WAREHOUSE_TALLY");
|
||||
assertThat(codes(matrix)).containsExactlyInAnyOrder(
|
||||
"platform:overseas-warehouse:last-mile-order:list",
|
||||
"platform:overseas-warehouse:inbound-order:list",
|
||||
"platform:overseas-warehouse:relabel-order:list",
|
||||
"platform:overseas-warehouse:inventory:list",
|
||||
"platform:overseas-warehouse:warehouse:list",
|
||||
"platform:overseas-warehouse:location:list",
|
||||
"platform:overseas-warehouse:pallet:list",
|
||||
"platform:overseas-business:purchase:list",
|
||||
"platform:overseas-business:truck:list",
|
||||
"platform:overseas-business:looseboard:list");
|
||||
assertThat(matrix).doesNotContain("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
||||
assertThat(matrix).doesNotContain(":create'", ":edit'", ":status'", ":export'", ":adjust'");
|
||||
assertThat(sql).contains("assert_phase4_warehouse_role_minimum", "<> 20",
|
||||
"role.role_code IN ('WAREHOUSE_OPERATOR','OVERSEAS_WAREHOUSE_PDA_OPERATOR','WAREHOUSE_TALLY','CN_FIRSTLEG_PDA_OPERATOR')");
|
||||
String matrix = matrixBlock(moduleSql);
|
||||
assertThat(matrix).contains("WAREHOUSE_OPERATOR", "WAREHOUSE_TALLY");
|
||||
assertThat(codes(matrix)).containsExactlyInAnyOrder(
|
||||
"platform:overseas-warehouse:last-mile-order:list",
|
||||
"platform:overseas-warehouse:inbound-order:list",
|
||||
"platform:overseas-warehouse:relabel-order:list",
|
||||
"platform:overseas-warehouse:inventory:list",
|
||||
"platform:overseas-warehouse:warehouse:list",
|
||||
"platform:overseas-warehouse:location:list",
|
||||
"platform:overseas-warehouse:pallet:list",
|
||||
"platform:overseas-business:purchase:list",
|
||||
"platform:overseas-business:truck:list",
|
||||
"platform:overseas-business:looseboard:list");
|
||||
assertThat(matrix).doesNotContain("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
||||
assertThat(matrix).doesNotContain(":create'", ":edit'", ":status'", ":export'", ":adjust'");
|
||||
assertThat(moduleSql).contains("assert_phase4_warehouse_role_minimum", "<> 20",
|
||||
"role.role_code IN ('WAREHOUSE_OPERATOR','OVERSEAS_WAREHOUSE_PDA_OPERATOR','WAREHOUSE_TALLY','CN_FIRSTLEG_PDA_OPERATOR')");
|
||||
|
||||
for (int permissionId : new int[] {5107000, 5107010, 5107020, 5107030, 5107040,
|
||||
5107050, 5107060, 5107100, 5107110, 5107120}) {
|
||||
assertThat(unifiedSql)
|
||||
.contains("(4000004," + permissionId + ",0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000006," + permissionId + ",0,CURRENT_TIMESTAMP(3))");
|
||||
}
|
||||
assertThat(unifiedSql)
|
||||
.doesNotContain("(4000005,5107000,0,CURRENT_TIMESTAMP(3))")
|
||||
.doesNotContain("(4000007,5107000,0,CURRENT_TIMESTAMP(3))");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+14
-21
@@ -143,35 +143,28 @@ class Phase5DeliveryPermissionSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void domesticUnifiedEntryInlinesTheCompleteModuleSqlAfterPhase4Permissions() throws Exception {
|
||||
String moduleSql = sql().trim();
|
||||
void domesticCleanInitializationContainsTheCompletePermissionCatalog() throws Exception {
|
||||
String unifiedSql = repositoryFile("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
int overseasCatalog = unifiedSql.indexOf("-- 海外权限统一目录");
|
||||
int outboundDelivery = unifiedSql.indexOf("-- 海外排车、出库与派送权限(来源:iam/db/phase5-delivery-permissions.sql)");
|
||||
int bodyStart = unifiedSql.indexOf("USE baiyue_iam;", outboundDelivery);
|
||||
int nextSection = unifiedSql.indexOf("-- IAM 客户提交即成功", bodyStart);
|
||||
|
||||
assertThat(overseasCatalog).isGreaterThanOrEqualTo(0);
|
||||
assertThat(outboundDelivery).isGreaterThan(overseasCatalog);
|
||||
assertThat(bodyStart).isGreaterThan(outboundDelivery);
|
||||
assertThat(nextSection).isGreaterThan(bodyStart);
|
||||
assertThat(unifiedSql.substring(bodyStart, nextSection).trim()).isEqualTo(moduleSql);
|
||||
assertThat(codes(unifiedSql.substring(outboundDelivery, nextSection)))
|
||||
.containsExactlyInAnyOrderElementsOf(REQUIRED);
|
||||
assertThat(codes(unifiedSql)).containsAll(REQUIRED);
|
||||
assertThat(unifiedSql)
|
||||
.contains("(4000004,5107064,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000004,5107094,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000100,5306000,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000100,5306001,0,CURRENT_TIMESTAMP(3))")
|
||||
.doesNotContain("guard_phase5_delivery_permission_ids", "tmp_phase5_delivery_role_permission");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deploymentGuideUsesOnlyTheDomesticUnifiedEntry() throws Exception {
|
||||
void deploymentGuideDefinesCleanRegionalInitializationOnly() throws Exception {
|
||||
String readme = repositoryFile("deploy/sql/README.md");
|
||||
|
||||
assertThat(readme).contains(
|
||||
"海外排车、出库与派送权限",
|
||||
"测试和部署只执行固定入口,不再单独执行模块脚本",
|
||||
"排车列表/编辑/取消/转派送",
|
||||
"异常订单列表/创建/处理和客户回访列表/编辑十六项权限",
|
||||
"merchant:warehouse:delivery:list",
|
||||
"merchant:warehouse:delivery:create",
|
||||
"PDA 岗位不得获得上述后台权限");
|
||||
"当前开发阶段只维护纯初始化",
|
||||
"两个 SQL 文件沿用原文件名",
|
||||
"国内 IAM 是全部平台与客户权限的唯一权威",
|
||||
"模块 `src/main/resources/db/` 用作结构来源和开发追溯",
|
||||
"不再向历史迁移文件追加发布步骤");
|
||||
}
|
||||
|
||||
private static String sql() throws Exception {
|
||||
|
||||
@@ -22,10 +22,7 @@ class QualificationSchemaContractTest {
|
||||
|
||||
assertThat(baseline).contains("original_file_name VARCHAR(255) NOT NULL COMMENT '上传时原始文件名'");
|
||||
assertThat(unified)
|
||||
.contains("original_file_name VARCHAR(255) NOT NULL COMMENT '上传时原始文件名'")
|
||||
.contains("CALL phase1_iam_add_column('iam_tenant_qualification', 'original_file_name',")
|
||||
.contains("table_name='iam_tenant_qualification'")
|
||||
.contains("column_name='original_file_name'")
|
||||
.contains("统一迁移失败:IAM客户资质缺少原始文件名列");
|
||||
.contains("original_file_name varchar(255) NOT NULL COMMENT '上传时原始文件名'")
|
||||
.doesNotContain("phase1_iam_add_column", "information_schema.columns");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,18 +26,17 @@ class SecurityEventSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationIncludesSecurityEventSchemaAndAssertion() throws Exception {
|
||||
void cleanInitializationIncludesSecurityEventSchema() throws Exception {
|
||||
Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
Path migration = Files.exists(direct)
|
||||
? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql").normalize();
|
||||
String sql = Files.readString(migration, StandardCharsets.UTF_8);
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS iam_security_subject_version")
|
||||
.contains("CREATE TABLE IF NOT EXISTS iam_security_tenant_version")
|
||||
.contains("CREATE TABLE IF NOT EXISTS iam_security_event_outbox")
|
||||
.contains("CREATE TABLE iam_security_subject_version")
|
||||
.contains("CREATE TABLE iam_security_tenant_version")
|
||||
.contains("CREATE TABLE iam_security_event_outbox")
|
||||
.contains("idx_security_outbox_publish", "idx_security_outbox_aggregate")
|
||||
.contains("CALL assert_iam_security_event_schema();");
|
||||
assertThat(sql.indexOf("CALL assert_iam_security_event_schema();"))
|
||||
.isLessThan(sql.lastIndexOf("SELECT 'PASS' AS migration_status"));
|
||||
.contains("SELECT 'PASS' AS initialization_status")
|
||||
.doesNotContain("assert_iam_security_event_schema", "CREATE TABLE IF NOT EXISTS iam_security_event");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,19 @@ class WarehouseRoleSqlContractTest {
|
||||
@Test
|
||||
void unifiedDeploymentContainsTheSameWarehouseRoleContract() throws Exception {
|
||||
String sql = unifiedSql();
|
||||
assertWarehouseRoleContract(sql);
|
||||
assertThat(roleMigrationBlock(sql))
|
||||
.doesNotContain("\n ADD COLUMN IF NOT EXISTS")
|
||||
.doesNotContain("DROP CHECK IF EXISTS");
|
||||
assertThat(sql.lastIndexOf("SELECT 'PASS' AS migration_status"))
|
||||
.isGreaterThan(sql.indexOf("CALL assert_phase3_warehouse_roles_ready()"));
|
||||
assertThat(sql.trim()).endsWith("AS message;");
|
||||
assertThat(sql)
|
||||
.contains("(4000004,0,'PLATFORM','WAREHOUSE_OPERATOR','国内仓后台操作员','SYSTEM'")
|
||||
.contains("(4000005,0,'PLATFORM','OVERSEAS_WAREHOUSE_PDA_OPERATOR','海外仓 PDA 作业员','SYSTEM'")
|
||||
.contains("(4000006,0,'PLATFORM','WAREHOUSE_TALLY','国内仓理货复核员','SYSTEM'")
|
||||
.contains("(4000007,0,'PLATFORM','CN_FIRSTLEG_PDA_OPERATOR','国内头程 PDA 作业员','SYSTEM'")
|
||||
.contains("(6000004,4000004,0,'TENANT',NULL")
|
||||
.contains("(6000005,4000005,0,'TENANT',NULL")
|
||||
.contains("(6000006,4000006,0,'TENANT',NULL")
|
||||
.contains("(6000007,4000007,0,'TENANT',NULL")
|
||||
.contains("(4000005,5107015,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("(4000007,5102190,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("SELECT 'PASS' AS initialization_status")
|
||||
.doesNotContain("assert_phase3_warehouse_roles_ready", "ON DUPLICATE KEY UPDATE");
|
||||
}
|
||||
|
||||
private static void assertWarehouseRoleContract(String sql) {
|
||||
@@ -100,15 +106,6 @@ class WarehouseRoleSqlContractTest {
|
||||
return sql.substring(start, end > start ? end : sql.length());
|
||||
}
|
||||
|
||||
private static String roleMigrationBlock(String sql) {
|
||||
String marker = "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql";
|
||||
String endMarker = "-- BEGIN FINANCE FEE SOURCE MODULE MIGRATIONS";
|
||||
int start = sql.indexOf(marker);
|
||||
int end = sql.indexOf(endMarker, start + marker.length());
|
||||
if (start < 0 || end < 0) throw new IllegalStateException("统一入口缺少仓库角色连续区块");
|
||||
return sql.substring(start, end);
|
||||
}
|
||||
|
||||
private static String moduleSql() throws Exception {
|
||||
return new ClassPathResource("db/phase3-warehouse-roles.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
+7
-5
@@ -44,10 +44,12 @@ class DeliveryDocumentContractTest {
|
||||
@Test void baselineAndOverseasEntryContainSameImmutableRevisionTable() throws Exception {
|
||||
String baseline=Files.readString(Path.of("src/main/resources/db/lastleg.sql"));
|
||||
String overseas=Files.readString(Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql"));
|
||||
String marker="CREATE TABLE IF NOT EXISTS ll_delivery_document_revision (";
|
||||
String a=baseline.substring(baseline.indexOf(marker),baseline.indexOf(";",baseline.indexOf(marker))+1);
|
||||
String b=overseas.substring(overseas.indexOf(marker),overseas.indexOf(";",overseas.indexOf(marker))+1);
|
||||
assertThat(a.replaceAll("\\s+","")).isEqualTo(b.replaceAll("\\s+",""));
|
||||
assertThat(a).contains("uk_delivery_document_command","uk_delivery_document_version");
|
||||
for (String sql : List.of(baseline, overseas)) {
|
||||
assertThat(sql).contains("ll_delivery_document_revision",
|
||||
"uk_delivery_document_command", "uk_delivery_document_version",
|
||||
"snapshot_json", "request_hash");
|
||||
}
|
||||
assertThat(overseas).contains("CREATE TABLE ll_delivery_document_revision (")
|
||||
.doesNotContain("ALTER TABLE ll_delivery_document_revision");
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -68,11 +68,11 @@ class DeliverySchemaContractTest {
|
||||
|
||||
String overseas = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8).toLowerCase();
|
||||
assertTrue(overseas.contains("begin inline empty-database baseline: lastleg"));
|
||||
assertTrue(overseas.contains(createToken));
|
||||
assertTrue(overseas.contains("request_hash char(64) null"));
|
||||
assertTrue(overseas.indexOf(createToken) < overseas.indexOf("assert_phase5_lastleg_identifiers_migratable"));
|
||||
assertTrue(overseas.contains("alter table ll_delivery_order add column overseas_warehouse_name"));
|
||||
String overseasCreateToken = "create table ll_delivery_order";
|
||||
assertTrue(overseas.contains(overseasCreateToken));
|
||||
assertTrue(overseas.contains("overseas_warehouse_name varchar(255)"));
|
||||
assertTrue(overseas.contains("unique key uk_ll_delivery_outbound_destination"));
|
||||
assertFalse(overseas.contains("alter table ll_delivery_order"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+12
-20
@@ -5,34 +5,26 @@ import org.junit.jupiter.api.Test;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class Phase5PdaDeploymentSqlContractTest {
|
||||
@Test
|
||||
void overseasUnifiedMigrationConvergesPhase5IdentifiersWithoutBackfill() throws Exception {
|
||||
void overseasCleanInitializationDefinesFinalPhase5IdentifiersWithoutBackfill() throws Exception {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
int start = sql.indexOf("-- 五期排车、出库与海外PDA正式业务号/幂等约束");
|
||||
int end = sql.indexOf("USE baiyue_lastleg;", start);
|
||||
assertTrue(start >= 0 && end > start);
|
||||
String phase5 = sql.substring(start, end);
|
||||
String normalized = sql.replaceAll("\\s+", "");
|
||||
|
||||
assertTrue(phase5.contains("assert_phase5_lastleg_identifiers_migratable"));
|
||||
assertTrue(phase5.contains("assert_phase5_lastleg_identifiers_ready"));
|
||||
assertTrue(phase5.contains("warehouse_id IS NULL OR TRIM(warehouse_id)='' OR TRIM(warehouse_id)='0'"));
|
||||
assertTrue(phase5.contains("SIGNAL SQLSTATE '45000'"));
|
||||
assertTrue(phase5.contains("MODIFY COLUMN warehouse_id VARCHAR(128) NOT NULL"));
|
||||
assertTrue(phase5.contains("ADD COLUMN completion_idempotency_key VARCHAR(255) NULL"));
|
||||
assertTrue(phase5.contains("ADD UNIQUE KEY uk_ll_dispatch_order_no(dispatch_no)"));
|
||||
assertTrue(phase5.contains("ADD UNIQUE KEY uk_ll_outbound_no(outbound_no)"));
|
||||
assertTrue(phase5.contains("ADD UNIQUE KEY uk_ll_dispatch_pda_task_no(task_no)"));
|
||||
assertTrue(phase5.contains("ADD UNIQUE KEY uk_ll_outbound_pda_task_no(task_no)"));
|
||||
assertTrue(phase5.contains("ADD UNIQUE KEY uk_ll_outbound_pda_complete_key(customer_id,completion_idempotency_key)"));
|
||||
assertFalse(Pattern.compile("(?im)^\\s*UPDATE\\s+").matcher(phase5).find());
|
||||
assertFalse(phase5.contains("JOIN ll_dispatch_order"));
|
||||
assertTrue(sql.contains("warehouse_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL"));
|
||||
assertTrue(sql.contains("completion_idempotency_key varchar(255)"));
|
||||
assertTrue(normalized.contains("UNIQUEKEYuk_ll_dispatch_order_no(dispatch_no)"));
|
||||
assertTrue(normalized.contains("UNIQUEKEYuk_ll_outbound_no(outbound_no)"));
|
||||
assertTrue(normalized.contains("UNIQUEKEYuk_ll_dispatch_pda_task_no(task_no)"));
|
||||
assertTrue(normalized.contains("UNIQUEKEYuk_ll_outbound_pda_task_no(task_no)"));
|
||||
assertTrue(normalized.contains("UNIQUEKEYuk_ll_outbound_pda_complete_key(customer_id,completion_idempotency_key)"));
|
||||
assertFalse(sql.contains("assert_phase5_lastleg_identifiers_migratable"));
|
||||
assertFalse(sql.contains("ALTER TABLE ll_dispatch_order"));
|
||||
assertFalse(sql.contains("ALTER TABLE ll_outbound_order"));
|
||||
|
||||
assertFalse(sql.contains("uk_ll_dispatch_order_no(customer_id,dispatch_no)"));
|
||||
assertFalse(sql.contains("uk_ll_outbound_no(customer_id,outbound_no)"));
|
||||
|
||||
+14
-20
@@ -84,8 +84,7 @@ class ReceivingContainerContractTest {
|
||||
for (String table : Set.of("ll_inbound_plan_line", "ll_inbound_plan_pallet",
|
||||
"ll_inbound_plan_pallet_allocation")) {
|
||||
assertTrue(tableBlock(schema, table).contains("customer_id varchar(128) not null"), table);
|
||||
assertTrue(unified.contains("table_name='" + table + "' and column_name='customer_id'"), table);
|
||||
assertTrue(unified.contains("update " + table), table);
|
||||
assertTrue(tableBlock(unified, table).contains("customer_id varchar(128)"), table);
|
||||
}
|
||||
assertFalse(schema.contains("tenant_id varchar(64)"));
|
||||
assertFalse(unified.contains("column_name='tenant_id'"));
|
||||
@@ -99,9 +98,9 @@ class ReceivingContainerContractTest {
|
||||
assertFalse(tableBlock(schema, "ll_receiving_container_session").contains("customer_id"));
|
||||
assertFalse(tableBlock(schema, "ll_receiving_container_operation").contains("customer_id"));
|
||||
assertFalse(tableBlock(schema, "ll_container_shipping_gate").contains("customer_id"));
|
||||
assertTrue(unified.contains("alter table ll_receiving_container_session drop column customer_id"));
|
||||
assertTrue(unified.contains("alter table ll_receiving_container_operation drop column customer_id"));
|
||||
assertTrue(unified.contains("alter table ll_container_shipping_gate drop column customer_id"));
|
||||
assertFalse(tableBlock(unified, "ll_receiving_container_session").contains("customer_id"));
|
||||
assertFalse(tableBlock(unified, "ll_receiving_container_operation").contains("customer_id"));
|
||||
assertFalse(tableBlock(unified, "ll_container_shipping_gate").contains("customer_id"));
|
||||
assertTrue(schema.contains("create table if not exists ll_receiving_sku_receipt"));
|
||||
assertTrue(schema.contains("unique key uk_ll_receiving_sku_pallet (customer_id,line_id,pallet_id,sku_code)"));
|
||||
assertTrue(schema.contains("create table if not exists ll_receiving_fba_pallet_count"));
|
||||
@@ -117,22 +116,16 @@ class ReceivingContainerContractTest {
|
||||
assertTrue(receivingSession.contains("overseas_warehouse_id bigint not null"));
|
||||
assertTrue(receivingSession.contains("overseas_warehouse_name varchar(255) not null"));
|
||||
assertFalse(receivingSession.contains("photo_file_ids json"));
|
||||
assertTrue(unified.contains("create table if not exists ll_receiving_sku_receipt"));
|
||||
assertTrue(unified.contains("create table if not exists ll_receiving_fba_pallet_count"));
|
||||
assertTrue(unified.contains("column_name='sign_photos'"));
|
||||
assertTrue(unified.contains("create table ll_receiving_sku_receipt"));
|
||||
assertTrue(unified.contains("create table ll_receiving_fba_pallet_count"));
|
||||
assertTrue(tableBlock(unified, "ll_receiving_container_session").contains("sign_photos json"));
|
||||
assertTrue(schema.contains("wait_sign、signed、receiving、completed"));
|
||||
assertTrue(unified.contains("customer_id is null or l.customer_id=''"));
|
||||
assertTrue(unified.contains("customer_id is null or v.customer_id=''"));
|
||||
assertTrue(unified.contains("customer_id is null or a.customer_id=''"));
|
||||
assertTrue(unified.contains("signal sqlstate '45000'"));
|
||||
assertTrue(unified.contains("assert_receiving_global_locks_migratable"));
|
||||
assertTrue(unified.contains("where status in ('completed','completed_with_difference') and (active_operator_key is not null or active_container_key is not null)"));
|
||||
assertTrue(unified.contains("concat('operator:',operator_key)"));
|
||||
assertTrue(unified.contains("concat('container:',upper(trim(container_no)))"));
|
||||
assertTrue(unified.contains("assert_receiving_operation_global_key_migratable"));
|
||||
assertTrue(unified.contains("group_concat(column_name order by seq_in_index)= 'operator_key,idempotency_key'"));
|
||||
assertTrue(unified.contains("assert_pending_orders_do_not_use_source_warehouse"));
|
||||
assertTrue(unified.contains("assert_active_receiving_session_has_warehouse"));
|
||||
assertTrue(tableBlock(unified, "ll_receiving_container_session")
|
||||
.contains("unique key uk_ll_receiving_active_operator (active_operator_key)"));
|
||||
assertTrue(tableBlock(unified, "ll_receiving_container_operation")
|
||||
.contains("unique key uk_ll_receiving_operation (operator_key,idempotency_key)"));
|
||||
assertFalse(unified.contains("assert_receiving_global_locks_migratable"));
|
||||
assertFalse(unified.contains("alter table ll_receiving_container_session"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -178,6 +171,7 @@ class ReceivingContainerContractTest {
|
||||
|
||||
private static String tableBlock(String schema, String table) {
|
||||
int start = schema.indexOf("create table if not exists " + table + " (");
|
||||
if (start < 0) start = schema.indexOf("create table " + table + " (");
|
||||
int end = schema.indexOf(") engine=innodb", start);
|
||||
return start < 0 || end < 0 ? "" : schema.substring(start, end);
|
||||
}
|
||||
|
||||
+12
-4
@@ -28,11 +28,19 @@ class LocalLooseboardSchemaContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedIncrementalContainsHistoryIdempotencyAndOperationLedger() throws Exception {
|
||||
void unifiedInitializationContainsHistoryIdempotencyAndOperationLedger() throws Exception {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_local_looseboard_operation"));
|
||||
assertTrue(sql.contains("uk_ll_local_looseboard_operation_key(customer_id,order_id,idempotency_key)"));
|
||||
assertTrue(sql.contains("table_name='ll_local_looseboard_history' AND column_name='idempotency_key'"));
|
||||
String normalized = sql.replaceAll("\\s+", "");
|
||||
assertTrue(sql.contains("CREATE TABLE ll_local_looseboard_operation"));
|
||||
assertTrue(normalized.contains("uk_ll_local_looseboard_operation_key(customer_id,order_id,idempotency_key)"));
|
||||
assertTrue(tableBlock(sql, "ll_local_looseboard_history").contains("idempotency_key varchar(255)"));
|
||||
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("ALTER TABLE ll_local_looseboard_history"));
|
||||
}
|
||||
|
||||
private static String tableBlock(String sql, String table) {
|
||||
int start = sql.indexOf("CREATE TABLE " + table + " (");
|
||||
int end = sql.indexOf(";", start);
|
||||
return start < 0 || end < 0 ? "" : sql.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -45,6 +45,7 @@ class DomesticPartnerWarehouseClientContractTest {
|
||||
assertThat(compose).contains("INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN}")
|
||||
.contains("DOMESTIC_WAREHOUSE_URL: ${DOMESTIC_WAREHOUSE_URL}");
|
||||
assertThat(deploy).contains("FIRSTLEG_EVENT_BRIDGE_URL DOMESTIC_WAREHOUSE_URL")
|
||||
.contains("NACOS_SERVER_ADDR OVERSEAS_NACOS_NAMESPACE INTERNAL_SERVICE_TOKEN");
|
||||
.contains("NACOS_SERVER_ADDR OVERSEAS_NACOS_USERNAME OVERSEAS_NACOS_PASSWORD "
|
||||
+ "OVERSEAS_NACOS_NAMESPACE INTERNAL_SERVICE_TOKEN");
|
||||
}
|
||||
}
|
||||
|
||||
+17
-14
@@ -30,23 +30,26 @@ class OverseasProcurementSchemaContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedIncrementalContainsHistoryIdempotencyKeyForNewAndExistingDatabases() throws Exception {
|
||||
void unifiedInitializationContainsFinalProcurementAndInboxShapes() throws Exception {
|
||||
String sql = Files.readString(
|
||||
Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(sql.contains("idempotency_key VARCHAR(255) NULL COMMENT '操作幂等键'"));
|
||||
assertTrue(sql.contains(
|
||||
"ALTER TABLE ll_overseas_procurement_status_history ADD COLUMN idempotency_key VARCHAR(255) NULL"));
|
||||
assertTrue(sql.contains(
|
||||
"ALTER TABLE ll_overseas_procurement_order ADD COLUMN review_idempotency_key VARCHAR(255) NULL"));
|
||||
assertTrue(sql.contains(
|
||||
"ALTER TABLE ll_overseas_procurement_order ADD COLUMN review_request_hash CHAR(64) NULL"));
|
||||
assertTrue(sql.contains(
|
||||
"ALTER TABLE ll_overseas_procurement_order ADD COLUMN outbound_details JSON NULL"));
|
||||
assertTrue(sql.contains(
|
||||
"table_name='ll_overseas_procurement_status_history' AND column_name='idempotency_key'"));
|
||||
assertTrue(sql.contains("ALTER TABLE ll_wallet_event_inbox"));
|
||||
assertTrue(sql.contains("MODIFY COLUMN payload_hash VARCHAR(71) NOT NULL"));
|
||||
String lower = sql.toLowerCase();
|
||||
assertTrue(tableBlock(lower, "ll_overseas_procurement_status_history")
|
||||
.contains("idempotency_key varchar(255)"));
|
||||
String order = tableBlock(lower, "ll_overseas_procurement_order");
|
||||
assertTrue(order.contains("review_idempotency_key varchar(255)"));
|
||||
assertTrue(order.contains("review_request_hash char(64)"));
|
||||
assertTrue(order.contains("outbound_details json"));
|
||||
assertTrue(tableBlock(lower, "ll_wallet_event_inbox").contains("payload_hash varchar(71)"));
|
||||
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("ALTER TABLE ll_overseas_procurement"));
|
||||
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("ALTER TABLE ll_wallet_event_inbox"));
|
||||
}
|
||||
|
||||
private static String tableBlock(String sql, String table) {
|
||||
int start = sql.indexOf("create table " + table + " (");
|
||||
int end = sql.indexOf(';', start);
|
||||
return start < 0 || end < 0 ? "" : sql.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -23,12 +23,13 @@ class ReturnCartonQueryTest {
|
||||
java.nio.file.Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql"))) {
|
||||
String sql = java.nio.file.Files.readString(path);
|
||||
var matcher = java.util.regex.Pattern.compile(
|
||||
"CREATE TABLE IF NOT EXISTS ll_return_inventory_carton \\(([\\s\\S]*?)\\) ENGINE")
|
||||
"CREATE TABLE(?: IF NOT EXISTS)? ll_return_inventory_carton \\(([\\s\\S]*?)\\) ENGINE",
|
||||
java.util.regex.Pattern.CASE_INSENSITIVE)
|
||||
.matcher(sql);
|
||||
int definitions = 0;
|
||||
while (matcher.find()) {
|
||||
definitions++;
|
||||
assertThat(matcher.group(1)).as(path.toString()).contains("new_carton_mark VARCHAR(255)");
|
||||
assertThat(matcher.group(1).toLowerCase()).as(path.toString()).contains("new_carton_mark varchar(255)");
|
||||
}
|
||||
assertThat(definitions).isPositive();
|
||||
}
|
||||
|
||||
+5
-6
@@ -11,7 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LastlegOrderDetailSchemaContractTest {
|
||||
@Test
|
||||
void orderDetailHasCustomerIsolationInBaselineAndRepeatableUpgrade() throws Exception {
|
||||
void orderDetailHasCustomerIsolationInBaselineAndCleanInitialization() throws Exception {
|
||||
String baseline = new ClassPathResource("db/lastleg.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
|
||||
@@ -24,11 +24,10 @@ class LastlegOrderDetailSchemaContractTest {
|
||||
.contains("customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键'")
|
||||
.contains("KEY idx_ll_detail_customer_order(customer_id,order_id,id)");
|
||||
assertThat(unified)
|
||||
.contains("table_name='ll_order_detail' AND column_name='customer_id'")
|
||||
.contains("UPDATE ll_order_detail detail")
|
||||
.contains("SET detail.customer_id=orders.customer_id")
|
||||
.contains("MODIFY COLUMN customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键'")
|
||||
.contains("ADD KEY idx_ll_detail_customer_order(customer_id,order_id,id)");
|
||||
.contains("CREATE TABLE ll_order_detail")
|
||||
.contains("customer_id varchar(128)")
|
||||
.contains("KEY idx_ll_detail_customer_order (customer_id,order_id,id)")
|
||||
.doesNotContain("UPDATE ll_order_detail", "ALTER TABLE ll_order_detail");
|
||||
assertThat(service)
|
||||
.contains(".eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())")
|
||||
.contains(".eq(LastlegOrderStatusHistory::getCustomerId, order.getCustomerId())");
|
||||
|
||||
+9
-7
@@ -16,14 +16,16 @@ class Phase5ExceptionVisitSchemaContractTest {
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
for (String sql : new String[] { baseline, overseas }) {
|
||||
String normalized = sql.replaceAll("\\s+", "");
|
||||
assertThat(normalized).contains(
|
||||
"ll_exception_order(",
|
||||
"UNIQUEKEYuk_ll_exception_no(customer_id,exception_no)",
|
||||
"UNIQUEKEYuk_ll_exception_delivery_return(customer_id,delivery_order_id,exception_type)",
|
||||
"KEYidx_ll_exception_inbound(customer_id,inbound_code)",
|
||||
"ll_customer_visit(",
|
||||
"UNIQUEKEYuk_ll_customer_visit_delivery(customer_id,delivery_order_id)",
|
||||
"KEYidx_ll_customer_visit_list(customer_id,status,delivery_completed_at)");
|
||||
assertThat(sql).contains(
|
||||
"CREATE TABLE IF NOT EXISTS ll_exception_order",
|
||||
"UNIQUE KEY uk_ll_exception_no(customer_id,exception_no)",
|
||||
"UNIQUE KEY uk_ll_exception_delivery_return(customer_id,delivery_order_id,exception_type)",
|
||||
"KEY idx_ll_exception_inbound(customer_id,inbound_code)",
|
||||
"CREATE TABLE IF NOT EXISTS ll_customer_visit",
|
||||
"UNIQUE KEY uk_ll_customer_visit_delivery(customer_id,delivery_order_id)",
|
||||
"KEY idx_ll_customer_visit_list(customer_id,status,delivery_completed_at)",
|
||||
"COMMENT='海外仓异常订单'",
|
||||
"COMMENT='派送单维度客户回访记录'");
|
||||
}
|
||||
|
||||
+40
-51
@@ -20,88 +20,73 @@ class Phase5OverseasUpgradeSqlContractTest {
|
||||
"ll_exception_return_item", "ll_exception_order_command", "ll_customer_visit");
|
||||
|
||||
@Test
|
||||
void overseasFixedEntryCreatesEveryPhase5TableBeforeAnyPhase5AlterOrAssertion() throws Exception {
|
||||
void overseasFixedEntryCreatesEveryPhase5TableExactlyOnce() throws Exception {
|
||||
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
|
||||
int phase5Gate = overseas.indexOf("-- 五期排车、出库与海外PDA正式业务号/幂等约束");
|
||||
assertThat(phase5Gate).isPositive();
|
||||
|
||||
for (String table : PHASE5_TABLES) {
|
||||
String create = "CREATE TABLE IF NOT EXISTS " + table + " (";
|
||||
assertThat(overseas.indexOf(create)).as(table).isBetween(0, phase5Gate - 1);
|
||||
String create = "CREATE TABLE " + table + " (";
|
||||
assertThat(overseas.indexOf(create)).as(table).isGreaterThanOrEqualTo(0);
|
||||
assertThat(count(overseas, create)).as(table).isEqualTo(1);
|
||||
assertThat(overseas.indexOf(table)).as(table + " first reference")
|
||||
.isEqualTo(overseas.indexOf(create) + "CREATE TABLE IF NOT EXISTS ".length());
|
||||
.isEqualTo(overseas.indexOf(create) + "CREATE TABLE ".length());
|
||||
}
|
||||
assertThat(overseas).contains("<> 23", "五期完整业务表集合未部署");
|
||||
assertThat(overseas).doesNotContain("assert_phase5_lastleg_identifiers_migratable", "ALTER TABLE ll_");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overseasFixedEntryConvergesKnownIntermediatePhase5ShapesWithoutBackfill() throws Exception {
|
||||
void overseasFixedEntryContainsFinalPhase5ShapesWithoutMigrationBackfill() throws Exception {
|
||||
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
|
||||
String normalized = normalize(overseas);
|
||||
|
||||
assertThat(overseas).contains(
|
||||
"ALTER TABLE ll_outbound_order RENAME COLUMN tenant_id TO customer_id",
|
||||
"ALTER TABLE ll_outbound_pallet_item RENAME COLUMN tenant_id TO customer_id",
|
||||
"ALTER TABLE ll_delivery_order RENAME COLUMN tenant_id TO customer_id",
|
||||
"ALTER TABLE ll_outbound_order ADD COLUMN outbound_type",
|
||||
"ALTER TABLE ll_outbound_order ADD COLUMN lx_codes",
|
||||
"ALTER TABLE ll_outbound_pallet_item ADD COLUMN picked_package_count",
|
||||
"ALTER TABLE ll_delivery_order ADD COLUMN overseas_warehouse_name",
|
||||
"ALTER TABLE ll_delivery_order ADD COLUMN waybill_no",
|
||||
"ALTER TABLE ll_dropship_order_profile ADD COLUMN source_pallet_no",
|
||||
"ALTER TABLE ll_dropship_order_profile ADD COLUMN manifest_version",
|
||||
"ALTER TABLE ll_dropship_order_profile ADD COLUMN manifest_package_count",
|
||||
"ALTER TABLE ll_dropship_order_profile ADD COLUMN manifest_fragment_count",
|
||||
"ALTER TABLE ll_dropship_order_profile ADD COLUMN manifest_hash",
|
||||
"ALTER TABLE ll_inbound_plan_pallet ADD COLUMN version",
|
||||
"ADD KEY idx_ll_dropship_profile_manifest(source_pallet_no,manifest_hash)",
|
||||
"ADD UNIQUE KEY uk_ll_dropship_sku_inventory(profile_id,sku_code)",
|
||||
"MODIFY COLUMN source_pallet_no VARCHAR(128) NOT NULL");
|
||||
int relabelCreateEnd = overseas.indexOf(") ENGINE=InnoDB COMMENT='换标货物冻结明细';");
|
||||
int relabelModify = overseas.indexOf("MODIFY COLUMN destination_fba_warehouse_code", relabelCreateEnd);
|
||||
for (String column : List.of("destination_fba_warehouse_code", "source_inventory_balance_id",
|
||||
"source_inventory_position_id", "source_inbound_order_id")) {
|
||||
assertThat(overseas.indexOf("ALTER TABLE ll_relabel_item ADD COLUMN " + column, relabelCreateEnd))
|
||||
.as(column + " converges before grouped MODIFY")
|
||||
.isBetween(relabelCreateEnd, relabelModify - 1);
|
||||
}
|
||||
int start = overseas.indexOf("-- 五期正式结构必须由海外固定入口自行补齐");
|
||||
int end = overseas.indexOf("USE baiyue_lastleg;", start);
|
||||
String phase5 = overseas.substring(start, end);
|
||||
assertThat(phase5).doesNotContain("\nUPDATE ", "\nDELETE ", "\nINSERT ");
|
||||
assertThat(createStatement(overseas, "ll_outbound_order")).contains(
|
||||
"customer_id", "outbound_type", "lx_codes", "request_hash");
|
||||
assertThat(createStatement(overseas, "ll_outbound_pallet_item")).contains("picked_package_count");
|
||||
assertThat(createStatement(overseas, "ll_delivery_order")).contains(
|
||||
"customer_id", "overseas_warehouse_name", "waybill_no");
|
||||
assertThat(createStatement(overseas, "ll_dropship_order_profile")).contains(
|
||||
"source_pallet_no", "manifest_version", "manifest_package_count",
|
||||
"manifest_fragment_count", "manifest_hash");
|
||||
assertThat(normalized).contains(
|
||||
"keyidx_ll_dropship_profile_manifest(source_pallet_no,manifest_hash)",
|
||||
"uniquekeyuk_ll_dropship_sku_inventory(profile_id,sku_code)");
|
||||
assertThat(createStatement(overseas, "ll_relabel_item")).contains(
|
||||
"destination_fba_warehouse_code", "source_inventory_balance_id",
|
||||
"source_inventory_position_id", "source_inbound_order_id");
|
||||
assertThat(overseas).doesNotContain("\nUPDATE ", "\nDELETE ", "ALTER TABLE ll_");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overseasFixedEntryUsesTheCurrentBaselineDefinitions() throws Exception {
|
||||
void overseasFixedEntryAndModuleBaselineBothDefineCompletePhase5Tables() throws Exception {
|
||||
String baseline = repositoryFile("lastleg-service/src/main/resources/db/lastleg.sql");
|
||||
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
|
||||
|
||||
for (String table : PHASE5_TABLES) {
|
||||
assertThat(normalize(createStatement(overseas, table)))
|
||||
.as(table)
|
||||
.isEqualTo(normalize(createStatement(baseline, table)));
|
||||
assertThat(createStatement(overseas, table)).as(table)
|
||||
.contains("PRIMARY KEY", "COMMENT='");
|
||||
assertThat(createStatement(baseline, table)).as(table)
|
||||
.contains("PRIMARY KEY", "COMMENT='");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void overseasFixedAdminProjectionAssertionMatchesItsAuthorityArray() throws Exception {
|
||||
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
|
||||
int start = overseas.indexOf("SET @platform_admin_overseas_authorities = JSON_ARRAY(");
|
||||
int end = overseas.indexOf(");", start);
|
||||
String authorityArray = overseas.substring(start, end);
|
||||
int authorityCount = count(authorityArray, "'platform:");
|
||||
int start = overseas.indexOf("VALUES ('AUTHORIZATION','1000001'");
|
||||
int end = overseas.indexOf(";", start);
|
||||
String authorityRow = overseas.substring(start, end);
|
||||
int authorityCount = count(authorityRow, "platform:");
|
||||
|
||||
assertThat(authorityCount).isPositive();
|
||||
assertThat(overseas).contains("JSON_LENGTH(authorities_json) <> " + authorityCount);
|
||||
assertThat(authorityRow).contains("baseline-platform-admin-authorization-v0");
|
||||
assertThat(overseas).doesNotContain("@platform_admin_overseas_authorities");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overseasReturnOrderBackfillNormalizesLegacyColumnCollations() throws Exception {
|
||||
void overseasReturnOrdersUseTheFinalCleanSchemaWithoutLegacyBackfill() throws Exception {
|
||||
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
|
||||
|
||||
assertThat(overseas).contains(
|
||||
"o.source_order_id COLLATE utf8mb4_unicode_ci = CAST(r.id AS CHAR) COLLATE utf8mb4_unicode_ci",
|
||||
"o.source_order_no COLLATE utf8mb4_unicode_ci = r.return_no COLLATE utf8mb4_unicode_ci");
|
||||
assertThat(overseas).contains("CREATE TABLE ll_return_order (", "CREATE TABLE ll_return_order_item (")
|
||||
.doesNotContain("o.source_order_id COLLATE", "o.source_order_no COLLATE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -122,6 +107,10 @@ class Phase5OverseasUpgradeSqlContractTest {
|
||||
private static String createStatement(String sql, String table) {
|
||||
String token = "CREATE TABLE IF NOT EXISTS " + table + " (";
|
||||
int start = sql.indexOf(token);
|
||||
if (start < 0) {
|
||||
token = "CREATE TABLE " + table + " (";
|
||||
start = sql.indexOf(token);
|
||||
}
|
||||
int end = sql.indexOf(';', start);
|
||||
assertThat(start).as(table).isGreaterThanOrEqualTo(0);
|
||||
assertThat(end).as(table).isGreaterThan(start);
|
||||
|
||||
+21
-6
@@ -25,8 +25,8 @@ class SchemaChineseCommentContractTest {
|
||||
|
||||
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fff]");
|
||||
private static final Pattern TABLE = Pattern.compile(
|
||||
"(?is)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+(ll_[a-z0-9_]+)"
|
||||
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB\\s+COMMENT\\s*=\\s*'([^']*)'\\s*;");
|
||||
"(?is)CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(ll_[a-z0-9_]+)"
|
||||
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB[^;]*?COMMENT\\s*=\\s*'([^']*)'\\s*;");
|
||||
|
||||
@Test
|
||||
void allLastlegTablesAndColumnsHaveChineseComments() throws Exception {
|
||||
@@ -39,7 +39,7 @@ class SchemaChineseCommentContractTest {
|
||||
|
||||
List<TableDefinition> baselineTables = assertAllChineseComments(baseline, "基线 SQL", 56);
|
||||
assertAllChineseComments(incremental, "增量 SQL", 50);
|
||||
assertAllChineseComments(overseasUnified, "海外统一增量 SQL", 10);
|
||||
assertAllChineseComments(overseasUnified, "海外统一初始化 SQL", 127);
|
||||
assertCommentMigrationCovers(incremental, overseasUnified, baselineTables);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@ class SchemaChineseCommentContractTest {
|
||||
for (MigrationColumn column : required) {
|
||||
assertSpecificAlterationHasChineseComment(baseline, column);
|
||||
}
|
||||
assertAllAlteredColumnsHaveChineseComments(overseasUnified, "海外统一增量 SQL");
|
||||
for (MigrationColumn column : required) {
|
||||
assertTableColumnHasChineseComment(overseasUnified, column, "海外统一初始化 SQL");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TableDefinition> assertAllChineseComments(String sql, String source, int expectedCount) {
|
||||
@@ -157,8 +159,8 @@ class SchemaChineseCommentContractTest {
|
||||
|
||||
private static boolean hasCommentedCreate(String sql, String table) {
|
||||
return Pattern.compile(
|
||||
"(?is)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+" + Pattern.quote(table)
|
||||
+ "\\s*\\(.*?\\)\\s*ENGINE\\s*=\\s*InnoDB\\s+COMMENT\\s*=\\s*'[^']*[\\u4e00-\\u9fff][^']*'\\s*;")
|
||||
"(?is)CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?" + Pattern.quote(table)
|
||||
+ "\\s*\\(.*?\\)\\s*ENGINE\\s*=\\s*InnoDB[^;]*?COMMENT\\s*=\\s*'[^']*[\\u4e00-\\u9fff][^']*'\\s*;")
|
||||
.matcher(sql).find();
|
||||
}
|
||||
|
||||
@@ -199,6 +201,19 @@ class SchemaChineseCommentContractTest {
|
||||
"字段增量缺少中文备注: " + target.table() + "." + target.column());
|
||||
}
|
||||
|
||||
private static void assertTableColumnHasChineseComment(String sql, MigrationColumn target, String source) {
|
||||
Matcher table = Pattern.compile(
|
||||
"(?is)CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?" + Pattern.quote(target.table())
|
||||
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB").matcher(sql);
|
||||
assertTrue(table.find(), source + " 缺少表: " + target.table());
|
||||
String definition = splitTopLevel(table.group(1)).stream()
|
||||
.filter(value -> value.stripLeading().replace("`", "").startsWith(target.column() + " "))
|
||||
.findFirst().orElse("");
|
||||
assertTrue(Pattern.compile("(?is)\\bCOMMENT\\s+'[^']*[\\u4e00-\\u9fff][^']*'")
|
||||
.matcher(definition).find(),
|
||||
source + " 字段缺少中文备注: " + target.table() + "." + target.column());
|
||||
}
|
||||
|
||||
private static void assertAllAlteredColumnsHaveChineseComments(String sql, String source) {
|
||||
Pattern pattern = Pattern.compile("(?i)\\b(?:ADD|MODIFY)\\s+COLUMN\\s+([a-z0-9_]+)\\b");
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
|
||||
+18
-4
@@ -26,8 +26,7 @@ class LastlegSecurityContractTest {
|
||||
"table_name='ll_security_projection' AND column_name='authorities_json'",
|
||||
"UPDATE ll_security_projection SET roles_json=JSON_ARRAY() WHERE roles_json IS NULL",
|
||||
"UPDATE ll_security_projection SET authorities_json=JSON_ARRAY() WHERE authorities_json IS NULL");
|
||||
for (String sql : new String[]{baseline, migration}) {
|
||||
assertThat(sql).contains("SET @platform_admin_overseas_roles = JSON_ARRAY('PLATFORM_ADMIN')",
|
||||
assertThat(baseline).contains("SET @platform_admin_overseas_roles = JSON_ARRAY('PLATFORM_ADMIN')",
|
||||
"SET @platform_admin_overseas_authorities = JSON_ARRAY(",
|
||||
"('USER','1000001',0,'ACTIVE',0,JSON_ARRAY(),JSON_ARRAY(),0",
|
||||
"('AUTHORIZATION','1000001',0,NULL,0,@platform_admin_overseas_roles,@platform_admin_overseas_authorities,0",
|
||||
@@ -42,10 +41,25 @@ class LastlegSecurityContractTest {
|
||||
"AND aggregate_version=0 AND last_event_id='baseline-platform-admin-authorization-v0'")
|
||||
.doesNotContain("('AUTHORIZATION','1000001',0,NULL,0,JSON_ARRAY(),JSON_ARRAY(),0",
|
||||
"baseline-merchant-admin");
|
||||
}
|
||||
assertThat(migration).contains(
|
||||
"CREATE TABLE ll_security_event_inbox (",
|
||||
"CREATE TABLE ll_security_projection (",
|
||||
"VALUES ('AUTHORIZATION','1000001',0,NULL,0,",
|
||||
"VALUES ('USER','1000001',0,'ACTIVE',0,'[]','[]',0",
|
||||
"PLATFORM_ADMIN",
|
||||
"baseline-platform-admin-user-v0",
|
||||
"baseline-platform-admin-authorization-v0",
|
||||
"platform:overseas-warehouse:last-mile-order:list",
|
||||
"platform:overseas-warehouse:inbound-order:list",
|
||||
"platform:overseas-warehouse:dispatch:list",
|
||||
"platform:overseas-business:purchase:list",
|
||||
"platform:overseas-master-data:quote:list",
|
||||
"platform:finance:fee-source:review")
|
||||
.doesNotContain("SET @platform_admin_overseas_roles", "UPDATE ll_security_projection",
|
||||
"baseline-merchant-admin");
|
||||
assertThat(authSeed).contains("(1000001, 'platform_admin'", "default_tenant_id",
|
||||
"default_app_code");
|
||||
assertThat(migration).contains("USE baiyue_lastleg;", "table_schema='baiyue_lastleg'")
|
||||
assertThat(migration).contains("USE baiyue_lastleg;")
|
||||
.doesNotContain("CREATE DATABASE IF NOT EXISTS baiyue_overseas_security",
|
||||
"USE baiyue_overseas_security;", "CREATE TABLE IF NOT EXISTS overseas_security_event_inbox",
|
||||
"CREATE TABLE IF NOT EXISTS overseas_security_projection");
|
||||
|
||||
+5
-3
@@ -67,9 +67,11 @@ class ContainerPlanProgressContractTest {
|
||||
assertFalse(openApi.contains("platform:overseas-warehouse:order:list"));
|
||||
assertFalse(openApi.contains("platform:overseas-warehouse:order:edit"));
|
||||
assertTrue(openApi.contains("ll_order.estimated_warehouse_at"));
|
||||
assertTrue(schema.contains("idx_ll_order_container_progress(source_container_booking_id,estimated_warehouse_at,created_at,id)"));
|
||||
assertTrue(deployment.contains("idx_ll_order_container_progress(source_container_booking_id,estimated_warehouse_at,created_at,id)"));
|
||||
assertTrue(deployment.contains("assert_container_progress_index_ready"));
|
||||
assertTrue(schema.replaceAll("\\s+", "").contains(
|
||||
"idx_ll_order_container_progress(source_container_booking_id,estimated_warehouse_at,created_at,id)"));
|
||||
assertTrue(deployment.replaceAll("\\s+", "").contains(
|
||||
"idx_ll_order_container_progress(source_container_booking_id,estimated_warehouse_at,created_at,id)"));
|
||||
assertFalse(deployment.contains("assert_container_progress_index_ready"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+15
-5
@@ -30,12 +30,22 @@ class UsTruckOrderSchemaContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedIncrementalContainsOperationLedgerAndHistoryColumns() throws Exception {
|
||||
void unifiedInitializationContainsOperationLedgerAndHistoryColumns() throws Exception {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order_operation"));
|
||||
assertTrue(sql.contains("uk_ll_us_truck_operation_key(customer_id,order_id,idempotency_key)"));
|
||||
assertTrue(sql.contains("table_name='ll_us_truck_order_status_history' AND column_name='operation_type'"));
|
||||
assertTrue(sql.contains("table_name='ll_us_truck_order_status_history' AND column_name='idempotency_key'"));
|
||||
String lower = sql.toLowerCase();
|
||||
String normalized = lower.replaceAll("\\s+", "");
|
||||
assertTrue(lower.contains("create table ll_us_truck_order_operation"));
|
||||
assertTrue(normalized.contains("uk_ll_us_truck_operation_key(customer_id,order_id,idempotency_key)"));
|
||||
String history = tableBlock(lower, "ll_us_truck_order_status_history");
|
||||
assertTrue(history.contains("operation_type varchar(64)"));
|
||||
assertTrue(history.contains("idempotency_key varchar(255)"));
|
||||
org.junit.jupiter.api.Assertions.assertFalse(lower.contains("alter table ll_us_truck_order_status_history"));
|
||||
}
|
||||
|
||||
private static String tableBlock(String sql, String table) {
|
||||
int start = sql.indexOf("create table " + table + " (");
|
||||
int end = sql.indexOf(';', start);
|
||||
return start < 0 || end < 0 ? "" : sql.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -30,8 +30,9 @@ class DomesticWarehouseOrderExportPermissionSqlContractTest {
|
||||
}
|
||||
assertThat(iamSeed).contains("WHERE id IN (5102019, 5102024, 5102025, 5102026, 5102027)");
|
||||
assertThat(unified)
|
||||
.contains("WHERE id IN (5102019, 5102024, 5102025, 5102026, 5102027)")
|
||||
.contains("管理员或订单运营角色缺少订单导出与下载中心权限");
|
||||
.contains("VALUES (4000001,5102026,0,CURRENT_TIMESTAMP(3))")
|
||||
.contains("VALUES (4000003,5102026,0,CURRENT_TIMESTAMP(3))")
|
||||
.doesNotContain("管理员或订单运营角色缺少订单导出与下载中心权限");
|
||||
}
|
||||
|
||||
private static Path resolve(String relative) {
|
||||
|
||||
+5
-6
@@ -99,13 +99,12 @@ class OrderDeclarationUpdateContractTest {
|
||||
Path iam = Files.exists(iamDirect) ? iamDirect : Path.of("..").resolve(iamDirect);
|
||||
String iamSource = Files.readString(iam, StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(source)
|
||||
.contains("expected_arrival_at DATETIME(3) NULL COMMENT '客户申报预计到货时间'")
|
||||
.contains("channel_changed TINYINT(1) NOT NULL DEFAULT 0 COMMENT '订单历史上是否由平台变更过渠道'")
|
||||
.contains("CALL unified_assert_column('baiyue_order', 'ord_order', 'expected_arrival_at')")
|
||||
.contains("CALL unified_assert_column('baiyue_order', 'ord_order', 'channel_changed')")
|
||||
assertThat(source.toLowerCase())
|
||||
.contains("expected_arrival_at datetime(3) default null comment '客户申报预计到货时间'")
|
||||
.contains("channel_changed tinyint(1) not null default '0' comment '订单历史上是否由平台变更过渠道'")
|
||||
.contains("platform:order:declaration:edit")
|
||||
.contains("WHERE id IN (5102019, 5102024, 5102025, 5102026, 5102027)");
|
||||
.contains("values (4000001,5102027,0,current_timestamp(3))")
|
||||
.contains("values (4000003,5102027,0,current_timestamp(3))");
|
||||
assertThat(iamSource)
|
||||
.contains("platform:order:declaration:edit")
|
||||
.contains("WHERE id IN (5102019, 5102024, 5102025, 5102026, 5102027)");
|
||||
|
||||
+8
-5
@@ -38,12 +38,15 @@ class InboundCodeSequenceSqlContractTest {
|
||||
String baseline = new ClassPathResource("db/order.sql").getContentAsString(StandardCharsets.UTF_8);
|
||||
String unified = Files.readString(resolveUnified(), StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(baseline)
|
||||
.contains("CREATE TABLE IF NOT EXISTS ord_inbound_code_daily_sequence");
|
||||
for (String sql : java.util.List.of(baseline, unified)) {
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS ord_inbound_code_daily_sequence")
|
||||
.contains("business_date DATE NOT NULL")
|
||||
.contains("current_value BIGINT UNSIGNED NOT NULL")
|
||||
.contains("PRIMARY KEY (business_date)")
|
||||
assertThat(sql.toLowerCase())
|
||||
.contains("create table")
|
||||
.contains("ord_inbound_code_daily_sequence")
|
||||
.contains("business_date date not null")
|
||||
.contains("current_value bigint unsigned not null")
|
||||
.contains("primary key (business_date)")
|
||||
.contains("按上海日期重新从1开始");
|
||||
}
|
||||
}
|
||||
|
||||
+14
-11
@@ -47,19 +47,22 @@ class OrderParentChildSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedEntryContainsTheExactOrderBlockBeforeFirstleg() throws Exception {
|
||||
void unifiedEntryContainsTheFinalOrderParentChildSchemaAndTriggers() throws Exception {
|
||||
String unified = read(resolve("deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
String module = read(resolveModule()).trim();
|
||||
String marker = "-- 来源:order/src/main/resources/db/phase3-order-parent-child-schema.sql";
|
||||
String next = "-- 来源:firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql";
|
||||
int start = unified.indexOf(marker);
|
||||
int end = unified.indexOf(next, start + marker.length());
|
||||
|
||||
assertThat(start).isGreaterThanOrEqualTo(0);
|
||||
assertThat(end).isGreaterThan(start);
|
||||
assertThat(unified.substring(start + marker.length(), end).trim()).startsWith(module);
|
||||
assertThat(unified.lastIndexOf("SELECT 'PASS' AS migration_status"))
|
||||
.isGreaterThan(end);
|
||||
assertThat(unified)
|
||||
.contains("CREATE TABLE ord_order_fulfillment_split")
|
||||
.contains("source_order_id bigint unsigned NOT NULL")
|
||||
.contains("request_hash char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL")
|
||||
.contains("UNIQUE KEY uk_order_split_child_role (tenant_id,child_order_id,split_role)")
|
||||
.contains("KEY idx_order_split_source (tenant_id,source_order_id,split_role)")
|
||||
.contains("CREATE TRIGGER trg_order_fulfillment_split_insert")
|
||||
.contains("NEW.split_role='LOADED' AND NEW.child_order_id<>NEW.source_order_id")
|
||||
.contains("CREATE TRIGGER trg_order_fulfillment_split_update")
|
||||
.contains("订单履约分片来源、子片与角色不可修改")
|
||||
.contains("SELECT 'PASS' AS initialization_status, 'CN' AS region")
|
||||
.doesNotContain("phase3_order_add_column")
|
||||
.doesNotContain("phase3_order_add_index");
|
||||
}
|
||||
|
||||
private static Path resolve(String path) {
|
||||
|
||||
@@ -20,18 +20,17 @@ class OrderPickupSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedScriptRelaxesInboundCodeBeforeClearingDraftsAndAddsPickupColumn() throws Exception {
|
||||
void unifiedScriptCreatesTheFinalPickupColumnsWithoutMigrationSteps() throws Exception {
|
||||
Path direct = Path.of("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
Path path = Files.exists(direct) ? direct : Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
String sql = Files.readString(path);
|
||||
int relax = sql.indexOf("ALTER TABLE baiyue_order.ord_order MODIFY COLUMN inbound_code VARCHAR(40) NULL");
|
||||
int clearDraft = sql.indexOf("SET inbound_code = NULL");
|
||||
assertThat(relax).isGreaterThanOrEqualTo(0).isLessThan(clearDraft);
|
||||
String sql = Files.readString(path).toLowerCase();
|
||||
assertThat(sql).contains(
|
||||
"column_name = 'requested_pickup_at'",
|
||||
"ALTER TABLE baiyue_order.ord_order ADD COLUMN requested_pickup_at DATETIME(3) NULL",
|
||||
"CREATE PROCEDURE assert_submitted_order_inbound_code()",
|
||||
"SIGNAL SQLSTATE '45000'");
|
||||
assertThat(sql).doesNotContain("CONCAT('RK-', o.id)");
|
||||
"create table ord_order",
|
||||
"inbound_code varchar(40) default null",
|
||||
"requested_pickup_at datetime(3) default null");
|
||||
assertThat(sql)
|
||||
.doesNotContain("alter table baiyue_order.ord_order modify column inbound_code")
|
||||
.doesNotContain("set inbound_code = null")
|
||||
.doesNotContain("concat('rk-', o.id)");
|
||||
}
|
||||
}
|
||||
|
||||
+8
-6
@@ -13,8 +13,14 @@ class InitialWarehouseResourceSeedSqlContractTest {
|
||||
void initializesReusablePalletsAndYiwuLocationGrid() throws Exception {
|
||||
assertModuleSeed(Files.readString(Path.of("src", "main", "resources", "db", "phase2-pallet-seed.sql"),
|
||||
StandardCharsets.UTF_8));
|
||||
assertUnifiedSeed(Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8));
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertThat(unified)
|
||||
.contains("CREATE TABLE wh_pallet (")
|
||||
.contains("CREATE TABLE wh_location (")
|
||||
.doesNotContain("assert_yiwu_location_seed")
|
||||
.doesNotContain("INSERT INTO wh_location_reservation")
|
||||
.doesNotContain("INSERT INTO wh_location_occupancy");
|
||||
}
|
||||
|
||||
private static void assertModuleSeed(String sql) {
|
||||
@@ -22,10 +28,6 @@ class InitialWarehouseResourceSeedSqlContractTest {
|
||||
assertThat(sql).doesNotContain("color,status,remark");
|
||||
}
|
||||
|
||||
private static void assertUnifiedSeed(String sql) {
|
||||
assertSeed(sql, "193000000000200001", "193000000000200002", "193000000000200");
|
||||
}
|
||||
|
||||
private static void assertSeed(String sql, String yiwuId, String dongguanId, String idPrefix) {
|
||||
String compactSql = sql.replaceAll("\\s+", "");
|
||||
int locationSeedStart = sql.indexOf("WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201");
|
||||
|
||||
+11
-35
@@ -14,32 +14,15 @@ class OverseasStorageFeeSqlContractTest {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_rule")
|
||||
.contains("CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_tier")
|
||||
.containsOnlyOnce("CREATE PROCEDURE migrate_overseas_storage_fee_schema()")
|
||||
.doesNotContain("ADD COLUMN IF NOT EXISTS initialization_key")
|
||||
.doesNotContain("ADD COLUMN IF NOT EXISTS fee_type")
|
||||
.doesNotContain("ADD COLUMN IF NOT EXISTS billing_unit")
|
||||
.contains("table_name = 'wh_overseas_storage_fee_rule'")
|
||||
.contains("column_name = 'initialization_key'")
|
||||
.contains("column_name = 'fee_type'")
|
||||
.contains("column_name = 'billing_unit'")
|
||||
.contains("PREPARE overseas_fee_statement FROM @overseas_fee_ddl")
|
||||
.contains("EXECUTE overseas_fee_statement")
|
||||
.contains("DEALLOCATE PREPARE overseas_fee_statement")
|
||||
.contains("constraint_name = 'chk_overseas_fee_tier_days'")
|
||||
.contains("constraint_name = 'chk_overseas_fee_tier_price'")
|
||||
.contains("index_columns = 'rule_id,fee_type,active_start_day'")
|
||||
.contains("Warehouse 迁移失败:海外仓费用阶梯存在非法天数或负价格")
|
||||
.contains("Warehouse 迁移失败:海外仓费用阶梯目标唯一键存在重复数据")
|
||||
.contains("CALL migrate_overseas_storage_fee_schema()")
|
||||
.contains("LOWER(column_type) = 'varchar(191)'")
|
||||
.contains("LOWER(column_type) = 'varchar(32)'")
|
||||
.contains("LOWER(column_type) = 'varchar(16)'")
|
||||
.contains("LOWER(column_type) = 'smallint unsigned'")
|
||||
.contains("LOWER(column_type) = 'decimal(18,6)'")
|
||||
.contains("统一迁移失败:海外仓阶梯费用字段签名不正确")
|
||||
.contains("统一迁移失败:海外仓阶梯费用唯一键或 CHECK 结构不正确")
|
||||
.contains("CREATE TABLE wh_overseas_storage_fee_rule")
|
||||
.contains("CREATE TABLE wh_overseas_storage_fee_tier")
|
||||
.contains("initialization_key varchar(191) DEFAULT NULL")
|
||||
.contains("fee_type varchar(32) NOT NULL DEFAULT 'STORAGE'")
|
||||
.contains("billing_unit varchar(16) NOT NULL DEFAULT 'CBM'")
|
||||
.contains("start_day smallint unsigned NOT NULL")
|
||||
.contains("unit_price decimal(18,6) NOT NULL")
|
||||
.contains("CONSTRAINT chk_overseas_fee_tier_days CHECK")
|
||||
.contains("CONSTRAINT chk_overseas_fee_tier_price CHECK")
|
||||
.contains("uk_overseas_fee_active_customer")
|
||||
.contains("idx_overseas_fee_page")
|
||||
.contains("idx_overseas_fee_customer_name")
|
||||
@@ -49,14 +32,7 @@ class OverseasStorageFeeSqlContractTest {
|
||||
.contains("platform:overseas-storage-fee:create")
|
||||
.contains("platform:overseas-storage-fee:edit")
|
||||
.contains("platform:overseas-storage-fee:delete")
|
||||
.contains("CALL unified_assert_overseas_storage_fee()")
|
||||
.contains("统一增量全部执行完成");
|
||||
|
||||
int addColumns = sql.indexOf("column_name = 'fee_type'");
|
||||
int migrate = sql.indexOf("CALL migrate_overseas_storage_fee_schema()");
|
||||
int assertSchema = sql.indexOf("CALL unified_assert_overseas_storage_fee()");
|
||||
assertThat(addColumns).isGreaterThan(0);
|
||||
assertThat(migrate).isGreaterThan(addColumns);
|
||||
assertThat(assertSchema).isGreaterThan(migrate);
|
||||
.doesNotContain("migrate_overseas_storage_fee_schema")
|
||||
.doesNotContain("unified_assert_overseas_storage_fee");
|
||||
}
|
||||
}
|
||||
|
||||
+18
-13
@@ -17,13 +17,15 @@ class PalletLocationReleaseIdempotencySqlContractTest {
|
||||
"头程数据库统一增量-MySQL8.sql"));
|
||||
|
||||
for (String sql : new String[] {moduleSql, unifiedSql}) {
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS wh_pallet_location_release_operation")
|
||||
.contains("idempotency_key VARCHAR(128) NOT NULL")
|
||||
.contains("request_hash CHAR(64) NOT NULL")
|
||||
.contains("response_snapshot JSON NULL")
|
||||
String normalized = sql.toLowerCase().replace(" default null", " null").replace(", ", ",");
|
||||
assertThat(normalized)
|
||||
.contains("create table ")
|
||||
.contains("wh_pallet_location_release_operation")
|
||||
.contains("idempotency_key varchar(128) not null")
|
||||
.contains("request_hash char(64) not null")
|
||||
.contains("response_snapshot json null")
|
||||
.contains("uk_pallet_location_release_idempotency")
|
||||
.contains("tenant_id, source_service, business_id, idempotency_key");
|
||||
.contains("tenant_id,source_service,business_id,idempotency_key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,14 +37,17 @@ class PalletLocationReleaseIdempotencySqlContractTest {
|
||||
"头程数据库统一增量-MySQL8.sql"));
|
||||
|
||||
for (String sql : new String[] {moduleSql, unifiedSql}) {
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS wh_pallet_location_settlement_operation")
|
||||
.contains("source_business_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("target_business_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("response_snapshot JSON NULL")
|
||||
String normalized = sql.toLowerCase().replace(" default null", " null").replace(", ", ",");
|
||||
assertThat(normalized)
|
||||
.contains("create table ")
|
||||
.contains("wh_pallet_location_settlement_operation")
|
||||
.contains("source_business_id bigint unsigned not null")
|
||||
.contains("target_business_id bigint unsigned not null")
|
||||
.contains("response_snapshot json null")
|
||||
.contains("uk_pallet_location_settlement_idempotency")
|
||||
.contains("tenant_id, source_service, source_business_id, idempotency_key")
|
||||
.contains("DROP INDEX uk_pallet_location_settlement_idempotency");
|
||||
.contains("tenant_id,source_service,source_business_id,idempotency_key");
|
||||
}
|
||||
assertThat(moduleSql).contains("DROP INDEX uk_pallet_location_settlement_idempotency");
|
||||
assertThat(unifiedSql).doesNotContain("DROP INDEX uk_pallet_location_settlement_idempotency");
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -29,11 +29,13 @@ class SupplierSqlContractTest {
|
||||
|
||||
@Test void unifiedMigrationContainsSupplierSchemaAndPermissionContract() throws IOException {
|
||||
String sql = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8);
|
||||
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_supplier", "CREATE TABLE IF NOT EXISTS wh_supplier_carrier",
|
||||
String normalized = sql.replace("PRIMARY KEY (", "PRIMARY KEY(");
|
||||
assertThat(normalized).contains("CREATE TABLE wh_supplier", "CREATE TABLE wh_supplier_carrier",
|
||||
"PRIMARY KEY(supplier_id,carrier_code)", "idx_wh_supplier_carrier_code", "uk_wh_supplier_name",
|
||||
"idx_wh_supplier_filter", "platform:supplier:list", "platform:supplier:create",
|
||||
"platform:supplier:edit", "platform:supplier:status", "assert_platform_supplier_permissions");
|
||||
String supplierSection = sql.substring(sql.indexOf("-- 平台公共供应商业务档案及权限"));
|
||||
"platform:supplier:edit", "platform:supplier:status")
|
||||
.doesNotContain("assert_platform_supplier_permissions");
|
||||
String supplierSection = sql.substring(sql.indexOf("CREATE TABLE wh_supplier"));
|
||||
assertThat(supplierSection).doesNotContain("chk_wh_supplier_scope", "订舱分类范围");
|
||||
}
|
||||
|
||||
|
||||
+23
-20
@@ -130,43 +130,40 @@ class WarehouseSchemaContractTest {
|
||||
.contains("193000000000400003,0,193000000000200003,'RECEIVING','海外收货',1")
|
||||
.contains("'洛杉矶仓海外接柜入仓收货能力'")
|
||||
.contains("193000000000400004,0,193000000000200004,'RECEIVING','海外收货',1")
|
||||
.contains("'纽约仓海外接柜入仓收货能力'")
|
||||
.contains("固定平台作业仓收货能力写入校验失败")
|
||||
.contains("warehouse.warehouse_type = 'FBA'")
|
||||
.contains("FBA公共地址不得创建平台作业能力");
|
||||
.contains("'纽约仓海外接柜入仓收货能力'");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedSqlShouldConvergePalletsToReusableMasterData() throws IOException {
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
|
||||
String palletDefinition = tableDefinition(unified, "wh_pallet");
|
||||
String palletDefinition = tableDefinition(unified.toLowerCase(), "wh_pallet").replace(",", ", ");
|
||||
|
||||
assertThat(palletDefinition)
|
||||
.contains("idx_pallet_warehouse (tenant_id, warehouse_id, deleted, pallet_code)")
|
||||
.contains("idx_pallet_type (tenant_id, warehouse_id, pallet_type, capacity_cbm, deleted)")
|
||||
.doesNotContain("status VARCHAR")
|
||||
.doesNotContain("status varchar")
|
||||
.doesNotContain("idx_pallet_available");
|
||||
assertThat(unified)
|
||||
.contains("ALTER TABLE wh_pallet DROP COLUMN status")
|
||||
.contains("ALTER TABLE wh_location_reservation DROP COLUMN active_pallet_reservation")
|
||||
.contains("ALTER TABLE wh_location_occupancy DROP COLUMN active_pallet_occupancy")
|
||||
.contains("托盘动态资源状态或活动唯一约束仍存在")
|
||||
.contains("库位活动预占或占用唯一约束不完整");
|
||||
.doesNotContain("ALTER TABLE wh_pallet DROP COLUMN status")
|
||||
.doesNotContain("active_pallet_reservation")
|
||||
.doesNotContain("active_pallet_occupancy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedSqlUsesOnlyNonSupplierPartnerTypes() throws IOException {
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
|
||||
assertThat(tableDefinition(unified, "wh_partner"))
|
||||
.contains("chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))")
|
||||
.contains("carrier_code VARCHAR(16) NULL")
|
||||
.contains("chk_wh_partner_carrier_code CHECK (carrier_code IS NULL OR carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))")
|
||||
.doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH");
|
||||
String partner = tableDefinition(unified.toLowerCase(), "wh_partner").replace("_utf8mb4", "");
|
||||
assertThat(partner)
|
||||
.contains("chk_wh_partner_type check")
|
||||
.contains("partner_type in ('carrier','shipper','consignee')")
|
||||
.contains("carrier_code varchar(16) default null")
|
||||
.contains("chk_wh_partner_carrier_code check")
|
||||
.contains("carrier_code in ('matson','sml','zim','cosco','emc','hede','other')")
|
||||
.doesNotContain("customs_broker", "supplier或both");
|
||||
assertThat(unified)
|
||||
.contains("CALL add_column_if_missing('wh_partner', 'carrier_code', 'VARCHAR(16) NULL")
|
||||
.contains("UPDATE wh_partner SET carrier_code = 'OTHER'")
|
||||
.contains("queue_column_comment('baiyue_warehouse', 'wh_partner', 'carrier_code'");
|
||||
.doesNotContain("CALL add_column_if_missing('wh_partner'")
|
||||
.doesNotContain("queue_column_comment('baiyue_warehouse', 'wh_partner', 'carrier_code'");
|
||||
}
|
||||
|
||||
private static String read(String file) throws IOException {
|
||||
@@ -192,8 +189,14 @@ class WarehouseSchemaContractTest {
|
||||
}
|
||||
|
||||
private static String tableDefinition(String sql, String table) {
|
||||
int start = sql.indexOf("CREATE TABLE IF NOT EXISTS " + table + " (");
|
||||
String marker = sql.contains("CREATE TABLE IF NOT EXISTS " + table + " (")
|
||||
? "CREATE TABLE IF NOT EXISTS " + table + " ("
|
||||
: "create table " + table + " (";
|
||||
int start = sql.indexOf(marker);
|
||||
int end = sql.indexOf("ENGINE=InnoDB", start);
|
||||
if (end < 0) {
|
||||
end = sql.indexOf("engine=innodb", start);
|
||||
}
|
||||
return sql.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user