2026-05-29 18:39:32 +08:00
|
|
|
|
package sync
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"gitea.com/red-future/common/db/gfdb"
|
|
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ColumnDef 列定义
|
|
|
|
|
|
type ColumnDef struct {
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
|
Comment string `json:"comment,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TableDefinition 表结构定义
|
|
|
|
|
|
type TableDefinition struct {
|
|
|
|
|
|
TableName string `json:"table_name"`
|
|
|
|
|
|
Columns []ColumnDef `json:"columns"`
|
|
|
|
|
|
ConflictKeys []string `json:"conflict_keys,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ParseTableDefinition 解析 table_definition JSON
|
|
|
|
|
|
func ParseTableDefinition(raw map[string]interface{}) (*TableDefinition, error) {
|
|
|
|
|
|
td := &TableDefinition{}
|
|
|
|
|
|
name, _ := raw["table_name"].(string)
|
|
|
|
|
|
if name == "" {
|
|
|
|
|
|
return nil, fmt.Errorf("table_definition 缺少 table_name")
|
|
|
|
|
|
}
|
|
|
|
|
|
td.TableName = name
|
|
|
|
|
|
|
|
|
|
|
|
colsRaw, _ := raw["columns"].([]interface{})
|
|
|
|
|
|
for _, c := range colsRaw {
|
|
|
|
|
|
cm, _ := c.(map[string]interface{})
|
|
|
|
|
|
if cm == nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
n, _ := cm["name"].(string)
|
|
|
|
|
|
t, _ := cm["type"].(string)
|
|
|
|
|
|
comment, _ := cm["comment"].(string)
|
|
|
|
|
|
if n == "" || t == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
td.Columns = append(td.Columns, ColumnDef{Name: n, Type: t, Comment: comment})
|
|
|
|
|
|
}
|
|
|
|
|
|
if keys, _ := raw["conflict_keys"].([]interface{}); keys != nil {
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
|
|
if s, ok := k.(string); ok {
|
|
|
|
|
|
td.ConflictKeys = append(td.ConflictKeys, s)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(td.Columns) == 0 {
|
|
|
|
|
|
return nil, fmt.Errorf("table_definition 列定义为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
return td, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// EnsureTable 确保表存在
|
|
|
|
|
|
func EnsureTable(ctx context.Context, td *TableDefinition) error {
|
|
|
|
|
|
sql := buildCreateSQL(td)
|
|
|
|
|
|
logrus.Infof("建表: %s", td.TableName)
|
|
|
|
|
|
_, err := gfdb.DB(ctx).Exec(ctx, sql)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("建表失败 [%s]: %w", td.TableName, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
logrus.Infof("表 %s 已就绪", td.TableName)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildCreateSQL(td *TableDefinition) string {
|
|
|
|
|
|
cols := []string{
|
|
|
|
|
|
"id BIGSERIAL PRIMARY KEY",
|
|
|
|
|
|
"tenant_id BIGINT NOT NULL DEFAULT 0",
|
|
|
|
|
|
"creator VARCHAR(64) DEFAULT ''",
|
|
|
|
|
|
"created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()",
|
|
|
|
|
|
"updater VARCHAR(64) DEFAULT ''",
|
|
|
|
|
|
"updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()",
|
|
|
|
|
|
"deleted_at TIMESTAMP WITH TIME ZONE",
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, c := range td.Columns {
|
|
|
|
|
|
cols = append(cols, fmt.Sprintf("%s %s", c.Name, c.Type))
|
|
|
|
|
|
}
|
|
|
|
|
|
cols = append(cols, "raw_data JSONB DEFAULT '{}'")
|
|
|
|
|
|
|
2026-06-01 14:08:17 +08:00
|
|
|
|
// 添加复合唯一索引(用于 ON CONFLICT upsert)
|
2026-05-29 18:39:32 +08:00
|
|
|
|
var constraints []string
|
|
|
|
|
|
if len(td.ConflictKeys) > 0 {
|
2026-06-01 14:08:17 +08:00
|
|
|
|
ck := strings.Join(td.ConflictKeys, ", ")
|
2026-05-29 18:39:32 +08:00
|
|
|
|
indexName := fmt.Sprintf("uq_%s_conflict", td.TableName)
|
|
|
|
|
|
constraints = append(constraints,
|
2026-06-01 14:08:17 +08:00
|
|
|
|
fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (%s)", indexName, td.TableName, ck))
|
2026-05-29 18:39:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sql := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n %s\n);\n", td.TableName, strings.Join(cols, ",\n "))
|
2026-06-01 14:08:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加唯一索引
|
2026-05-29 18:39:32 +08:00
|
|
|
|
if len(constraints) > 0 {
|
2026-06-01 14:08:17 +08:00
|
|
|
|
sql += strings.Join(constraints, ";\n") + ";\n"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加字段注释(COMMENT ON COLUMN)
|
|
|
|
|
|
for _, c := range td.Columns {
|
|
|
|
|
|
if c.Comment != "" {
|
|
|
|
|
|
escaped := strings.ReplaceAll(c.Comment, "'", "''")
|
|
|
|
|
|
sql += fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s';\n", td.TableName, c.Name, escaped)
|
|
|
|
|
|
}
|
2026-05-29 18:39:32 +08:00
|
|
|
|
}
|
2026-06-01 14:08:17 +08:00
|
|
|
|
|
2026-05-29 18:39:32 +08:00
|
|
|
|
return sql
|
|
|
|
|
|
}
|