From 3a7d3387e0d008acc20ca829b1edd632d102b89a Mon Sep 17 00:00:00 2001 From: yangjianbo Date: Mon, 29 Dec 2025 10:43:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=95=B0=E6=8D=AE=E5=BA=93):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E9=BB=98=E8=AE=A4=E5=88=86=E7=BB=84=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E4=B8=8E=E8=BF=81=E7=A7=BB=E9=94=81=E9=98=BB=E5=A1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过迁移补种默认 groups 记录,避免新装空分组 迁移锁改为 try lock + 重试并加入超时 写入 usage_logs 时保留 rate_multiplier=0 语义 测试: go test ./... --- backend/internal/infrastructure/ent.go | 5 ++++- .../infrastructure/migrations_runner.go | 22 +++++++++++++++---- backend/internal/repository/usage_log_repo.go | 3 --- backend/internal/setup/setup.go | 4 +++- backend/migrations/008_seed_default_group.sql | 4 ++++ 5 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 backend/migrations/008_seed_default_group.sql diff --git a/backend/internal/infrastructure/ent.go b/backend/internal/infrastructure/ent.go index 0e15c471..13184a83 100644 --- a/backend/internal/infrastructure/ent.go +++ b/backend/internal/infrastructure/ent.go @@ -5,6 +5,7 @@ package infrastructure import ( "context" "database/sql" + "time" "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/internal/config" @@ -54,7 +55,9 @@ func InitEnt(cfg *config.Config) (*ent.Client, *sql.DB, error) { // 确保数据库 schema 已准备就绪。 // SQL 迁移文件是 schema 的权威来源(source of truth)。 // 这种方式比 Ent 的自动迁移更可控,支持复杂的迁移场景。 - if err := applyMigrationsFS(context.Background(), drv.DB(), migrations.FS); err != nil { + migrationCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + if err := applyMigrationsFS(migrationCtx, drv.DB(), migrations.FS); err != nil { _ = drv.Close() // 迁移失败时关闭驱动,避免资源泄露 return nil, nil, err } diff --git a/backend/internal/infrastructure/migrations_runner.go b/backend/internal/infrastructure/migrations_runner.go index 69919a19..8477c031 100644 --- a/backend/internal/infrastructure/migrations_runner.go +++ b/backend/internal/infrastructure/migrations_runner.go @@ -10,6 +10,7 @@ import ( "io/fs" "sort" "strings" + "time" "github.com/Wei-Shaw/sub2api/migrations" ) @@ -31,6 +32,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations ( // 在多实例部署场景下,该锁确保同一时间只有一个实例执行迁移。 // 任何稳定的 int64 值都可以,只要不与同一数据库中的其他锁冲突即可。 const migrationsAdvisoryLockID int64 = 694208311321144027 +const migrationsLockRetryInterval = 500 * time.Millisecond // ApplyMigrations 将嵌入的 SQL 迁移文件应用到指定的数据库。 // @@ -166,11 +168,23 @@ func applyMigrationsFS(ctx context.Context, db *sql.DB, fsys fs.FS) error { // Advisory Lock 是一种轻量级的锁机制,不与任何特定的数据库对象关联。 // 它非常适合用于应用层面的分布式锁场景,如迁移序列化。 func pgAdvisoryLock(ctx context.Context, db *sql.DB) error { - _, err := db.ExecContext(ctx, "SELECT pg_advisory_lock($1)", migrationsAdvisoryLockID) - if err != nil { - return fmt.Errorf("acquire migrations lock: %w", err) + ticker := time.NewTicker(migrationsLockRetryInterval) + defer ticker.Stop() + + for { + var locked bool + if err := db.QueryRowContext(ctx, "SELECT pg_try_advisory_lock($1)", migrationsAdvisoryLockID).Scan(&locked); err != nil { + return fmt.Errorf("acquire migrations lock: %w", err) + } + if locked { + return nil + } + select { + case <-ctx.Done(): + return fmt.Errorf("acquire migrations lock: %w", ctx.Err()) + case <-ticker.C: + } } - return nil } // pgAdvisoryUnlock 释放 PostgreSQL Advisory Lock。 diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index 246285cf..5939c827 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -70,9 +70,6 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) } rateMultiplier := log.RateMultiplier - if rateMultiplier == 0 { - rateMultiplier = 1 - } query := ` INSERT INTO usage_logs ( diff --git a/backend/internal/setup/setup.go b/backend/internal/setup/setup.go index 759b930c..5565ab91 100644 --- a/backend/internal/setup/setup.go +++ b/backend/internal/setup/setup.go @@ -260,7 +260,9 @@ func initializeDatabase(cfg *SetupConfig) error { } }() - return infrastructure.ApplyMigrations(context.Background(), db) + migrationCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + return infrastructure.ApplyMigrations(migrationCtx, db) } func createAdminUser(cfg *SetupConfig) error { diff --git a/backend/migrations/008_seed_default_group.sql b/backend/migrations/008_seed_default_group.sql new file mode 100644 index 00000000..44522152 --- /dev/null +++ b/backend/migrations/008_seed_default_group.sql @@ -0,0 +1,4 @@ +-- Seed a default group for fresh installs. +INSERT INTO groups (name, description) +SELECT 'default', 'Default group' +WHERE NOT EXISTS (SELECT 1 FROM groups);