Files
data-engine/common/report/ddlsync/creator.go
2026-06-11 13:06:54 +08:00

211 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}