diff --git a/docs/changelog/index.md b/docs/changelog/index.md index ae05ca06..78f3e56a 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -20,6 +20,8 @@ sidebar: false - 配置版本列表按 `created_at` 倒序展示,最新发布版本固定显示在列表顶部。 +- 修复 WAF 规则组保存/绑定网站时报 `of_waf_rule_group_bindings_pkey` 冲突:PostgreSQL 在迁移导入显式 ID 后同步绑定表序列,并在写入前自动校正序列。 + - 修复 WAF 规则组 PoW 策略发布后边缘不生效:统一 WAF 绑定站点名与 OpenResty 路由 `site_name` 解析逻辑,并为所有已启用网站生成 `site_rule_groups` 条目(含仅依赖全局规则组的站点)。 - 收敛子代理站点标识双轨逻辑:新增 `routeidentity` 统一包,`proxy_route`、`config_version`、`uptimekuma`、`flared` 与 OpenResty 渲染共用 `ResolveSiteName` / `DecodeDomains`;移除废弃 `RenderPoWConfig`;PoW Lua 与 WAF 一致仅依赖 `$openflare_waf_site`。 diff --git a/internal/db/migrator/202606200006_migrate_legacy_data.go b/internal/db/migrator/202606200006_migrate_legacy_data.go index 21f0b616..ee6b1cc4 100644 --- a/internal/db/migrator/202606200006_migrate_legacy_data.go +++ b/internal/db/migrator/202606200006_migrate_legacy_data.go @@ -98,6 +98,17 @@ func up202606200006(ctx context.Context, tx *sql.Tx) error { } } + if dialect == dialectPostgres { + if _, err := tx.ExecContext(ctx, ` + SELECT setval( + pg_get_serial_sequence('of_waf_rule_group_bindings', 'id'), + COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) + ) + `); err != nil { + return fmt.Errorf("sync of_waf_rule_group_bindings sequence failed: %w", err) + } + } + return nil } diff --git a/internal/db/migrator/goose/postgres/202606200007_sync_of_waf_binding_sequence.sql b/internal/db/migrator/goose/postgres/202606200007_sync_of_waf_binding_sequence.sql new file mode 100644 index 00000000..cf7562d2 --- /dev/null +++ b/internal/db/migrator/goose/postgres/202606200007_sync_of_waf_binding_sequence.sql @@ -0,0 +1,8 @@ +-- +goose Up +SELECT setval( + pg_get_serial_sequence('of_waf_rule_group_bindings', 'id'), + COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) +); + +-- +goose Down +SELECT 1; \ No newline at end of file diff --git a/internal/db/migrator/goose/sqlite/202606200007_sync_of_waf_binding_sequence.sql b/internal/db/migrator/goose/sqlite/202606200007_sync_of_waf_binding_sequence.sql new file mode 100644 index 00000000..4fa75e57 --- /dev/null +++ b/internal/db/migrator/goose/sqlite/202606200007_sync_of_waf_binding_sequence.sql @@ -0,0 +1,5 @@ +-- +goose Up +SELECT 1; + +-- +goose Down +SELECT 1; \ No newline at end of file diff --git a/internal/model/openflare_waf.go b/internal/model/openflare_waf.go index ea887a20..eaab6b58 100644 --- a/internal/model/openflare_waf.go +++ b/internal/model/openflare_waf.go @@ -312,6 +312,28 @@ func ListOpenFlareWAFRuleGroupBindingsByRouteID(ctx context.Context, routeID uin return bindings, nil } +func syncWAFBindingIDSequence(tx *gorm.DB) error { + if tx == nil || tx.Dialector.Name() != "postgres" { + return nil + } + return tx.Exec(` + SELECT setval( + pg_get_serial_sequence('of_waf_rule_group_bindings', 'id'), + COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) + ) + `).Error +} + +func insertOpenFlareWAFRuleGroupBindings(tx *gorm.DB, bindings []OpenFlareWAFRuleGroupBinding) error { + if len(bindings) == 0 { + return nil + } + if err := syncWAFBindingIDSequence(tx); err != nil { + return err + } + return tx.Create(&bindings).Error +} + // ReplaceOpenFlareWAFRuleGroupBindings replaces bindings for a rule group. func ReplaceOpenFlareWAFRuleGroupBindings(ctx context.Context, groupID uint, routeIDs []uint) error { conn, err := wafDB(ctx) @@ -322,13 +344,14 @@ func ReplaceOpenFlareWAFRuleGroupBindings(ctx context.Context, groupID uint, rou if err = tx.Where("rule_group_id = ?", groupID).Delete(&OpenFlareWAFRuleGroupBinding{}).Error; err != nil { return err } + bindings := make([]OpenFlareWAFRuleGroupBinding, 0, len(routeIDs)) for _, routeID := range routeIDs { - binding := OpenFlareWAFRuleGroupBinding{RuleGroupID: groupID, ProxyRouteID: routeID} - if err = tx.Create(&binding).Error; err != nil { - return err - } + bindings = append(bindings, OpenFlareWAFRuleGroupBinding{ + RuleGroupID: groupID, + ProxyRouteID: routeID, + }) } - return nil + return insertOpenFlareWAFRuleGroupBindings(tx, bindings) }) } @@ -342,13 +365,14 @@ func ReplaceOpenFlareWAFSiteRuleGroupBindings(ctx context.Context, routeID uint, if err = tx.Where("proxy_route_id = ?", routeID).Delete(&OpenFlareWAFRuleGroupBinding{}).Error; err != nil { return err } + bindings := make([]OpenFlareWAFRuleGroupBinding, 0, len(groupIDs)) for _, groupID := range groupIDs { - binding := OpenFlareWAFRuleGroupBinding{RuleGroupID: groupID, ProxyRouteID: routeID} - if err = tx.Create(&binding).Error; err != nil { - return err - } + bindings = append(bindings, OpenFlareWAFRuleGroupBinding{ + RuleGroupID: groupID, + ProxyRouteID: routeID, + }) } - return nil + return insertOpenFlareWAFRuleGroupBindings(tx, bindings) }) } diff --git a/internal/model/openflare_waf_bindings_test.go b/internal/model/openflare_waf_bindings_test.go new file mode 100644 index 00000000..1faade42 --- /dev/null +++ b/internal/model/openflare_waf_bindings_test.go @@ -0,0 +1,54 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package model + +import ( + "context" + "testing" + + "github.com/Rain-kl/Wavelet/internal/db" + "github.com/glebarez/sqlite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func setupWAFBindingsTestDB(t *testing.T) func() { + t.Helper() + + sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + }) + require.NoError(t, err) + require.NoError(t, sqliteDB.AutoMigrate(&OpenFlareWAFRuleGroupBinding{})) + + db.SetDB(sqliteDB) + return func() { + db.SetDB(nil) + } +} + +func TestReplaceOpenFlareWAFRuleGroupBindingsAfterExplicitHighID(t *testing.T) { + cleanup := setupWAFBindingsTestDB(t) + defer cleanup() + ctx := context.Background() + + conn := db.DB(ctx) + require.NotNil(t, conn) + require.NoError(t, conn.Create(&OpenFlareWAFRuleGroupBinding{ + ID: 50, + RuleGroupID: 1, + ProxyRouteID: 1, + }).Error) + + require.NoError(t, ReplaceOpenFlareWAFRuleGroupBindings(ctx, 2, []uint{2, 3})) + + var bindings []OpenFlareWAFRuleGroupBinding + require.NoError(t, conn.Where("rule_group_id = ?", 2).Order("proxy_route_id asc").Find(&bindings).Error) + require.Len(t, bindings, 2) + assert.Equal(t, uint(2), bindings[0].ProxyRouteID) + assert.Equal(t, uint(3), bindings[1].ProxyRouteID) + assert.Greater(t, bindings[0].ID, uint(50)) + assert.Greater(t, bindings[1].ID, bindings[0].ID) +}