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 }