test(sql): align contracts with clean initialization
This commit is contained in:
+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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user