package ddlsync import ( "context" "fmt" "strings" "time" "dataengine/common/report/config" "dataengine/common/report/model" "gitea.redpowerfuture.com/red-future/common/db/gfdb" "github.com/sirupsen/logrus" ) // StatTableCreator 统计宽表创建器 type StatTableCreator struct { loader *config.ConfigLoader } // NewStatTableCreator 创建统计宽表创建器 func NewStatTableCreator() *StatTableCreator { return &StatTableCreator{ loader: config.GetLoader(), } } // AutoCreateStatTable 根据配置自动创建统计宽表 func (c *StatTableCreator) AutoCreateStatTable(ctx context.Context, businessCode, reportCode string) (*model.AutoCreateStatTableResp, error) { start := time.Now() logger := logrus.WithFields(logrus.Fields{ "businessCode": businessCode, "reportCode": reportCode, }) // 1. 获取报表配置 report, err := c.loader.GetReport(ctx, businessCode, reportCode) if err != nil { return nil, fmt.Errorf("获取报表配置失败: %w", err) } // 2. 获取字段配置 fields, err := c.loader.GetFields(ctx, businessCode, reportCode) if err != nil { return nil, fmt.Errorf("获取字段配置失败: %w", err) } if len(fields) == 0 { return nil, fmt.Errorf("报表字段配置为空,请先配置字段") } // 3. 构建建表SQL tableName := report.StatTableName sql := c.buildCreateTableSQL(tableName, report, fields) logger.Infof("创建统计宽表: %s", tableName) // 4. 执行建表 _, err = gfdb.DB(ctx).Exec(ctx, sql) if err != nil { return nil, fmt.Errorf("建表失败: %w", err) } // 5. 创建冲突唯一索引 if len(report.ConflictKeys) > 0 { indexName := fmt.Sprintf("uq_%s_conflict", tableName) indexCols := strings.Join(report.ConflictKeys, ", ") indexSQL := fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (%s)", indexName, tableName, indexCols) if _, err := gfdb.DB(ctx).Exec(ctx, indexSQL); err != nil { logger.Warnf("创建冲突索引失败: %v", err) } } // 6. 添加字段注释 for _, f := range fields { if f.FieldName != "" { commentSQL := fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s'", tableName, f.FieldCode, strings.ReplaceAll(f.FieldName, "'", "''")) if _, err := gfdb.DB(ctx).Exec(ctx, commentSQL); err != nil { logger.Warnf("添加字段注释失败 [%s]: %v", f.FieldCode, err) } } } execTime := time.Since(start).Milliseconds() logger.Infof("统计宽表 %s 创建完成,字段数: %d,耗时: %dms", tableName, len(fields), execTime) return &model.AutoCreateStatTableResp{ Success: true, TableName: tableName, ColumnCount: len(fields), ExecTimeMs: execTime, }, nil } // buildCreateTableSQL 构建建表SQL func (c *StatTableCreator) buildCreateTableSQL(tableName string, report *model.ReportConfig, fields []model.FieldConfig) string { var cols []string // 基础字段 cols = append(cols, "id BIGSERIAL PRIMARY KEY") cols = append(cols, "tenant_id BIGINT NOT NULL DEFAULT 0") cols = append(cols, "business_code VARCHAR(64) NOT NULL DEFAULT ''") cols = append(cols, "creator VARCHAR(64) DEFAULT ''") cols = append(cols, "created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()") cols = append(cols, "updater VARCHAR(64) DEFAULT ''") cols = append(cols, "updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()") cols = append(cols, "deleted_at TIMESTAMP WITH TIME ZONE") // 日期维度字段 dateField := report.DateField if dateField == "" { dateField = "stat_date" } cols = append(cols, fmt.Sprintf("%s VARCHAR(16) NOT NULL DEFAULT ''", dateField)) // 业务字段 for _, f := range fields { colDef := c.fieldTypeToColumn(f.FieldCode, f.FieldType) cols = append(cols, colDef) } // 原始数据 cols = append(cols, "raw_data JSONB DEFAULT '{}'") sql := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n %s\n)", tableName, strings.Join(cols, ",\n ")) return sql } // fieldTypeToColumn 字段类型转PG列类型 func (c *StatTableCreator) fieldTypeToColumn(fieldCode, fieldType string) string { switch fieldType { case model.FieldTypeInt, model.FieldTypeFloat: return fmt.Sprintf("%s NUMERIC(20,4) DEFAULT 0", fieldCode) case model.FieldTypeDate: return fmt.Sprintf("%s VARCHAR(16) DEFAULT ''", fieldCode) case model.FieldTypeDatetime: return fmt.Sprintf("%s TIMESTAMP WITH TIME ZONE", fieldCode) case model.FieldTypeJsonb: return fmt.Sprintf("%s JSONB DEFAULT '{}'", fieldCode) default: // STRING return fmt.Sprintf("%s VARCHAR(256) DEFAULT ''", fieldCode) } } // DropStatTable 删除统计宽表 func (c *StatTableCreator) DropStatTable(ctx context.Context, businessCode, reportCode string) error { report, err := c.loader.GetReport(ctx, businessCode, reportCode) if err != nil { return err } sql := fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE", report.StatTableName) _, err = gfdb.DB(ctx).Exec(ctx, sql) return err } // TableExists 检查表是否存在 func (c *StatTableCreator) TableExists(ctx context.Context, tableName string) (bool, error) { result, err := gfdb.DB(ctx).GetAll(ctx, "SELECT COUNT(*) FROM pg_tables WHERE tablename = $1", strings.ToLower(tableName)) if err != nil { return false, err } if len(result) == 0 { return false, nil } count := result[0]["count"].Int() return count > 0, nil } // GetTableColumns 获取表字段列表 func (c *StatTableCreator) GetTableColumns(ctx context.Context, tableName string) ([]string, error) { var columns []string sql := `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' ORDER BY ordinal_position` rows, err := gfdb.DB(ctx).GetAll(ctx, sql, strings.ToLower(tableName)) if err != nil { return nil, err } for _, row := range rows.List() { if col, ok := row["column_name"].(string); ok { columns = append(columns, col) } } return columns, nil } // AlterTableAddColumns 为已存在的表添加新字段 func (c *StatTableCreator) AlterTableAddColumns(ctx context.Context, tableName string, newFields []model.FieldConfig) error { existingCols, err := c.GetTableColumns(ctx, tableName) if err != nil { return err } existingMap := make(map[string]bool) for _, col := range existingCols { existingMap[col] = true } for _, f := range newFields { if existingMap[f.FieldCode] { continue } colDef := c.fieldTypeToColumn(f.FieldCode, f.FieldType) sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN IF NOT EXISTS %s", tableName, colDef) if _, err := gfdb.DB(ctx).Exec(ctx, sql); err != nil { return fmt.Errorf("添加字段 %s 失败: %w", f.FieldCode, err) } } return nil }