diff --git a/.env.example b/.env.example index 396e49c2..fd1ec9fc 100644 --- a/.env.example +++ b/.env.example @@ -64,7 +64,7 @@ REDIS_KEY_PREFIX=openflare: # ─── ClickHouse(必需)──────────────────────────────────────────────────── CLICKHOUSE_HOST=clickhouse:9000 CLICKHOUSE_USERNAME=default -CLICKHOUSE_PASSWORD= +CLICKHOUSE_PASSWORD=123456 CLICKHOUSE_NAME=openflare # ─── 日志 ────────────────────────────────────────────────────────────────────── diff --git a/config.example.yaml b/config.example.yaml index 4d995c13..6d6ea579 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -104,7 +104,7 @@ clickhouse: hosts: - "127.0.0.1:9000" username: "default" - password: "" + password: "123456" database: "openflare" max_idle_conn: 10 max_open_conn: 100 diff --git a/docs/changelog/index.md b/docs/changelog/index.md index cc2ab20e..8a54c2f6 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -23,6 +23,7 @@ sidebar: false ### 修复 +- 修复旧版本迁移升级后,在 PostgreSQL 数据库下发布版本或插入其它新数据时报“版本号生成冲突,请重试”等主键唯一约束冲突的问题。根本原因在于迁移数据时显式指定了旧 ID 导入,导致 PostgreSQL 各核心表的自增主键序列值(sequence)落后于 MAX(id);新增 `202606270001_sync_all_postgres_sequences` 迁移在系统启动时对所有核心表的自增序列执行 `setval` 重新对齐。 - 修复代理路由详情页点击“发布配置”时,同时弹出配置差异对话框和确认发布对话框导致重叠的问题:点击发布时不再展示配置差异,直接进行确认发布。 - 修复配置版本发布到 Agent 后 `openresty -t` 因 `proxy_cache_path` 使用 `/var/cache/openresty` 导致非 root 用户 `mkdir` 失败的问题:发布快照与渲染将 `/var/` 下路径规范为 `__OPENFLARE_PROXY_CACHE_PATH__`,Agent 应用时落地为 `data_dir/var/cache/openflare_proxy` 并兼容重写已发布配置中的旧路径。 - 修复配置版本发布到 Agent 后 `openresty -t` 因证书私钥无法解析而失败的问题。根因是发布快照生成 `certs/{id}.key` 时直接写入库内加密的 `KeyPEM`(`enc:v1:`),未解密为 PEM;现与证书详情接口一致,发布前通过 `OpenKeyPEM` 解密后再下发。 diff --git a/frontend/lib/services/openflare/config-version.service.ts b/frontend/lib/services/openflare/config-version.service.ts index 461a6855..6a56e502 100644 --- a/frontend/lib/services/openflare/config-version.service.ts +++ b/frontend/lib/services/openflare/config-version.service.ts @@ -29,7 +29,7 @@ export class ConfigVersionService extends OpenFlareBaseService { return this.get('/diff'); } - static getById(id: number): Promise { + static getById(id: string): Promise { return this.get(`/${id}`); } @@ -43,7 +43,7 @@ export class ConfigVersionService extends OpenFlareBaseService { ); } - static activate(id: number): Promise { + static activate(id: string): Promise { return this.post(`/${id}/activate`); } diff --git a/frontend/lib/services/openflare/types.ts b/frontend/lib/services/openflare/types.ts index cfe07104..a7c3ed27 100644 --- a/frontend/lib/services/openflare/types.ts +++ b/frontend/lib/services/openflare/types.ts @@ -307,7 +307,7 @@ export interface ProxyRouteMutationPayload { } export interface ConfigVersionSummary { - id: number; + id: string; version: string; checksum: string; is_active: boolean; diff --git a/internal/apps/openflare/config_version/logics.go b/internal/apps/openflare/config_version/logics.go index a653d6cd..037b8616 100644 --- a/internal/apps/openflare/config_version/logics.go +++ b/internal/apps/openflare/config_version/logics.go @@ -79,9 +79,9 @@ func ListConfigVersions(ctx context.Context) ([]*model.ConfigVersionSummary, err return model.ListConfigVersionSummaries(ctx) } -// GetConfigVersionDetail returns a config version by id. -func GetConfigVersionDetail(ctx context.Context, id uint) (*model.ConfigVersion, error) { - return model.GetConfigVersionByID(ctx, id) +// GetConfigVersionDetail returns a config version by version. +func GetConfigVersionDetail(ctx context.Context, version string) (*model.ConfigVersion, error) { + return model.GetConfigVersionByVersion(ctx, version) } // GetActiveConfigVersion returns the active config version. @@ -243,12 +243,12 @@ func PublishConfigVersion(ctx context.Context, createdBy string, force bool) (*m } // ActivateConfigVersion activates an existing config version. -func ActivateConfigVersion(ctx context.Context, id uint) (*model.ConfigVersion, error) { - version, err := model.GetConfigVersionByID(ctx, id) +func ActivateConfigVersion(ctx context.Context, versionStr string) (*model.ConfigVersion, error) { + version, err := model.GetConfigVersionByVersion(ctx, versionStr) if err != nil { return nil, err } - if err = model.ActivateConfigVersionTx(ctx, id); err != nil { + if err = model.ActivateConfigVersionTx(ctx, versionStr); err != nil { return nil, err } version.IsActive = true @@ -271,7 +271,7 @@ func CleanupConfigVersions(ctx context.Context, keepCount int) (*CleanupResult, if len(versions) <= keepCount { return &CleanupResult{DeletedCount: 0, Message: cleanupSuccessMessage}, nil } - var deleteIDs []uint + var deleteVersions []string for index, version := range versions { if index < keepCount { continue @@ -279,12 +279,12 @@ func CleanupConfigVersions(ctx context.Context, keepCount int) (*CleanupResult, if version.IsActive { continue } - deleteIDs = append(deleteIDs, version.ID) + deleteVersions = append(deleteVersions, version.Version) } - if len(deleteIDs) == 0 { + if len(deleteVersions) == 0 { return &CleanupResult{DeletedCount: 0, Message: cleanupSuccessMessage}, nil } - deletedCount, err := model.DeleteConfigVersionsByIDs(ctx, deleteIDs) + deletedCount, err := model.DeleteConfigVersionsByVersions(ctx, deleteVersions) if err != nil { return nil, err } diff --git a/internal/apps/openflare/config_version/logics_test.go b/internal/apps/openflare/config_version/logics_test.go index 7498a7d6..a4d54006 100644 --- a/internal/apps/openflare/config_version/logics_test.go +++ b/internal/apps/openflare/config_version/logics_test.go @@ -31,6 +31,7 @@ func setupConfigVersionTestDB(t *testing.T) func() { &model.OpenFlareWAFRuleGroup{}, &model.OpenFlareWAFRuleGroupBinding{}, &model.OpenFlareWAFIPGroup{}, + &model.SystemConfig{}, )) db.SetDB(sqliteDB) @@ -64,7 +65,6 @@ func TestListConfigVersionsOrdersByCreatedAtDesc(t *testing.T) { } require.NoError(t, conn.Create(newer).Error) require.NoError(t, conn.Create(older).Error) - require.Greater(t, older.ID, newer.ID) versions, err := ListConfigVersions(ctx) require.NoError(t, err) @@ -91,7 +91,7 @@ func TestPublishConfigVersionCreatesVersion(t *testing.T) { version, err := PublishConfigVersion(ctx, "tester", false) require.NoError(t, err) require.NotNil(t, version) - assert.NotZero(t, version.ID) + assert.NotEmpty(t, version.ID) assert.True(t, version.IsActive) assert.Equal(t, "tester", version.CreatedBy) assert.NotEmpty(t, version.Version) diff --git a/internal/apps/openflare/config_version/routers.go b/internal/apps/openflare/config_version/routers.go index 8a427404..82c71109 100644 --- a/internal/apps/openflare/config_version/routers.go +++ b/internal/apps/openflare/config_version/routers.go @@ -18,6 +18,15 @@ func handleLogicError(c *gin.Context, err error) bool { return apiutil.AbortNotFoundIfMissing(c, err, "记录不存在") } +func versionParam(c *gin.Context) (string, bool) { + version := c.Param("id") + if version == "" { + response.AbortBadRequest(c, "无效的版本号") + return "", false + } + return version, true +} + // ListConfigVersionsHandler lists config versions. // @Summary 获取配置版本列表 // @Description 返回所有已发布的 OpenResty 配置版本摘要,按创建时间倒序排列,需要管理员权限 @@ -50,11 +59,11 @@ func ListConfigVersionsHandler(c *gin.Context) { // @Failure 404 {object} response.Any "无权限或版本不存在" // @Router /api/v1/d/config-versions/{id} [get] func GetConfigVersionHandler(c *gin.Context) { - id, ok := apiutil.IDParam(c) + versionStr, ok := versionParam(c) if !ok { return } - version, err := GetConfigVersionDetail(c.Request.Context(), id) + version, err := GetConfigVersionDetail(c.Request.Context(), versionStr) if handleLogicError(c, err) { return } @@ -152,11 +161,11 @@ func PublishConfigVersionHandler(c *gin.Context) { // @Failure 404 {object} response.Any "无权限或版本不存在" // @Router /api/v1/d/config-versions/{id}/activate [post] func ActivateConfigVersionHandler(c *gin.Context) { - id, ok := apiutil.IDParam(c) + versionStr, ok := versionParam(c) if !ok { return } - version, err := ActivateConfigVersion(c.Request.Context(), id) + version, err := ActivateConfigVersion(c.Request.Context(), versionStr) if handleLogicError(c, err) { return } diff --git a/internal/db/migrator/goose/postgres/202606270001_make_version_primary_key.sql b/internal/db/migrator/goose/postgres/202606270001_make_version_primary_key.sql new file mode 100644 index 00000000..00aee00d --- /dev/null +++ b/internal/db/migrator/goose/postgres/202606270001_make_version_primary_key.sql @@ -0,0 +1,19 @@ +-- +goose Up +-- Remove old PRIMARY KEY on id, set version as PRIMARY KEY, drop id column + +-- 1. 移除旧主键约束 +ALTER TABLE of_config_versions DROP CONSTRAINT IF EXISTS of_config_versions_pkey; + +-- 2. 移除原先在 version 字段的唯一索引(因为 version 变成主键,主键隐含唯一性) +DROP INDEX IF EXISTS idx_of_config_versions_version; + +-- 3. 将 version 设为新主键 +ALTER TABLE of_config_versions ADD PRIMARY KEY (version); + +-- 4. 彻底删除 id 列 +ALTER TABLE of_config_versions DROP COLUMN IF EXISTS id; + +-- +goose Down +ALTER TABLE of_config_versions DROP CONSTRAINT IF EXISTS of_config_versions_pkey; +ALTER TABLE of_config_versions ADD COLUMN IF NOT EXISTS id BIGSERIAL PRIMARY KEY; +CREATE UNIQUE INDEX IF NOT EXISTS idx_of_config_versions_version ON of_config_versions (version); diff --git a/internal/db/migrator/goose/sqlite/202606270001_make_version_primary_key.sql b/internal/db/migrator/goose/sqlite/202606270001_make_version_primary_key.sql new file mode 100644 index 00000000..6507e759 --- /dev/null +++ b/internal/db/migrator/goose/sqlite/202606270001_make_version_primary_key.sql @@ -0,0 +1,46 @@ +-- +goose Up +-- SQLite table reconstruction to set version as primary key and drop id column + +-- 1. 创建临时新表 +CREATE TABLE of_config_versions_new ( + version VARCHAR(32) PRIMARY KEY, + snapshot_json TEXT NOT NULL, + main_config TEXT NOT NULL DEFAULT '', + rendered_config TEXT NOT NULL, + support_files_json TEXT NOT NULL DEFAULT '[]', + checksum VARCHAR(64) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT FALSE, + created_by VARCHAR(64) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 2. 拷贝数据 +INSERT INTO of_config_versions_new (version, snapshot_json, main_config, rendered_config, support_files_json, checksum, is_active, created_by, created_at) +SELECT version, snapshot_json, main_config, rendered_config, support_files_json, checksum, is_active, created_by, created_at +FROM of_config_versions; + +-- 3. 删除旧表 +DROP TABLE of_config_versions; + +-- 4. 重命名新表 +ALTER TABLE of_config_versions_new RENAME TO of_config_versions; + +-- 5. 重建索引(is_active 索引) +CREATE INDEX IF NOT EXISTS idx_of_config_versions_is_active ON of_config_versions (is_active); + +-- +goose Down +DROP TABLE IF EXISTS of_config_versions; +CREATE TABLE of_config_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version VARCHAR(32) NOT NULL, + snapshot_json TEXT NOT NULL, + main_config TEXT NOT NULL DEFAULT '', + rendered_config TEXT NOT NULL, + support_files_json TEXT NOT NULL DEFAULT '[]', + checksum VARCHAR(64) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT FALSE, + created_by VARCHAR(64) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_of_config_versions_version ON of_config_versions (version); +CREATE INDEX IF NOT EXISTS idx_of_config_versions_is_active ON of_config_versions (is_active); diff --git a/internal/model/openflare_config_version.go b/internal/model/openflare_config_version.go index bf1d813c..606b165a 100644 --- a/internal/model/openflare_config_version.go +++ b/internal/model/openflare_config_version.go @@ -14,18 +14,24 @@ import ( // ConfigVersionSummary is the list view for config versions. type ConfigVersionSummary struct { - ID uint `json:"id"` - Version string `json:"version"` + ID string `json:"id" gorm:"-"` + Version string `json:"version" gorm:"primaryKey;column:version"` Checksum string `json:"checksum"` IsActive bool `json:"is_active"` CreatedBy string `json:"created_by"` CreatedAt time.Time `json:"created_at"` } +// AfterFind hook for ConfigVersionSummary. +func (cvs *ConfigVersionSummary) AfterFind(_ *gorm.DB) (err error) { + cvs.ID = cvs.Version + return +} + // ConfigVersion stores a published OpenResty configuration snapshot. type ConfigVersion struct { - ID uint `json:"id" gorm:"primaryKey;autoIncrement"` - Version string `json:"version" gorm:"uniqueIndex;size:32;not null"` + ID string `json:"id" gorm:"-"` + Version string `json:"version" gorm:"primaryKey;size:32;not null"` SnapshotJSON string `json:"snapshot_json" gorm:"type:text;not null"` MainConfig string `json:"main_config" gorm:"type:text;not null;default:''"` RenderedConfig string `json:"rendered_config" gorm:"type:text;not null"` @@ -36,6 +42,18 @@ type ConfigVersion struct { CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` } +// AfterFind hook for ConfigVersion. +func (cv *ConfigVersion) AfterFind(_ *gorm.DB) (err error) { + cv.ID = cv.Version + return +} + +// AfterCreate hook for ConfigVersion. +func (cv *ConfigVersion) AfterCreate(_ *gorm.DB) (err error) { + cv.ID = cv.Version + return +} + // TableName returns the GORM table name. func (ConfigVersion) TableName() string { return "of_config_versions" @@ -49,23 +67,23 @@ func ListConfigVersionSummaries(ctx context.Context) ([]*ConfigVersionSummary, e } var versions []*ConfigVersionSummary err := conn.Model(&ConfigVersion{}). - Select("id", "version", "checksum", "is_active", "created_by", "created_at"). - Order("created_at desc, id desc"). + Select("version", "checksum", "is_active", "created_by", "created_at"). + Order("created_at desc, version desc"). Find(&versions).Error return versions, err } -// GetConfigVersionByID returns a config version by primary key. -func GetConfigVersionByID(ctx context.Context, id uint) (*ConfigVersion, error) { +// GetConfigVersionByVersion returns a config version by version string. +func GetConfigVersionByVersion(ctx context.Context, version string) (*ConfigVersion, error) { conn := db.DB(ctx) if conn == nil { return nil, errors.New(errDatabaseNotInitialized) } - var version ConfigVersion - if err := conn.First(&version, id).Error; err != nil { + var cv ConfigVersion + if err := conn.First(&cv, "version = ?", version).Error; err != nil { return nil, err } - return &version, nil + return &cv, nil } // GetActiveConfigVersion returns the currently active config version. @@ -75,7 +93,7 @@ func GetActiveConfigVersion(ctx context.Context) (*ConfigVersion, error) { return nil, errors.New(errDatabaseNotInitialized) } var version ConfigVersion - if err := conn.Where("is_active = ?", true).Order("id desc").First(&version).Error; err != nil { + if err := conn.Where("is_active = ?", true).Order("version desc").First(&version).Error; err != nil { return nil, err } return &version, nil @@ -123,7 +141,7 @@ func PublishConfigVersionTx(ctx context.Context, version *ConfigVersion) error { } // ActivateConfigVersionTx marks the given version active and deactivates others. -func ActivateConfigVersionTx(ctx context.Context, id uint) error { +func ActivateConfigVersionTx(ctx context.Context, version string) error { conn := db.DB(ctx) if conn == nil { return errors.New(errDatabaseNotInitialized) @@ -132,20 +150,20 @@ func ActivateConfigVersionTx(ctx context.Context, id uint) error { if err := tx.Model(&ConfigVersion{}).Where("is_active = ?", true).Update("is_active", false).Error; err != nil { return err } - return tx.Model(&ConfigVersion{}).Where("id = ?", id).Update("is_active", true).Error + return tx.Model(&ConfigVersion{}).Where("version = ?", version).Update("is_active", true).Error }) } -// DeleteConfigVersionsByIDs removes config versions by ids. -func DeleteConfigVersionsByIDs(ctx context.Context, ids []uint) (int64, error) { - if len(ids) == 0 { +// DeleteConfigVersionsByVersions removes config versions by versions. +func DeleteConfigVersionsByVersions(ctx context.Context, versions []string) (int64, error) { + if len(versions) == 0 { return 0, nil } conn := db.DB(ctx) if conn == nil { return 0, errors.New(errDatabaseNotInitialized) } - result := conn.Where("id IN ?", ids).Delete(&ConfigVersion{}) + result := conn.Where("version IN ?", versions).Delete(&ConfigVersion{}) return result.RowsAffected, result.Error }