Files
data-engine/common/report/api.go
T
2026-07-02 10:23:07 +08:00

901 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package report
import (
"context"
"fmt"
"strings"
"time"
"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/beans"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/database/gdb"
)
// getUserNameFromCtx 从上下文中获取当前用户名
func getUserNameFromCtx(ctx context.Context) string {
if user, ok := ctx.Value("user").(*beans.User); ok && user != nil && user.UserName != "" {
return user.UserName
}
return "system"
}
// 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: 批量回填数据
// 首次接入时对日期范围逐天抽取
// ============================================================
// Backfill 批量回填数据
// 遍历 startDate → endDate 逐天调用 ExtractDailyData
// 返回汇总统计
func (s *ReportService) Backfill(ctx context.Context, req *model.BackfillReq) (*model.BackfillResp, error) {
start := time.Now()
startDate, err := time.Parse("2006-01-02", req.StartDate)
if err != nil {
return nil, fmt.Errorf("开始日期格式错误: %w", err)
}
endDate, err := time.Parse("2006-01-02", req.EndDate)
if err != nil {
return nil, fmt.Errorf("结束日期格式错误: %w", err)
}
if endDate.Before(startDate) {
return nil, fmt.Errorf("结束日期不能早于开始日期")
}
totalDays := 0
successDays := 0
failDays := 0
var lastErr error
for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) {
totalDays++
dateStr := d.Format("2006-01-02")
resp, err := s.ExtractDailyData(ctx, req.BusinessCode, req.ReportCode, dateStr, req.Executor)
if err != nil {
failDays++
lastErr = fmt.Errorf("回填 %s 失败: %w", dateStr, err)
continue
}
if !resp.Success {
failDays++
lastErr = fmt.Errorf("回填 %s 异常: %s", dateStr, resp.ErrorMsg)
continue
}
successDays++
}
execTime := time.Since(start).Milliseconds()
resp := &model.BackfillResp{
Success: failDays == 0,
TotalDays: totalDays,
SuccessDays: successDays,
FailDays: failDays,
ExecTimeMs: execTime,
}
if lastErr != nil {
resp.ErrorMsg = lastErr.Error()
}
return resp, nil
}
// ============================================================
// 核心接口 4: 用户选择查询(最核心)
// 前端用户选择条件 → 实时构建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 获取所有启用业务列表
// ListBusinesses 分页获取业务列表
func (s *ReportService) ListBusinesses(ctx context.Context, pageNum, pageSize int) ([]model.BusinessConfig, int, error) {
return s.configLoader.ListBusinesses(ctx, pageNum, pageSize)
}
func (s *ReportService) GetAllBusinesses(ctx context.Context) ([]model.BusinessConfig, error) {
return s.configLoader.GetAllBusinesses(ctx)
}
// GetAllReports 获取业务下所有报表列表
// ListReports 分页获取报表列表
func (s *ReportService) ListReports(ctx context.Context, businessCode, reportName string, pageNum, pageSize int) ([]model.ReportConfig, int, error) {
return s.configLoader.ListReports(ctx, businessCode, reportName, pageNum, pageSize)
}
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)
}
// 新增时校验 businessCode 唯一性
if req.ID == nil || *req.ID == 0 {
r, err := gfdb.DB(ctx).GetAll(ctx, "SELECT id FROM report_business_config WHERE business_code = $1 LIMIT 1", req.BusinessCode)
if err != nil {
return nil, fmt.Errorf("校验业务编码失败: %w", err)
}
if !r.IsEmpty() {
return nil, fmt.Errorf("业务编码 %s 已存在", req.BusinessCode)
}
}
userName := getUserNameFromCtx(ctx)
biz := &model.BusinessConfig{
SQLBaseDO: beans.SQLBaseDO{Creator: userName, Updater: userName, TenantId: 1},
BusinessCode: req.BusinessCode,
BusinessName: req.BusinessName,
Description: req.Description,
Status: req.Status,
Config: req.Config,
}
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
}
// 级联检查:该业务下是否存在报表
r, err := gfdb.DB(ctx).GetAll(ctx, "SELECT COUNT(1) AS cnt FROM report_report_config WHERE business_code = $1 AND deleted_at IS NULL LIMIT 1", biz.BusinessCode)
if err == nil && !r.IsEmpty() {
cnt := r[0]["cnt"].Int()
if cnt > 0 {
return &model.DeleteResult{Success: false, Message: fmt.Sprintf("业务 %s 下存在 %d 个报表配置,请先删除关联报表后再删除业务", biz.BusinessCode, cnt)}, nil
}
}
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)
}
// 新增时校验 reportCode 唯一性(同业务下)
if req.ID == nil || *req.ID == 0 {
r, err := gfdb.DB(ctx).GetAll(ctx, "SELECT id FROM report_report_config WHERE business_code = $1 AND report_code = $2 LIMIT 1",
req.BusinessCode, req.ReportCode)
if err != nil {
return nil, fmt.Errorf("校验报表编码失败: %w", err)
}
if !r.IsEmpty() {
return nil, fmt.Errorf("业务 %s 下报表编码 %s 已存在", req.BusinessCode, req.ReportCode)
}
}
userName := getUserNameFromCtx(ctx)
rpt := &model.ReportConfig{
SQLBaseDO: beans.SQLBaseDO{Creator: userName, Updater: userName, TenantId: 1},
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,
}
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
}
// 级联检查:该报表下是否存在未删除的字段
r, err := gfdb.DB(ctx).GetAll(ctx, "SELECT COUNT(1) AS cnt FROM report_field_config WHERE business_code = $1 AND report_code = $2 AND deleted_at IS NULL LIMIT 1",
rpt.BusinessCode, rpt.ReportCode)
if err == nil && !r.IsEmpty() && r[0]["cnt"].Int() > 0 {
return &model.DeleteResult{Success: false, Message: fmt.Sprintf("报表 %s 下存在字段配置,请先删除关联字段后再删除报表", rpt.ReportCode)}, nil
}
// 级联检查:该报表下是否存在抽取配置
r, err = gfdb.DB(ctx).GetAll(ctx, "SELECT COUNT(1) AS cnt FROM report_extract_config WHERE business_code = $1 AND report_code = $2 AND deleted_at IS NULL LIMIT 1",
rpt.BusinessCode, rpt.ReportCode)
if err == nil && !r.IsEmpty() && r[0]["cnt"].Int() > 0 {
return &model.DeleteResult{Success: false, Message: fmt.Sprintf("报表 %s 下存在抽取配置,请先删除关联抽取配置后再删除报表", rpt.ReportCode)}, nil
}
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) {
rpt, err := s.configLoader.GetReportByID(ctx, id)
if err != nil {
return nil, err
}
// 同时加载全部字段(含已失效),编辑时前端可直接回显
allFields, err := s.configLoader.GetAllFields(ctx, rpt.BusinessCode, rpt.ReportCode)
if err == nil {
rpt.Fields = allFields
}
return rpt, nil
}
// ============================================================
// 合并操作:报表+字段一起保存
// ============================================================
// SaveReportWithFields 保存报表及字段配置(报表+字段全量替换)
func (s *ReportService) SaveReportWithFields(ctx context.Context, req *model.SaveReportWithFieldsReq) (*model.SaveResult, error) {
if err := initTables(ctx); err != nil {
return nil, fmt.Errorf("初始化系统表失败: %w", err)
}
// 自动填充每个字段的 businessCode/reportCode(从报表级继承)
for i := range req.Fields {
if req.Fields[i].BusinessCode == "" {
req.Fields[i].BusinessCode = req.BusinessCode
}
if req.Fields[i].ReportCode == "" {
req.Fields[i].ReportCode = req.ReportCode
}
}
var reportID int64
userName := getUserNameFromCtx(ctx)
// 事务:要么全成功,要么不动
err := gfdb.DB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// 1. 保存报表(新增或更新)
now := time.Now()
if req.ID != nil && *req.ID > 0 {
// 更新报表
data := map[string]interface{}{
"business_code": req.BusinessCode,
"report_code": req.ReportCode,
"report_name": req.ReportName,
"description": req.Description,
"status": req.Status,
"stat_table_name": req.StatTableName,
"stat_table_comment": req.StatTableComment,
"date_field": req.DateField,
"primary_keys": req.PrimaryKeys,
"conflict_keys": req.ConflictKeys,
"config": req.Config,
"updated_at": now,
"updater": userName,
}
if _, err := tx.Model("report_report_config").Where("id", *req.ID).Data(data).Update(); err != nil {
return fmt.Errorf("更新报表失败: %w", err)
}
reportID = *req.ID
} else {
// 新增时校验 reportCode 唯一性
cnt, err := tx.Model("report_report_config").Where("business_code", req.BusinessCode).Where("report_code", req.ReportCode).Count()
if err != nil {
return fmt.Errorf("校验报表编码失败: %w", err)
}
if cnt > 0 {
return fmt.Errorf("业务 %s 下报表编码 %s 已存在", req.BusinessCode, req.ReportCode)
}
data := map[string]interface{}{
"business_code": req.BusinessCode,
"report_code": req.ReportCode,
"report_name": req.ReportName,
"description": req.Description,
"status": ifVal(req.Status, model.StatusActive),
"stat_table_name": req.StatTableName,
"stat_table_comment": req.StatTableComment,
"date_field": ifVal(req.DateField, "stat_date"),
"primary_keys": req.PrimaryKeys,
"conflict_keys": req.ConflictKeys,
"config": req.Config,
"created_at": now,
"updated_at": now,
"creator": userName,
"updater": userName,
"tenant_id": 1,
}
if data["primary_keys"] == nil || len(data["primary_keys"].([]string)) == 0 {
data["primary_keys"] = []string{"id"}
}
if data["conflict_keys"] == nil || len(data["conflict_keys"].([]string)) == 0 {
data["conflict_keys"] = []string{ifVal(req.DateField, "stat_date")}
}
result, err := tx.Model("report_report_config").Data(data).Insert()
if err != nil {
if strings.Contains(err.Error(), "duplicate key") {
return fmt.Errorf("业务 %s 下报表编码 %s 已存在", req.BusinessCode, req.ReportCode)
}
return fmt.Errorf("创建报表失败: %w", err)
}
rid, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("获取自增ID失败: %w", err)
}
reportID = rid
}
// 2. 获取报表下现有字段(含已删除,编辑时重新添加的字段需走 UPDATE 而非 INSERT
rows, err := tx.Model("report_field_config").
Where("business_code", req.BusinessCode).
Where("report_code", req.ReportCode).
All()
if err != nil {
return fmt.Errorf("获取现有字段失败: %w", err)
}
existingMap := make(map[string]int64) // fieldCode -> id
for _, row := range rows {
existingMap[row["field_code"].String()] = row["id"].Int64()
}
// 3. 处理请求中的字段(新增或更新)
for i := range req.Fields {
f := &req.Fields[i]
status := ifVal(f.Status, model.StatusActive)
dataType := ifVal(f.DataType, model.FieldTypeString)
if existingID, ok := existingMap[f.FieldCode]; ok {
// 已存在 → 更新
updateData := map[string]interface{}{
"field_name": f.FieldName,
"field_type": f.FieldType,
"data_type": dataType,
"field_role": f.FieldRole,
"is_aggregatable": f.IsAggregatable,
"is_filterable": f.IsFilterable,
"is_queryable": f.IsQueryable,
"is_sortable": f.IsSortable,
"default_aggregate": f.DefaultAggregate,
"valid_aggregates": f.ValidAggregates,
"filter_operators": f.FilterOperators,
"expression": f.Expression,
"expression_type": f.ExpressionType,
"format_pattern": f.FormatPattern,
"unit": f.Unit,
"dict_code": f.DictCode,
"sort_order": f.SortOrder,
"group_name": f.GroupName,
"status": status,
"updated_at": now,
"updater": userName,
}
if _, err := tx.Model("report_field_config").Where("id", existingID).Data(updateData).Update(); err != nil {
return fmt.Errorf("更新字段 %s 失败: %w", f.FieldCode, err)
}
delete(existingMap, f.FieldCode)
} else {
// 不存在 → 新增
insertData := map[string]interface{}{
"business_code": req.BusinessCode,
"report_code": req.ReportCode,
"field_code": f.FieldCode,
"field_name": f.FieldName,
"field_type": f.FieldType,
"data_type": dataType,
"field_role": f.FieldRole,
"is_aggregatable": f.IsAggregatable,
"is_filterable": f.IsFilterable,
"is_queryable": f.IsQueryable,
"is_sortable": f.IsSortable,
"default_aggregate": f.DefaultAggregate,
"valid_aggregates": f.ValidAggregates,
"filter_operators": f.FilterOperators,
"expression": f.Expression,
"expression_type": f.ExpressionType,
"format_pattern": f.FormatPattern,
"unit": f.Unit,
"dict_code": f.DictCode,
"sort_order": f.SortOrder,
"group_name": f.GroupName,
"status": status,
"created_at": now,
"updated_at": now,
"creator": userName,
"updater": userName,
"tenant_id": 1,
}
if _, err := tx.Model("report_field_config").Data(insertData).Insert(); err != nil {
if strings.Contains(err.Error(), "duplicate key") {
return fmt.Errorf("字段编码 %s 在当前报表下已存在,请更换字段编码", f.FieldCode)
}
return fmt.Errorf("创建字段 %s 失败: %w", f.FieldCode, err)
}
}
}
// 4. 删除剩余字段(不在请求中的旧字段)
for fieldCode, existingID := range existingMap {
if _, err := tx.Model("report_field_config").Where("id", existingID).Data(map[string]interface{}{
"status": model.StatusInactive,
"deleted_at": now,
"updated_at": now,
"updater": userName,
}).Update(); err != nil {
return fmt.Errorf("删除字段 %s 失败: %w", fieldCode, err)
}
}
return nil
})
if err != nil {
return nil, err
}
// 清理缓存
s.configLoader.InvalidateCache(req.BusinessCode, req.ReportCode)
return &model.SaveResult{Success: true, ID: reportID, Message: "报表及字段保存成功"}, nil
}
// ============================================================
// 配置 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)
}
if req.BusinessCode == "" {
return nil, fmt.Errorf("businessCode 不能为空")
}
if req.ReportCode == "" {
return nil, fmt.Errorf("reportCode 不能为空")
}
userName := getUserNameFromCtx(ctx)
field := &model.FieldConfig{
SQLBaseDO: beans.SQLBaseDO{Creator: userName, Updater: userName, TenantId: 1},
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,
}
// 校验字段类型
validFieldTypes := map[string]bool{
model.FieldTypeString: true,
model.FieldTypeInt: true,
model.FieldTypeFloat: true,
model.FieldTypeDate: true,
model.FieldTypeDatetime: true,
model.FieldTypeJsonb: true,
}
if req.FieldType != "" && !validFieldTypes[req.FieldType] {
return nil, fmt.Errorf("不支持的字段类型: %s,仅支持 STRING/INT/FLOAT/DATE/DATETIME/JSONB", req.FieldType)
}
if req.DataType != "" && !validFieldTypes[req.DataType] {
return nil, fmt.Errorf("不支持的存储类型: %s,仅支持 STRING/INT/FLOAT/DATE/DATETIME/JSONB", req.DataType)
}
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 {
existing, err := s.configLoader.GetReportFields(ctx, req.BusinessCode, req.ReportCode)
if err != nil {
return nil, fmt.Errorf("校验字段编码失败: %w", err)
}
allFields := append(append(existing.Dimensions, existing.Indicators...), existing.Filters...)
for _, f := range allFields {
if f.FieldCode == req.FieldCode && f.Status == model.StatusActive {
return nil, fmt.Errorf("字段编码 %s 已存在(业务: %s, 报表: %s", req.FieldCode, req.BusinessCode, req.ReportCode)
}
}
}
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)
}
// 新增时校验 extractCode 唯一性(同业务+报表下)
if req.ID == nil || *req.ID == 0 {
r, err := gfdb.DB(ctx).GetAll(ctx, "SELECT id FROM report_extract_config WHERE business_code = $1 AND report_code = $2 AND extract_code = $3 LIMIT 1",
req.BusinessCode, req.ReportCode, req.ExtractCode)
if err != nil {
return nil, fmt.Errorf("校验抽取编码失败: %w", err)
}
if !r.IsEmpty() {
return nil, fmt.Errorf("业务 %s/报表 %s 下抽取编码 %s 已存在", req.BusinessCode, req.ReportCode, req.ExtractCode)
}
}
userName := getUserNameFromCtx(ctx)
ec := &model.ExtractConfig{
SQLBaseDO: beans.SQLBaseDO{Creator: userName, Updater: userName, TenantId: 1},
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,
}
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 获取业务报表下所有抽取配置
// ListExtractConfigs 分页获取抽取配置列表
func (s *ReportService) ListExtractConfigs(ctx context.Context, businessCode, reportCode string, pageNum, pageSize int) ([]model.ExtractConfig, int, error) {
return s.configLoader.ListExtractConfigs(ctx, businessCode, reportCode, pageNum, pageSize)
}
func (s *ReportService) GetExtractConfigs(ctx context.Context, businessCode, reportCode string) ([]model.ExtractConfig, error) {
return s.configLoader.GetExtractConfigs(ctx, businessCode, reportCode)
}
// GetExtractConfigsAll 获取业务报表下所有抽取配置(不限 status,用于填充字段展示)
func (s *ReportService) GetExtractConfigsAll(ctx context.Context, businessCode, reportCode string) ([]model.ExtractConfig, error) {
r, err := gfdb.DB(ctx).GetAll(ctx,
"SELECT id, extract_code, extract_name, status FROM report_extract_config WHERE business_code = $1 AND report_code = $2 AND status != $3 ORDER BY id ASC",
businessCode, reportCode, model.StatusInactive)
if err != nil {
return nil, err
}
var configs []model.ExtractConfig
for _, record := range r {
var ec model.ExtractConfig
if err := record.Struct(&ec); err != nil {
return nil, err
}
configs = append(configs, ec)
}
return configs, nil
}
// ifVal 三目运算:非空返回 s,否则返回 fallback
func ifVal(s, fallback string) string {
if s == "" {
return fallback
}
return s
}