重构数据引擎和报表引擎

This commit is contained in:
2026-06-11 13:06:54 +08:00
parent 285a0fc632
commit 419473f266
53 changed files with 8434 additions and 375 deletions

477
common/report/api.go Normal file
View File

@@ -0,0 +1,477 @@
package report
import (
"context"
"fmt"
"strings"
"dataengine/common/report/config"
"dataengine/common/report/ddlsync"
"dataengine/common/report/executor"
"dataengine/common/report/extract"
"dataengine/common/report/model"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
)
// ReportService 报表公共服务
// 对外暴露的统一接口
type ReportService struct {
configLoader *config.ConfigLoader
tableCreator *ddlsync.StatTableCreator
queryExecutor *executor.QueryExecutor
dailyExtractor *extract.DailyExtractor
}
var defaultService *ReportService
// GetService 获取报表服务单例
func GetService() *ReportService {
if defaultService == nil {
defaultService = &ReportService{
configLoader: config.GetLoader(),
tableCreator: ddlsync.NewStatTableCreator(),
queryExecutor: executor.NewQueryExecutor(),
dailyExtractor: extract.NewDailyExtractor(),
}
}
return defaultService
}
// ============================================================
// 核心接口 1: 自动创建统计宽表
// 首次抽取前调用
// ============================================================
// AutoCreateStatTable 根据配置自动创建统计宽表
// businessCode: 业务编码
// reportCode: 报表编码
func (s *ReportService) AutoCreateStatTable(ctx context.Context, businessCode, reportCode string) (*model.AutoCreateStatTableResp, error) {
// 初始化系统表
if err := initTables(ctx); err != nil {
return nil, fmt.Errorf("初始化系统表失败: %w", err)
}
resp, err := s.tableCreator.AutoCreateStatTable(ctx, businessCode, reportCode)
if err != nil {
return nil, fmt.Errorf("AutoCreateStatTable 失败: %w", err)
}
return resp, nil
}
// ============================================================
// 核心接口 2: 按天抽取数据
// 业务层定时任务调用
// ============================================================
// ExtractDailyData 按天抽取数据
// businessCode: 业务编码
// reportCode: 报表编码
// statDate: 统计日期 yyyy-MM-dd
// executor: 执行人
func (s *ReportService) ExtractDailyData(ctx context.Context, businessCode, reportCode, statDate, executor string) (*model.ExtractDailyDataResp, error) {
// 1. 先确保统计宽表存在
report, err := s.configLoader.GetReport(ctx, businessCode, reportCode)
if err != nil {
return nil, fmt.Errorf("获取报表配置失败: %w", err)
}
// 检查表是否存在
result, err := gfdb.DB(ctx).GetAll(ctx,
"SELECT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = $1) AS exists",
strings.ToLower(report.StatTableName))
if err != nil {
return nil, fmt.Errorf("检查统计宽表失败: %w", err)
}
tableExists := false
if len(result) > 0 {
tableExists = result[0]["exists"].Bool()
}
if !tableExists {
// 表不存在,先创建
if _, createErr := s.AutoCreateStatTable(ctx, businessCode, reportCode); createErr != nil {
return nil, fmt.Errorf("创建统计宽表失败: %w", createErr)
}
}
resp, err := s.dailyExtractor.ExtractDailyData(ctx, businessCode, reportCode, statDate, executor)
if err != nil {
return nil, fmt.Errorf("ExtractDailyData 失败: %w", err)
}
// 清除缓存
s.configLoader.InvalidateCache(businessCode, reportCode)
return resp, nil
}
// ============================================================
// 核心接口 3: 用户选择查询(最核心)
// 前端用户选择条件 → 实时构建SQL → 返回报表数据
// ============================================================
// QueryReportByUserSelect 根据用户选择实时查询报表数据
// 不是自动生成报表,是用户在前端选择维度/指标/筛选/时间后实时查询展示
func (s *ReportService) QueryReportByUserSelect(ctx context.Context, req *model.UserSelectQueryReq) (*model.UserSelectQueryResp, error) {
// 参数校验
if req.BusinessCode == "" {
return nil, fmt.Errorf("businessCode 不能为空")
}
if req.ReportCode == "" {
return nil, fmt.Errorf("reportCode 不能为空")
}
resp, err := s.queryExecutor.QueryReportByUserSelect(ctx, req)
if err != nil {
return nil, fmt.Errorf("QueryReportByUserSelect 失败: %w", err)
}
return resp, nil
}
// ============================================================
// 辅助接口
// ============================================================
// GetReportFields 获取报表可用字段(按维度/指标/筛选分类)
func (s *ReportService) GetReportFields(ctx context.Context, businessCode, reportCode string) (*model.GetReportFieldsResp, error) {
resp, err := s.configLoader.GetReportFields(ctx, businessCode, reportCode)
if err != nil {
return nil, fmt.Errorf("GetReportFields 失败: %w", err)
}
return resp, nil
}
// GetAllBusinesses 获取所有启用业务列表
func (s *ReportService) GetAllBusinesses(ctx context.Context) ([]model.BusinessConfig, error) {
return s.configLoader.GetAllBusinesses(ctx)
}
// GetAllReports 获取业务下所有报表列表
func (s *ReportService) GetAllReports(ctx context.Context, businessCode string) ([]model.ReportConfig, error) {
return s.configLoader.GetAllReports(ctx, businessCode)
}
// InvalidateCache 失效指定业务报表缓存
func (s *ReportService) InvalidateCache(businessCode, reportCode string) {
s.configLoader.InvalidateCache(businessCode, reportCode)
}
// InitSystemTables 初始化系统表
func (s *ReportService) InitSystemTables(ctx context.Context) error {
return initTables(ctx)
}
// ============================================================
// 配置 CRUD: 业务
// ============================================================
// SaveBusiness 保存业务配置(新增/修改合一)
func (s *ReportService) SaveBusiness(ctx context.Context, req *model.SaveBusinessReq) (*model.SaveResult, error) {
if err := initTables(ctx); err != nil {
return nil, fmt.Errorf("初始化系统表失败: %w", err)
}
biz := &model.BusinessConfig{
BusinessCode: req.BusinessCode,
BusinessName: req.BusinessName,
Description: req.Description,
Status: req.Status,
Config: req.Config,
Creator: req.Operator,
Updater: req.Operator,
}
if req.Status == "" {
biz.Status = model.StatusActive
}
if biz.Config == nil {
biz.Config = make(map[string]interface{})
}
if req.ID != nil && *req.ID > 0 {
// 更新
biz.ID = *req.ID
if err := s.configLoader.UpdateBusiness(ctx, biz); err != nil {
return nil, err
}
return &model.SaveResult{Success: true, ID: *req.ID, Message: "更新成功"}, nil
}
// 新增
id, err := s.configLoader.CreateBusiness(ctx, biz)
if err != nil {
return nil, err
}
return &model.SaveResult{Success: true, ID: id, Message: "创建成功"}, nil
}
// DeleteBusiness 删除业务配置
func (s *ReportService) DeleteBusiness(ctx context.Context, id int64) (*model.DeleteResult, error) {
biz, err := s.configLoader.GetBusinessByID(ctx, id)
if err != nil {
return nil, err
}
if err := s.configLoader.DeleteBusiness(ctx, id, biz.BusinessCode); err != nil {
return nil, err
}
return &model.DeleteResult{Success: true, Message: "删除成功"}, nil
}
// GetBusiness 获取单个业务配置
func (s *ReportService) GetBusiness(ctx context.Context, id int64) (*model.BusinessConfig, error) {
return s.configLoader.GetBusinessByID(ctx, id)
}
// ============================================================
// 配置 CRUD: 报表
// ============================================================
// SaveReport 保存报表配置(新增/修改合一)
func (s *ReportService) SaveReport(ctx context.Context, req *model.SaveReportReq) (*model.SaveResult, error) {
if err := initTables(ctx); err != nil {
return nil, fmt.Errorf("初始化系统表失败: %w", err)
}
rpt := &model.ReportConfig{
BusinessCode: req.BusinessCode,
ReportCode: req.ReportCode,
ReportName: req.ReportName,
Description: req.Description,
Status: req.Status,
StatTableName: req.StatTableName,
StatTableComment: req.StatTableComment,
DateField: req.DateField,
PrimaryKeys: req.PrimaryKeys,
ConflictKeys: req.ConflictKeys,
Config: req.Config,
Creator: req.Operator,
Updater: req.Operator,
}
if req.Status == "" {
rpt.Status = model.StatusActive
}
if rpt.DateField == "" {
rpt.DateField = "stat_date"
}
if rpt.PrimaryKeys == nil {
rpt.PrimaryKeys = []string{"id"}
}
if rpt.ConflictKeys == nil {
rpt.ConflictKeys = []string{rpt.DateField}
}
if rpt.Config == nil {
rpt.Config = make(map[string]interface{})
}
if req.ID != nil && *req.ID > 0 {
rpt.ID = *req.ID
if err := s.configLoader.UpdateReport(ctx, rpt); err != nil {
return nil, err
}
return &model.SaveResult{Success: true, ID: *req.ID, Message: "更新成功"}, nil
}
id, err := s.configLoader.CreateReport(ctx, rpt)
if err != nil {
return nil, err
}
return &model.SaveResult{Success: true, ID: id, Message: "创建成功"}, nil
}
// DeleteReport 删除报表配置
func (s *ReportService) DeleteReport(ctx context.Context, id int64) (*model.DeleteResult, error) {
rpt, err := s.configLoader.GetReportByID(ctx, id)
if err != nil {
return nil, err
}
if err := s.configLoader.DeleteReport(ctx, id, rpt.BusinessCode, rpt.ReportCode); err != nil {
return nil, err
}
return &model.DeleteResult{Success: true, Message: "删除成功"}, nil
}
// GetReport 获取单个报表配置
func (s *ReportService) GetReport(ctx context.Context, id int64) (*model.ReportConfig, error) {
return s.configLoader.GetReportByID(ctx, id)
}
// ============================================================
// 配置 CRUD: 字段
// ============================================================
// SaveField 保存字段配置(新增/修改合一)
func (s *ReportService) SaveField(ctx context.Context, req *model.SaveFieldReq) (*model.SaveResult, error) {
if err := initTables(ctx); err != nil {
return nil, fmt.Errorf("初始化系统表失败: %w", err)
}
field := &model.FieldConfig{
BusinessCode: req.BusinessCode,
ReportCode: req.ReportCode,
FieldCode: req.FieldCode,
FieldName: req.FieldName,
FieldType: req.FieldType,
DataType: req.DataType,
FieldRole: req.FieldRole,
IsAggregatable: req.IsAggregatable,
IsFilterable: req.IsFilterable,
IsQueryable: req.IsQueryable,
IsSortable: req.IsSortable,
DefaultAggregate: req.DefaultAggregate,
ValidAggregates: req.ValidAggregates,
FilterOperators: req.FilterOperators,
Expression: req.Expression,
ExpressionType: req.ExpressionType,
FormatPattern: req.FormatPattern,
Unit: req.Unit,
DictCode: req.DictCode,
SortOrder: req.SortOrder,
GroupName: req.GroupName,
Status: req.Status,
Creator: req.Operator,
Updater: req.Operator,
}
if req.Status == "" {
field.Status = model.StatusActive
}
if field.DataType == "" {
field.DataType = model.FieldTypeString
}
if field.ValidAggregates == nil {
field.ValidAggregates = []string{}
}
if field.FilterOperators == nil {
field.FilterOperators = []string{"=", "!=", ">", "<", ">=", "<=", "IN", "LIKE", "BETWEEN"}
}
if req.ID != nil && *req.ID > 0 {
field.ID = *req.ID
if err := s.configLoader.UpdateField(ctx, field); err != nil {
return nil, err
}
return &model.SaveResult{Success: true, ID: *req.ID, Message: "更新成功"}, nil
}
id, err := s.configLoader.CreateField(ctx, field)
if err != nil {
return nil, err
}
return &model.SaveResult{Success: true, ID: id, Message: "创建成功"}, nil
}
// DeleteField 删除字段配置
func (s *ReportService) DeleteField(ctx context.Context, id int64) (*model.DeleteResult, error) {
field, err := s.configLoader.GetFieldByID(ctx, id)
if err != nil {
return nil, err
}
if err := s.configLoader.DeleteField(ctx, id, field.BusinessCode, field.ReportCode); err != nil {
return nil, err
}
return &model.DeleteResult{Success: true, Message: "删除成功"}, nil
}
// GetField 获取单个字段配置
func (s *ReportService) GetField(ctx context.Context, id int64) (*model.FieldConfig, error) {
return s.configLoader.GetFieldByID(ctx, id)
}
// ============================================================
// 配置 CRUD: 抽取配置
// ============================================================
// SaveExtractConfig 保存抽取配置(新增/修改合一)
func (s *ReportService) SaveExtractConfig(ctx context.Context, req *model.SaveExtractConfigReq) (*model.SaveResult, error) {
if err := initTables(ctx); err != nil {
return nil, fmt.Errorf("初始化系统表失败: %w", err)
}
ec := &model.ExtractConfig{
BusinessCode: req.BusinessCode,
ReportCode: req.ReportCode,
ExtractCode: req.ExtractCode,
ExtractName: req.ExtractName,
SourceTableName: req.SourceTableName,
SourceTableAlias: req.SourceTableAlias,
TargetTableName: req.TargetTableName,
IsEnabled: req.IsEnabled,
ExtractType: req.ExtractType,
ExtractMode: req.ExtractMode,
ExtractKeyField: req.ExtractKeyField,
ExtractKeyFormat: req.ExtractKeyFormat,
GroupByFields: req.GroupByFields,
FilterExpression: req.FilterExpression,
JoinConfigs: req.JoinConfigs,
FieldMappings: req.FieldMappings,
TransformRules: req.TransformRules,
BatchSize: req.BatchSize,
Status: req.Status,
Creator: req.Operator,
Updater: req.Operator,
}
if req.Status == "" {
ec.Status = model.StatusActive
}
if ec.ExtractType == "" {
ec.ExtractType = model.ExtractTypeIncremental
}
if ec.ExtractMode == "" {
ec.ExtractMode = model.ExtractModeDirect
}
if ec.BatchSize == 0 {
ec.BatchSize = 1000
}
if ec.JoinConfigs == nil {
ec.JoinConfigs = []model.JoinConfig{}
}
if ec.FieldMappings == nil {
ec.FieldMappings = []model.FieldMapping{}
}
if ec.TransformRules == nil {
ec.TransformRules = []model.TransformRule{}
}
if ec.GroupByFields == nil {
ec.GroupByFields = []string{}
}
if req.ID != nil && *req.ID > 0 {
ec.ID = *req.ID
if err := s.configLoader.UpdateExtractConfig(ctx, ec); err != nil {
return nil, err
}
return &model.SaveResult{Success: true, ID: *req.ID, Message: "更新成功"}, nil
}
id, err := s.configLoader.CreateExtractConfig(ctx, ec)
if err != nil {
return nil, err
}
return &model.SaveResult{Success: true, ID: id, Message: "创建成功"}, nil
}
// DeleteExtractConfig 删除抽取配置
func (s *ReportService) DeleteExtractConfig(ctx context.Context, id int64) (*model.DeleteResult, error) {
ec, err := s.configLoader.GetExtractConfigByID(ctx, id)
if err != nil {
return nil, err
}
if err := s.configLoader.DeleteExtractConfig(ctx, id, ec.BusinessCode, ec.ReportCode); err != nil {
return nil, err
}
return &model.DeleteResult{Success: true, Message: "删除成功"}, nil
}
// GetExtractConfig 获取单个抽取配置
func (s *ReportService) GetExtractConfig(ctx context.Context, id int64) (*model.ExtractConfig, error) {
return s.configLoader.GetExtractConfigByID(ctx, id)
}
// GetExtractConfigs 获取业务报表下所有抽取配置
func (s *ReportService) GetExtractConfigs(ctx context.Context, businessCode, reportCode string) ([]model.ExtractConfig, error) {
return s.configLoader.GetExtractConfigs(ctx, businessCode, reportCode)
}

View File

@@ -0,0 +1,518 @@
package builder
import (
"context"
"fmt"
"regexp"
"strings"
"dataengine/common/report/config"
"dataengine/common/report/model"
)
// SQLBuilder 动态SQL构建器
type SQLBuilder struct {
loader *config.ConfigLoader
}
// NewSQLBuilder 创建SQL构建器
func NewSQLBuilder() *SQLBuilder {
return &SQLBuilder{
loader: config.GetLoader(),
}
}
// BuildQuerySQL 根据用户选择构建查询SQL
func (b *SQLBuilder) BuildQuerySQL(ctx context.Context, req *model.UserSelectQueryReq) (string, []interface{}, map[string]interface{}, error) {
// 1. 校验配置
report, err := b.loader.GetReport(ctx, req.BusinessCode, req.ReportCode)
if err != nil {
return "", nil, nil, fmt.Errorf("获取报表配置失败: %w", err)
}
fieldMap, err := b.loader.GetFieldMap(ctx, req.BusinessCode, req.ReportCode)
if err != nil {
return "", nil, nil, fmt.Errorf("获取字段配置失败: %w", err)
}
tableName := report.StatTableName
// 2. 构建 SELECT 部分
selectClause, err := b.buildSelectClause(req, fieldMap, report)
if err != nil {
return "", nil, nil, err
}
// 3. 构建 FROM 部分
fromClause := tableName
// 4. 构建 WHERE 部分
whereClause, whereArgs, err := b.buildWhereClause(ctx, req, fieldMap, report)
if err != nil {
return "", nil, nil, err
}
// 5. 构建 GROUP BY 部分
groupByClause, err := b.buildGroupByClause(req, fieldMap)
if err != nil {
return "", nil, nil, err
}
// 6. 构建 ORDER BY 部分
orderByClause, err := b.buildOrderByClause(req, fieldMap)
if err != nil {
return "", nil, nil, err
}
// 7. 组合完整SQL
var sql strings.Builder
sql.WriteString("SELECT ")
sql.WriteString(selectClause)
sql.WriteString(" FROM ")
sql.WriteString(fromClause)
if whereClause != "" {
sql.WriteString(" WHERE ")
sql.WriteString(whereClause)
}
if groupByClause != "" {
sql.WriteString(" GROUP BY ")
sql.WriteString(groupByClause)
}
if orderByClause != "" {
sql.WriteString(" ORDER BY ")
sql.WriteString(orderByClause)
}
// 8. 统计总数SQL
countSql := "SELECT COUNT(*) FROM " + fromClause
if whereClause != "" {
countSql += " WHERE " + whereClause
}
if groupByClause != "" {
countSql = fmt.Sprintf("SELECT COUNT(*) FROM (SELECT 1 FROM %s WHERE %s GROUP BY %s) AS t",
fromClause, whereClause, groupByClause)
}
metadata := map[string]interface{}{
"countSql": countSql,
"tableName": tableName,
"reportConfig": report,
}
return sql.String(), whereArgs, metadata, nil
}
// buildSelectClause 构建SELECT子句
func (b *SQLBuilder) buildSelectClause(req *model.UserSelectQueryReq, fieldMap map[string]*model.FieldConfig, report *model.ReportConfig) (string, error) {
var selectParts []string
// 1. 添加维度字段
for _, dim := range req.Dimensions {
dim = strings.TrimSpace(dim)
if dim == "" {
continue
}
fc, ok := fieldMap[dim]
if !ok {
return "", fmt.Errorf("维度字段不存在: %s", dim)
}
if fc.FieldRole != model.RoleDimension && fc.FieldRole != model.RoleFilter {
return "", fmt.Errorf("字段 %s 不可作为维度", dim)
}
selectParts = append(selectParts, dim)
}
// 2. 添加指标字段(含聚合)
if len(req.Indicators) == 0 {
return "", fmt.Errorf("必须选择至少一个指标")
}
for _, ind := range req.Indicators {
fc, ok := fieldMap[ind.FieldCode]
if !ok {
return "", fmt.Errorf("指标字段不存在: %s", ind.FieldCode)
}
alias := ind.Alias
if alias == "" {
alias = ind.FieldCode
}
agg := strings.ToUpper(ind.Aggregate)
if agg == "" {
agg = fc.DefaultAggregate
if agg == "" {
agg = model.AggregateSum
}
}
// 校验聚合方式
if len(fc.ValidAggregates) > 0 {
valid := false
for _, v := range fc.ValidAggregates {
if strings.ToUpper(v) == agg {
valid = true
break
}
}
if !valid {
return "", fmt.Errorf("字段 %s 不支持聚合方式 %s", ind.FieldCode, agg)
}
}
// 处理衍生指标(表达式)
if fc.ExpressionType == "CALCULATED" && fc.Expression != "" {
expr := b.parseExpression(fc.Expression, req.Indicators)
selectParts = append(selectParts, fmt.Sprintf("%s AS %s", expr, alias))
} else {
selectParts = append(selectParts, fmt.Sprintf("%s(%s) AS %s", agg, ind.FieldCode, alias))
}
}
// 3. 添加时间分组字段
if req.TimeGroup != "" && req.TimeGroup != "day" {
dateField := report.DateField
if dateField == "" {
dateField = "stat_date"
}
timeGroupExpr := b.buildTimeGroupExpr(dateField, req.TimeGroup)
selectParts = append(selectParts, timeGroupExpr)
}
return strings.Join(selectParts, ", "), nil
}
// buildWhereClause 构建WHERE子句
func (b *SQLBuilder) buildWhereClause(ctx context.Context, req *model.UserSelectQueryReq, fieldMap map[string]*model.FieldConfig, report *model.ReportConfig) (string, []interface{}, error) {
var conditions []string
var args []interface{}
// 1. 租户过滤
conditions = append(conditions, "tenant_id = 1")
// 2. 时间范围过滤
if req.TimeRange != nil {
dateField := report.DateField
if dateField == "" {
dateField = "stat_date"
}
if req.TimeRange.StartDate != "" {
conditions = append(conditions, fmt.Sprintf("%s >= ?", dateField))
args = append(args, req.TimeRange.StartDate)
}
if req.TimeRange.EndDate != "" {
conditions = append(conditions, fmt.Sprintf("%s <= ?", dateField))
args = append(args, req.TimeRange.EndDate)
}
}
// 3. 业务过滤
conditions = append(conditions, "business_code = ?")
args = append(args, req.BusinessCode)
// 4. 用户筛选条件
for _, filter := range req.Filters {
fc, ok := fieldMap[filter.FieldCode]
if !ok {
return "", nil, fmt.Errorf("筛选字段不存在: %s", filter.FieldCode)
}
if !fc.IsFilterable {
return "", nil, fmt.Errorf("字段 %s 不可用于筛选", filter.FieldCode)
}
op := strings.ToUpper(filter.Operator)
if op == "" {
op = "="
}
// 校验操作符
if len(fc.FilterOperators) > 0 {
valid := false
for _, v := range fc.FilterOperators {
if strings.ToUpper(v) == op {
valid = true
break
}
}
if !valid {
return "", nil, fmt.Errorf("字段 %s 不支持操作符 %s", filter.FieldCode, op)
}
}
cond, vals, err := b.buildFilterCondition(filter, op, fc.FieldType)
if err != nil {
return "", nil, err
}
conditions = append(conditions, cond)
args = append(args, vals...)
}
return strings.Join(conditions, " AND "), args, nil
}
// buildFilterCondition 构建单个筛选条件
func (b *SQLBuilder) buildFilterCondition(filter model.FilterCondition, op string, fieldType string) (string, []interface{}, error) {
field := filter.FieldCode
var args []interface{}
switch op {
case "=":
return fmt.Sprintf("%s = ?", field), []interface{}{filter.Value}, nil
case "!=":
return fmt.Sprintf("%s != ?", field), []interface{}{filter.Value}, nil
case ">":
return fmt.Sprintf("%s > ?", field), []interface{}{filter.Value}, nil
case "<":
return fmt.Sprintf("%s < ?", field), []interface{}{filter.Value}, nil
case ">=":
return fmt.Sprintf("%s >= ?", field), []interface{}{filter.Value}, nil
case "<=":
return fmt.Sprintf("%s <= ?", field), []interface{}{filter.Value}, nil
case "IN":
values, err := b.convertToSlice(filter.Value)
if err != nil {
return "", nil, err
}
placeholders := make([]string, len(values))
for i := range values {
placeholders[i] = "?"
args = append(args, values[i])
}
return fmt.Sprintf("%s IN (%s)", field, strings.Join(placeholders, ",")), args, nil
case "LIKE":
return fmt.Sprintf("%s LIKE ?", field), []interface{}{"%" + fmt.Sprintf("%v", filter.Value) + "%"}, nil
case "BETWEEN":
return fmt.Sprintf("%s BETWEEN ? AND ?", field), []interface{}{filter.Value, filter.Value2}, nil
default:
return fmt.Sprintf("%s = ?", field), []interface{}{filter.Value}, nil
}
}
// buildGroupByClause 构建GROUP BY子句
func (b *SQLBuilder) buildGroupByClause(req *model.UserSelectQueryReq, fieldMap map[string]*model.FieldConfig) (string, error) {
var groupFields []string
for _, dim := range req.Dimensions {
fc, ok := fieldMap[dim]
if !ok {
continue
}
if fc.FieldRole == model.RoleDimension || fc.FieldRole == model.RoleFilter {
groupFields = append(groupFields, dim)
}
}
if len(groupFields) == 0 {
return "", nil
}
return strings.Join(groupFields, ", "), nil
}
// buildOrderByClause 构建ORDER BY子句
func (b *SQLBuilder) buildOrderByClause(req *model.UserSelectQueryReq, fieldMap map[string]*model.FieldConfig) (string, error) {
if len(req.OrderBy) == 0 {
return "", nil
}
var orderParts []string
for _, order := range req.OrderBy {
field := order.FieldCode
dir := strings.ToUpper(order.Direction)
if dir == "" {
dir = "ASC"
}
if dir != "ASC" && dir != "DESC" {
return "", fmt.Errorf("排序方向必须是 ASC 或 DESC")
}
fc, ok := fieldMap[field]
if !ok {
return "", fmt.Errorf("排序字段不存在: %s", field)
}
if !fc.IsSortable {
return "", fmt.Errorf("字段 %s 不可排序", field)
}
orderParts = append(orderParts, fmt.Sprintf("%s %s", field, dir))
}
return strings.Join(orderParts, ", "), nil
}
// buildTimeGroupExpr 构建时间分组表达式
func (b *SQLBuilder) buildTimeGroupExpr(dateField, timeGroup string) string {
switch timeGroup {
case "week":
return fmt.Sprintf("DATE_TRUNC('week', %s::date)::text AS time_group", dateField)
case "month":
return fmt.Sprintf("TO_CHAR(%s::date, 'YYYY-MM') AS time_group", dateField)
case "quarter":
return "TO_CHAR(" + dateField + "::date, 'YYYY-\"Q\"Q') AS time_group"
default:
return dateField + " AS time_group"
}
}
// parseExpression 解析衍生指标表达式
func (b *SQLBuilder) parseExpression(expr string, indicators []model.IndicatorSelect) string {
re := regexp.MustCompile(`\{([^}]+)\}`)
return re.ReplaceAllStringFunc(expr, func(match string) string {
fieldCode := match[1 : len(match)-1]
for _, ind := range indicators {
if ind.FieldCode == fieldCode {
return fieldCode
}
}
return match
})
}
// convertToSlice 转换为切片
func (b *SQLBuilder) convertToSlice(v interface{}) ([]interface{}, error) {
switch val := v.(type) {
case []interface{}:
return val, nil
case []string:
result := make([]interface{}, len(val))
for i, s := range val {
result[i] = s
}
return result, nil
case string:
parts := strings.Split(val, ",")
result := make([]interface{}, len(parts))
for i, p := range parts {
result[i] = strings.TrimSpace(p)
}
return result, nil
default:
return []interface{}{v}, nil
}
}
// BuildCountSQL 构建统计总数SQL
func (b *SQLBuilder) BuildCountSQL(sql string) string {
sql = regexp.MustCompile(`(?i)SELECT\s+.*?\s+FROM`).ReplaceAllString(sql, "SELECT COUNT(*) FROM")
return sql
}
// AddLimit 添加分页
func (b *SQLBuilder) AddLimit(sql string, page, pageSize int) string {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
if pageSize > 1000 {
pageSize = 1000
}
offset := (page - 1) * pageSize
return fmt.Sprintf("%s LIMIT %d OFFSET %d", sql, pageSize, offset)
}
// GenerateInsertSQL 生成upsert SQL
func (b *SQLBuilder) GenerateInsertSQL(tableName string, columns []string, conflictKeys []string) string {
cols := strings.Join(columns, ", ")
placeholders := make([]string, len(columns))
for i := range columns {
placeholders[i] = fmt.Sprintf("$%d", i+1)
}
placeholdersStr := strings.Join(placeholders, ", ")
var updateParts []string
for _, col := range columns {
if col == "id" || col == "created_at" {
continue
}
updateParts = append(updateParts, fmt.Sprintf("%s = EXCLUDED.%s", col, col))
}
sql := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", tableName, cols, placeholdersStr)
if len(conflictKeys) > 0 {
sql += " ON CONFLICT (" + strings.Join(conflictKeys, ", ") + ")"
sql += " DO UPDATE SET " + strings.Join(updateParts, ", ")
}
return sql
}
// BuildExtractSQL 构建数据抽取SQL
func (b *SQLBuilder) BuildExtractSQL(ctx context.Context, extractConfig *model.ExtractConfig, statDate string) (string, []interface{}, error) {
var selectParts []string
var args []interface{}
// 基础字段
selectParts = append(selectParts, "tenant_id")
selectParts = append(selectParts, fmt.Sprintf("'%s' AS business_code", extractConfig.BusinessCode))
selectParts = append(selectParts, fmt.Sprintf("'%s' AS stat_date", statDate))
// 字段映射
sourceTable := extractConfig.SourceTableName
if extractConfig.SourceTableAlias != "" {
sourceTable = extractConfig.SourceTableAlias
}
for _, mapping := range extractConfig.FieldMappings {
targetField := mapping.TargetField
sourceField := mapping.SourceField
if mapping.TransformRule != nil {
expr := b.applyTransformRule(mapping.TransformRule, sourceField)
selectParts = append(selectParts, fmt.Sprintf("%s AS %s", expr, targetField))
} else {
selectParts = append(selectParts, fmt.Sprintf("%s.%s AS %s", sourceTable, sourceField, targetField))
}
}
// 构建 FROM 和 JOIN
fromClause := extractConfig.SourceTableName
if extractConfig.SourceTableAlias != "" {
fromClause += " " + extractConfig.SourceTableAlias
}
for _, join := range extractConfig.JoinConfigs {
joinType := "LEFT JOIN"
if strings.ToUpper(join.JoinType) == "INNER" {
joinType = "INNER JOIN"
} else if strings.ToUpper(join.JoinType) == "RIGHT" {
joinType = "RIGHT JOIN"
}
fromClause += fmt.Sprintf(" %s %s %s ON %s", joinType, join.JoinTable, join.JoinAlias, join.JoinCondition)
}
// WHERE 条件
whereClause := ""
if extractConfig.FilterExpression != "" {
whereClause = " WHERE " + extractConfig.FilterExpression
}
sql := fmt.Sprintf("SELECT %s FROM %s%s", strings.Join(selectParts, ", "), fromClause, whereClause)
return sql, args, nil
}
// applyTransformRule 应用转换规则
func (b *SQLBuilder) applyTransformRule(rule *model.TransformRule, sourceField string) string {
switch rule.RuleType {
case "CALCULATE":
if rule.Expression != "" {
return strings.ReplaceAll(rule.Expression, "{source}", sourceField)
}
case "FORMAT":
if rule.Format != "" {
return fmt.Sprintf("TO_CHAR(%s, '%s')", sourceField, rule.Format)
}
case "MAPPING":
// 运行时映射,需要在代码中处理
return sourceField
}
return sourceField
}

View File

@@ -0,0 +1,664 @@
package config
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"sync"
"dataengine/common/report/model"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/frame/g"
)
// ConfigLoader 配置加载器
type ConfigLoader struct {
mu sync.RWMutex
// 缓存
businessCache map[string]*model.BusinessConfig
reportCache map[string]*model.ReportConfig
fieldCache map[string][]model.FieldConfig
extractCache map[string][]model.ExtractConfig
}
var (
defaultLoader *ConfigLoader
once sync.Once
)
// GetLoader 获取配置加载器单例
func GetLoader() *ConfigLoader {
once.Do(func() {
defaultLoader = &ConfigLoader{
businessCache: make(map[string]*model.BusinessConfig),
reportCache: make(map[string]*model.ReportConfig),
fieldCache: make(map[string][]model.FieldConfig),
extractCache: make(map[string][]model.ExtractConfig),
}
})
return defaultLoader
}
// GetBusiness 获取业务配置
func (l *ConfigLoader) GetBusiness(ctx context.Context, businessCode string) (*model.BusinessConfig, error) {
l.mu.RLock()
if biz, ok := l.businessCache[businessCode]; ok {
l.mu.RUnlock()
return biz, nil
}
l.mu.RUnlock()
var biz model.BusinessConfig
r, err := gfdb.DB(ctx).Model(ctx, "report_business_config").
Where("business_code", businessCode).
Where("status", model.StatusActive).
One()
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("业务配置不存在: %s", businessCode)
}
return nil, err
}
if r.IsEmpty() {
return nil, fmt.Errorf("业务配置不存在: %s", businessCode)
}
if err = r.Struct(&biz); err != nil {
return nil, err
}
if biz.Config == nil {
biz.Config = make(map[string]interface{})
}
l.mu.Lock()
l.businessCache[businessCode] = &biz
l.mu.Unlock()
return &biz, nil
}
// GetReport 获取报表配置
func (l *ConfigLoader) GetReport(ctx context.Context, businessCode, reportCode string) (*model.ReportConfig, error) {
key := businessCode + ":" + reportCode
l.mu.RLock()
if rpt, ok := l.reportCache[key]; ok {
l.mu.RUnlock()
return rpt, nil
}
l.mu.RUnlock()
var rpt model.ReportConfig
r, err := gfdb.DB(ctx).Model(ctx, "report_report_config").
Where("business_code", businessCode).
Where("report_code", reportCode).
Where("status", model.StatusActive).
One()
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("报表配置不存在: %s/%s", businessCode, reportCode)
}
return nil, err
}
if r.IsEmpty() {
return nil, fmt.Errorf("报表配置不存在: %s/%s", businessCode, reportCode)
}
if err = r.Struct(&rpt); err != nil {
return nil, err
}
if rpt.PrimaryKeys == nil {
rpt.PrimaryKeys = []string{"id"}
}
if rpt.ConflictKeys == nil {
rpt.ConflictKeys = []string{"stat_date"}
}
if rpt.Config == nil {
rpt.Config = make(map[string]interface{})
}
l.mu.Lock()
l.reportCache[key] = &rpt
l.mu.Unlock()
return &rpt, nil
}
// GetFields 获取报表字段配置
func (l *ConfigLoader) GetFields(ctx context.Context, businessCode, reportCode string) ([]model.FieldConfig, error) {
key := businessCode + ":" + reportCode
l.mu.RLock()
if fields, ok := l.fieldCache[key]; ok {
l.mu.RUnlock()
return fields, nil
}
l.mu.RUnlock()
var fields []model.FieldConfig
err := gfdb.DB(ctx).Model(ctx, "report_field_config").
Where("business_code", businessCode).
Where("report_code", reportCode).
Where("status", model.StatusActive).
Order("sort_order ASC").
Scan(&fields)
if err != nil {
return nil, err
}
for i := range fields {
if fields[i].ValidAggregates == nil {
fields[i].ValidAggregates = []string{}
}
if fields[i].FilterOperators == nil {
fields[i].FilterOperators = []string{"=", "!=", ">", "<", ">=", "<=", "IN", "LIKE", "BETWEEN"}
}
}
l.mu.Lock()
l.fieldCache[key] = fields
l.mu.Unlock()
return fields, nil
}
// GetFieldMap 获取字段配置Map
func (l *ConfigLoader) GetFieldMap(ctx context.Context, businessCode, reportCode string) (map[string]*model.FieldConfig, error) {
fields, err := l.GetFields(ctx, businessCode, reportCode)
if err != nil {
return nil, err
}
fieldMap := make(map[string]*model.FieldConfig)
for i := range fields {
fieldMap[fields[i].FieldCode] = &fields[i]
}
return fieldMap, nil
}
// GetExtractConfigs 获取抽取配置
func (l *ConfigLoader) GetExtractConfigs(ctx context.Context, businessCode, reportCode string) ([]model.ExtractConfig, error) {
key := businessCode + ":" + reportCode
l.mu.RLock()
if configs, ok := l.extractCache[key]; ok {
l.mu.RUnlock()
return configs, nil
}
l.mu.RUnlock()
var configs []model.ExtractConfig
err := gfdb.DB(ctx).Model(ctx, "report_extract_config").
Where("business_code", businessCode).
Where("report_code", reportCode).
Where("status", model.StatusActive).
Where("is_enabled", true).
Scan(&configs)
if err != nil {
return nil, err
}
for i := range configs {
if configs[i].JoinConfigs == nil {
configs[i].JoinConfigs = []model.JoinConfig{}
}
if configs[i].FieldMappings == nil {
configs[i].FieldMappings = []model.FieldMapping{}
}
if configs[i].TransformRules == nil {
configs[i].TransformRules = []model.TransformRule{}
}
if configs[i].GroupByFields == nil {
configs[i].GroupByFields = []string{}
}
if configs[i].ExtractMode == "" {
configs[i].ExtractMode = model.ExtractModeDirect
}
}
l.mu.Lock()
l.extractCache[key] = configs
l.mu.Unlock()
return configs, nil
}
// GetExtractLog 获取抽取记录
func (l *ConfigLoader) GetExtractLog(ctx context.Context, businessCode, reportCode, extractCode, statDate string) (*model.ExtractLog, error) {
var log model.ExtractLog
r, err := gfdb.DB(ctx).Model(ctx, "report_extract_log").
Where("business_code", businessCode).
Where("report_code", reportCode).
Where("extract_code", extractCode).
Where("stat_date", statDate).
One()
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
if r.IsEmpty() {
return nil, nil
}
if err = r.Struct(&log); err != nil {
return nil, err
}
return &log, nil
}
// CreateExtractLog 创建抽取记录
func (l *ConfigLoader) CreateExtractLog(ctx context.Context, log *model.ExtractLog) error {
data, _ := json.Marshal(log)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
_, err := gfdb.DB(ctx).Model(ctx, "report_extract_log").Data(m).Save()
return err
}
// UpdateExtractLog 更新抽取记录
func (l *ConfigLoader) UpdateExtractLog(ctx context.Context, log *model.ExtractLog) error {
data, _ := json.Marshal(log)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
delete(m, "created_at")
_, err := gfdb.DB(ctx).Model(ctx, "report_extract_log").
Where("business_code", log.BusinessCode).
Where("report_code", log.ReportCode).
Where("extract_code", log.ExtractCode).
Where("stat_date", log.StatDate).
Data(m).
Update()
return err
}
// InvalidateCache 失效缓存
func (l *ConfigLoader) InvalidateCache(businessCode, reportCode string) {
l.mu.Lock()
delete(l.businessCache, businessCode)
delete(l.reportCache, businessCode+":"+reportCode)
delete(l.fieldCache, businessCode+":"+reportCode)
delete(l.extractCache, businessCode+":"+reportCode)
l.mu.Unlock()
}
// InvalidateBusinessCache 只失效业务缓存(不影响报表/字段)
func (l *ConfigLoader) InvalidateBusinessCache(businessCode string) {
l.mu.Lock()
delete(l.businessCache, businessCode)
l.mu.Unlock()
}
// ============================================================
// CRUD: BusinessConfig
// ============================================================
// CreateBusiness 创建业务配置
func (l *ConfigLoader) CreateBusiness(ctx context.Context, biz *model.BusinessConfig) (int64, error) {
data, _ := json.Marshal(biz)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
delete(m, "created_at")
delete(m, "updated_at")
delete(m, "deleted_at")
result, err := gfdb.DB(ctx).Model(ctx, "report_business_config").Data(m).Insert()
if err != nil {
return 0, fmt.Errorf("创建业务配置失败: %w", err)
}
id, _ := result.LastInsertId()
l.InvalidateBusinessCache(biz.BusinessCode)
return id, nil
}
// UpdateBusiness 更新业务配置
func (l *ConfigLoader) UpdateBusiness(ctx context.Context, biz *model.BusinessConfig) error {
data, _ := json.Marshal(biz)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
delete(m, "created_at")
delete(m, "deleted_at")
_, err := gfdb.DB(ctx).Model(ctx, "report_business_config").
Where("id", biz.ID).
Data(m).
Update()
if err != nil {
return fmt.Errorf("更新业务配置失败: %w", err)
}
l.InvalidateBusinessCache(biz.BusinessCode)
return nil
}
// DeleteBusiness 删除业务配置(软删除)
func (l *ConfigLoader) DeleteBusiness(ctx context.Context, id int64, businessCode string) error {
_, err := gfdb.DB(ctx).Model(ctx, "report_business_config").
Where("id", id).
Data(map[string]interface{}{
"status": model.StatusInactive,
"deleted_at": "NOW()",
}).
Update()
if err != nil {
return fmt.Errorf("删除业务配置失败: %w", err)
}
l.InvalidateBusinessCache(businessCode)
return nil
}
// GetBusinessByID 根据ID获取业务配置
func (l *ConfigLoader) GetBusinessByID(ctx context.Context, id int64) (*model.BusinessConfig, error) {
var biz model.BusinessConfig
r, err := gfdb.DB(ctx).Model(ctx, "report_business_config").
Where("id", id).
One()
if err != nil {
g.Log().Infof(ctx, "[GetBusinessByID] id=%d, err=%v", id, err)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("业务配置不存在: id=%d", id)
}
return nil, err
}
if r.IsEmpty() {
return nil, fmt.Errorf("业务配置不存在: id=%d", id)
}
if err = r.Struct(&biz); err != nil {
return nil, err
}
g.Log().Infof(ctx, "[GetBusinessByID] id=%d, biz.ID=%d, biz.BusinessCode=%s",
id, biz.ID, biz.BusinessCode)
return &biz, nil
}
// ============================================================
// CRUD: ReportConfig
// ============================================================
// CreateReport 创建报表配置
func (l *ConfigLoader) CreateReport(ctx context.Context, rpt *model.ReportConfig) (int64, error) {
data, _ := json.Marshal(rpt)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
delete(m, "created_at")
delete(m, "updated_at")
delete(m, "deleted_at")
result, err := gfdb.DB(ctx).Model(ctx, "report_report_config").Data(m).Insert()
if err != nil {
return 0, fmt.Errorf("创建报表配置失败: %w", err)
}
id, _ := result.LastInsertId()
l.InvalidateCache(rpt.BusinessCode, rpt.ReportCode)
return id, nil
}
// UpdateReport 更新报表配置
func (l *ConfigLoader) UpdateReport(ctx context.Context, rpt *model.ReportConfig) error {
data, _ := json.Marshal(rpt)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
delete(m, "created_at")
delete(m, "deleted_at")
_, err := gfdb.DB(ctx).Model(ctx, "report_report_config").
Where("id", rpt.ID).
Data(m).
Update()
if err != nil {
return fmt.Errorf("更新报表配置失败: %w", err)
}
l.InvalidateCache(rpt.BusinessCode, rpt.ReportCode)
return nil
}
// DeleteReport 删除报表配置(软删除)
func (l *ConfigLoader) DeleteReport(ctx context.Context, id int64, businessCode, reportCode string) error {
_, err := gfdb.DB(ctx).Model(ctx, "report_report_config").
Where("id", id).
Data(map[string]interface{}{
"status": model.StatusInactive,
"deleted_at": "NOW()",
}).
Update()
if err != nil {
return fmt.Errorf("删除报表配置失败: %w", err)
}
l.InvalidateCache(businessCode, reportCode)
return nil
}
// GetReportByID 根据ID获取报表配置
func (l *ConfigLoader) GetReportByID(ctx context.Context, id int64) (*model.ReportConfig, error) {
var rpt model.ReportConfig
r, err := gfdb.DB(ctx).Model(ctx, "report_report_config").
Where("id", id).
One()
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("报表配置不存在: id=%d", id)
}
return nil, err
}
if r.IsEmpty() {
return nil, fmt.Errorf("报表配置不存在: id=%d", id)
}
if err = r.Struct(&rpt); err != nil {
return nil, err
}
return &rpt, nil
}
// ============================================================
// CRUD: FieldConfig
// ============================================================
// CreateField 创建字段配置
func (l *ConfigLoader) CreateField(ctx context.Context, field *model.FieldConfig) (int64, error) {
data, _ := json.Marshal(field)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
delete(m, "created_at")
delete(m, "updated_at")
delete(m, "deleted_at")
result, err := gfdb.DB(ctx).Model(ctx, "report_field_config").Data(m).Insert()
if err != nil {
return 0, fmt.Errorf("创建字段配置失败: %w", err)
}
id, _ := result.LastInsertId()
l.InvalidateCache(field.BusinessCode, field.ReportCode)
return id, nil
}
// UpdateField 更新字段配置
func (l *ConfigLoader) UpdateField(ctx context.Context, field *model.FieldConfig) error {
data, _ := json.Marshal(field)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
delete(m, "created_at")
delete(m, "deleted_at")
_, err := gfdb.DB(ctx).Model(ctx, "report_field_config").
Where("id", field.ID).
Data(m).
Update()
if err != nil {
return fmt.Errorf("更新字段配置失败: %w", err)
}
l.InvalidateCache(field.BusinessCode, field.ReportCode)
return nil
}
// DeleteField 删除字段配置(软删除)
func (l *ConfigLoader) DeleteField(ctx context.Context, id int64, businessCode, reportCode string) error {
_, err := gfdb.DB(ctx).Model(ctx, "report_field_config").
Where("id", id).
Data(map[string]interface{}{
"status": model.StatusInactive,
"deleted_at": "NOW()",
}).
Update()
if err != nil {
return fmt.Errorf("删除字段配置失败: %w", err)
}
l.InvalidateCache(businessCode, reportCode)
return nil
}
// GetFieldByID 根据ID获取字段配置
func (l *ConfigLoader) GetFieldByID(ctx context.Context, id int64) (*model.FieldConfig, error) {
var field model.FieldConfig
r, err := gfdb.DB(ctx).Model(ctx, "report_field_config").
Where("id", id).
One()
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("字段配置不存在: id=%d", id)
}
return nil, err
}
if r.IsEmpty() {
return nil, fmt.Errorf("字段配置不存在: id=%d", id)
}
if err = r.Struct(&field); err != nil {
return nil, err
}
return &field, nil
}
// ============================================================
// CRUD: ExtractConfig
// ============================================================
// CreateExtractConfig 创建抽取配置
func (l *ConfigLoader) CreateExtractConfig(ctx context.Context, ec *model.ExtractConfig) (int64, error) {
data, _ := json.Marshal(ec)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
delete(m, "created_at")
delete(m, "updated_at")
delete(m, "deleted_at")
result, err := gfdb.DB(ctx).Model(ctx, "report_extract_config").Data(m).Insert()
if err != nil {
return 0, fmt.Errorf("创建抽取配置失败: %w", err)
}
id, _ := result.LastInsertId()
l.InvalidateCache(ec.BusinessCode, ec.ReportCode)
return id, nil
}
// UpdateExtractConfig 更新抽取配置
func (l *ConfigLoader) UpdateExtractConfig(ctx context.Context, ec *model.ExtractConfig) error {
data, _ := json.Marshal(ec)
var m map[string]interface{}
json.Unmarshal(data, &m)
delete(m, "id")
delete(m, "created_at")
delete(m, "deleted_at")
_, err := gfdb.DB(ctx).Model(ctx, "report_extract_config").
Where("id", ec.ID).
Data(m).
Update()
if err != nil {
return fmt.Errorf("更新抽取配置失败: %w", err)
}
l.InvalidateCache(ec.BusinessCode, ec.ReportCode)
return nil
}
// DeleteExtractConfig 删除抽取配置(软删除)
func (l *ConfigLoader) DeleteExtractConfig(ctx context.Context, id int64, businessCode, reportCode string) error {
_, err := gfdb.DB(ctx).Model(ctx, "report_extract_config").
Where("id", id).
Data(map[string]interface{}{
"status": model.StatusInactive,
"deleted_at": "NOW()",
}).
Update()
if err != nil {
return fmt.Errorf("删除抽取配置失败: %w", err)
}
l.InvalidateCache(businessCode, reportCode)
return nil
}
// GetExtractConfigByID 根据ID获取抽取配置
func (l *ConfigLoader) GetExtractConfigByID(ctx context.Context, id int64) (*model.ExtractConfig, error) {
var ec model.ExtractConfig
r, err := gfdb.DB(ctx).Model(ctx, "report_extract_config").
Where("id", id).
One()
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("抽取配置不存在: id=%d", id)
}
return nil, err
}
if r.IsEmpty() {
return nil, fmt.Errorf("抽取配置不存在: id=%d", id)
}
if err = r.Struct(&ec); err != nil {
return nil, err
}
return &ec, nil
}
// GetAllBusinesses 获取所有业务配置
func (l *ConfigLoader) GetAllBusinesses(ctx context.Context) ([]model.BusinessConfig, error) {
var businesses []model.BusinessConfig
err := gfdb.DB(ctx).Model(ctx, "report_business_config").
Where("status", model.StatusActive).
Order("id ASC").
Scan(&businesses)
return businesses, err
}
// GetAllReports 获取所有报表配置
func (l *ConfigLoader) GetAllReports(ctx context.Context, businessCode string) ([]model.ReportConfig, error) {
var reports []model.ReportConfig
err := gfdb.DB(ctx).Model(ctx, "report_report_config").
Where("business_code", businessCode).
Where("status", model.StatusActive).
Order("id ASC").
Scan(&reports)
return reports, err
}
// GetReportFields 获取报表可用字段(按角色分类)
func (l *ConfigLoader) GetReportFields(ctx context.Context, businessCode, reportCode string) (*model.GetReportFieldsResp, error) {
fields, err := l.GetFields(ctx, businessCode, reportCode)
if err != nil {
return nil, err
}
resp := &model.GetReportFieldsResp{
BusinessCode: businessCode,
ReportCode: reportCode,
Dimensions: []model.FieldConfig{},
Indicators: []model.FieldConfig{},
Filters: []model.FieldConfig{},
}
for _, f := range fields {
switch f.FieldRole {
case model.RoleDimension:
resp.Dimensions = append(resp.Dimensions, f)
case model.RoleIndicator:
resp.Indicators = append(resp.Indicators, f)
case model.RoleFilter, model.RoleFilterOnly:
resp.Filters = append(resp.Filters, f)
}
}
return resp, nil
}

View File

@@ -0,0 +1,210 @@
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
}

View File

@@ -0,0 +1,666 @@
package report
// ============================================================
// 通用报表引擎 - 完整调用示例
// ============================================================
//
// 包名: dataengine/common/report
// 入口: report.GetService() → *ReportService
//
// 接口一览:
// ┌──────────────┬─────────────────────────────────────────────────┐
// │ 分类 │ 接口 │
// ├──────────────┼─────────────────────────────────────────────────┤
// │ 配置 CRUD │ SaveBusiness / DelBusiness / GetBusiness │
// │ │ SaveReport / DelReport / GetReport │
// │ │ SaveField / DelField / GetField │
// │ │ SaveExtractConfig / DelExtractConfig / Get.. │
// ├──────────────┼─────────────────────────────────────────────────┤
// │ 数据抽取 │ ExtractDailyData / AutoCreateStatTable │
// ├──────────────┼─────────────────────────────────────────────────┤
// │ 报表查询 │ QueryReportByUserSelect │
// ├──────────────┼─────────────────────────────────────────────────┤
// │ 辅助查询 │ GetAllBusinesses / GetAllReports │
// │ │ GetReportFields / GetExtractConfigs │
// └──────────────┴─────────────────────────────────────────────────┘
//
// ============================================================
// ============================================================================
// 场景一:新平台零代码接入(快手电商为例)
// ============================================================================
//
// 背景:快手订单数据已通过数据引擎同步到 kuaishou_order_list 表(每条订单一行)。
// 需求:按店铺+天聚合订单数据 → 自动建统计宽表 → 抽取 → 前端自由查询报表。
//
// 全程只调 API 不写 SQL前端管理后台可直接操作。
/*
package example
import (
"context"
"encoding/json"
"fmt"
"time"
"dataengine/common/report"
"dataengine/common/report/model"
)
func KuaishouExample() {
ctx := context.Background()
svc := report.GetService()
// ─── Step 1: 注册业务 ───────────────────────────────────────────
// 一个业务就是一个数据源平台(快手/抖音/淘宝/...
_, _ = svc.SaveBusiness(ctx, &model.SaveBusinessReq{
BusinessCode: "KUAISHOU", // 唯一标识,后续所有接口都用它
BusinessName: "快手电商",
Description: "快手平台电商业务线",
Status: model.StatusActive,
Operator: "admin",
})
// ─── Step 2: 注册报表 ───────────────────────────────────────────
// 一个业务可以有多个报表(店铺日报、商品日报、主播日报...
_, _ = svc.SaveReport(ctx, &model.SaveReportReq{
BusinessCode: "KUAISHOU",
ReportCode: "shop_daily_report", // 报表唯一编码
ReportName: "快手店铺日报",
StatTableName: "stat_kuaishou_shop_daily", // 统计宽表名(自动创建)
DateField: "stat_date", // 日期字段
ConflictKeys: []string{"shop_id", "stat_date"}, // 唯一约束upsert 依据)
Operator: "admin",
})
// ─── Step 3: 配置字段(前端可选择的所有维度/指标/筛选) ─────────
// 这是前端"自定义报表"的数据源 —— 前端建好字段后,
// 用户选择哪些维度、哪些指标、怎么筛选,实时查。
// 3a. 维度字段:分组依据,不可聚合
dimensions := []model.SaveFieldReq{
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "shop_id", FieldName: "店铺ID", FieldType: "STRING",
FieldRole: "DIMENSION", IsSortable: true, SortOrder: 1, GroupName: "店铺", Operator: "admin"},
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "shop_name", FieldName: "店铺名称", FieldType: "STRING",
FieldRole: "DIMENSION", IsSortable: true, SortOrder: 2, GroupName: "店铺", Operator: "admin"},
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "stat_date", FieldName: "统计日期", FieldType: "DATE",
FieldRole: "DIMENSION", IsSortable: true, SortOrder: 3, GroupName: "时间", Operator: "admin"},
}
// 3b. 指标字段:聚合度量,前端可选 SUM/COUNT/AVG/MAX/MIN
indicators := []model.SaveFieldReq{
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "order_count", FieldName: "订单数", FieldType: "INT",
FieldRole: "INDICATOR", IsAggregatable: true, DefaultAggregate: "SUM",
ValidAggregates: []string{"SUM", "COUNT", "AVG", "MAX", "MIN"},
SortOrder: 10, GroupName: "订单", Operator: "admin"},
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "order_amount", FieldName: "订单金额(元)", FieldType: "FLOAT",
FieldRole: "INDICATOR", IsAggregatable: true, DefaultAggregate: "SUM",
ValidAggregates: []string{"SUM", "AVG", "MAX", "MIN"},
SortOrder: 11, GroupName: "金额", Unit: "元", Operator: "admin"},
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "paid_amount", FieldName: "实付金额(元)", FieldType: "FLOAT",
FieldRole: "INDICATOR", IsAggregatable: true, DefaultAggregate: "SUM",
ValidAggregates: []string{"SUM", "AVG"},
SortOrder: 12, GroupName: "金额", Unit: "元", Operator: "admin"},
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "refund_amount", FieldName: "退款金额(元)", FieldType: "FLOAT",
FieldRole: "INDICATOR", IsAggregatable: true, DefaultAggregate: "SUM",
ValidAggregates: []string{"SUM", "AVG"},
SortOrder: 13, GroupName: "退款", Unit: "元", Operator: "admin"},
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "buyer_count", FieldName: "下单买家数", FieldType: "INT",
FieldRole: "INDICATOR", IsAggregatable: true, DefaultAggregate: "COUNT",
ValidAggregates: []string{"COUNT"},
SortOrder: 14, GroupName: "用户", Operator: "admin"},
// 衍生指标:退款率 = 退款金额 / 订单金额 * 100
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "refund_rate", FieldName: "退款率", FieldType: "FLOAT",
FieldRole: "INDICATOR", IsAggregatable: true, DefaultAggregate: "AVG",
Expression: "{refund_amount} / NULLIF({order_amount}, 0) * 100",
ExpressionType: "CALCULATED", FormatPattern: "#,##0.00",
Unit: "%", SortOrder: 20, GroupName: "退款", Operator: "admin"},
}
// 3c. 筛选字段:纯筛选,不出现在 SELECT 中
filters := []model.SaveFieldReq{
{BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
FieldCode: "order_status", FieldName: "订单状态", FieldType: "STRING",
FieldRole: "FILTER", IsFilterable: true,
FilterOperators: []string{"=", "IN"},
SortOrder: 30, GroupName: "筛选", Operator: "admin"},
}
for _, f := range dimensions {
_, _ = svc.SaveField(ctx, &f)
}
for _, f := range indicators {
_, _ = svc.SaveField(ctx, &f)
}
for _, f := range filters {
_, _ = svc.SaveField(ctx, &f)
}
// ─── Step 4: 配置数据抽取规则 ────────────────────────────────────
// 关键AGGREGATE 模式,从源表聚合到统计宽表
_, _ = svc.SaveExtractConfig(ctx, &model.SaveExtractConfigReq{
BusinessCode: "KUAISHOU",
ReportCode: "shop_daily_report",
ExtractCode: "extract_shop_daily",
ExtractName: "快手店铺订单按天聚合",
SourceTableName: "kuaishou_order_list", // ← 源表(订单明细)
SourceTableAlias: "o",
TargetTableName: "stat_kuaishou_shop_daily", // ← 目标(统计宽表)
ExtractType: model.ExtractTypeIncremental, // 增量抽取
ExtractMode: model.ExtractModeAggregate, // ← 聚合模式
ExtractKeyField: "created_at", // 增量依据字段
GroupByFields: []string{"shop_id"}, // ← GROUP BY
FilterExpression: "o.order_status != 'CANCELLED'", // 过滤取消订单
// 字段映射:源表字段 → 目标表字段 + 聚合函数
FieldMappings: []model.FieldMapping{
{SourceField: "shop_id", TargetField: "shop_id", FieldType: "STRING"},
{SourceField: "shop_name", TargetField: "shop_name", FieldType: "STRING"},
{SourceField: "id", TargetField: "order_count", FieldType: "INT", AggregateFunction: "COUNT"},
{SourceField: "order_amount", TargetField: "order_amount", FieldType: "FLOAT", AggregateFunction: "SUM"},
{SourceField: "paid_amount", TargetField: "paid_amount", FieldType: "FLOAT", AggregateFunction: "SUM"},
{SourceField: "refund_amount",TargetField: "refund_amount",FieldType: "FLOAT", AggregateFunction: "SUM"},
{SourceField: "buyer_id", TargetField: "buyer_count", FieldType: "INT", AggregateFunction: "COUNT"},
},
BatchSize: 1000,
Operator: "admin",
})
// ─── Step 5: 每天定时抽取cron/k8s CronJob 中调用) ─────────
// ExtractDailyData 内部:
// 1. 检测 stat_kuaishou_shop_daily 表是否存在
// 2. 不存在 → 根据 FieldConfig 自动 CREATE TABLE
// 3. 从 kuaishou_order_list 按 AGGREGATE 模式抽取当天数据
// 4. UPSERT 到统计宽表
today := time.Now().Format("2006-01-02")
resp, _ := svc.ExtractDailyData(ctx, "KUAISHOU", "shop_daily_report", today, "cron")
fmt.Printf("[%s] 抽取完成: 总%d 成功%d 失败%d 耗时%dms\n",
today, resp.TotalCount, resp.SuccessCount, resp.FailCount, resp.ExecTimeMs)
// 实际抽取生成的 SQLAGGREGATE 模式):
//
// INSERT INTO stat_kuaishou_shop_daily (...)
// SELECT ROW_NUMBER() OVER () AS id,
// '2026-06-10' AS stat_date,
// o.shop_id, o.shop_name,
// COUNT(o.id) AS order_count,
// SUM(o.order_amount) AS order_amount,
// SUM(o.paid_amount) AS paid_amount,
// SUM(o.refund_amount) AS refund_amount,
// COUNT(o.buyer_id) AS buyer_count
// FROM kuaishou_order_list o
// WHERE o.created_at::date = '2026-06-10'
// GROUP BY o.shop_id
// ─── Step 6: 前端查询 ──────────────────────────────────────────
// 6a. 前端先拉取可用字段列表
fields, _ := svc.GetReportFields(ctx, "KUAISHOU", "shop_daily_report")
// 返回:
// dimensions: [shop_id, shop_name, stat_date]
// indicators: [order_count, order_amount, paid_amount, refund_amount, buyer_count, refund_rate]
// filters: [order_status]
// 前端据此渲染选择器面板
// 6b. 用户选择: 维度=店铺, 指标=金额+订单数, 时间=近7天, 排名=TOP10
top10Req := &model.UserSelectQueryReq{
BusinessCode: "KUAISHOU",
ReportCode: "shop_daily_report",
Dimensions: []string{"shop_id", "shop_name"},
Indicators: []model.IndicatorSelect{
{FieldCode: "order_amount", Aggregate: "SUM", Alias: "total_amount"},
{FieldCode: "order_count", Aggregate: "SUM", Alias: "total_orders"},
{FieldCode: "paid_amount", Aggregate: "SUM", Alias: "total_paid"},
{FieldCode: "refund_rate", Aggregate: "AVG", Alias: "avg_refund_rate"},
},
TimeRange: &model.TimeRange{
StartDate: time.Now().AddDate(0, 0, -7).Format("2006-01-02"),
EndDate: today,
},
OrderBy: []model.OrderCondition{{FieldCode: "total_amount", Direction: "DESC"}},
Page: 1,
PageSize: 10,
}
top10Resp, _ := svc.QueryReportByUserSelect(ctx, top10Req)
fmt.Printf("TOP10 销售额排行 (总数=%d):\n", top10Resp.Total)
for i, row := range top10Resp.List {
b, _ := json.Marshal(row)
fmt.Printf(" %d. %s\n", i+1, string(b))
}
// 实际生成 SQL:
// SELECT o.shop_id, o.shop_name,
// SUM(o.order_amount) AS total_amount,
// SUM(o.order_count) AS total_orders,
// SUM(o.paid_amount) AS total_paid,
// AVG(refund_amount / NULLIF(order_amount,0) * 100) AS avg_refund_rate
// FROM stat_kuaishou_shop_daily o
// WHERE stat_date BETWEEN '2026-06-03' AND '2026-06-10'
// GROUP BY o.shop_id, o.shop_name
// ORDER BY total_amount DESC
// LIMIT 10 OFFSET 0
// 6c. 用户切换维度: 每日趋势(聚合所有店铺)
trendReq := &model.UserSelectQueryReq{
BusinessCode: "KUAISHOU",
ReportCode: "shop_daily_report",
Dimensions: []string{"stat_date"},
Indicators: []model.IndicatorSelect{
{FieldCode: "order_amount", Aggregate: "SUM", Alias: "daily_amount"},
{FieldCode: "order_count", Aggregate: "SUM", Alias: "daily_orders"},
{FieldCode: "refund_amount",Aggregate: "SUM", Alias: "daily_refund"},
},
TimeRange: &model.TimeRange{
StartDate: time.Now().AddDate(0, 0, -30).Format("2006-01-02"),
EndDate: today,
},
TimeGroup: "day",
OrderBy: []model.OrderCondition{{FieldCode: "stat_date", Direction: "ASC"}},
PageSize: 100,
}
trendResp, _ := svc.QueryReportByUserSelect(ctx, trendReq)
fmt.Printf("30天趋势 (共%d行):\n", trendResp.Total)
// 6d. 加筛选条件: 只看活跃订单,金额>10000
filteredReq := &model.UserSelectQueryReq{
BusinessCode: "KUAISHOU",
ReportCode: "shop_daily_report",
Dimensions: []string{"shop_id", "shop_name"},
Indicators: []model.IndicatorSelect{
{FieldCode: "order_amount", Aggregate: "SUM", Alias: "total_amount"},
},
Filters: []model.FilterCondition{
{FieldCode: "order_status", Operator: "=", Value: "ACTIVE"},
{FieldCode: "total_amount", Operator: ">=", Value: 10000}, // 指标别名也可筛选
},
OrderBy: []model.OrderCondition{{FieldCode: "total_amount", Direction: "DESC"}},
PageSize: 20,
}
_ = filteredReq
// 6e. 按周汇总趋势
weeklyReq := &model.UserSelectQueryReq{
BusinessCode: "KUAISHOU",
ReportCode: "shop_daily_report",
Dimensions: []string{"shop_id"},
Indicators: []model.IndicatorSelect{
{FieldCode: "order_amount", Aggregate: "SUM", Alias: "weekly_amount"},
},
TimeGroup: "week",
OrderBy: []model.OrderCondition{{FieldCode: "weekly_amount", Direction: "DESC"}},
PageSize: 50,
}
_ = weeklyReq
}
// ============================================================================
// 场景二已有配置管理CRUD 二次开发参考)
// ============================================================================
func CRUDExample() {
ctx := context.Background()
svc := report.GetService()
// ── 业务 CRUD ────────────────────────────────────────────
// 新增
result, _ := svc.SaveBusiness(ctx, &model.SaveBusinessReq{
BusinessCode: "DOUYIN", BusinessName: "抖音电商",
Operator: "admin",
})
businessId := result.ID
// 修改(传 ID 即修改)
result, _ = svc.SaveBusiness(ctx, &model.SaveBusinessReq{
ID: &businessId, BusinessCode: "DOUYIN",
BusinessName: "抖音电商(新版)", Operator: "admin",
})
// 查询
biz, _ := svc.GetBusiness(ctx, businessId)
fmt.Printf("%s: %s\n", biz.BusinessCode, biz.BusinessName)
// 删除
svc.DeleteBusiness(ctx, businessId)
// 全部列表
allBiz, _ := svc.GetAllBusinesses(ctx)
for _, b := range allBiz {
fmt.Printf("- %s (%s)\n", b.BusinessCode, b.BusinessName)
}
// ── 报表 CRUD ────────────────────────────────────────────
reportResult, _ := svc.SaveReport(ctx, &model.SaveReportReq{
BusinessCode: "DOUYIN",
ReportCode: "shop_daily_report",
ReportName: "抖音店铺日报",
StatTableName: "stat_douyin_shop_daily",
ConflictKeys: []string{"shop_id", "stat_date"},
Operator: "admin",
})
reportId := reportResult.ID
rpt, _ := svc.GetReport(ctx, reportId)
svc.DeleteReport(ctx, reportId)
_ = rpt
// ── 字段 CRUD ────────────────────────────────────────────
// 新增字段id 不传 = 新增)
fieldResult, _ := svc.SaveField(ctx, &model.SaveFieldReq{
BusinessCode: "DOUYIN", ReportCode: "shop_daily_report",
FieldCode: "order_amount", FieldName: "订单金额",
FieldType: "FLOAT", FieldRole: "INDICATOR",
IsAggregatable: true, DefaultAggregate: "SUM",
ValidAggregates: []string{"SUM", "AVG", "MAX", "MIN"},
SortOrder: 10, GroupName: "金额", Operator: "admin",
})
fieldId := fieldResult.ID
// 修改字段(传 id = 更新)
svc.SaveField(ctx, &model.SaveFieldReq{
ID: &fieldId,
FieldName: "订单金额(元)",
Operator: "admin",
// ... 只传要修改的字段,未传的保持原值
})
f, _ := svc.GetField(ctx, fieldId)
svc.DeleteField(ctx, fieldId)
_ = f
// ── 抽取配置 CRUD ────────────────────────────────────────
ecResult, _ := svc.SaveExtractConfig(ctx, &model.SaveExtractConfigReq{
BusinessCode: "DOUYIN", ReportCode: "shop_daily_report",
ExtractCode: "extract_daily", ExtractName: "按天聚合抽取",
SourceTableName: "douyin_order_list", SourceTableAlias: "o",
TargetTableName: "stat_douyin_shop_daily",
ExtractMode: "AGGREGATE",
ExtractKeyField: "created_at",
GroupByFields: []string{"shop_id"},
FieldMappings: []model.FieldMapping{
{SourceField: "id", TargetField: "order_count", FieldType: "INT", AggregateFunction: "COUNT"},
{SourceField: "order_amount", TargetField: "order_amount", FieldType: "FLOAT", AggregateFunction: "SUM"},
},
Operator: "admin",
})
ecId := ecResult.ID
ec, _ := svc.GetExtractConfig(ctx, ecId)
allEc, _ := svc.GetExtractConfigs(ctx, "DOUYIN", "shop_daily_report")
svc.DeleteExtractConfig(ctx, ecId)
_, _ = ec, allEc
}
// ============================================================================
// 场景三:任意平台接入的通用模式(零硬编码)
// ============================================================================
//
// 假设要接入淘宝平台taobao_order_list 表已有数据。
// 全程不写任何代码,只需调 CRUD API。
func GenericPlatformExample() {
ctx := context.Background()
svc := report.GetService()
// 1. 前端注册业务
svc.SaveBusiness(ctx, &model.SaveBusinessReq{
BusinessCode: "TAOBAO", BusinessName: "淘宝电商",
Operator: "admin",
})
// 2. 注册报表 + 指定统计宽表名(以后表名不再手工管理)
svc.SaveReport(ctx, &model.SaveReportReq{
BusinessCode: "TAOBAO",
ReportCode: "shop_daily_report",
ReportName: "淘宝店铺日报",
StatTableName: "stat_taobao_shop_daily",
ConflictKeys: []string{"shop_id", "stat_date"},
Operator: "admin",
})
// 3. 前端选择统计维度(自由组合,随时增删改)
svc.SaveField(ctx, &model.SaveFieldReq{
BusinessCode: "TAOBAO", ReportCode: "shop_daily_report",
FieldCode: "shop_id", FieldName: "店铺ID",
FieldType: "STRING", FieldRole: "DIMENSION",
SortOrder: 1, GroupName: "店铺", Operator: "admin",
})
svc.SaveField(ctx, &model.SaveFieldReq{
BusinessCode: "TAOBAO", ReportCode: "shop_daily_report",
FieldCode: "shop_name", FieldName: "店铺名称",
FieldType: "STRING", FieldRole: "DIMENSION",
SortOrder: 2, GroupName: "店铺", Operator: "admin",
})
svc.SaveField(ctx, &model.SaveFieldReq{
BusinessCode: "TAOBAO", ReportCode: "shop_daily_report",
FieldCode: "stat_date", FieldName: "统计日期",
FieldType: "DATE", FieldRole: "DIMENSION",
SortOrder: 3, GroupName: "时间", Operator: "admin",
})
// 4. 前端选择统计指标(自由组合,随时增删改)
svc.SaveField(ctx, &model.SaveFieldReq{
BusinessCode: "TAOBAO", ReportCode: "shop_daily_report",
FieldCode: "order_count", FieldName: "订单数",
FieldType: "INT", FieldRole: "INDICATOR",
IsAggregatable: true, DefaultAggregate: "SUM",
ValidAggregates: []string{"SUM", "COUNT", "AVG"},
SortOrder: 10, GroupName: "订单", Operator: "admin",
})
svc.SaveField(ctx, &model.SaveFieldReq{
BusinessCode: "TAOBAO", ReportCode: "shop_daily_report",
FieldCode: "order_amount", FieldName: "订单金额",
FieldType: "FLOAT", FieldRole: "INDICATOR",
IsAggregatable: true, DefaultAggregate: "SUM",
ValidAggregates: []string{"SUM", "AVG", "MAX", "MIN"},
SortOrder: 11, GroupName: "金额", Unit: "元", Operator: "admin",
})
svc.SaveField(ctx, &model.SaveFieldReq{
BusinessCode: "TAOBAO", ReportCode: "shop_daily_report",
FieldCode: "buyer_count", FieldName: "下单买家数",
FieldType: "INT", FieldRole: "INDICATOR",
IsAggregatable: true, DefaultAggregate: "COUNT",
ValidAggregates: []string{"COUNT"},
SortOrder: 12, GroupName: "用户", Operator: "admin",
})
// 5. 配置抽取规则
svc.SaveExtractConfig(ctx, &model.SaveExtractConfigReq{
BusinessCode: "TAOBAO", ReportCode: "shop_daily_report",
ExtractCode: "extract_daily", ExtractName: "淘宝按天聚合",
SourceTableName: "taobao_order_list", SourceTableAlias: "o",
TargetTableName: "stat_taobao_shop_daily",
ExtractMode: "AGGREGATE",
ExtractKeyField: "created_at",
GroupByFields: []string{"shop_id"},
FieldMappings: []model.FieldMapping{
{SourceField: "shop_id", TargetField: "shop_id", FieldType: "STRING"},
{SourceField: "shop_name", TargetField: "shop_name", FieldType: "STRING"},
{SourceField: "id", TargetField: "order_count", FieldType: "INT", AggregateFunction: "COUNT"},
{SourceField: "order_amount", TargetField: "order_amount", FieldType: "FLOAT", AggregateFunction: "SUM"},
{SourceField: "buyer_id", TargetField: "buyer_count", FieldType: "INT", AggregateFunction: "COUNT"},
},
BatchSize: 1000,
Operator: "admin",
})
// 6. 定时任务每天执行
svc.ExtractDailyData(ctx, "TAOBAO", "shop_daily_report", "2026-06-10", "cron")
// 7. 前端实时查询
req := &model.UserSelectQueryReq{
BusinessCode: "TAOBAO", ReportCode: "shop_daily_report",
Dimensions: []string{"shop_id", "shop_name"},
Indicators: []model.IndicatorSelect{
{FieldCode: "order_amount", Aggregate: "SUM", Alias: "total"},
{FieldCode: "order_count", Aggregate: "SUM", Alias: "orders"},
},
TimeRange: &model.TimeRange{StartDate: "2026-06-01", EndDate: "2026-06-10"},
Page: 1, PageSize: 20,
}
resp, _ := svc.QueryReportByUserSelect(ctx, req)
fmt.Printf("查询结果: 共%d条, 耗时%dms\n", resp.Total, resp.ExecTimeMs)
// 平台接入完毕。全程零代码改动。
}
// ============================================================================
// 场景四:在外部业务服务(如 goview-report中调用
// ============================================================================
func ExternalServiceExample() {
ctx := context.Background()
svc := report.GetService()
// 直接用,不需要初始化,内部自动懒加载和建表。
today := time.Now().Format("2006-01-02")
// 按天抽取
svc.ExtractDailyData(ctx, "KUAISHOU", "shop_daily_report", today, "cron")
// 实时查询
svc.QueryReportByUserSelect(ctx, &model.UserSelectQueryReq{
BusinessCode: "KUAISHOU", ReportCode: "shop_daily_report",
Dimensions: []string{"shop_id", "shop_name"},
Indicators: []model.IndicatorSelect{
{FieldCode: "order_amount", Aggregate: "SUM", Alias: "total_amount"},
},
TimeRange: &model.TimeRange{StartDate: "2026-06-01", EndDate: today},
Page: 1, PageSize: 20,
})
}
// ============================================================================
// 场景五Direct 模式(逐行抽取,不做聚合)
// ============================================================================
//
// 适用于源表已经是一行一条统计记录的场景(如已预聚合的报表源表)。
func DirectModeExample() {
ctx := context.Background()
svc := report.GetService()
svc.SaveBusiness(ctx, &model.SaveBusinessReq{
BusinessCode: "HDWL", BusinessName: "HDWL业务", Operator: "admin",
})
svc.SaveReport(ctx, &model.SaveReportReq{
BusinessCode: "HDWL", ReportCode: "shop_daily_stat",
ReportName: "HDWL店铺日报", StatTableName: "stat_hdwl_shop_daily",
ConflictKeys: []string{"report_date"},
Operator: "admin",
})
// Direct 模式:不聚合,逐行映射
svc.SaveExtractConfig(ctx, &model.SaveExtractConfigReq{
BusinessCode: "HDWL", ReportCode: "shop_daily_stat",
ExtractCode: "extract_direct", ExtractName: "直接逐行抽取",
SourceTableName: "hdwl_daily_summary", SourceTableAlias: "s",
TargetTableName: "stat_hdwl_shop_daily",
ExtractMode: "DIRECT", // ← DIRECT 模式
ExtractKeyField: "report_date",
FieldMappings: []model.FieldMapping{
{SourceField: "shop_id", TargetField: "shop_id", FieldType: "STRING"},
{SourceField: "shop_name", TargetField: "shop_name", FieldType: "STRING"},
{SourceField: "total_sale", TargetField: "sale_amount", FieldType: "FLOAT"},
{SourceField: "total_orders", TargetField: "order_count", FieldType: "INT"},
},
Operator: "admin",
})
svc.ExtractDailyData(ctx, "HDWL", "shop_daily_stat", "2026-06-10", "cron")
}
// ============================================================================
// 场景六:前端交互流程参考
// ============================================================================
//
// 前端代码调用示例(伪代码,展示 API 调用顺序):
//
// // 1. 页面加载 → 获取业务列表 → 渲染下拉框
// GET /api/report/businesses
// 返回: [{ businessCode: "KUAISHOU", businessName: "快手电商" }, ...]
//
// // 2. 用户选择业务 → 获取报表列表
// GET /api/report/reports?businessCode=KUAISHOU
// 返回: [{ reportCode: "shop_daily_report", reportName: "快手店铺日报" }, ...]
//
// // 3. 用户选择报表 → 获取可用字段 → 渲染维度/指标/筛选选择器
// GET /api/report/fields?businessCode=KUAISHOU&reportCode=shop_daily_report
// 返回: { dimensions: [...], indicators: [...], filters: [...] }
//
// // 4. 用户选好条件 → 查询
// POST /api/report/query
// Body: {
// "businessCode": "KUAISHOU", "reportCode": "shop_daily_report",
// "dimensions": ["shop_id", "shop_name"],
// "indicators": [
// {"fieldCode": "order_amount", "aggregate": "SUM", "alias": "total"},
// {"fieldCode": "order_count", "aggregate": "SUM", "alias": "count"}
// ],
// "timeRange": {"startDate": "2026-06-01", "endDate": "2026-06-10"},
// "orderBy": [{"fieldCode": "total", "direction": "DESC"}],
// "page": 1, "pageSize": 20
// }
// 返回: { list: [...], total: 152, page: 1, totalPages: 8, execTimeMs: 45 }
//
// // 5. 翻页/换维度/换指标 → 重新调 Step 4
//
// 管理后台(用户可自行维护配置):
//
// // 新增业务
// POST /api/report/business/save
// Body: { "businessCode": "TAOBAO", "businessName": "淘宝电商" }
//
// // 新增/修改字段(用户自定义统计维度)
// POST /api/report/field/save
// Body: { "businessCode": "TAOBAO", "reportCode": "shop_daily_report",
// "fieldCode": "category", "fieldName": "商品类目",
// "fieldType": "STRING", "fieldRole": "DIMENSION" }
//
// // 配置抽取规则
// POST /api/report/extractConfig/save
// Body: { "businessCode": "TAOBAO", "reportCode": "shop_daily_report",
// "extractCode": "extract_daily", "sourceTableName": "taobao_order_list",
// "extractMode": "AGGREGATE", "groupByFields": ["shop_id"],
// "fieldMappings": [...] }
// ============================================================================
// Direct vs AGGREGATE 模式对比
// ============================================================================
//
// ┌──────────┬──────────────────────────────────────────────┐
// │ 模式 │ 行为 │
// ├──────────┼──────────────────────────────────────────────┤
// │ DIRECT │ 逐行映射1:1 从源表复制到目标表 │
// │ │ 适用:源表已是一行一统计 │
// │ │ SQL: SELECT ... FROM source │
// ├──────────┼──────────────────────────────────────────────┤
// │ AGGREGATE│ GROUP BY + 聚合函数N:1 聚合 │
// │ │ 适用:源表是明细表(如订单行) │
// │ │ SQL: SELECT ... SUM() COUNT() ... │
// │ │ FROM source GROUP BY groupByFields │
// └──────────┴──────────────────────────────────────────────┘
//
// 可聚合函数: SUM / COUNT / AVG / MAX / MIN
// 字段角色: DIMENSION(维度) / INDICATOR(指标) / FILTER(筛选) / FILTER_ONLY
// 字段类型: STRING / INT / FLOAT / DATE / DATETIME / JSONB
// 操作符: = / != / > / < / >= / <= / IN / LIKE / BETWEEN
// 时间分组: day / week / month / quarter
*/

View File

@@ -0,0 +1,192 @@
package executor
import (
"context"
"fmt"
"math"
"strings"
"time"
"dataengine/common/report/builder"
"dataengine/common/report/model"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/sirupsen/logrus"
)
// QueryExecutor SQL执行器
type QueryExecutor struct {
sqlBuilder *builder.SQLBuilder
}
// NewQueryExecutor 创建查询执行器
func NewQueryExecutor() *QueryExecutor {
return &QueryExecutor{
sqlBuilder: builder.NewSQLBuilder(),
}
}
// QueryReportByUserSelect 根据用户选择实时查询报表数据(核心接口)
func (e *QueryExecutor) QueryReportByUserSelect(ctx context.Context, req *model.UserSelectQueryReq) (*model.UserSelectQueryResp, error) {
start := time.Now()
logger := logrus.WithFields(logrus.Fields{
"businessCode": req.BusinessCode,
"reportCode": req.ReportCode,
})
// 1. 参数校验
if err := e.validateReq(req); err != nil {
return nil, fmt.Errorf("参数校验失败: %w", err)
}
// 2. 构建查询SQL
querySQL, queryArgs, metadata, err := e.sqlBuilder.BuildQuerySQL(ctx, req)
if err != nil {
return nil, fmt.Errorf("构建查询SQL失败: %w", err)
}
logger.Debugf("查询SQL: %s args: %v", querySQL, queryArgs)
// 3. 获取记录总数
total, err := e.executeCount(ctx, metadata, queryArgs)
if err != nil {
return nil, fmt.Errorf("查询总数失败: %w", err)
}
// 4. 分页查询
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 {
req.PageSize = 20
}
if req.PageSize > 1000 {
req.PageSize = 1000
}
pagedSQL := e.sqlBuilder.AddLimit(querySQL, req.Page, req.PageSize)
logger.Debugf("分页SQL: %s", pagedSQL)
dataResult, err := gfdb.DB(ctx).GetAll(ctx, pagedSQL, queryArgs...)
if err != nil {
return nil, fmt.Errorf("查询数据失败: %w", err)
}
var list []map[string]interface{}
if dataResult.Len() > 0 {
list = dataResult.List()
}
// 计算总页数
totalPages := int(math.Ceil(float64(total) / float64(req.PageSize)))
execTime := time.Since(start).Milliseconds()
resp := &model.UserSelectQueryResp{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
Sql: querySQL,
ExecTimeMs: execTime,
}
logger.Infof("报表查询完成, 总数:%d 耗时:%dms", total, execTime)
return resp, nil
}
// executeCount 执行总数统计
func (e *QueryExecutor) executeCount(ctx context.Context, metadata map[string]interface{}, args []interface{}) (int64, error) {
countSql, ok := metadata["countSql"].(string)
if !ok || countSql == "" {
return 0, nil
}
result, err := gfdb.DB(ctx).GetAll(ctx, countSql, args...)
if err != nil {
return 0, fmt.Errorf("统计总数失败: %w", err)
}
var total int64
if result.Len() > 0 {
for k, v := range result.List()[0] {
_ = k
if n, ok := v.(int64); ok {
total = n
} else {
total = result[0]["count"].Int64()
}
break
}
}
return total, nil
}
// validateReq 校验请求参数
func (e *QueryExecutor) validateReq(req *model.UserSelectQueryReq) error {
if req.BusinessCode == "" {
return fmt.Errorf("业务编码不能为空")
}
if req.ReportCode == "" {
return fmt.Errorf("报表编码不能为空")
}
if len(req.Indicators) == 0 {
return fmt.Errorf("必须选择至少一个指标")
}
// 校验指标
for i, ind := range req.Indicators {
if ind.FieldCode == "" {
return fmt.Errorf("第%d个指标字段编码不能为空", i+1)
}
if ind.Aggregate != "" {
agg := strings.ToUpper(ind.Aggregate)
validAggs := map[string]bool{
"SUM": true,
"COUNT": true,
"AVG": true,
"MAX": true,
"MIN": true,
}
if !validAggs[agg] {
return fmt.Errorf("不支持的聚合方式: %s", ind.Aggregate)
}
}
}
// 校验时间分组
if req.TimeGroup != "" {
validGroups := map[string]bool{
"day": true,
"week": true,
"month": true,
"quarter": true,
}
if !validGroups[req.TimeGroup] {
return fmt.Errorf("不支持的时间分组: %s", req.TimeGroup)
}
}
return nil
}
// RawQuery 执行原始SQL查询用于特殊场景
func (e *QueryExecutor) RawQuery(ctx context.Context, sql string, args ...interface{}) ([]map[string]interface{}, error) {
result, err := gfdb.DB(ctx).GetAll(ctx, sql, args...)
if err != nil {
return nil, fmt.Errorf("原始查询失败: %w", err)
}
if result.Len() > 0 {
return result.List(), nil
}
return nil, nil
}
// ExecRaw 执行原始SQLINSERT/UPDATE/DELETE
func (e *QueryExecutor) ExecRaw(ctx context.Context, sql string, args ...interface{}) error {
_, err := gfdb.DB(ctx).Exec(ctx, sql, args...)
return err
}

View File

@@ -0,0 +1,644 @@
package extract
import (
"context"
"fmt"
"strings"
"time"
"dataengine/common/report/config"
"dataengine/common/report/model"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/util/gconv"
"github.com/sirupsen/logrus"
)
// DailyExtractor 天级数据抽取器
type DailyExtractor struct {
loader *config.ConfigLoader
}
// NewDailyExtractor 创建抽取器
func NewDailyExtractor() *DailyExtractor {
return &DailyExtractor{
loader: config.GetLoader(),
}
}
// ExtractDailyData 按天抽取数据(业务层定时任务调用)
func (e *DailyExtractor) ExtractDailyData(ctx context.Context, businessCode, reportCode, statDate, executor string) (*model.ExtractDailyDataResp, error) {
start := time.Now()
logger := logrus.WithFields(logrus.Fields{
"businessCode": businessCode,
"reportCode": reportCode,
"statDate": statDate,
})
// 1. 获取报表配置
report, err := e.loader.GetReport(ctx, businessCode, reportCode)
if err != nil {
return nil, fmt.Errorf("获取报表配置失败: %w", err)
}
// 2. 获取抽取配置
extractConfigs, err := e.loader.GetExtractConfigs(ctx, businessCode, reportCode)
if err != nil {
return nil, fmt.Errorf("获取抽取配置失败: %w", err)
}
if len(extractConfigs) == 0 {
return nil, fmt.Errorf("没有可用的抽取配置")
}
// 3. 获取字段配置
fieldMap, err := e.loader.GetFieldMap(ctx, businessCode, reportCode)
if err != nil {
return nil, fmt.Errorf("获取字段配置失败: %w", err)
}
// 4. 确保统计宽表存在
if err := e.ensureStatTableExists(ctx, report, fieldMap); err != nil {
return nil, fmt.Errorf("确保统计宽表存在失败: %w", err)
}
totalCount := 0
successCount := 0
failCount := 0
var lastErr error
// 5. 遍历每个抽取配置
for _, ec := range extractConfigs {
// 检查幂等性
exLog, err := e.loader.GetExtractLog(ctx, businessCode, reportCode, ec.ExtractCode, statDate)
if err != nil {
logger.Errorf("获取抽取记录失败: %v", err)
}
if exLog != nil && exLog.Status == model.ExtractStatusSuccess {
logger.Infof("抽取配置 %s 日期 %s 已完成,跳过", ec.ExtractCode, statDate)
continue
}
// 创建抽取记录
extractLog := &model.ExtractLog{
BusinessCode: businessCode,
ReportCode: reportCode,
ExtractCode: ec.ExtractCode,
StatDate: statDate,
ExtractType: ec.ExtractType,
Status: model.ExtractStatusRunning,
Executor: executor,
StartTime: &start,
}
_ = e.loader.CreateExtractLog(ctx, extractLog)
// 执行抽取
c, s, f, err := e.executeExtract(ctx, &ec, report, fieldMap, statDate)
totalCount += c
successCount += s
failCount += f
// 更新抽取记录
now := time.Now()
extractLog.EndTime = &now
extractLog.TotalCount = c
extractLog.SuccessCount = s
extractLog.FailCount = f
if err != nil {
extractLog.Status = model.ExtractStatusFailed
extractLog.ErrorMessage = err.Error()
lastErr = err
logger.Errorf("抽取配置 %s 执行失败: %v", ec.ExtractCode, err)
} else {
extractLog.Status = model.ExtractStatusSuccess
logger.Infof("抽取配置 %s 完成, 总数:%d 成功:%d 失败:%d", ec.ExtractCode, c, s, f)
}
if updateErr := e.loader.UpdateExtractLog(ctx, extractLog); updateErr != nil {
logger.Errorf("更新抽取记录失败: %v", updateErr)
}
}
execTime := time.Since(start).Milliseconds()
logger.Infof("按天抽取完成, 总数:%d 成功:%d 失败:%d 耗时:%dms", totalCount, successCount, failCount, execTime)
resp := &model.ExtractDailyDataResp{
Success: lastErr == nil,
TotalCount: totalCount,
SuccessCount: successCount,
FailCount: failCount,
ExecTimeMs: execTime,
}
if lastErr != nil {
resp.ErrorMsg = lastErr.Error()
}
return resp, nil
}
// executeExtract 执行单个抽取配置
func (e *DailyExtractor) executeExtract(ctx context.Context, ec *model.ExtractConfig, report *model.ReportConfig, fieldMap map[string]*model.FieldConfig, statDate string) (total, success, fail int, err error) {
logger := logrus.WithField("extractCode", ec.ExtractCode)
// 1. 构建抽取SQL
extractSQL, whereArgs, err := e.buildExtractSQL(ctx, ec, report, statDate)
if err != nil {
return 0, 0, 0, fmt.Errorf("构建抽取SQL失败: %w", err)
}
logger.Debugf("抽取SQL: %s", extractSQL)
// 2. 分批抽取
batchSize := ec.BatchSize
if batchSize <= 0 {
batchSize = 1000
}
offset := 0
for {
// 添加分页
pagedSQL := fmt.Sprintf("%s LIMIT %d OFFSET %d", extractSQL, batchSize, offset)
args := append(whereArgs)
rows, queryErr := gfdb.DB(ctx).GetAll(ctx, pagedSQL, args...)
if queryErr != nil {
return total, success, fail, fmt.Errorf("抽取查询失败: %w", queryErr)
}
batchCount := rows.Len()
if batchCount == 0 {
break
}
// 3. 应用转换规则(仅 DIRECT 模式需注入审计字段AGGREGATE 模式已由SQL处理
dataList := rows.List()
if ec.ExtractMode != model.ExtractModeAggregate {
for i := range dataList {
e.applyTransformRules(ec, dataList[i])
dataList[i]["tenant_id"] = 1
dataList[i]["business_code"] = ec.BusinessCode
}
}
// 4. 写入统计宽表
c, _, writeErr := e.batchUpsert(ctx, report.StatTableName, report.ConflictKeys, dataList)
if writeErr != nil {
logger.Errorf("批量写入失败 (offset=%d): %v", offset, writeErr)
fail += batchCount
} else {
success += c
}
total += batchCount
offset += batchSize
if batchCount < batchSize {
break
}
}
return total, success, fail, nil
}
// buildExtractSQL 构建抽取SQL
func (e *DailyExtractor) buildExtractSQL(ctx context.Context, ec *model.ExtractConfig, report *model.ReportConfig, statDate string) (string, []interface{}, error) {
var args []interface{}
sourceTable := ec.SourceTableName
if ec.SourceTableAlias != "" {
sourceTable = ec.SourceTableAlias
} else {
sourceTable = "s"
}
// 日期字段
dateField := report.DateField
if dateField == "" {
dateField = "stat_date"
}
// 判断抽取模式
mode := ec.ExtractMode
if mode == "" {
mode = model.ExtractModeDirect
}
if mode == model.ExtractModeAggregate {
return e.buildAggregateExtractSQL(ec, report, sourceTable, dateField, statDate)
}
// === 默认 DIRECT 模式:逐行抽取 ===
return e.buildDirectExtractSQL(ec, report, sourceTable, dateField, statDate), args, nil
}
// buildDirectExtractSQL 逐行抽取模式SQL直接映射不做聚合
func (e *DailyExtractor) buildDirectExtractSQL(ec *model.ExtractConfig, report *model.ReportConfig, sourceTable, dateField, statDate string) string {
var selectParts []string
// 基础审计字段(常量注入)
selectParts = append(selectParts, "0 AS id")
selectParts = append(selectParts, "1 AS tenant_id")
selectParts = append(selectParts, fmt.Sprintf("'%s' AS business_code", ec.BusinessCode))
selectParts = append(selectParts, "'system' AS creator")
selectParts = append(selectParts, "NOW() AS created_at")
selectParts = append(selectParts, "'system' AS updater")
selectParts = append(selectParts, "NOW() AS updated_at")
selectParts = append(selectParts, "NULL::TIMESTAMP AS deleted_at")
// 日期字段
selectParts = append(selectParts, fmt.Sprintf("'%s' AS %s", statDate, dateField))
// 原始数据
selectParts = append(selectParts, "'{}'::JSONB AS raw_data")
// 字段映射
for _, mapping := range ec.FieldMappings {
targetField := mapping.TargetField
sourceField := mapping.SourceField
var expr string
if mapping.TransformRule != nil {
expr = e.applyTransformExpr(mapping.TransformRule, fmt.Sprintf("%s.%s", sourceTable, sourceField))
} else {
expr = fmt.Sprintf("%s.%s", sourceTable, sourceField)
}
if mapping.DefaultValue != nil {
expr = fmt.Sprintf("COALESCE(%s, '%v')", expr, mapping.DefaultValue)
}
selectParts = append(selectParts, fmt.Sprintf("%s AS %s", expr, targetField))
}
// FROM + JOIN
fromClause := e.buildFromClause(ec, sourceTable)
// JOIN 字段映射
selectParts = append(selectParts, e.buildJoinFieldSelects(ec)...)
// WHERE
whereClause := e.buildWhereClause(ec, sourceTable, statDate)
return fmt.Sprintf("SELECT %s FROM %s %s", strings.Join(selectParts, ", "), fromClause, whereClause)
}
// buildAggregateExtractSQL 聚合抽取模式SQLGROUP BY + SUM/COUNT/AVG
func (e *DailyExtractor) buildAggregateExtractSQL(ec *model.ExtractConfig, report *model.ReportConfig, sourceTable, dateField, statDate string) (string, []interface{}, error) {
var selectParts []string
var groupByParts []string
var args []interface{}
// 基础审计字段(聚合模式下用常量)
selectParts = append(selectParts, "ROW_NUMBER() OVER () AS id") // 伪自增ID
selectParts = append(selectParts, "1 AS tenant_id")
selectParts = append(selectParts, fmt.Sprintf("'%s' AS business_code", ec.BusinessCode))
selectParts = append(selectParts, "'system' AS creator")
selectParts = append(selectParts, "NOW() AS created_at")
selectParts = append(selectParts, "'system' AS updater")
selectParts = append(selectParts, "NOW() AS updated_at")
selectParts = append(selectParts, "NULL::TIMESTAMP AS deleted_at")
// 日期字段(常量)
selectParts = append(selectParts, fmt.Sprintf("'%s' AS %s", statDate, dateField))
// 原始数据
selectParts = append(selectParts, "'{}'::JSONB AS raw_data")
// GroupByFields 集合(快速查找)
gbySet := make(map[string]bool)
for _, gbf := range ec.GroupByFields {
gbySet[gbf] = true
}
// 添加 GroupBy 字段到 SELECT 和 GROUP BY
for _, gbf := range ec.GroupByFields {
selectParts = append(selectParts, fmt.Sprintf("%s.%s", sourceTable, gbf))
groupByParts = append(groupByParts, fmt.Sprintf("%s.%s", sourceTable, gbf))
}
// 字段映射:根据 AggregateFunction 决定聚合方式
for _, mapping := range ec.FieldMappings {
targetField := mapping.TargetField
sourceField := mapping.SourceField
// 构建源表达式
var sourceExpr string
if mapping.TransformRule != nil {
sourceExpr = e.applyTransformExpr(mapping.TransformRule, fmt.Sprintf("%s.%s", sourceTable, sourceField))
} else {
sourceExpr = fmt.Sprintf("%s.%s", sourceTable, sourceField)
}
// 判断是否需要聚合
aggFunc := strings.ToUpper(mapping.AggregateFunction)
if aggFunc != "" && !gbySet[sourceField] {
// 聚合字段SUM(s.xxx) / COUNT(s.xxx) / AVG(s.xxx)
expr := fmt.Sprintf("%s(%s)", aggFunc, sourceExpr)
if mapping.DefaultValue != nil {
expr = fmt.Sprintf("COALESCE(%s, %v)", expr, mapping.DefaultValue)
}
selectParts = append(selectParts, fmt.Sprintf("%s AS %s", expr, targetField))
} else if gbySet[sourceField] {
// GroupBy 字段不需要重复加入 SELECT已通过 groupByFields 处理)
continue
} else {
// 非聚合字段,也未在 GroupBy 中 → 用 MAX/MIN 取值(兼容 PG only_full_group_by
expr := fmt.Sprintf("MAX(%s)", sourceExpr)
if mapping.DefaultValue != nil {
expr = fmt.Sprintf("COALESCE(%s, %v)", expr, mapping.DefaultValue)
}
selectParts = append(selectParts, fmt.Sprintf("%s AS %s", expr, targetField))
}
}
// FROM + JOIN
fromClause := e.buildFromClause(ec, sourceTable)
// WHERE
whereClause := e.buildWhereClause(ec, sourceTable, statDate)
// 组合 SQL
sql := fmt.Sprintf("SELECT %s FROM %s %s",
strings.Join(selectParts, ", "),
fromClause,
whereClause)
// GROUP BY
if len(groupByParts) > 0 {
sql += " GROUP BY " + strings.Join(groupByParts, ", ")
}
return sql, args, nil
}
// buildFromClause 构建FROM + JOIN子句
func (e *DailyExtractor) buildFromClause(ec *model.ExtractConfig, sourceTable string) string {
fromClause := fmt.Sprintf("%s %s", ec.SourceTableName, sourceTable)
for _, join := range ec.JoinConfigs {
joinType := "LEFT JOIN"
jType := strings.ToUpper(join.JoinType)
if jType == "INNER" {
joinType = "INNER JOIN"
} else if jType == "RIGHT" {
joinType = "RIGHT JOIN"
}
joinAlias := join.JoinAlias
if joinAlias == "" {
joinAlias = join.JoinTable
}
fromClause += fmt.Sprintf(" %s %s %s ON %s", joinType, join.JoinTable, joinAlias, join.JoinCondition)
}
return fromClause
}
// buildJoinFieldSelects 构建JOIN表的字段映射SELECT部分
func (e *DailyExtractor) buildJoinFieldSelects(ec *model.ExtractConfig) []string {
var parts []string
for _, join := range ec.JoinConfigs {
joinAlias := join.JoinAlias
if joinAlias == "" {
joinAlias = join.JoinTable
}
for _, jm := range join.FieldMappings {
targetField := jm.TargetField
sourceExpr := fmt.Sprintf("%s.%s", joinAlias, jm.SourceField)
if jm.TransformRule != nil {
sourceExpr = e.applyTransformExpr(jm.TransformRule, sourceExpr)
}
parts = append(parts, fmt.Sprintf("%s AS %s", sourceExpr, targetField))
}
}
return parts
}
// buildWhereClause 构建WHERE子句
func (e *DailyExtractor) buildWhereClause(ec *model.ExtractConfig, sourceTable, statDate string) string {
var whereConditions []string
// 日期范围(增量抽取)
if ec.ExtractType == model.ExtractTypeIncremental && ec.ExtractKeyField != "" {
dateCondition := fmt.Sprintf("%s.%s::date = '%s'", sourceTable, ec.ExtractKeyField, statDate)
whereConditions = append(whereConditions, dateCondition)
}
// 自定义过滤条件
if ec.FilterExpression != "" {
whereConditions = append(whereConditions, ec.FilterExpression)
}
if len(whereConditions) == 0 {
return ""
}
return "WHERE " + strings.Join(whereConditions, " AND ")
}
// applyTransformExpr 应用转换表达式
func (e *DailyExtractor) applyTransformExpr(rule *model.TransformRule, sourceExpr string) string {
switch rule.RuleType {
case "CALCULATE":
if rule.Expression != "" {
return strings.ReplaceAll(rule.Expression, "{source}", sourceExpr)
}
case "FORMAT":
if rule.Format != "" {
return fmt.Sprintf("TO_CHAR(%s, '%s')", sourceExpr, rule.Format)
}
case "MAPPING":
// 在代码中运行时做映射
return sourceExpr
}
return sourceExpr
}
// applyTransformRules 应用运行时转换规则(映射等代码转换)
func (e *DailyExtractor) applyTransformRules(ec *model.ExtractConfig, row map[string]interface{}) {
for _, rule := range ec.TransformRules {
if rule.RuleType != "MAPPING" {
continue
}
sourceField := rule.Expression // 存储源字段名
targetField := rule.RuleCode // 存储目标字段名
if sourceVal, ok := row[sourceField]; ok {
strVal := gconv.String(sourceVal)
if mapped, exists := rule.Mapping[strVal]; exists {
row[targetField] = mapped
}
}
}
}
// ensureStatTableExists 确保统计宽表存在
func (e *DailyExtractor) ensureStatTableExists(ctx context.Context, report *model.ReportConfig, fieldMap map[string]*model.FieldConfig) error {
tableName := report.StatTableName
// 检查表是否存在
result, err := gfdb.DB(ctx).GetAll(ctx, "SELECT COUNT(*) FROM pg_tables WHERE tablename = $1", strings.ToLower(tableName))
if err != nil {
return err
}
count := 0
if len(result) > 0 {
count = result[0]["count"].Int()
}
if count == 0 {
// 需要建表
return e.createStatTable(ctx, report, fieldMap)
}
logrus.Infof("统计宽表 %s 已存在", tableName)
return nil
}
// createStatTable 创建统计宽表
func (e *DailyExtractor) createStatTable(ctx context.Context, report *model.ReportConfig, fieldMap map[string]*model.FieldConfig) error {
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 _, fc := range fieldMap {
fc := fc
colType := fieldTypeToPG(fc.FieldType)
cols = append(cols, fmt.Sprintf("%s %s", fc.FieldCode, colType))
}
// 原始数据
cols = append(cols, "raw_data JSONB DEFAULT '{}'")
tableName := report.StatTableName
sql := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n %s\n)", tableName, strings.Join(cols, ",\n "))
logrus.Infof("创建统计宽表: %s", tableName)
if _, err := gfdb.DB(ctx).Exec(ctx, sql); err != nil {
return fmt.Errorf("建表失败: %w", err)
}
// 冲突唯一索引
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 {
logrus.Warnf("创建冲突索引失败: %v", err)
}
}
// 字段注释
for _, fc := range fieldMap {
fc := fc
if fc.FieldName != "" {
escaped := strings.ReplaceAll(fc.FieldName, "'", "''")
commentSQL := fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s'", tableName, fc.FieldCode, escaped)
if _, err := gfdb.DB(ctx).Exec(ctx, commentSQL); err != nil {
logrus.Warnf("添加字段注释失败 [%s.%s]: %v", tableName, fc.FieldCode, err)
}
}
}
return nil
}
// batchUpsert 批量upsert写入
func (e *DailyExtractor) batchUpsert(ctx context.Context, tableName string, conflictKeys []string, rows []map[string]interface{}) (int, []string, error) {
if len(rows) == 0 {
return 0, nil, nil
}
now := time.Now()
for i := range rows {
if rows[i] == nil {
rows[i] = make(map[string]interface{})
}
rows[i]["updated_at"] = now
}
batchSize := 100
total := 0
var allColumns []string
for i := 0; i < len(rows); i += batchSize {
end := i + batchSize
if end > len(rows) {
end = len(rows)
}
batch := rows[i:end]
m := gfdb.DB(ctx).Model(ctx, tableName).Data(batch)
if len(conflictKeys) > 0 {
keys := make([]interface{}, len(conflictKeys))
for j, k := range conflictKeys {
keys[j] = k
}
m = m.OnConflict(keys...)
}
_, err := m.Save()
if err != nil {
logrus.Errorf("批量写入 %s 失败: %v", tableName, err)
// 逐条重试
for _, row := range batch {
mm := gfdb.DB(ctx).Model(ctx, tableName).Data(row)
if len(conflictKeys) > 0 {
keys := make([]interface{}, len(conflictKeys))
for j, k := range conflictKeys {
keys[j] = k
}
mm = mm.OnConflict(keys...)
}
if _, e := mm.Save(); e != nil {
logrus.Errorf("逐条写入失败: %v", e)
} else {
total++
}
}
} else {
total += len(batch)
}
}
return total, allColumns, nil
}
// fieldTypeToPG 字段类型转PG类型
func fieldTypeToPG(fieldType string) string {
switch fieldType {
case model.FieldTypeInt:
return "NUMERIC(20,0) DEFAULT 0"
case model.FieldTypeFloat:
return "NUMERIC(20,4) DEFAULT 0"
case model.FieldTypeDate:
return "VARCHAR(16) DEFAULT ''"
case model.FieldTypeDatetime:
return "TIMESTAMP WITH TIME ZONE"
case model.FieldTypeJsonb:
return "JSONB DEFAULT '{}'"
default:
return "VARCHAR(256) DEFAULT ''"
}
}

View File

@@ -0,0 +1,424 @@
package model
import (
"time"
)
// ============================================================
// 实体定义
// ============================================================
// BusinessConfig 业务配置
type BusinessConfig struct {
ID int64 `orm:"id" json:"id"`
TenantId uint64 `orm:"tenant_id" json:"tenant_id"`
BusinessCode string `orm:"business_code" json:"businessCode"`
BusinessName string `orm:"business_name" json:"businessName"`
Description string `orm:"description" json:"description"`
Status string `orm:"status" json:"status"`
Config map[string]interface{} `orm:"config" json:"config"`
Creator string `orm:"creator" json:"creator"`
CreatedAt *time.Time `orm:"created_at" json:"createdAt"`
Updater string `orm:"updater" json:"updater"`
UpdatedAt *time.Time `orm:"updated_at" json:"updatedAt"`
DeletedAt *time.Time `orm:"deleted_at" json:"deletedAt"`
}
// ReportConfig 报表配置
type ReportConfig struct {
ID int64 `orm:"id" json:"id"`
TenantId uint64 `orm:"tenant_id" json:"tenant_id"`
BusinessCode string `orm:"business_code" json:"businessCode"`
ReportCode string `orm:"report_code" json:"reportCode"`
ReportName string `orm:"report_name" json:"reportName"`
Description string `orm:"description" json:"description"`
Status string `orm:"status" json:"status"`
StatTableName string `orm:"stat_table_name" json:"statTableName"`
StatTableComment string `orm:"stat_table_comment" json:"statTableComment"`
DateField string `orm:"date_field" json:"dateField"`
PrimaryKeys []string `orm:"primary_keys" json:"primaryKeys"`
ConflictKeys []string `orm:"conflict_keys" json:"conflictKeys"`
Config map[string]interface{} `orm:"config" json:"config"`
Creator string `orm:"creator" json:"creator"`
CreatedAt *time.Time `orm:"created_at" json:"createdAt"`
Updater string `orm:"updater" json:"updater"`
UpdatedAt *time.Time `orm:"updated_at" json:"updatedAt"`
DeletedAt *time.Time `orm:"deleted_at" json:"deletedAt"`
}
// FieldConfig 字段配置
type FieldConfig struct {
ID int64 `orm:"id" json:"id"`
TenantId uint64 `orm:"tenant_id" json:"tenant_id"`
BusinessCode string `orm:"business_code" json:"businessCode"`
ReportCode string `orm:"report_code" json:"reportCode"`
FieldCode string `orm:"field_code" json:"fieldCode"`
FieldName string `orm:"field_name" json:"fieldName"`
FieldType string `orm:"field_type" json:"fieldType"`
DataType string `orm:"data_type" json:"dataType"`
FieldRole string `orm:"field_role" json:"fieldRole"`
IsAggregatable bool `orm:"is_aggregatable" json:"isAggregatable"`
IsFilterable bool `orm:"is_filterable" json:"isFilterable"`
IsQueryable bool `orm:"is_queryable" json:"isQueryable"`
IsSortable bool `orm:"is_sortable" json:"isSortable"`
DefaultAggregate string `orm:"default_aggregate" json:"defaultAggregate"`
ValidAggregates []string `orm:"valid_aggregates" json:"validAggregates"`
FilterOperators []string `orm:"filter_operators" json:"filterOperators"`
Expression string `orm:"expression" json:"expression"`
ExpressionType string `orm:"expression_type" json:"expressionType"`
FormatPattern string `orm:"format_pattern" json:"formatPattern"`
Unit string `orm:"unit" json:"unit"`
DictCode string `orm:"dict_code" json:"dictCode"`
SortOrder int `orm:"sort_order" json:"sortOrder"`
GroupName string `orm:"group_name" json:"groupName"`
Status string `orm:"status" json:"status"`
Creator string `orm:"creator" json:"creator"`
CreatedAt *time.Time `orm:"created_at" json:"createdAt"`
Updater string `orm:"updater" json:"updater"`
UpdatedAt *time.Time `orm:"updated_at" json:"updatedAt"`
DeletedAt *time.Time `orm:"deleted_at" json:"deletedAt"`
}
// ExtractConfig 抽取配置
type ExtractConfig struct {
ID int64 `orm:"id" json:"id"`
TenantId uint64 `orm:"tenant_id" json:"tenant_id"`
BusinessCode string `orm:"business_code" json:"businessCode"`
ReportCode string `orm:"report_code" json:"reportCode"`
ExtractCode string `orm:"extract_code" json:"extractCode"`
ExtractName string `orm:"extract_name" json:"extractName"`
SourceTableName string `orm:"source_table_name" json:"sourceTableName"`
SourceTableAlias string `orm:"source_table_alias" json:"sourceTableAlias"`
TargetTableName string `orm:"target_table_name" json:"targetTableName"`
IsEnabled bool `orm:"is_enabled" json:"isEnabled"`
ExtractType string `orm:"extract_type" json:"extractType"`
ExtractMode string `orm:"extract_mode" json:"extractMode"`
ExtractKeyField string `orm:"extract_key_field" json:"extractKeyField"`
ExtractKeyFormat string `orm:"extract_key_format" json:"extractKeyFormat"`
GroupByFields []string `orm:"group_by_fields" json:"groupByFields"`
FilterExpression string `orm:"filter_expression" json:"filterExpression"`
JoinConfigs []JoinConfig `orm:"join_configs" json:"joinConfigs"`
FieldMappings []FieldMapping `orm:"field_mappings" json:"fieldMappings"`
TransformRules []TransformRule `orm:"transform_rules" json:"transformRules"`
BatchSize int `orm:"batch_size" json:"batchSize"`
Status string `orm:"status" json:"status"`
Creator string `orm:"creator" json:"creator"`
CreatedAt *time.Time `orm:"created_at" json:"createdAt"`
Updater string `orm:"updater" json:"updater"`
UpdatedAt *time.Time `orm:"updated_at" json:"updatedAt"`
DeletedAt *time.Time `orm:"deleted_at" json:"deletedAt"`
}
// ExtractLog 抽取记录
type ExtractLog struct {
ID int64 `orm:"id" json:"id"`
BusinessCode string `orm:"business_code" json:"businessCode"`
ReportCode string `orm:"report_code" json:"reportCode"`
ExtractCode string `orm:"extract_code" json:"extractCode"`
StatDate string `orm:"stat_date" json:"statDate"`
ExtractType string `orm:"extract_type" json:"extractType"`
Status string `orm:"status" json:"status"`
TotalCount int `orm:"total_count" json:"totalCount"`
SuccessCount int `orm:"success_count" json:"successCount"`
FailCount int `orm:"fail_count" json:"failCount"`
StartTime *time.Time `orm:"start_time" json:"startTime"`
EndTime *time.Time `orm:"end_time" json:"endTime"`
ErrorMessage string `orm:"error_message" json:"errorMessage"`
Executor string `orm:"executor" json:"executor"`
CreatedAt *time.Time `orm:"created_at" json:"createdAt"`
UpdatedAt *time.Time `orm:"updated_at" json:"updatedAt"`
}
// ============================================================
// 辅助结构
// ============================================================
// JoinConfig 关联配置
type JoinConfig struct {
JoinTable string `json:"joinTable"`
JoinAlias string `json:"joinAlias"`
JoinType string `json:"joinType"` // LEFT/RIGHT/INNER
JoinCondition string `json:"joinCondition"`
FieldMappings []FieldMapping `json:"fieldMappings"`
}
// FieldMapping 字段映射
type FieldMapping struct {
SourceField string `json:"sourceField"`
TargetField string `json:"targetField"`
FieldType string `json:"fieldType"`
AggregateFunction string `json:"aggregateFunction"`
DefaultValue interface{} `json:"defaultValue"`
TransformRule *TransformRule `json:"transformRule,omitempty"`
}
// TransformRule 转换规则
type TransformRule struct {
RuleCode string `json:"ruleCode"`
RuleType string `json:"ruleType"` // DIRECT/MAPPING/FORMAT/CALCULATE
Expression string `json:"expression"`
Format string `json:"format"`
Mapping map[string]interface{} `json:"mapping"`
}
// ============================================================
// 前端请求/响应结构体
// ============================================================
// UserSelectQueryReq 用户选择查询请求
type UserSelectQueryReq struct {
BusinessCode string `json:"businessCode" v:"required" dc:"业务编码"`
ReportCode string `json:"reportCode" v:"required" dc:"报表编码"`
Dimensions []string `json:"dimensions" dc:"统计维度列表,如 shop_id/anchor_id/date"`
Indicators []IndicatorSelect `json:"indicators" dc:"统计指标列表(含聚合方式)"`
Filters []FilterCondition `json:"filters" dc:"筛选条件列表"`
TimeRange *TimeRange `json:"timeRange" dc:"时间范围"`
TimeGroup string `json:"timeGroup" dc:"时间分组: day/week/month/quarter"`
OrderBy []OrderCondition `json:"orderBy" dc:"排序条件"`
Page int `json:"page" dc:"页码" d:"1"`
PageSize int `json:"pageSize" dc:"每页条数" d:"20"`
}
// IndicatorSelect 指标选择
type IndicatorSelect struct {
FieldCode string `json:"fieldCode" dc:"字段编码"`
Aggregate string `json:"aggregate" dc:"聚合方式: SUM/COUNT/AVG/MAX/MIN"`
Alias string `json:"alias" dc:"别名"`
}
// FilterCondition 筛选条件
type FilterCondition struct {
FieldCode string `json:"fieldCode" dc:"字段编码"`
Operator string `json:"operator" dc:"操作符: =/!=/>/</>=/<=/IN/LIKE/BETWEEN"`
Value interface{} `json:"value" dc:"值"`
Value2 interface{} `json:"value2" dc:"第二个值(BETWEEN时使用)"`
}
// TimeRange 时间范围
type TimeRange struct {
StartDate string `json:"startDate" dc:"开始日期 yyyy-MM-dd"`
EndDate string `json:"endDate" dc:"结束日期 yyyy-MM-dd"`
}
// OrderCondition 排序条件
type OrderCondition struct {
FieldCode string `json:"fieldCode" dc:"字段编码"`
Direction string `json:"direction" dc:"排序方向: ASC/DESC"`
}
// UserSelectQueryResp 用户选择查询响应
type UserSelectQueryResp struct {
List []map[string]interface{} `json:"list" dc:"数据列表"`
Total int64 `json:"total" dc:"总数"`
Page int `json:"page" dc:"当前页"`
PageSize int `json:"pageSize" dc:"每页条数"`
TotalPages int `json:"totalPages" dc:"总页数"`
Sql string `json:"sql,omitempty" dc:"执行的SQL(调试用)"`
ExecTimeMs int64 `json:"execTimeMs" dc:"执行耗时(毫秒)"`
}
// ExtractDailyDataReq 按天抽取数据请求
type ExtractDailyDataReq struct {
BusinessCode string `json:"businessCode" v:"required" dc:"业务编码"`
ReportCode string `json:"reportCode" v:"required" dc:"报表编码"`
StatDate string `json:"statDate" v:"required" dc:"统计日期 yyyy-MM-dd"`
Executor string `json:"executor" dc:"执行人"`
}
// ExtractDailyDataResp 按天抽取数据响应
type ExtractDailyDataResp struct {
Success bool `json:"success" dc:"是否成功"`
TotalCount int `json:"totalCount" dc:"总记录数"`
SuccessCount int `json:"successCount" dc:"成功记录数"`
FailCount int `json:"failCount" dc:"失败记录数"`
ExecTimeMs int64 `json:"execTimeMs" dc:"执行耗时(毫秒)"`
ErrorMsg string `json:"errorMsg" dc:"错误信息"`
}
// AutoCreateStatTableReq 自动创建统计宽表请求
type AutoCreateStatTableReq struct {
BusinessCode string `json:"businessCode" v:"required" dc:"业务编码"`
ReportCode string `json:"reportCode" v:"required" dc:"报表编码"`
Creator string `json:"creator" dc:"创建人"`
}
// AutoCreateStatTableResp 自动创建统计宽表响应
type AutoCreateStatTableResp struct {
Success bool `json:"success" dc:"是否成功"`
TableName string `json:"tableName" dc:"创建的表名"`
ColumnCount int `json:"columnCount" dc:"字段数量"`
ExecTimeMs int64 `json:"execTimeMs" dc:"执行耗时(毫秒)"`
}
// GetReportFieldsResp 获取报表可用字段响应
type GetReportFieldsResp struct {
BusinessCode string `json:"businessCode" dc:"业务编码"`
ReportCode string `json:"reportCode" dc:"报表编码"`
Dimensions []FieldConfig `json:"dimensions" dc:"维度字段列表"`
Indicators []FieldConfig `json:"indicators" dc:"指标字段列表"`
Filters []FieldConfig `json:"filters" dc:"筛选字段列表"`
}
// ============================================================
// 配置 CRUD 请求/响应
// ============================================================
// SaveBusinessReq 保存业务配置请求(新增/修改合一)
type SaveBusinessReq struct {
ID *int64 `json:"id"` // 有值为更新,无值为新增
BusinessCode string `json:"businessCode" v:"required" dc:"业务编码"`
BusinessName string `json:"businessName" v:"required" dc:"业务名称"`
Description string `json:"description" dc:"描述"`
Status string `json:"status" dc:"状态 ACTIVE/INACTIVE" d:"ACTIVE"`
Config map[string]interface{} `json:"config" dc:"扩展配置"`
Operator string `json:"operator" dc:"操作人"`
}
// SaveReportReq 保存报表配置请求
type SaveReportReq struct {
ID *int64 `json:"id"`
BusinessCode string `json:"businessCode" v:"required" dc:"业务编码"`
ReportCode string `json:"reportCode" v:"required" dc:"报表编码"`
ReportName string `json:"reportName" v:"required" dc:"报表名称"`
Description string `json:"description" dc:"描述"`
Status string `json:"status" dc:"状态" d:"ACTIVE"`
StatTableName string `json:"statTableName" v:"required" dc:"统计宽表名"`
StatTableComment string `json:"statTableComment" dc:"统计宽表注释"`
DateField string `json:"dateField" dc:"日期字段" d:"stat_date"`
PrimaryKeys []string `json:"primaryKeys" dc:"主键字段"`
ConflictKeys []string `json:"conflictKeys" dc:"冲突键(唯一索引)"`
Config map[string]interface{} `json:"config" dc:"扩展配置"`
Operator string `json:"operator" dc:"操作人"`
}
// SaveFieldReq 保存字段配置请求
type SaveFieldReq struct {
ID *int64 `json:"id"`
BusinessCode string `json:"businessCode" v:"required" dc:"业务编码"`
ReportCode string `json:"reportCode" v:"required" dc:"报表编码"`
FieldCode string `json:"fieldCode" v:"required" dc:"字段编码"`
FieldName string `json:"fieldName" v:"required" dc:"字段名称"`
FieldType string `json:"fieldType" v:"required" dc:"字段类型 STRING/INT/FLOAT/DATE/DATETIME/JSONB"`
DataType string `json:"dataType" dc:"数据存储类型" d:"STRING"`
FieldRole string `json:"fieldRole" v:"required" dc:"字段角色 DIMENSION/INDICATOR/FILTER/FILTER_ONLY"`
IsAggregatable bool `json:"isAggregatable" dc:"是否可聚合"`
IsFilterable bool `json:"isFilterable" dc:"是否可筛选" d:"true"`
IsQueryable bool `json:"isQueryable" dc:"是否可查询" d:"true"`
IsSortable bool `json:"isSortable" dc:"是否可排序" d:"true"`
DefaultAggregate string `json:"defaultAggregate" dc:"默认聚合方式"`
ValidAggregates []string `json:"validAggregates" dc:"可选聚合列表"`
FilterOperators []string `json:"filterOperators" dc:"可选操作符列表"`
Expression string `json:"expression" dc:"表达式(衍生字段)"`
ExpressionType string `json:"expressionType" dc:"表达式类型 DIRECT/CALCULATED"`
FormatPattern string `json:"formatPattern" dc:"格式化模板"`
Unit string `json:"unit" dc:"单位"`
DictCode string `json:"dictCode" dc:"字典编码"`
SortOrder int `json:"sortOrder" dc:"排序"`
GroupName string `json:"groupName" dc:"分组名称"`
Status string `json:"status" dc:"状态" d:"ACTIVE"`
Operator string `json:"operator" dc:"操作人"`
}
// SaveExtractConfigReq 保存抽取配置请求
type SaveExtractConfigReq struct {
ID *int64 `json:"id"`
BusinessCode string `json:"businessCode" v:"required" dc:"业务编码"`
ReportCode string `json:"reportCode" v:"required" dc:"报表编码"`
ExtractCode string `json:"extractCode" v:"required" dc:"抽取编码"`
ExtractName string `json:"extractName" v:"required" dc:"抽取名称"`
SourceTableName string `json:"sourceTableName" v:"required" dc:"源表名"`
SourceTableAlias string `json:"sourceTableAlias" dc:"源表别名"`
TargetTableName string `json:"targetTableName" v:"required" dc:"目标表名"`
IsEnabled bool `json:"isEnabled" dc:"是否启用" d:"true"`
ExtractType string `json:"extractType" dc:"抽取类型 FULL/INCREMENTAL" d:"INCREMENTAL"`
ExtractMode string `json:"extractMode" dc:"抽取模式 DIRECT/AGGREGATE" d:"DIRECT"`
ExtractKeyField string `json:"extractKeyField" dc:"抽取关键字段(增量依据)"`
ExtractKeyFormat string `json:"extractKeyFormat" dc:"关键字段格式"`
GroupByFields []string `json:"groupByFields" dc:"GROUP BY 字段列表"`
FilterExpression string `json:"filterExpression" dc:"过滤表达式"`
JoinConfigs []JoinConfig `json:"joinConfigs" dc:"JOIN配置"`
FieldMappings []FieldMapping `json:"fieldMappings" dc:"字段映射列表"`
TransformRules []TransformRule `json:"transformRules" dc:"转换规则列表"`
BatchSize int `json:"batchSize" dc:"批处理大小" d:"1000"`
Status string `json:"status" dc:"状态" d:"ACTIVE"`
Operator string `json:"operator" dc:"操作人"`
}
// IdReq 通用 ID 请求
type IdReq struct {
ID int64 `json:"id" v:"required" dc:"主键ID"`
}
// SaveResult 写操作通用返回
type SaveResult struct {
Success bool `json:"success"`
ID int64 `json:"id"`
Message string `json:"message"`
}
// DeleteResult 删除结果
type DeleteResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
// GetExtractConfigsReq 获取抽取配置列表请求
type GetExtractConfigsReq struct {
BusinessCode string `json:"businessCode" v:"required" dc:"业务编码"`
ReportCode string `json:"reportCode" v:"required" dc:"报表编码"`
}
// ============================================================
// 常量定义
// ============================================================
const (
// 状态
StatusActive = "ACTIVE"
StatusInactive = "INACTIVE"
// 字段角色
RoleDimension = "DIMENSION"
RoleIndicator = "INDICATOR"
RoleFilter = "FILTER"
RoleFilterOnly = "FILTER_ONLY"
// 字段类型
FieldTypeString = "STRING"
FieldTypeInt = "INT"
FieldTypeFloat = "FLOAT"
FieldTypeDate = "DATE"
FieldTypeDatetime = "DATETIME"
FieldTypeJsonb = "JSONB"
// 聚合方式
AggregateSum = "SUM"
AggregateCount = "COUNT"
AggregateAvg = "AVG"
AggregateMax = "MAX"
AggregateMin = "MIN"
// 操作符
OperatorEq = "="
OperatorNe = "!="
OperatorGt = ">"
OperatorLt = "<"
OperatorGe = ">="
OperatorLe = "<="
OperatorIn = "IN"
OperatorLike = "LIKE"
OperatorBetween = "BETWEEN"
// 抽取类型
ExtractTypeFull = "FULL"
ExtractTypeIncremental = "INCREMENTAL"
// 抽取模式
ExtractModeDirect = "DIRECT" // 逐行抽取(默认,源表每行 → 宽表一行)
ExtractModeAggregate = "AGGREGATE" // 聚合抽取(按 GROUP BY 聚合SUM/COUNT/AVG
// 抽取状态
ExtractStatusRunning = "RUNNING"
ExtractStatusSuccess = "SUCCESS"
ExtractStatusFailed = "FAILED"
)

146
common/report/report.go Normal file
View File

@@ -0,0 +1,146 @@
package report
import (
"context"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
)
// initTables 初始化系统表
func initTables(ctx context.Context) error {
ddls := []string{
// 业务配置表
`CREATE TABLE IF NOT EXISTS report_business_config (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
business_code VARCHAR(64) NOT NULL,
business_name VARCHAR(128) NOT NULL,
description TEXT DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
config JSONB DEFAULT '{}',
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
)`,
// 报表配置表
`CREATE TABLE IF NOT EXISTS report_report_config (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
business_code VARCHAR(64) NOT NULL,
report_code VARCHAR(64) NOT NULL,
report_name VARCHAR(128) NOT NULL,
description TEXT DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
stat_table_name VARCHAR(128) NOT NULL,
stat_table_comment VARCHAR(256) DEFAULT '',
date_field VARCHAR(64) DEFAULT 'stat_date',
primary_keys JSONB DEFAULT '["id"]'::jsonb,
conflict_keys JSONB DEFAULT '["stat_date"]'::jsonb,
config JSONB DEFAULT '{}',
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,
CONSTRAINT uk_business_report_code UNIQUE (tenant_id, business_code, report_code)
)`,
// 字段配置表
`CREATE TABLE IF NOT EXISTS report_field_config (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
business_code VARCHAR(64) NOT NULL,
report_code VARCHAR(64) NOT NULL,
field_code VARCHAR(64) NOT NULL,
field_name VARCHAR(128) NOT NULL,
field_type VARCHAR(32) NOT NULL,
data_type VARCHAR(32) NOT NULL DEFAULT 'STRING',
field_role VARCHAR(32) NOT NULL,
is_aggregatable BOOLEAN DEFAULT FALSE,
is_filterable BOOLEAN DEFAULT TRUE,
is_queryable BOOLEAN DEFAULT TRUE,
is_sortable BOOLEAN DEFAULT TRUE,
default_aggregate VARCHAR(32) DEFAULT '',
valid_aggregates JSONB DEFAULT '[]'::jsonb,
filter_operators JSONB DEFAULT '["=","!=",">","<",">=","<=","IN","LIKE","BETWEEN"]'::jsonb,
expression VARCHAR(512) DEFAULT '',
expression_type VARCHAR(32) DEFAULT '',
format_pattern VARCHAR(64) DEFAULT '',
unit VARCHAR(32) DEFAULT '',
dict_code VARCHAR(64) DEFAULT '',
sort_order INT DEFAULT 0,
group_name VARCHAR(64) DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
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,
CONSTRAINT uk_business_report_field_code UNIQUE (tenant_id, business_code, report_code, field_code)
)`,
// 抽取配置表
`CREATE TABLE IF NOT EXISTS report_extract_config (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
business_code VARCHAR(64) NOT NULL,
report_code VARCHAR(64) NOT NULL,
extract_code VARCHAR(64) NOT NULL,
extract_name VARCHAR(128) NOT NULL,
source_table_name VARCHAR(128) NOT NULL,
source_table_alias VARCHAR(64) DEFAULT '',
target_table_name VARCHAR(128) NOT NULL,
is_enabled BOOLEAN DEFAULT TRUE,
extract_type VARCHAR(32) NOT NULL DEFAULT 'FULL',
extract_mode VARCHAR(32) NOT NULL DEFAULT 'DIRECT',
extract_key_field VARCHAR(64) DEFAULT '',
extract_key_format VARCHAR(64) DEFAULT '',
group_by_fields JSONB DEFAULT '[]'::jsonb,
filter_expression TEXT DEFAULT '',
join_configs JSONB DEFAULT '[]'::jsonb,
field_mappings JSONB DEFAULT '[]'::jsonb,
transform_rules JSONB DEFAULT '[]'::jsonb,
batch_size INT DEFAULT 1000,
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
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,
CONSTRAINT uk_business_report_extract_code UNIQUE (tenant_id, business_code, report_code, extract_code)
)`,
// 抽取记录表
`CREATE TABLE IF NOT EXISTS report_extract_log (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 0,
business_code VARCHAR(64) NOT NULL,
report_code VARCHAR(64) NOT NULL,
extract_code VARCHAR(64) NOT NULL,
stat_date VARCHAR(16) NOT NULL,
extract_type VARCHAR(32) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'RUNNING',
total_count INT DEFAULT 0,
success_count INT DEFAULT 0,
fail_count INT DEFAULT 0,
start_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
end_time TIMESTAMP WITH TIME ZONE,
error_message TEXT DEFAULT '',
executor VARCHAR(64) DEFAULT '',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT uk_extract_keys UNIQUE (tenant_id, business_code, report_code, extract_code, stat_date)
)`,
}
for _, ddl := range ddls {
if _, err := gfdb.DB(ctx).Exec(ctx, ddl); err != nil {
return err
}
}
return nil
}