211 lines
6.4 KiB
Go
211 lines
6.4 KiB
Go
|
|
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
|
|||
|
|
}
|