diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/AppController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/AppController.java index 2a5ecbd..bfb528e 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/AppController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/AppController.java @@ -69,8 +69,8 @@ public class AppController { @PostMapping public ResponseEntity> create(@RequestBody CreateAppRequest req, @AuthenticationPrincipal String tenantId) { - requireNonBlank(req.packageName(), "packageName"); requireNonBlank(req.name(), "name"); + requireAtLeastOnePackageName(req); return ResponseEntity.ok(ApiResponse.success(appService.create(tenantId, req))); } @@ -78,8 +78,8 @@ public class AppController { public ResponseEntity> update(@PathVariable String appKey, @RequestBody CreateAppRequest req, @AuthenticationPrincipal String tenantId) { - requireNonBlank(req.packageName(), "packageName"); requireNonBlank(req.name(), "name"); + requireAtLeastOnePackageName(req); return ResponseEntity.ok(ApiResponse.success(appService.update(appKey, tenantId, req))); } @@ -89,6 +89,15 @@ public class AppController { } } + private static void requireAtLeastOnePackageName(CreateAppRequest req) { + boolean hasAny = (req.packageName() != null && !req.packageName().isBlank()) + || (req.iosBundleId() != null && !req.iosBundleId().isBlank()) + || (req.harmonyBundleName() != null && !req.harmonyBundleName().isBlank()); + if (!hasAny) { + throw new BusinessException(400, "至少填写一个平台的包名(packageName / iosBundleId / harmonyBundleName)"); + } + } + @DeleteMapping("/{appKey}") public ResponseEntity> delete(@PathVariable String appKey, @AuthenticationPrincipal String tenantId) { diff --git a/update-service/src/main/java/com/xuqm/update/config/SchemaMigrationRunner.java b/update-service/src/main/java/com/xuqm/update/config/SchemaMigrationRunner.java new file mode 100644 index 0000000..c79ca6e --- /dev/null +++ b/update-service/src/main/java/com/xuqm/update/config/SchemaMigrationRunner.java @@ -0,0 +1,124 @@ +package com.xuqm.update.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.PriorityOrdered; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +/** + * update-service 数据库迁移,在 Hibernate 初始化之前执行。 + * + * 使用 BeanFactoryPostProcessor + PriorityOrdered 确保在所有 bean 创建之前运行, + * 避免旧的 GrayMode 枚举值(IM_PUSH_USERS/CUSTOMER_SYNC/CUSTOMER_CALLBACK) + * 导致 Hibernate 反序列化失败。 + */ +@Component +public class SchemaMigrationRunner implements BeanFactoryPostProcessor, PriorityOrdered { + + private static final Logger log = LoggerFactory.getLogger(SchemaMigrationRunner.class); + + @Override + public int getOrder() { + return PriorityOrdered.HIGHEST_PRECEDENCE; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + try { + DataSource dataSource = beanFactory.getBean(DataSource.class); + migrate_v20260610_gray_mode_simplify(dataSource); + } catch (Exception e) { + log.warn("Schema migration skipped (DataSource not ready): {}", e.getMessage()); + } + } + + /** + * 将旧灰度模式迁移到新的 PERCENT / MEMBERS 模式。 + * + * 旧值:IM_PUSH_USERS / CUSTOMER_SYNC / CUSTOMER_CALLBACK → MEMBERS + * 清理:grayCallbackUrl 旧数据(已废弃,改为配置级 publishCallbackUrl) + */ + private void migrate_v20260610_gray_mode_simplify(DataSource dataSource) { + String migrationId = "v20260610_gray_mode_simplify"; + try (Connection conn = dataSource.getConnection()) { + // 确保迁移记录表存在 + try (Statement stmt = conn.createStatement()) { + stmt.execute(""" + CREATE TABLE IF NOT EXISTS _schema_migrations ( + id VARCHAR(128) NOT NULL PRIMARY KEY, + applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + description VARCHAR(255) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + """); + } + + // 检查是否已执行 + try (var ps = conn.prepareStatement( + "SELECT COUNT(*) FROM _schema_migrations WHERE id = ?")) { + ps.setString(1, migrationId); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next() && rs.getInt(1) > 0) { + return; // 已执行,跳过 + } + } + } + + log.info("Applying migration: {}", migrationId); + + // 迁移 update_app_version 表 + int appVersions = 0; + try (var ps = conn.prepareStatement( + "UPDATE update_app_version SET gray_mode = 'MEMBERS' " + + "WHERE gray_mode IN ('IM_PUSH_USERS', 'CUSTOMER_SYNC', 'CUSTOMER_CALLBACK')")) { + appVersions = ps.executeUpdate(); + } + + // 迁移 update_rn_bundle 表 + int rnBundles = 0; + try (var ps = conn.prepareStatement( + "UPDATE update_rn_bundle SET gray_mode = 'MEMBERS' " + + "WHERE gray_mode IN ('IM_PUSH_USERS', 'CUSTOMER_SYNC', 'CUSTOMER_CALLBACK')")) { + rnBundles = ps.executeUpdate(); + } + + // 清理旧的 grayCallbackUrl + try (var ps = conn.prepareStatement( + "UPDATE update_app_version SET gray_callback_url = NULL " + + "WHERE gray_callback_url IS NOT NULL AND gray_callback_url != ''")) { + int cleaned = ps.executeUpdate(); + if (cleaned > 0) { + log.info("Cleared {} old grayCallbackUrl from update_app_version", cleaned); + } + } + try (var ps = conn.prepareStatement( + "UPDATE update_rn_bundle SET gray_callback_url = NULL " + + "WHERE gray_callback_url IS NOT NULL AND gray_callback_url != ''")) { + int cleaned = ps.executeUpdate(); + if (cleaned > 0) { + log.info("Cleared {} old grayCallbackUrl from update_rn_bundle", cleaned); + } + } + + // 记录迁移 + try (var ps = conn.prepareStatement( + "INSERT IGNORE INTO _schema_migrations (id, description) VALUES (?, ?)")) { + ps.setString(1, migrationId); + ps.setString(2, "Simplify GrayMode to PERCENT/MEMBERS, clear grayCallbackUrl"); + ps.executeUpdate(); + } + + log.info("Migration {} done: {} app versions, {} RN bundles converted to MEMBERS", + migrationId, appVersions, rnBundles); + + } catch (Exception e) { + log.error("Migration {} failed: {}", migrationId, e.getMessage(), e); + } + } +}