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/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" ) func (r *Repository) FindRestartMain(ctx context.Context, batchNo string) (*entity.BatchMain, error) { 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.BatchTypeSimCardRestart). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Scan(&main); err != nil { return nil, gerror.Wrap(err, "query restart batch main") } return main, nil } func (r *Repository) LoadRestartStop(ctx context.Context, batchNo string) (*lifecycle.Task, error) { return r.loadRestartTask(ctx, batchNo, consts.BatchDetailStatusRunning, lifecycle.Operation{ BatchType: consts.BatchTypeSimCardRestart, OperType: consts.SimCardStatusOperTypeStop, ExpectedStatus: consts.SimBossCardStatusActivated, TargetStatus: consts.SimBossCardStatusStopped, }) } func (r *Repository) LoadRestartResume(ctx context.Context, batchNo string) (*lifecycle.Task, error) { return r.loadRestartTask(ctx, batchNo, consts.BatchDetailStatusRunning, lifecycle.Operation{ BatchType: consts.BatchTypeSimCardRestart, OperType: consts.SimCardStatusOperTypeStart, ExpectedStatus: consts.SimBossCardStatusStopped, TargetStatus: consts.SimBossCardStatusActivated, }) } func (r *Repository) loadRestartTask(ctx context.Context, batchNo string, detailStatus int, operation lifecycle.Operation) (*lifecycle.Task, error) { if batchNo == "" { return nil, gerror.New("restart batch number is empty") } batch := 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.BatchTypeSimCardRestart). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Scan(&batch); err != nil { return nil, gerror.Wrap(err, "query restart batch") } if batch.Id == 0 { return nil, gerror.New("processing restart batch does not exist") } 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.BatchTypeSimCardRestart). Where(dao.BatchDetail.Columns().DetailStatus, detailStatus). OrderAsc(dao.BatchDetail.Columns().RowNo). Scan(&batchDetails); err != nil { return nil, gerror.Wrap(err, "query restart details") } if len(batchDetails) == 0 { return nil, gerror.New("restart batch has no runnable 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 } task.Details = append(task.Details, restartDetail(batchDetail)) } if len(task.Details) == 0 { return nil, gerror.New("restart batch has no readable details") } if err := r.loadCards(ctx, task); err != nil { return nil, err } return task, nil } func restartDetail(batchDetail *entity.BatchDetail) *lifecycle.Detail { 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()) } return detail } func (r *Repository) SaveRestartValidationFailure(ctx context.Context, task *lifecycle.Task, stage string) error { return r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { for _, detail := range task.Details { if detail == nil { continue } if strings.TrimSpace(detail.Reason) == "" { detail.Reason = "重启校验未通过,未发起上游请求" } outcome := lifecycle.Outcome{Message: detail.Reason} 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.BatchTypeSimCardRestart). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning). Data(do.BatchDetail{ DetailStatus: consts.BatchDetailStatusFailed, ErrorCode: strconv.Itoa(outcome.Code), ErrorReason: detail.Reason, Snapshot: restartSnapshot(task, detail, outcome, stage, gtime.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.Newf("restart detail %d is no longer running", detail.ID) } } return nil }) } func (r *Repository) SaveRestartResult(ctx context.Context, task *lifecycle.Task, detail *lifecycle.Detail, outcome lifecycle.Outcome, stage string) (lifecycle.Outcome, error) { if detail == nil { return lifecycle.Outcome{}, gerror.New("restart detail is nil") } resolved := outcome now := gtime.Now() 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: 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().BatchType, consts.BatchTypeSimCardRestart). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning). Data(do.BatchDetail{ DetailStatus: detailStatus, ErrorCode: strconv.Itoa(resolved.Code), ErrorReason: detail.Reason, Snapshot: restartSnapshot(task, detail, resolved, stage, 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.Newf("restart detail %d is no longer running", detail.ID) } return nil }) return resolved, err } func (r *Repository) WaitRestartResume(ctx context.Context, task *lifecycle.Task, resumeAt *gtime.Time) error { info := buildRestartBatchInfo(task.Batch.BatchInfo) restart := restartInfo(info) restart["phase"] = consts.RestartPhaseWaitingResume restart["stopCompletedAt"] = gtime.Now().String() restart["resumeAt"] = resumeAt.String() restart["validationOK"] = true info = setRestartInfo(info, restart) return r.updateRestartInfo(ctx, task.Batch.BatchNo, info, task.Batch.CreatedBy) } func (r *Repository) ClaimRestartResume(ctx context.Context, batchNo string, now *gtime.Time) (claimed bool, err error) { main, err := r.FindRestartMain(ctx, batchNo) if err != nil { return false, err } if main == nil || restartPhase(main.BatchInfo) != consts.RestartPhaseWaitingResume { return false, nil } resumeAt := restartResumeAt(main.BatchInfo) if resumeAt == nil || resumeAt.After(now) { return false, nil } info := buildRestartBatchInfo(main.BatchInfo) restart := restartInfo(info) restart["phase"] = consts.RestartPhaseResuming restart["resumeStartedAt"] = now.String() info = setRestartInfo(info, restart) err = r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { mainColumns := dao.BatchMain.Columns() result, updateErr := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Where(mainColumns.BatchNo, batchNo). Where(mainColumns.BatchType, consts.BatchTypeSimCardRestart). Where(mainColumns.BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Where("JSON_UNQUOTE(JSON_EXTRACT("+mainColumns.BatchInfo+", '$.restart.phase')) = ?", consts.RestartPhaseWaitingResume). Data(do.BatchMain{ BatchInfo: info, UpdatedBy: main.CreatedBy, }).Update() if updateErr != nil { return updateErr } affected, affectedErr := result.RowsAffected() if affectedErr != nil { return affectedErr } if affected == 0 { return nil } if _, updateErr = tx.Ctx(ctx).Model(dao.BatchDetail.Table()). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypeSimCardRestart). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusSuccess). Data(do.BatchDetail{ DetailStatus: consts.BatchDetailStatusRunning, UpdatedBy: main.CreatedBy, }).Update(); updateErr != nil { return updateErr } claimed = true return nil }) return claimed, err } func (r *Repository) FinalizeRestart(ctx context.Context, batchNo string) error { main, err := r.FindRestartMain(ctx, batchNo) if err != nil { return err } if main == nil { return gerror.New("processing restart batch does not exist") } var details []*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.BatchTypeSimCardRestart). OrderAsc(dao.BatchDetail.Columns().RowNo). Scan(&details); err != nil { return gerror.Wrap(err, "query restart final details") } if len(details) == 0 { return gerror.New("restart batch has no final details") } successCount := 0 for _, detail := range details { if detail != nil && detail.DetailStatus == consts.BatchDetailStatusSuccess { successCount++ } } failCount := len(details) - successCount batchAsyncStatus, runResult := batchResult(successCount, failCount) failReason := "" if failCount > 0 { failReason = storedFailureSummary(details, successCount, failCount) } validCount := 0 if restartValidationOK(main.BatchInfo) { validCount = len(details) } artifact, err := r.artifactGenerator().Create(ctx, restartArtifactRequest(main, details)) if err != nil { return gerror.Wrap(err, "generate restart 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: main.TenantId, FileName: artifact.FileName, OriginalName: artifact.OriginalName, FileSuffix: artifact.FileSuffix, Path: artifact.Path, Url: artifact.URL, CreatedDept: main.CreatedDept, CreatedBy: main.CreatedBy, UpdatedBy: main.CreatedBy, Service: "local", Md5: artifact.MD5, FileSize: artifact.Size, }).InsertAndGetId() if err != nil { return gerror.Wrap(err, "insert restart result OSS") } downloadID, err := tx.Ctx(ctx).Model(dao.TResourceBatchDownload.Table()).Data(do.TResourceBatchDownload{ BatchNo: main.BatchNo, TaskName: batchresult.TaskName(consts.BatchTypeSimCardRestart), ResourceType: consts.ResourceTypeExcel, ExportScene: consts.BatchTypeSimCardRestart, TotalCount: len(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: main.CreatedDept, StartTime: main.StartTime, FinishTime: now, TenantId: main.TenantId, CreatedDept: main.CreatedDept, CreatedBy: main.CreatedBy, UpdatedBy: main.CreatedBy, }).InsertAndGetId() if err != nil { return gerror.Wrap(err, "insert restart result download") } result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Where(dao.BatchMain.Columns().BatchNo, main.BatchNo). Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimCardRestart). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Data(do.BatchMain{ BatchAsyncStatus: batchAsyncStatus, ValidCount: validCount, SuccessCount: successCount, FailCount: failCount, RunResult: runResult, FailReason: failReason, ResultOssId: lifecycleOssIDValue(ossID), BatchInfo: buildRestartFinishedInfo(main.BatchInfo, ossID, downloadID, now), EndTime: now, UpdatedBy: main.CreatedBy, }).Update() if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return gerror.New("restart batch is no longer processing") } return nil }) if err != nil { batchresult.RemoveLocalFile(artifact) return err } return nil } func (r *Repository) updateRestartInfo(ctx context.Context, batchNo string, info *gjson.Json, updatedBy int64) error { result, err := r.database().Model(dao.BatchMain.Table()).Ctx(ctx). Where(dao.BatchMain.Columns().BatchNo, batchNo). Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimCardRestart). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Data(do.BatchMain{ BatchInfo: info, UpdatedBy: updatedBy, }).Update() if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return gerror.New("restart batch info was not updated") } return nil } func restartArtifactRequest(main *entity.BatchMain, details []*entity.BatchDetail) batchresult.Request { rows := make([]batchresult.Row, 0, len(details)) for _, detail := range details { if detail == nil { continue } rows = append(rows, restartArtifactRow(detail)) } return batchresult.Request{BatchNo: main.BatchNo, BatchType: consts.BatchTypeSimCardRestart, TenantID: main.TenantId, Rows: rows} } func restartArtifactRow(detail *entity.BatchDetail) batchresult.Row { row := batchresult.Row{ RowNo: detail.RowNo, ICCID: strings.TrimSpace(detail.ItemId), Success: detail.DetailStatus == consts.BatchDetailStatusSuccess, Reason: strings.TrimSpace(detail.ErrorReason), } if detail.Snapshot != nil { if value := strings.TrimSpace(detail.Snapshot.Get("ICCID").String()); value != "" { row.ICCID = value } row.MSISDN = strings.TrimSpace(detail.Snapshot.Get("MSISDN").String()) row.IMSI = strings.TrimSpace(detail.Snapshot.Get("IMSI").String()) row.OrderNum = strings.TrimSpace(detail.Snapshot.Get("restart.resume.orderNum").String()) if row.OrderNum == "" { row.OrderNum = strings.TrimSpace(detail.Snapshot.Get("restart.stop.orderNum").String()) } } if code, err := strconv.Atoi(strings.TrimSpace(detail.ErrorCode)); err == nil { row.UpstreamCode = code } return row } func restartSnapshot(task *lifecycle.Task, detail *lifecycle.Detail, outcome lifecycle.Outcome, stage string, now *gtime.Time) *gjson.Json { base := snapshotMap(detail) base["ICCID"] = detail.ICCID base["inputMsisdn"] = detail.InputMSISDN base["inputImsi"] = detail.InputIMSI base["MSISDN"] = detail.MSISDN base["IMSI"] = detail.IMSI base["authorized"] = detail.Authorized base["chanKey"] = detail.ChanKey base["statusBefore"] = detail.StatusBefore base["orderNum"] = detail.OrderNum base["upstreamCode"] = detail.UpstreamCode base["upstreamMessage"] = detail.UpstreamReason restart := snapshotNestedMap(base["restart"]) statusAfter := detail.StatusBefore if outcome.Success { statusAfter = task.Operation.TargetStatus } restart[stage] = map[string]any{ "success": outcome.Success, "reason": detail.Reason, "upstreamCode": outcome.Code, "upstreamMessage": strings.TrimSpace(outcome.Message), "orderNum": strings.TrimSpace(outcome.OrderNum), "operType": task.Operation.OperType, "statusBefore": detail.StatusBefore, "statusAfter": statusAfter, "processedAt": now.String(), } base["restart"] = restart return gjson.New(base) } func snapshotMap(detail *lifecycle.Detail) map[string]any { if detail != nil && detail.OriginalSnapshot != nil { return copyMap(detail.OriginalSnapshot.Map()) } if detail != nil { return copyMap(detail.Snapshot().Map()) } return map[string]any{} } func snapshotNestedMap(value any) map[string]any { if nested, ok := value.(map[string]any); ok { return copyMap(nested) } return map[string]any{} } func buildRestartBatchInfo(info *gjson.Json) *gjson.Json { data := copyMap(nil) if info != nil { data = copyMap(info.Map()) } data["operation"] = "restart" data["stopOperType"] = consts.SimCardStatusOperTypeStop data["resumeOperType"] = consts.SimCardStatusOperTypeStart if _, ok := data["resumeDelaySeconds"]; !ok { data["resumeDelaySeconds"] = 300 } data["restart"] = restartInfo(info) return gjson.New(data) } func buildRestartFinishedInfo(info *gjson.Json, ossID, downloadID int64, finishedAt *gtime.Time) *gjson.Json { updated := buildRestartBatchInfo(info) restart := restartInfo(updated) restart["phase"] = consts.RestartPhaseFinished restart["finishedAt"] = finishedAt.String() data := updated.Map() data["resultFileOssId"] = ossID data["resultDownloadId"] = downloadID data["restart"] = restart return gjson.New(data) } func setRestartInfo(info *gjson.Json, restart map[string]any) *gjson.Json { data := map[string]any{} if info != nil { data = copyMap(info.Map()) } data["restart"] = restart return gjson.New(data) } func restartInfo(info *gjson.Json) map[string]any { if info == nil { return map[string]any{} } return copyMap(info.Get("restart").Map()) } func restartPhase(info *gjson.Json) string { if info == nil { return "" } return strings.TrimSpace(info.Get("restart.phase").String()) } func restartResumeAt(info *gjson.Json) *gtime.Time { if info == nil { return nil } value := strings.TrimSpace(info.Get("restart.resumeAt").String()) if value == "" { return nil } parsed := gtime.NewFromStr(value) if parsed == nil || parsed.IsZero() { return nil } return parsed } func restartValidationOK(info *gjson.Json) bool { if info == nil { return false } return info.Get("restart.validationOK").Bool() } func copyMap(source map[string]any) map[string]any { copied := make(map[string]any, len(source)) for key, value := range source { copied[key] = value } return copied }