901 lines
31 KiB
Go
901 lines
31 KiB
Go
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
|
||
}
|