From a86b753508a349f08c3b528afd5c9079245e8532 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Tue, 28 Jul 2026 21:50:28 +0800 Subject: [PATCH] fix(update): make store migration production-safe --- Jenkinsfile | 18 ++++++++ .../migration/V5__store_monitoring_domain.sql | 18 ++++---- .../config/MigrationSchemaContractTest.java | 41 +++++++++++++++++++ 3 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 update-service/src/test/java/com/xuqm/update/config/MigrationSchemaContractTest.java diff --git a/Jenkinsfile b/Jenkinsfile index b16ece0..33c6ff3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -165,6 +165,24 @@ pipeline { ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${PROD_USER}@${PROD_HOST} "docker image prune -f 2>/dev/null || true; docker pull ${latestImage} || exit 1; docker compose -f ${COMPOSE_FILE} up -d --no-deps --force-recreate ${svc} || exit 1; docker image prune -f" """ } + + // Jenkins 不能仅以 docker compose 返回 0 判定部署成功。Spring 服务可能 + // 在容器启动后因配置或数据库迁移失败进入重启循环,必须等待真实健康端点。 + def healthPortByService = [ + 'tenant-service': 9001, + 'update-service': 8084, + 'xuqm-bugcollect-service': 9006, + ] + def healthPort = healthPortByService[svc] + if (healthPort != null) { + retry(20) { + sleep time: 3, unit: 'SECONDS' + bat """ + chcp 65001 >nul + ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${PROD_USER}@${PROD_HOST} "curl -fsS http://127.0.0.1:${healthPort}/actuator/health | grep -q '\\\"status\\\":\\\"UP\\\"'" + """ + } + } } // 合并更新 versions.json(只改动本次构建涉及的服务,不覆盖其它服务或 web 条目) diff --git a/update-service/src/main/resources/db/migration/V5__store_monitoring_domain.sql b/update-service/src/main/resources/db/migration/V5__store_monitoring_domain.sql index 40189e1..931087f 100644 --- a/update-service/src/main/resources/db/migration/V5__store_monitoring_domain.sql +++ b/update-service/src/main/resources/db/migration/V5__store_monitoring_domain.sql @@ -21,7 +21,7 @@ CREATE TABLE update_store_application ( updated_at DATETIME(6) NOT NULL, UNIQUE KEY uk_store_application (app_key, store_type), INDEX idx_store_application_sync (enabled, last_synced_at) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE update_store_credential_profile ( id VARCHAR(64) NOT NULL PRIMARY KEY, @@ -37,7 +37,7 @@ CREATE TABLE update_store_credential_profile ( created_at DATETIME(6) NOT NULL, updated_at DATETIME(6) NOT NULL, UNIQUE KEY uk_store_credential_purpose (app_key, store_type, purpose) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE update_store_secret ( secret_ref VARCHAR(256) NOT NULL PRIMARY KEY, @@ -46,7 +46,7 @@ CREATE TABLE update_store_secret ( key_version VARCHAR(32) NOT NULL, created_at DATETIME(6) NOT NULL, updated_at DATETIME(6) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE update_store_version ( id VARCHAR(64) NOT NULL PRIMARY KEY, @@ -67,7 +67,7 @@ CREATE TABLE update_store_version ( CONSTRAINT fk_store_version_application FOREIGN KEY (store_application_id) REFERENCES update_store_application(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE update_store_region_availability ( id VARCHAR(64) NOT NULL PRIMARY KEY, @@ -80,7 +80,7 @@ CREATE TABLE update_store_region_availability ( CONSTRAINT fk_store_region_version FOREIGN KEY (store_version_id) REFERENCES update_store_version(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE update_store_state_event ( id VARCHAR(64) NOT NULL PRIMARY KEY, @@ -104,7 +104,7 @@ CREATE TABLE update_store_state_event ( CONSTRAINT fk_store_event_version FOREIGN KEY (store_version_id) REFERENCES update_store_version(id) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE update_store_update_link ( id VARCHAR(64) NOT NULL PRIMARY KEY, @@ -120,7 +120,7 @@ CREATE TABLE update_store_update_link ( CONSTRAINT fk_store_link_app_version FOREIGN KEY (app_version_id) REFERENCES update_app_version(id) ON DELETE RESTRICT -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE update_store_notification_policy ( id VARCHAR(64) NOT NULL PRIMARY KEY, @@ -134,7 +134,7 @@ CREATE TABLE update_store_notification_policy ( created_at DATETIME(6) NOT NULL, updated_at DATETIME(6) NOT NULL, UNIQUE KEY uk_store_notification_policy (app_key) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE update_store_notification_outbox ( id VARCHAR(64) NOT NULL PRIMARY KEY, @@ -152,4 +152,4 @@ CREATE TABLE update_store_notification_outbox ( CONSTRAINT fk_store_notification_event FOREIGN KEY (store_event_id) REFERENCES update_store_state_event(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/update-service/src/test/java/com/xuqm/update/config/MigrationSchemaContractTest.java b/update-service/src/test/java/com/xuqm/update/config/MigrationSchemaContractTest.java new file mode 100644 index 0000000..e61207c --- /dev/null +++ b/update-service/src/test/java/com/xuqm/update/config/MigrationSchemaContractTest.java @@ -0,0 +1,41 @@ +package com.xuqm.update.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class MigrationSchemaContractTest { + + @Test + void storeMonitoringTablesUseLegacyCompatibleCollation() throws IOException { + var resource = getClass().getResourceAsStream( + "/db/migration/V5__store_monitoring_domain.sql" + ); + assertTrue(resource != null, "V5 migration must be packaged"); + + String sql; + try (resource) { + sql = new String(resource.readAllBytes(), StandardCharsets.UTF_8); + } + + assertEquals(9, occurrences(sql, "CREATE TABLE update_store_")); + assertEquals( + 9, + occurrences(sql, "DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"), + "Every V5 table must match the collation of existing varchar foreign keys" + ); + } + + private static int occurrences(String value, String token) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(token, offset)) >= 0) { + count++; + offset += token.length(); + } + return count; + } +}