package warningconfig import ( "context" "strings" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult" ) const packageStatusEffective = consts.PackageStatusEffective type Repository struct { db gdb.DB results batchresult.Generator } func NewRepository() *Repository { return &Repository{results: batchresult.New()} } func (r *Repository) database() gdb.DB { if r.db != nil { return r.db } return g.DB() } func (r *Repository) Load(ctx context.Context, batchNo string) (*Task, error) { if batchNo == "" { return nil, gerror.New("预警配置批次号不能为空") } var main *entity.BatchMain if err := r.database().Model(dao.BatchMain.Table()).Safe().Ctx(ctx). Where(dao.BatchMain.Columns().BatchNo, batchNo). Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimWarningConfig). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Scan(&main); err != nil { return nil, gerror.Wrap(err, "查询预警配置批次主表失败") } if main == nil || main.Id == 0 { return nil, gerror.New("处理中预警配置批次不存在") } config, err := configFromBatchInfo(main.BatchInfo) if err != nil { return nil, err } var batchDetails []*entity.BatchDetail if err = r.database().Model(dao.BatchDetail.Table()).Safe().Ctx(ctx). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypeSimWarningConfig). OrderAsc(dao.BatchDetail.Columns().RowNo). Scan(&batchDetails); err != nil { return nil, gerror.Wrap(err, "查询预警配置批次明细失败") } if len(batchDetails) == 0 { return nil, gerror.New("预警配置批次没有运行中明细") } task := &Task{ Batch: Batch{ ID: main.Id, BatchNo: main.BatchNo, BatchType: main.BatchType, TenantID: main.TenantId, CreatedDept: main.CreatedDept, CreatedBy: main.CreatedBy, StartTime: main.StartTime, BatchInfo: main.BatchInfo, }, Config: config, Details: make([]*Detail, 0, len(batchDetails)), } for _, batchDetail := range batchDetails { if batchDetail == nil { continue } detail := &Detail{ ID: batchDetail.Id, RowNo: batchDetail.RowNo, DetailStatus: batchDetail.DetailStatus, ItemType: config.ItemType, Reason: strings.TrimSpace(batchDetail.ErrorReason), Config: config, } if detail.DetailStatus != consts.BatchDetailStatusPending && detail.DetailStatus != consts.BatchDetailStatusRunning && detail.Reason == "" { detail.Reason = "批次明细已处理,不能重复执行" } if config.ItemType == warnItemTypeFlowPool { detail.FlowPoolCode = strings.TrimSpace(batchDetail.ItemId) } else { detail.ICCID = strings.TrimSpace(batchDetail.ItemId) } if batchDetail.Snapshot != nil { if value := batchDetail.Snapshot.Get("itemType").Int(); value > 0 { detail.ItemType = value } if value := strings.TrimSpace(batchDetail.Snapshot.Get("ICCID").String()); value != "" { detail.ICCID = value } detail.MSISDN = strings.TrimSpace(batchDetail.Snapshot.Get("MSISDN").String()) detail.IMSI = strings.TrimSpace(batchDetail.Snapshot.Get("IMSI").String()) if value := strings.TrimSpace(batchDetail.Snapshot.Get("flowPoolCode").String()); value != "" { detail.FlowPoolCode = value } detail.Authorized = batchDetail.Snapshot.Get("authorized").Bool() detail.Config = configFromDetailSnapshot(batchDetail.Snapshot, config) } task.Details = append(task.Details, detail) } if len(task.Details) == 0 { return nil, gerror.New("预警配置批次没有可读取明细") } if err = r.loadItems(ctx, task); err != nil { return nil, err } return task, nil } func (r *Repository) loadItems(ctx context.Context, task *Task) error { if task == nil { return nil } if task.Config.ItemType == warnItemTypeFlowPool { return r.loadFlowPools(ctx, task) } return r.loadCards(ctx, task) } func (r *Repository) loadCards(ctx context.Context, task *Task) error { iccids := make([]string, 0, len(task.Details)) for _, detail := range task.Details { if detail != nil && strings.TrimSpace(detail.ICCID) != "" { iccids = append(iccids, strings.TrimSpace(detail.ICCID)) } } if len(iccids) == 0 { return nil } var cards []*entity.TSimCardInfo columns := dao.TSimCardInfo.Columns() if err := r.database().Model(dao.TSimCardInfo.Table()).Safe().Ctx(ctx). Where(columns.TenantId, task.Batch.TenantID). WhereIn(columns.Iccid, iccids). Scan(&cards); err != nil { return gerror.Wrap(err, "查询SIM卡档案失败") } byICCID := make(map[string]*entity.TSimCardInfo, len(cards)) for _, card := range cards { if card != nil { byICCID[strings.TrimSpace(card.Iccid)] = card } } for _, detail := range task.Details { if detail == nil { continue } card := byICCID[strings.TrimSpace(detail.ICCID)] if card == nil { continue } detail.Card = &Card{ ID: card.Id, ICCID: strings.TrimSpace(card.Iccid), MSISDN: strings.TrimSpace(card.Msisdn), IMSI: strings.TrimSpace(card.Imsi), CurrentPackageCode: strings.TrimSpace(card.CurrentPackageCode), PackageStatus: card.PackageStatus, BossCardStatus: card.BossCardStatus, IsFlowPool: card.IsFlowPool, FlowPkgEndAt: card.FlowPkgEndAt, PackageTotalFlowMb: card.PackageTotalFlowMb, } } return nil } func (r *Repository) loadFlowPools(ctx context.Context, task *Task) error { codes := make([]string, 0, len(task.Details)) for _, detail := range task.Details { if detail != nil && strings.TrimSpace(detail.FlowPoolCode) != "" { codes = append(codes, strings.TrimSpace(detail.FlowPoolCode)) } } if len(codes) == 0 { return nil } var pools []*entity.TSimFlowPool columns := dao.TSimFlowPool.Columns() if err := r.database().Model(dao.TSimFlowPool.Table()).Safe().Ctx(ctx). Fields( columns.Id, columns.FlowPoolCode, columns.TotalFlowMb, columns.UsedFlowMb, columns.RemainFlowMb, columns.PoolStatus, ). Where(columns.TenantId, task.Batch.TenantID). WhereIn(columns.FlowPoolCode, codes). Scan(&pools); err != nil { return gerror.Wrap(err, "查询流量池档案失败") } byCode := make(map[string]*entity.TSimFlowPool, len(pools)) for _, pool := range pools { if pool != nil { byCode[strings.TrimSpace(pool.FlowPoolCode)] = pool } } for _, detail := range task.Details { if detail == nil { continue } pool := byCode[strings.TrimSpace(detail.FlowPoolCode)] if pool == nil { continue } detail.Pool = &Pool{ ID: pool.Id, FlowPoolCode: strings.TrimSpace(pool.FlowPoolCode), TotalFlowMb: pool.TotalFlowMb, UsedFlowMb: pool.UsedFlowMb, RemainFlowMb: pool.RemainFlowMb, PoolStatus: pool.PoolStatus, } } return nil } func (r *Repository) Commit(ctx context.Context, task *Task) error { if task == nil { return gerror.New("预警配置任务不能为空") } if _, failCount := countResult(task); failCount > 0 { markAtomicFailure(task, "预警配置批次存在失败明细,整批未执行") } successCount, failCount := countResult(task) artifact, artifactErr := r.createResultFile(ctx, task) if artifactErr != nil { g.Log().Warningf(ctx, "warning config result file generation failed, batchNo:%s, err:%+v", task.Batch.BatchNo, artifactErr) } now := gtime.Now() err := r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { for _, detail := range task.Details { if detail == nil { continue } if detail.Success() { if err := r.upsertWarningConfig(ctx, tx, task, detail, now); err != nil { return err } } if err := r.updateDetail(ctx, tx, task, detail, now); err != nil { return err } } var ossID, downloadID int64 if artifactErr == nil { var err error ossID, downloadID, err = r.persistArtifact(ctx, tx, task, artifact, successCount, failCount, now) if err != nil { return err } } return r.finalizeBatch(ctx, tx, task, successCount, failCount, ossID, downloadID, now) }) if err != nil && artifactErr == nil && artifact.Path != "" { batchresult.RemoveLocalFile(artifact) } return gerror.Wrap(err, "保存预警配置批次结果失败") } func (r *Repository) upsertWarningConfig(ctx context.Context, tx gdb.TX, task *Task, detail *Detail, now *gtime.Time) error { itemID, itemCode, err := warningConfigItemSnapshot(task, detail) if err != nil { return err } config := detail.Config columns := dao.TWarnConfig.Columns() excludeFields := "id,tenant_id,item_type,item_id,warn_unit,created_dept,created_by,created_at" if !config.AppNotifyProvided { excludeFields += ",app_notify_enabled" } if !config.WxTemplateProvided { excludeFields += ",wx_template_id" } _, err = tx.Ctx(ctx).Model(dao.TWarnConfig.Table()).Safe(). Data(do.TWarnConfig{ TenantId: task.Batch.TenantID, BatchNo: task.Batch.BatchNo, ItemType: config.ItemType, ItemId: itemID, ItemCode: itemCode, WarnUnit: config.WarnUnit, FirstWarn: config.FirstWarn, SecondWarn: config.SecondWarn, ThirdWarn: config.ThirdWarn, WarnMethods: config.WarnMethods, WarnPhones: config.WarnPhones, WarnEmails: config.WarnEmails, AutoAction: config.AutoAction, IsEnabled: config.IsEnabled, WxTemplateId: config.WxTemplateID, AppNotifyEnabled: config.AppNotifyEnabled, Remark: config.Remark, CreatedDept: task.Batch.CreatedDept, CreatedBy: task.Batch.CreatedBy, CreatedAt: now, UpdatedBy: task.Batch.CreatedBy, UpdatedAt: now, }). OnConflict(columns.TenantId, columns.ItemType, columns.ItemId, columns.WarnUnit). OnDuplicateEx(excludeFields). Save() return gerror.Wrap(err, "写入预警配置失败") } func warningConfigItemSnapshot(task *Task, detail *Detail) (int64, string, error) { if task == nil || detail == nil { return 0, "", gerror.New("预警配置明细为空") } if detail.Config.ItemType == warnItemTypeFlowPool { if detail.Pool == nil { return 0, "", gerror.New("预警配置明细缺少流量池档案") } itemCode := strings.TrimSpace(detail.Pool.FlowPoolCode) if itemCode == "" { itemCode = strings.TrimSpace(detail.FlowPoolCode) } return detail.Pool.ID, itemCode, nil } if detail.Card == nil { return 0, "", gerror.New("预警配置明细缺少SIM卡档案") } itemCode := strings.TrimSpace(detail.Card.ICCID) if itemCode == "" { itemCode = strings.TrimSpace(detail.ICCID) } return detail.Card.ID, itemCode, nil } func (r *Repository) updateDetail(ctx context.Context, tx gdb.TX, task *Task, detail *Detail, now *gtime.Time) error { if detail == nil || (detail.DetailStatus != consts.BatchDetailStatusPending && detail.DetailStatus != consts.BatchDetailStatusRunning) { return nil } status := consts.BatchDetailStatusFailed if detail.Success() { status = consts.BatchDetailStatusSuccess } result, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()). Where(dao.BatchDetail.Columns().Id, detail.ID). Where(dao.BatchDetail.Columns().BatchNo, task.Batch.BatchNo). Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypeSimWarningConfig). WhereIn(dao.BatchDetail.Columns().DetailStatus, []int{ consts.BatchDetailStatusPending, consts.BatchDetailStatusRunning, }). Data(do.BatchDetail{ DetailStatus: status, ErrorReason: detail.Reason, Snapshot: detail.Snapshot(detail.Config), UpdatedBy: task.Batch.CreatedBy, UpdatedAt: now, }).Update() if err != nil { return gerror.Wrap(err, "更新预警配置批次明细失败") } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return gerror.Newf("预警配置批次明细%d已不是运行中状态", detail.ID) } return nil } func (r *Repository) finalizeBatch(ctx context.Context, tx gdb.TX, task *Task, successCount, failCount int, ossID, downloadID int64, now *gtime.Time) error { batchAsyncStatus, runResult := batchResult(successCount, failCount) result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Where(dao.BatchMain.Columns().BatchNo, task.Batch.BatchNo). Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimWarningConfig). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Data(do.BatchMain{ BatchAsyncStatus: batchAsyncStatus, RunResult: runResult, TotalCount: len(task.Details), ValidCount: successCount, SuccessCount: successCount, FailCount: failCount, FailReason: task.FailureReason, BatchInfo: mergeBatchInfo(task.Batch.BatchInfo, ossID, downloadID), EndTime: now, UpdatedBy: task.Batch.CreatedBy, }).Update() if err != nil { return gerror.Wrap(err, "更新预警配置批次主表失败") } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return gerror.New("预警配置批次已不是处理中状态") } return nil } func (r *Repository) createResultFile(ctx context.Context, task *Task) (batchresult.Artifact, error) { if r.results == nil { return batchresult.Artifact{}, gerror.New("result generator is not configured") } return r.results.Create(ctx, batchresult.Request{ BatchNo: task.Batch.BatchNo, BatchType: task.Batch.BatchType, TenantID: task.Batch.TenantID, Rows: task.ResultRows(), }) } func (r *Repository) persistArtifact(ctx context.Context, tx gdb.TX, task *Task, artifact batchresult.Artifact, successCount, failCount int, now *gtime.Time) (int64, int64, error) { ossID, err := tx.Ctx(ctx).Model(dao.SysOss.Table()).Data(do.SysOss{ TenantId: task.Batch.TenantID, FileName: artifact.FileName, OriginalName: artifact.OriginalName, FileSuffix: artifact.FileSuffix, Path: artifact.Path, Url: artifact.URL, CreatedDept: task.Batch.CreatedDept, CreatedBy: task.Batch.CreatedBy, UpdatedBy: task.Batch.CreatedBy, Service: "local", Md5: artifact.MD5, FileSize: artifact.Size, }).InsertAndGetId() if err != nil { return 0, 0, gerror.Wrap(err, "写入预警配置结果文件OSS失败") } downloadID, err := tx.Ctx(ctx).Model(dao.TResourceBatchDownload.Table()).Data(do.TResourceBatchDownload{ BatchNo: task.Batch.BatchNo, TaskName: batchresult.TaskName(task.Batch.BatchType), ResourceType: consts.ResourceTypeExcel, ExportScene: consts.BatchTypeSimWarningConfig, TotalCount: len(task.Details), SuccessCount: successCount, FailCount: failCount, DownloadStatus: consts.DownloadStatusGenerated, OssId: ossID, FileName: artifact.FileName, FileExt: artifact.FileSuffix, FileUrl: artifact.URL, FilePath: artifact.Path, FileSize: artifact.Size, FileMd5: artifact.MD5, DownloadCount: 0, MaxDownloadCount: 0, DeptId: task.Batch.CreatedDept, StartTime: task.Batch.StartTime, FinishTime: now, TenantId: task.Batch.TenantID, CreatedDept: task.Batch.CreatedDept, CreatedBy: task.Batch.CreatedBy, UpdatedBy: task.Batch.CreatedBy, }).InsertAndGetId() if err != nil { return 0, 0, gerror.Wrap(err, "写入预警配置结果下载记录失败") } return ossID, downloadID, nil } func configFromBatchInfo(info *gjson.Json) (Config, error) { if info == nil { return Config{}, gerror.New("预警配置批次缺少批次参数") } config := Config{ ItemType: info.Get("itemType").Int(), WarnUnit: info.Get("warnUnit").Int(), FirstWarn: info.Get("firstWarn").Float64(), SecondWarn: info.Get("secondWarn").Float64(), ThirdWarn: info.Get("thirdWarn").Float64(), WarnMethods: strings.TrimSpace(info.Get("warnMethods").String()), WarnPhones: strings.TrimSpace(info.Get("warnPhones").String()), WarnEmails: strings.TrimSpace(info.Get("warnEmails").String()), AutoAction: info.Get("autoAction").Int(), IsEnabled: info.Get("isEnabled").Int(), WxTemplateID: strings.TrimSpace(info.Get("wxTemplateId").String()), AppNotifyEnabled: info.Get("appNotifyEnabled").Int(), AppNotifyProvided: !info.Get("appNotifyEnabled").IsNil(), WxTemplateProvided: !info.Get("wxTemplateId").IsNil(), Remark: strings.TrimSpace(info.Get("remark").String()), } if config.WarnMethods == "" { config.WarnMethods = warnDefaultMethod } if config.ItemType == 0 { config.ItemType = warnItemTypeCard } config.WarnType = deriveWarnType(config.ItemType, config.WarnUnit) if config.ItemType != warnItemTypeCard && config.ItemType != warnItemTypeFlowPool { return Config{}, gerror.New("预警配置对象类型无效") } if config.WarnUnit == warnUnitDay || config.WarnUnit == warnUnitMonth { return Config{}, gerror.New("日期预警由系统默认规则处理,不支持手动配置") } if config.AppNotifyEnabled != 0 && config.AppNotifyEnabled != 1 { return Config{}, gerror.New("微信提醒启停状态无效") } if info.Get("rowConfig").Bool() { if err := validateWarnTypeUnit(config.ItemType, config.WarnType, config.WarnUnit); err != nil { return Config{}, err } return config, nil } if config.IsEnabled != 0 && config.IsEnabled != 1 { return Config{}, gerror.New("预警配置启停状态无效") } if config.AutoAction != 0 && config.AutoAction != warnAutoActionSuspend { return Config{}, gerror.New("预警配置自动动作无效") } if err := validateWarnTypeUnit(config.ItemType, config.WarnType, config.WarnUnit); err != nil { return Config{}, err } if config.ItemType == warnItemTypeFlowPool && config.AutoAction != 0 { return Config{}, gerror.New("流量池预警暂不支持第三次关停") } if err := validateConfigThresholds(&config); err != nil { return Config{}, err } return config, nil } func configFromDetailSnapshot(snapshot *gjson.Json, fallback Config) Config { if snapshot == nil { return fallback } if !snapshot.Get("configSet").Bool() && snapshot.Get("configItemType").Int() == 0 && snapshot.Get("warnUnit").Int() == 0 { return fallback } config := fallback if value := snapshot.Get("configItemType").Int(); value > 0 { config.ItemType = value } if value := snapshot.Get("warnUnit").Int(); value > 0 { config.WarnUnit = value } config.FirstWarn = snapshot.Get("firstWarn").Float64() config.SecondWarn = snapshot.Get("secondWarn").Float64() config.ThirdWarn = snapshot.Get("thirdWarn").Float64() if value := strings.TrimSpace(snapshot.Get("warnMethods").String()); value != "" { config.WarnMethods = value } config.WarnPhones = strings.TrimSpace(snapshot.Get("warnPhones").String()) config.WarnEmails = strings.TrimSpace(snapshot.Get("warnEmails").String()) config.AutoAction = snapshot.Get("autoAction").Int() if snapshot.Get("isEnabled").Val() != nil { config.IsEnabled = snapshot.Get("isEnabled").Int() } if !snapshot.Get("wxTemplateId").IsNil() { config.WxTemplateID = strings.TrimSpace(snapshot.Get("wxTemplateId").String()) config.WxTemplateProvided = true } if snapshot.Get("appNotifyEnabled").Val() != nil { config.AppNotifyEnabled = snapshot.Get("appNotifyEnabled").Int() config.AppNotifyProvided = true } config.Remark = strings.TrimSpace(snapshot.Get("remark").String()) config.WarnType = deriveWarnType(config.ItemType, config.WarnUnit) return config } func validateConfigThresholds(config *Config) error { if config == nil { return gerror.New("预警配置参数不能为空") } if config.FirstWarn <= 0 { return gerror.New("第一档阈值必须大于0") } if config.SecondWarn < 0 || config.ThirdWarn < 0 { return gerror.New("预警阈值不能小于0") } if config.AutoAction == warnAutoActionSuspend && config.ThirdWarn <= 0 { return gerror.New("开启第三次自动停机时必须配置第三档阈值") } switch config.WarnType { case warnTypeCardTraffic: if config.WarnUnit == warnUnitPercent { config.FirstWarn = normalizePercentThreshold(config.FirstWarn) config.SecondWarn = normalizePercentThreshold(config.SecondWarn) config.ThirdWarn = normalizePercentThreshold(config.ThirdWarn) if config.FirstWarn > 100 || config.SecondWarn > 100 || config.ThirdWarn > 100 { return gerror.New("百分比阈值不能大于100") } } return validateGreaterThresholds(config) case warnTypeFlowPoolPercent: config.FirstWarn = normalizePercentThreshold(config.FirstWarn) config.SecondWarn = normalizePercentThreshold(config.SecondWarn) config.ThirdWarn = normalizePercentThreshold(config.ThirdWarn) if config.FirstWarn > 100 || config.SecondWarn > 100 || config.ThirdWarn > 100 { return gerror.New("百分比阈值不能大于100") } return validateGreaterThresholds(config) default: return gerror.New("预警类型无效") } } func validateGreaterThresholds(config *Config) error { if config.SecondWarn > 0 && config.SecondWarn <= config.FirstWarn { return gerror.New("流量预警第二档阈值必须大于第一档") } if config.ThirdWarn > 0 { if config.SecondWarn <= 0 { return gerror.New("配置第三档时必须先配置第二档") } if config.ThirdWarn <= config.SecondWarn { return gerror.New("流量预警第三档阈值必须大于第二档") } } return nil } func normalizePercentThreshold(value float64) float64 { if value > 0 && value <= 1 { return value * 100 } return value } func deriveWarnType(itemType, warnUnit int) int { if itemType == warnItemTypeFlowPool { return warnTypeFlowPoolPercent } switch warnUnit { case warnUnitMb, warnUnitPercent: return warnTypeCardTraffic case warnUnitDay: return warnTypeExpireDay case warnUnitMonth: return warnTypeExpireMonth default: return 0 } } func validateWarnTypeUnit(itemType, warnType, warnUnit int) error { switch warnType { case warnTypeCardTraffic: if itemType != warnItemTypeCard { return gerror.New("单卡流量预警只能配置单卡对象") } if warnUnit != warnUnitMb && warnUnit != warnUnitPercent { return gerror.New("单卡流量预警仅支持MB或百分比单位") } case warnTypeExpireDay: return gerror.New("日期预警由系统默认规则处理,不支持手动配置") case warnTypeExpireMonth: return gerror.New("日期预警由系统默认规则处理,不支持手动配置") case warnTypeFlowPoolPercent: if itemType != warnItemTypeFlowPool { return gerror.New("流量池预警只能配置流量池对象") } if warnUnit != warnUnitPercent { return gerror.New("流量池预警仅支持百分比单位") } default: return gerror.New("预警类型无效") } return nil } func mergeBatchInfo(info *gjson.Json, ossID, downloadID int64) *gjson.Json { data := make(map[string]any) if info != nil { for key, value := range info.Map() { data[key] = value } } if ossID > 0 { data["resultFileOssId"] = ossID } if downloadID > 0 { data["resultDownloadId"] = downloadID data["resultFile"] = map[string]any{"status": "generated"} } else { data["resultFile"] = map[string]any{"status": "skipped"} } return gjson.New(data) } func batchResult(successCount, failCount int) (int, int) { if failCount > 0 || successCount == 0 { return consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed } return consts.BatchAsyncStatusSuccess, consts.BatchRunResultSuccess }