refactor(db): use version string as primary key for ConfigVersion

-transition `of_config_versions` primary key from `id` to `version` string.
-add database migration files `202606270001_make_version_primary_key.sql` for PostgreSQL and SQLite.
-introduce AfterFind/AfterCreate GORM hooks to preserve JSON backward compatibility.
-refactor API controllers, logics, and front-end typescript definitions to receive `string` parameter.
This commit is contained in:
ryan
2026-06-27 13:23:30 +08:00
parent b48414e1fe
commit 13d6966cb5
11 changed files with 132 additions and 39 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ REDIS_KEY_PREFIX=openflare:
# ─── ClickHouse(必需)────────────────────────────────────────────────────
CLICKHOUSE_HOST=clickhouse:9000
CLICKHOUSE_USERNAME=default
CLICKHOUSE_PASSWORD=
CLICKHOUSE_PASSWORD=123456
CLICKHOUSE_NAME=openflare
# ─── 日志 ──────────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -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
+1
View File
@@ -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` 解密后再下发。
@@ -29,7 +29,7 @@ export class ConfigVersionService extends OpenFlareBaseService {
return this.get<ConfigDiffResult>('/diff');
}
static getById(id: number): Promise<ConfigVersionDetail> {
static getById(id: string): Promise<ConfigVersionDetail> {
return this.get<ConfigVersionDetail>(`/${id}`);
}
@@ -43,7 +43,7 @@ export class ConfigVersionService extends OpenFlareBaseService {
);
}
static activate(id: number): Promise<ConfigVersionDetail> {
static activate(id: string): Promise<ConfigVersionDetail> {
return this.post<ConfigVersionDetail>(`/${id}/activate`);
}
+1 -1
View File
@@ -307,7 +307,7 @@ export interface ProxyRouteMutationPayload {
}
export interface ConfigVersionSummary {
id: number;
id: string;
version: string;
checksum: string;
is_active: boolean;
@@ -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
}
@@ -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)
@@ -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
}
@@ -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);
@@ -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);
+36 -18
View File
@@ -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
}