From f3a4fb569f479fe296065663fb96e4daddb1f361 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Mon, 14 Sep 2026 09:02:42 +0000 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E5=85=A8=E6=96=B0=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E9=93=BE=E8=B7=AF=E5=81=A5=E5=A3=AE=E6=80=A7=EF=BC=88?= =?UTF-8?q?0004=20=E5=B9=82=E7=AD=89=20+=20=E8=BF=81=E7=A7=BB=E5=89=8D?= =?UTF-8?q?=E7=BD=AE=20+=20=E7=A9=BA=E5=BA=93=E5=AE=88=E5=8D=AB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 0004_metric_aggregate_unique 改为幂等:新库基线已由模型 uniqueIndex 建出 uniq_metric_agg,再次 CREATE 会以 1061 重复键名中断 migrate up, 导致 0005_agent_settings 建不出来(全新部署实测复现) - 新增 database.IndexExists:跨 SQLite/MySQL/PostgreSQL 判断索引是否存在 - 新增 database.EnsureSchemaInitialized:服务启动遇到空库时明确失败并给出 执行 aiops migrate up 的指引,避免「服务起来了但页面全是空数据」 - run.sh docker up/upgrade/update 先跑一次 aiops migrate up 再启动服务 // Logan.Li --- backend/main.go | 5 +++++ backend/migrations/migrations.go | 11 +++++++++++ backend/pkg/database/database.go | 12 ++++++++++++ backend/pkg/database/dialect.go | 22 ++++++++++++++++++++++ run.sh | 7 +++++++ 5 files changed, 57 insertions(+) diff --git a/backend/main.go b/backend/main.go index 31ac9d8..449ac9f 100644 --- a/backend/main.go +++ b/backend/main.go @@ -155,6 +155,11 @@ func main() { log.Warn("--skip-auto-migrate 已废弃;服务启动始终不会执行数据库迁移") } + // 空库直接失败并给出明确指引,避免服务带病启动(全新部署需先执行一次 migrate up)。 // Logan.Li + if err := database.EnsureSchemaInitialized(db); err != nil { + log.Fatal("数据库尚未初始化:请先执行 `aiops migrate up` 建表(服务启动不会自动建表)", zap.Error(err)) + } + // 初始化种子数据 seed.SeedData(db) // 中间件监控默认目标(aiops-redis / aiops-mysql)// Logan.Li diff --git a/backend/migrations/migrations.go b/backend/migrations/migrations.go index ffa201a..d88dd08 100644 --- a/backend/migrations/migrations.go +++ b/backend/migrations/migrations.go @@ -70,6 +70,17 @@ func All() []Migration { Version: "0004_metric_aggregate_unique", Description: "指标聚合表唯一约束(upsert 生效)", Apply: func(db *gorm.DB) error { + // 新库基线已由模型自带 uniq_metric_agg(uniqueIndex 标签)建出该索引, + // 此时必须跳过:否则 CREATE UNIQUE INDEX 会以 1061 重复键名中断整个 migrate up, + // 导致后面的迁移(如 0005_agent_settings)永远建不出来。 + // 只有老库(历史 idx_agg 为非唯一索引)才需要去重 + 重建唯一索引。 // Logan.Li + exists, err := database.IndexExists(db, "monitor_metric_aggregates", "uniq_metric_agg") + if err != nil { + return err + } + if exists { + return nil + } return executeDialectSQL(db, "0004_metric_aggregate_unique") }, }, diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index a0a09bd..5981be3 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -84,6 +84,18 @@ func (n indexNamedNaming) UniqueName(table, column string) string { return n.NamingStrategy.IndexName(table, column) } +// EnsureSchemaInitialized 校验库结构是否已初始化(即已执行过 `aiops migrate up`)。 +// 服务启动不做结构迁移,但遇到空库必须明确失败,避免「服务已起来、页面全是空数据」的静默故障。 // Logan.Li +func EnsureSchemaInitialized(db *gorm.DB) error { + if db == nil { + return fmt.Errorf("数据库连接为空") + } + if !db.Migrator().HasTable("iam_users") { + return fmt.Errorf("未检测到业务表,数据库尚未初始化") + } + return nil +} + // BootstrapSchema 为空数据库创建初始基线结构。 // 该函数只能由显式的 `aiops migrate up` 调用,服务启动路径禁止调用。 // Logan.Li func BootstrapSchema(db *gorm.DB) error { diff --git a/backend/pkg/database/dialect.go b/backend/pkg/database/dialect.go index 7627a88..2370397 100644 --- a/backend/pkg/database/dialect.go +++ b/backend/pkg/database/dialect.go @@ -61,3 +61,25 @@ func CleanupSNMPMetricsSQL(driver string, retentionDays int) string { ) } } + +// IndexExists 判断某表上是否已存在指定名称的索引(跨 SQLite / MySQL / PostgreSQL)。 +// 供版本化迁移做幂等判断:新库基线已由模型(uniqueIndex 标签)建出同名索引时, +// 迁移不应再建一次,否则 MySQL 会以 1061 重复键名中断整个 migrate up。 // Logan.Li +func IndexExists(db *gorm.DB, table string, index string) (bool, error) { + var ( + query string + count int64 + ) + switch DriverName(db) { + case "sqlite": + query = "SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND name = ?" + case "postgres": + query = "SELECT COUNT(*) FROM pg_indexes WHERE schemaname = current_schema() AND tablename = ? AND indexname = ?" + default: + query = "SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?" + } + if err := db.Raw(query, table, index).Scan(&count).Error; err != nil { + return false, fmt.Errorf("检查索引 %s.%s 是否存在失败: %w", table, index, err) + } + return count > 0, nil +} diff --git a/run.sh b/run.sh index 6dd899c..7f3e3f4 100755 --- a/run.sh +++ b/run.sh @@ -759,6 +759,13 @@ cmd_docker() { case "$action" in up|upgrade|update) banner "========== Docker Compose 启动/升级 ($BUILD_VERSION) ==========" + # 全新部署时库是空的(服务启动刻意不建表),先显式跑一次结构迁移再启动服务, + # 避免容器先起来面对空库;已初始化的库此步幂等,只会输出「迁移已是最新状态」。 // Logan.Li + info "数据库结构迁移 (aiops migrate up)..." + if ! $DOCKER_COMPOSE run --rm --build aiops ./aiops migrate up; then + error "数据库迁移失败:请检查上方输出(历史库缺少迁移记录时需先执行 aiops migrate baseline)" + exit 1 + fi # Docker 在容器内构建,无需清理宿主机产物;每次 prune 缓存会导致下次全量编译 // Logan.Li if [ "$fresh_build" -eq 1 ]; then info "全量重建镜像 (--no-cache)..."