package trafficcutoffconfig import ( "context" "math" "sort" "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" ) type Repository struct { db gdb.DB results batchresult.Generator lockEnabled bool } func NewRepository() *Repository { return &Repository{results: batchresult.New(), lockEnabled: true} } func (r *Repository) database() gdb.DB { if r.db != nil { return r.db } return g.DB() } func (r *Repository) applyLock(model *gdb.Model) *gdb.Model { if r.lockEnabled { return model.LockUpdate() } return model } func (r *Repository) Load(ctx context.Context, batchNo string) (*Task, error) { batchNo = strings.TrimSpace(batchNo) 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.BatchTypeTrafficCutoffConfig). 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.BatchTypeTrafficCutoffConfig). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning). 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, ICCID: strings.TrimSpace(batchDetail.ItemId), Reason: strings.TrimSpace(batchDetail.ErrorReason), } if batchDetail.Snapshot != nil { if value := firstSnapshot(batchDetail.Snapshot, "ICCID", "iccid"); value != "" { detail.ICCID = value } detail.MSISDN = firstSnapshot(batchDetail.Snapshot, "MSISDN", "msisdn", "inputMsisdn", "input_msisdn") detail.IMSI = firstSnapshot(batchDetail.Snapshot, "IMSI", "imsi", "inputImsi", "input_imsi") detail.Authorized = batchDetail.Snapshot.Get("authorized").Bool() } task.Details = append(task.Details, detail) } if len(task.Details) == 0 { return nil, gerror.New("达量断网配置批次没有可读取明细") } if err = r.loadCards(ctx, task); err != nil { return nil, err } return task, nil } func firstSnapshot(snapshot *gjson.Json, keys ...string) string { if snapshot == nil { return "" } for _, key := range keys { if value := strings.TrimSpace(snapshot.Get(key).String()); value != "" { return value } } return "" } 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)) } } iccids = uniqueSortedStrings(iccids) 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, PackageTotalFlowMb: card.PackageTotalFlowMb, TrafficCutoffStatus: card.TrafficCutoffStatus, TrafficCutoffThreshold: card.TrafficCutoffThreshold, } } return nil } func (r *Repository) Commit(ctx context.Context, task *Task) error { if task == nil { return gerror.New("达量断网配置任务不能为空") } successCount, failCount := countResult(task) artifact, artifactErr := r.createResultFile(ctx, task) if artifactErr != nil { g.Log().Warningf(ctx, "traffic cutoff 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 { if successCount > 0 { if err := r.updateAllCardConfigs(ctx, tx, task); err != nil { return err } } if err := r.updateDetails(ctx, tx, task); 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, artifactErr != nil) }) if err != nil && artifactErr == nil && artifact.Path != "" { batchresult.RemoveLocalFile(artifact) } return gerror.Wrap(err, "保存达量断网配置批次结果失败") } func (r *Repository) updateAllCardConfigs(ctx context.Context, tx gdb.TX, task *Task) error { cardIDs := successDetailCardIDs(task.Details) if len(cardIDs) == 0 { return nil } columns := dao.TSimCardInfo.Columns() var lockedCards []*entity.TSimCardInfo model := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).Safe(). Fields(columns.Id). Where(columns.TenantId, task.Batch.TenantID). WhereIn(columns.Id, cardIDs) if err := r.applyLock(model).Scan(&lockedCards); err != nil { return gerror.Wrap(err, "锁定达量断网配置卡档案失败") } if len(lockedCards) != len(cardIDs) { return gerror.New("部分达量断网配置卡档案已发生变化") } _, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).Safe(). Where(columns.TenantId, task.Batch.TenantID). WhereIn(columns.Id, cardIDs). Data(trafficCutoffConfigData(task)).Update() if err != nil { return gerror.Wrap(err, "保存达量断网配置失败") } return nil } func (r *Repository) updateDetails(ctx context.Context, tx gdb.TX, task *Task) error { for _, detail := range task.Details { if detail == nil { continue } if err := r.updateDetail(ctx, tx, task, detail); err != nil { return err } } return nil } func (r *Repository) updateDetail(ctx context.Context, tx gdb.TX, task *Task, detail *Detail) error { 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.BatchTypeTrafficCutoffConfig). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning). Data(do.BatchDetail{ DetailStatus: status, ErrorReason: detail.Reason, Snapshot: detail.Snapshot(task.Config), 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.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, resultFileFailed bool) 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.BatchTypeTrafficCutoffConfig). 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, resultFileFailed), 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.BatchTypeTrafficCutoffConfig, 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 || info.IsNil() { return Config{}, gerror.New("达量断网配置批次缺少批次参数") } status := info.Get("trafficCutoffStatus") if status.IsNil() { return Config{}, gerror.New("达量断网配置批次缺少trafficCutoffStatus") } config := Config{ TrafficCutoffCrossMonthHold: info.Get("trafficCutoffCrossMonthHold").Int(), TrafficCutoffStatus: status.Int(), TrafficCutoffThreshold: info.Get("trafficCutoffThreshold").Float64(), } if config.TrafficCutoffStatus != 0 && config.TrafficCutoffStatus != 1 && config.TrafficCutoffStatus != 2 { return Config{}, gerror.New("达量断网启停状态无效") } if config.TrafficCutoffThreshold < 0 { return Config{}, gerror.New("达量断网阈值不能小于0") } if config.TrafficCutoffStatus == 0 && config.TrafficCutoffThreshold != 0 { return Config{}, gerror.New("未配置达量断网时阈值必须为0") } if config.TrafficCutoffStatus == 1 && config.TrafficCutoffThreshold <= 0 { return Config{}, gerror.New("开启达量断网时阈值必须大于0") } if config.TrafficCutoffCrossMonthHold != 0 && config.TrafficCutoffCrossMonthHold != 1 { return Config{}, gerror.New("invalid cross-month traffic cutoff hold value") } if config.TrafficCutoffStatus != 1 && config.TrafficCutoffCrossMonthHold != 0 { return Config{}, gerror.New("cross-month hold requires traffic cutoff to be enabled") } rounded := math.Round(config.TrafficCutoffThreshold*1000) / 1000 if math.Abs(config.TrafficCutoffThreshold-rounded) > 0.0000001 { return Config{}, gerror.New("达量断网阈值最多保留3位小数") } return config, nil } func trafficCutoffConfigData(task *Task) do.TSimCardTrafficCutoffConfig { data := do.TSimCardTrafficCutoffConfig{ TrafficCutoffStatus: task.Config.TrafficCutoffStatus, TrafficCutoffThreshold: task.Config.TrafficCutoffThreshold, TrafficCutoffCrossMonthHold: task.Config.TrafficCutoffCrossMonthHold, UpdatedBy: task.Batch.CreatedBy, } if task.Config.TrafficCutoffStatus != 1 || task.Config.TrafficCutoffCrossMonthHold == 0 { data.TrafficCutoffTriggeredAt = gdb.Raw("NULL") } return data } func mergeBatchInfo(info *gjson.Json, ossID, downloadID int64, resultFileFailed bool) *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 resultFileFailed { data["resultFile"] = map[string]any{"status": "failed"} } else 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 { return consts.BatchAsyncStatusSuccess, consts.BatchRunResultSuccess } if successCount > 0 { return consts.BatchAsyncStatusPartialSuccess, consts.BatchRunResultPartialSuccess } return consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed } func successDetailCardIDs(details []*Detail) []int64 { ids := make([]int64, 0, len(details)) seen := make(map[int64]struct{}, len(details)) for _, detail := range details { if detail != nil && detail.Success() && detail.Card != nil && detail.Card.ID > 0 { if _, exists := seen[detail.Card.ID]; exists { continue } seen[detail.Card.ID] = struct{}{} ids = append(ids, detail.Card.ID) } } sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) return ids } func uniqueSortedStrings(values []string) []string { seen := make(map[string]struct{}, len(values)) out := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} out = append(out, value) } sort.Strings(out) return out }