fix(lastleg): route all relabel pickups directly to outbound and verify final schemas

This commit is contained in:
2026-10-15 21:05:34 +08:00
parent 4277601999
commit 607aaf74dc
27 changed files with 717 additions and 185 deletions
@@ -56,6 +56,22 @@ class Phase5OverseasUpgradeSqlContractTest {
assertThat(overseas).doesNotContain("\nUPDATE ", "\nDELETE ", "ALTER TABLE ll_");
}
@Test
void transferFinalMeasurementColumnsAreCreatedBeforeAnyBusinessWrite() throws Exception {
for (String path : List.of("lastleg-service/src/main/resources/db/lastleg.sql",
"deploy/sql/海外区域统一增量-MySQL8.sql")) {
String sql = repositoryFile(path);
String carton = createStatement(sql, "ll_warehouse_transfer_carton");
for (String column : List.of("customer_carton_mark", "length_cm", "width_cm", "height_cm",
"gross_weight_kg", "counted_quantity", "count_idempotency_key", "count_request_hash",
"counted_by", "counted_at", "discrepancy_reason", "transfer_pallet_id", "sku_items_json")) {
assertThat(carton).as(path + ":" + column).contains(column);
}
assertThat(createStatement(sql, "ll_warehouse_transfer_item")).contains("declared_quantity");
assertThat(createStatement(sql, "ll_cargo_discard_item")).contains("idx_ll_cargo_discard_return_carton");
}
}
@Test
void overseasFixedEntryAndModuleBaselineBothDefineCompletePhase5Tables() throws Exception {
String baseline = repositoryFile("lastleg-service/src/main/resources/db/lastleg.sql");
@@ -26,7 +26,7 @@ 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[^;]*?COMMENT\\s*=\\s*'([^']*)'\\s*;");
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB([^;]*);");
@Test
void allLastlegTablesAndColumnsHaveChineseComments() throws Exception {
@@ -104,7 +104,8 @@ class SchemaChineseCommentContractTest {
Matcher matcher = TABLE.matcher(sql);
while (matcher.find()) {
String table = matcher.group(1);
assertTrue(CHINESE.matcher(matcher.group(3)).find(), source + " 表备注必须包含中文: " + table);
assertTrue(Pattern.compile("(?is)\\bCOMMENT\\s*=\\s*'[^']*[\\u4e00-\\u9fff][^']*'")
.matcher(matcher.group(3)).find(), source + " 表备注必须包含中文: " + table);
List<String> columns = splitTopLevel(matcher.group(2));
assertFalse(columns.isEmpty(), source + " 表定义没有字段: " + table);
List<String> columnNames = new ArrayList<>();
@@ -6,35 +6,62 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.*;
class SelfoperatedAnalyticsSchemaContractTest {
private static final Pattern TABLE = Pattern.compile(
"(?s)CREATE TABLE (ll_selfop_\\w+) \\(.*?\\) ENGINE=InnoDB[^;]+;");
"(?s)CREATE TABLE (?:IF NOT EXISTS )?(ll_selfop_\\w+) \\(.*?\\) ENGINE=InnoDB[^;]+;");
private static final Set<String> EXPECTED_TABLES = Set.of(
"ll_selfop_outbound_discovery_scope", "ll_selfop_sync_scope", "ll_selfop_sync_task",
"ll_selfop_sync_run", "ll_selfop_sync_stage", "ll_selfop_outbound_order",
"ll_selfop_outbound_item", "ll_selfop_inventory_snapshot", "ll_selfop_sku_daily_sales",
"ll_selfop_warehouse_daily_sales", "ll_selfop_sku_profile", "ll_selfop_owner",
"ll_selfop_product", "ll_selfop_inventory_flow", "ll_selfop_inventory_day_version",
"ll_selfop_inventory_daily");
private static Map<String, String> tables(String sql) {
var result = new LinkedHashMap<String, String>();
var matcher = TABLE.matcher(sql);
var matcher = TABLE.matcher(sql.replace("\r\n", "\n"));
while (matcher.find()) {
assertNull(result.put(matcher.group(1), matcher.group()), "duplicate table definition");
assertNull(result.put(matcher.group(1), matcher.group().replaceFirst("CREATE TABLE IF NOT EXISTS", "CREATE TABLE")),
"duplicate table definition");
}
return result;
}
@Test
void sourceAndEmptyDatabaseBaselineContainIdenticalElevenIndependentTables() throws Exception {
void sourceAndEmptyDatabaseBaselineContainAllCurrentIndependentTables() throws Exception {
var source = tables(Files.readString(Path.of("src/main/resources/db/lastleg.sql")));
var deployment = tables(Files.readString(Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql")));
assertEquals(11, source.size());
assertEquals(EXPECTED_TABLES, source.keySet());
assertEquals(source, deployment);
for (var entry : source.entrySet()) {
var ddl = entry.getValue();
assertTrue(ddl.contains("COLLATE=utf8mb4_bin"), entry.getKey());
assertTrue(ddl.contains("id BIGINT NOT NULL"));
assertTrue(ddl.contains("created_at DATETIME(3) NOT NULL"));
assertTrue(ddl.contains("updated_at DATETIME(3) NOT NULL"));
boolean master = Set.of("ll_selfop_owner", "ll_selfop_product").contains(entry.getKey());
boolean history = Set.of("ll_selfop_inventory_flow", "ll_selfop_inventory_day_version",
"ll_selfop_inventory_daily").contains(entry.getKey());
if (master) {
assertTrue(ddl.contains("source_account VARCHAR(64) COLLATE utf8mb4_bin NOT NULL"));
assertTrue(ddl.contains("customer_code VARCHAR(64) COLLATE utf8mb4_bin NOT NULL"));
assertTrue(ddl.contains("observed_at_utc DATETIME(6) NOT NULL"));
} else {
assertTrue(ddl.contains("COLLATE=utf8mb4_bin"), entry.getKey());
}
if (!master && !history) {
assertTrue(ddl.contains("created_at DATETIME(3) NOT NULL"), entry.getKey());
assertTrue(ddl.contains("updated_at DATETIME(3) NOT NULL"), entry.getKey());
}
assertTrue(ddl.contains("PRIMARY KEY ("), entry.getKey());
if (entry.getKey().equals("ll_selfop_inventory_daily")) {
assertTrue(ddl.contains("PRIMARY KEY (day_version_id,customer_code,sku_code,product_quality)"));
} else if (entry.getKey().equals("ll_selfop_warehouse_daily_sales")) {
assertTrue(ddl.contains("PRIMARY KEY (sales_run_id,source_account,warehouse_code,business_date)"));
} else {
assertTrue(ddl.contains("\n id BIGINT NOT NULL"), entry.getKey());
}
assertFalse(ddl.contains("AUTO_INCREMENT"));
assertFalse(ddl.contains("tenant_id"));
assertFalse(ddl.contains("FOREIGN KEY"));
@@ -107,21 +134,34 @@ class SelfoperatedAnalyticsSchemaContractTest {
}
@Test
void readAndSyncPermissionsAreIndependentWithThreeAuthorizedAdminMenus() throws Exception {
void readSyncAndExportPermissionsAreIndependentWithFourAuthorizedAdminMenus() throws Exception {
var seed = Files.readString(Path.of("../iam/src/main/resources/db/selfoperated-analytics-permissions.sql"));
var unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
assertTrue(unified.contains(seed.strip()));
// Compare executable seed statements; explanatory comments and line endings are not SQL facts.
String seedSql = seed.replace("\r\n", "\n").replaceAll("(?m)^\\s*--[^\n]*", "");
String unifiedSql = unified.replace("\r\n", "\n").replaceAll("(?m)^\\s*--[^\n]*", "");
int comparedStatements = 0;
for (String statement : seedSql.split(";")) {
if (statement.strip().startsWith("INSERT INTO")) {
assertTrue(unifiedSql.contains(statement.strip() + ";"), statement);
comparedStatements++;
}
}
assertEquals(2, comparedStatements, "permission and role grant INSERT statements must both be checked");
assertTrue(seed.contains("platform:self-operated-analytics:list"));
assertTrue(seed.contains("platform:self-operated-analytics:sync"));
for (var route : new String[]{"/self-operated/sales", "/self-operated/inventory", "/self-operated/collection"}) {
assertTrue(seed.contains("platform:self-operated-analytics:export"));
for (var route : new String[]{"/self-operated/overview", "/self-operated/sales", "/self-operated/inventory", "/self-operated/collection"}) {
assertTrue(seed.contains(route));
}
assertTrue(seed.contains("'API',5118100,'platform:self-operated-analytics:list'"));
assertTrue(seed.contains("'API',5118130,'platform:self-operated-analytics:sync'"));
assertTrue(seed.contains("'API',5118120,'platform:self-operated-analytics:export'"));
assertFalse(seed.contains("'MERCHANT'"));
assertFalse(seed.contains("INSERT IGNORE"));
assertFalse(seed.contains("ON DUPLICATE KEY"));
assertTrue(seed.contains("(4000001,5118000,0,"));
assertTrue(seed.contains("(4000001,5118001,0,"));
assertTrue(seed.contains("(4000001,5118002,0,"));
}
}