Files
data-engine/common/report/ddlsync/creator.go

211 lines
6.4 KiB
Go
Raw Normal View History

2026-06-11 13:06:54 +08:00
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
}