package lifecycle import ( "context" "strconv" "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" lifecycle "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/lifecycle" ) type Repository struct { db gdb.DB resultFiles batchresult.Generator } func New() *Repository { return &Repository{resultFiles: 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) (*lifecycle.Task, error) { if batchNo == "" { return nil, gerror.New("lifecycle batch number is empty") } batch := entity.BatchMain{} if err := r.database().Model(dao.BatchMain.Table()).Safe().Ctx(ctx). Where(dao.BatchMain.Columns().BatchNo, batchNo). WhereIn(dao.BatchMain.Columns().BatchType, []int{consts.BatchTypeSimCardStart, consts.BatchTypeSimCardStop}). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Scan(&batch); err != nil { return nil, gerror.Wrap(err, "query lifecycle batch") } if batch.Id == 0 { return nil, gerror.New("processing lifecycle batch does not exist") } operation, err := lifecycle.OperationByBatchType(batch.BatchType) 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, batch.BatchType). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning). OrderAsc(dao.BatchDetail.Columns().RowNo). Scan(&batchDetails); err != nil { return nil, gerror.Wrap(err, "query running lifecycle details") } if len(batchDetails) == 0 { return nil, gerror.New("lifecycle batch has no running details") } task := &lifecycle.Task{ Batch: lifecycle.Batch{ ID: batch.Id, BatchNo: batch.BatchNo, BatchType: batch.BatchType, TenantID: batch.TenantId, CreatedBy: batch.CreatedBy, CreatedDept: batch.CreatedDept, StartTime: batch.StartTime, BatchInfo: batch.BatchInfo, }, Operation: operation, Details: make([]*lifecycle.Detail, 0, len(batchDetails)), } for _, batchDetail := range batchDetails { if batchDetail == nil { continue } detail := &lifecycle.Detail{ ID: batchDetail.Id, RowNo: batchDetail.RowNo, ICCID: strings.TrimSpace(batchDetail.ItemId), Reason: strings.TrimSpace(batchDetail.ErrorReason), OriginalSnapshot: batchDetail.Snapshot, } if batchDetail.Snapshot != nil { if value := strings.TrimSpace(batchDetail.Snapshot.Get("ICCID").String()); value != "" { detail.ICCID = value } detail.InputMSISDN = strings.TrimSpace(batchDetail.Snapshot.Get("MSISDN").String()) detail.InputIMSI = strings.TrimSpace(batchDetail.Snapshot.Get("IMSI").String()) detail.MSISDN = detail.InputMSISDN detail.IMSI = detail.InputIMSI detail.Authorized = batchDetail.Snapshot.Get("authorized").Bool() detail.ChanKey = strings.TrimSpace(batchDetail.Snapshot.Get("chanKey").String()) } task.Details = append(task.Details, detail) } if len(task.Details) == 0 { return nil, gerror.New("lifecycle batch has no readable running details") } if err = r.loadCards(ctx, task); err != nil { return nil, err } return task, nil } func (r *Repository) loadCards(ctx context.Context, task *lifecycle.Task) error { iccids := make([]string, 0, len(task.Details)) for _, detail := range task.Details { if detail != nil && detail.ICCID != "" { iccids = append(iccids, detail.ICCID) } } if len(iccids) == 0 { return nil } var cards []*entity.TSimCardInfo if err := r.database().Model(dao.TSimCardInfo.Table()).Safe().Ctx(ctx). Where(dao.TSimCardInfo.Columns().TenantId, task.Batch.TenantID). WhereIn(dao.TSimCardInfo.Columns().Iccid, iccids). Scan(&cards); err != nil { return gerror.Wrap(err, "query lifecycle SIM cards") } 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 { card := byICCID[detail.ICCID] if card == nil { continue } detail.Card = &lifecycle.Card{ ID: card.Id, ICCID: strings.TrimSpace(card.Iccid), MSISDN: strings.TrimSpace(card.Msisdn), IMSI: strings.TrimSpace(card.Imsi), ChanKey: preferredLifecycleChanKey(appBatchUsesSnapshotChannel(task.Batch.BatchInfo), detail.ChanKey, card.ChanKey), BossCardStatus: card.BossCardStatus, } } return nil } func preferredLifecycleChanKey(useSnapshot bool, snapshotChanKey string, cardChanKey string) string { if useSnapshot { if value := strings.TrimSpace(snapshotChanKey); value != "" { return value } } return strings.TrimSpace(cardChanKey) } func appBatchUsesSnapshotChannel(info *gjson.Json) bool { if info == nil { return false } return strings.EqualFold(strings.TrimSpace(info.Get("source").String()), "app") } func (r *Repository) SaveValidationFailure(ctx context.Context, task *lifecycle.Task) error { return r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { for _, detail := range task.Details { if detail == nil { continue } 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().DetailStatus, consts.BatchDetailStatusRunning). Data(do.BatchDetail{ DetailStatus: consts.BatchDetailStatusFailed, ErrorReason: detail.Reason, Snapshot: detail.Snapshot(), UpdatedBy: task.Batch.CreatedBy, }).Update() if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return gerror.Newf("lifecycle detail %d is no longer running", detail.ID) } } return nil }) } func (r *Repository) SaveResult(ctx context.Context, task *lifecycle.Task, detail *lifecycle.Detail, outcome lifecycle.Outcome) (lifecycle.Outcome, error) { if detail == nil { return lifecycle.Outcome{}, gerror.New("lifecycle detail is nil") } resolved := outcome err := r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { if resolved.Success { if detail.Card == nil { resolved.Success = false resolved.Message = "卡记录不存在,未办理" } else { result, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()). Where(dao.TSimCardInfo.Columns().Id, detail.Card.ID). Where(dao.TSimCardInfo.Columns().TenantId, task.Batch.TenantID). Where(dao.TSimCardInfo.Columns().BossCardStatus, detail.StatusBefore). Data(do.TSimCardInfo{ BossCardStatus: task.Operation.TargetStatus, StatusSyncTime: gtime.Now(), UpdatedBy: task.Batch.CreatedBy, }).Update() if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { resolved.Success = false resolved.Message = "卡本地状态已变更" } } } detail.ApplyOutcome(resolved) detailStatus := consts.BatchDetailStatusFailed if resolved.Success { detailStatus = 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().DetailStatus, consts.BatchDetailStatusRunning). Data(do.BatchDetail{ DetailStatus: detailStatus, ErrorCode: strconv.Itoa(resolved.Code), ErrorReason: detail.Reason, Snapshot: detail.Snapshot(), UpdatedBy: task.Batch.CreatedBy, }).Update() if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return gerror.Newf("lifecycle detail %d is no longer running", detail.ID) } return nil }) return resolved, err } func (r *Repository) Finalize(ctx context.Context, task *lifecycle.Task) error { successCount := 0 for _, detail := range task.Details { if detail != nil && detail.Reason == "" { successCount++ } } failCount := len(task.Details) - successCount batchAsyncStatus, runResult := batchResult(successCount, failCount) if task.FailureReason == "" && failCount > 0 { task.FailureReason = lifecycleFailureSummary(task.Details, successCount, failCount) } validCount := 0 if task.ValidationOK { validCount = len(task.Details) } artifact, err := r.artifactGenerator().Create(ctx, artifactRequest(task)) if err != nil { return gerror.Wrap(err, "generate lifecycle result file") } now := gtime.Now() err = r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) 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 gerror.Wrap(err, "insert lifecycle result 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: task.Batch.BatchType, 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 gerror.Wrap(err, "insert lifecycle result download") } result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Where(dao.BatchMain.Columns().BatchNo, task.Batch.BatchNo). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Data(do.BatchMain{ BatchAsyncStatus: batchAsyncStatus, ValidCount: validCount, SuccessCount: successCount, FailCount: failCount, RunResult: runResult, FailReason: task.FailureReason, ResultOssId: lifecycleOssIDValue(ossID), BatchInfo: buildBatchInfo(task, ossID, downloadID), EndTime: now, UpdatedBy: task.Batch.CreatedBy, }).Update() if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return gerror.New("lifecycle batch is no longer processing") } return nil }) if err != nil { batchresult.RemoveLocalFile(artifact) return err } return nil } func (r *Repository) artifactGenerator() batchresult.Generator { if r.resultFiles != nil { return r.resultFiles } return batchresult.New() } func artifactRequest(task *lifecycle.Task) batchresult.Request { rows := make([]batchresult.Row, 0, len(task.Details)) for _, detail := range task.Details { if detail == nil { continue } rows = append(rows, batchresult.Row{ RowNo: detail.RowNo, ICCID: detail.ICCID, MSISDN: detail.MSISDN, IMSI: detail.IMSI, Success: detail.Reason == "", Reason: detail.Reason, UpstreamCode: detail.UpstreamCode, OrderNum: detail.OrderNum, }) } return batchresult.Request{BatchNo: task.Batch.BatchNo, BatchType: task.Batch.BatchType, TenantID: task.Batch.TenantID, Rows: rows} } func buildBatchInfo(task *lifecycle.Task, ossID, downloadID int64) *gjson.Json { info := make(map[string]any) if task.Batch.BatchInfo != nil { for key, value := range task.Batch.BatchInfo.Map() { info[key] = value } } operation := "start" if task.Batch.BatchType == consts.BatchTypeSimCardStop { operation = "stop" } info["operation"] = operation info["operType"] = task.Operation.OperType info["resultFileOssId"] = ossID info["resultDownloadId"] = downloadID return gjson.New(info) } func lifecycleOssIDValue(ossID int64) any { if ossID <= 0 { return nil } return ossID } func batchResult(successCount, failCount int) (int, int) { switch { case failCount == 0: return consts.BatchAsyncStatusSuccess, consts.BatchRunResultSuccess case successCount == 0: return consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed default: return consts.BatchAsyncStatusPartialSuccess, consts.BatchRunResultPartialSuccess } }