package app import ( "context" "strings" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchmetadata" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity" "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" ) type appCardLifecycleOperation struct { Code string Name string BatchType int BatchPrefix string TargetStatus int } type appCardLifecycleItem struct { Card *appCardQueryRow LockedCard *entity.TSimCardInfo RowNo int StatusBefore int } type appCardLifecycleExecutionRow struct { DetailID int64 RowNo int Iccid string Msisdn string Imsi string Imei string Authorized bool ChanKey string StatusBefore int OrderNum string UpstreamCode int UpstreamMessage string Reason string Card *entity.TSimCardInfo } const ( appLifecycleRestartDelaySeconds = 300 appLifecycleRestartPhaseCreated = "created" ) // Restart 兼容原重启接口。停机、复机和重启优先投递 worker;未启用 worker 时保留本地挡板降级。 func (s *sAppCard) Restart(ctx context.Context, param *model.AppCardRestartParam) (res *model.AppCardRestartModel, err error) { if param == nil { param = &model.AppCardRestartParam{} } lifecycle, err := s.LifecycleBatchSubmit(ctx, &model.AppCardLifecycleParam{ CardValue: param.CardValue, Operation: "restart", Remark: param.Remark, }) if err != nil { return nil, err } var card *model.AppCardModel if lifecycle != nil && len(lifecycle.Cards) > 0 { card = lifecycle.Cards[0] } return &model.AppCardRestartModel{ BatchNo: lifecycle.BatchNo, BatchType: lifecycle.BatchType, Status: lifecycle.Status, Card: card, }, nil } func (s *sAppCard) Stop(ctx context.Context, param *model.AppCardRestartParam) (res *model.AppCardLifecycleModel, err error) { if param == nil { param = &model.AppCardRestartParam{} } return s.LifecycleBatchSubmit(ctx, &model.AppCardLifecycleParam{ CardValue: param.CardValue, Operation: "stop", Remark: param.Remark, }) } func (s *sAppCard) Start(ctx context.Context, param *model.AppCardRestartParam) (res *model.AppCardLifecycleModel, err error) { if param == nil { param = &model.AppCardRestartParam{} } return s.LifecycleBatchSubmit(ctx, &model.AppCardLifecycleParam{ CardValue: param.CardValue, Operation: "start", Remark: param.Remark, }) } // LifecycleBatchSubmit 提交卡片停机、复机和重启批次。 // 终端用户仅允许重启;停机和复机仍然只开放给企业用户。 func (s *sAppCard) LifecycleBatchSubmit(ctx context.Context, param *model.AppCardLifecycleParam) (res *model.AppCardLifecycleModel, err error) { if param == nil { param = &model.AppCardLifecycleParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } if s.isTerminalUser(ctx, user) && strings.TrimSpace(strings.ToLower(param.Operation)) != "restart" { return nil, gerror.New("终端用户暂不能使用卡片停机和复机功能") } if user.DeptId <= 0 { return nil, gerror.New("当前账号未绑定企业,不能操作卡片") } operation, err := appCardLifecycleOperationByCode(param.Operation) if err != nil { return nil, err } if appCardLifecycleIsDataService(operation) && !appWorkerBatchAsyncEnabled(ctx) { return nil, gerror.New("数据服务开停依赖异步执行服务,当前环境未启用") } if err = appEnsureFeatureAccess(ctx, "card:"+operation.Code); err != nil { return nil, err } cardValues, mode, err := appNormalizeLifecycleCardValues(param.CardValue, param.CardValues) if err != nil { return nil, err } if len(cardValues) == 0 { return nil, gerror.New("请选择需要操作的卡") } if len(cardValues) > 100 { return nil, gerror.New("单次最多支持100张卡") } now := gtime.Now() batchNo, err := appNextBatchNo(ctx, operation.BatchPrefix) if err != nil { return nil, err } items := make([]*appCardLifecycleItem, 0, len(cardValues)) err = dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { seen := make(map[string]struct{}, len(cardValues)) for index, value := range cardValues { item, err := s.lockLifecycleCard(ctx, tx, user, value, index+2, operation) if err != nil { return err } if _, exists := seen[item.Card.Iccid]; exists { return gerror.Newf("批量操作卡重复:%s", item.Card.Iccid) } seen[item.Card.Iccid] = struct{}{} items = append(items, item) } return s.insertPendingLifecycleBatch(ctx, tx, user, items, operation, batchNo, strings.TrimSpace(param.Remark), mode, now) }) if err != nil { return nil, err } if appWorkerBatchAsyncEnabled(ctx) && appLifecycleShouldPublishWorker(operation) { if err = s.publishWorkerBatchExecute(ctx, batchNo, operation.BatchType); err != nil { _ = s.markLifecycleBatchFailed(ctx, batchNo, err.Error()) return nil, err } } else if err = s.executeMockLifecycleBatch(ctx, batchNo); err != nil { return nil, err } ownerType := appCardOwnerAgent if s.isTerminalUser(ctx, user) { ownerType = appCardOwnerTerminal } cards, err := s.loadLifecycleCardsForResponse(ctx, user, items, ownerType) if err != nil { return nil, err } status, err := s.loadLifecycleBatchStatus(ctx, batchNo) if err != nil { return nil, err } return &model.AppCardLifecycleModel{ BatchNo: batchNo, BatchType: operation.BatchType, Operation: operation.Code, Status: status, Quantity: len(cards), SuccessCount: 0, FailCount: 0, Cards: cards, }, nil } func appLifecycleShouldPublishWorker(operation appCardLifecycleOperation) bool { switch operation.BatchType { case consts.BatchTypeStop, consts.BatchTypeStart, consts.BatchTypeRestart, consts.BatchTypeDataStart, consts.BatchTypeDataStop: return true default: return false } } func (s *sAppCard) LifecycleBatchStatus(ctx context.Context, param *model.AppCardLifecycleStatusParam) (res *model.AppCardLifecycleModel, err error) { if param == nil { param = &model.AppCardLifecycleStatusParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } batchNo := strings.TrimSpace(param.BatchNo) if batchNo == "" { return nil, gerror.New("批次号不能为空") } var batch *entity.BatchMain columns := dao.BatchMain.Columns() if err = dao.BatchMain.Ctx(ctx). Fields( columns.BatchNo, columns.BatchType, columns.BatchStatus, columns.BatchAsyncStatus, columns.TotalCount, columns.SuccessCount, columns.FailCount, columns.FailReason, columns.TenantId, columns.CreatedBy, ). Where(columns.BatchNo, batchNo). Where(columns.TenantId, user.TenantId). Where(columns.CreatedBy, user.UserId). Limit(1). Scan(&batch); err != nil { return nil, gerror.Wrap(err, "查询卡片操作批次状态失败") } if batch == nil || batch.BatchNo == "" { return nil, gerror.New("卡片操作批次不存在或无权查看") } operation, err := appCardLifecycleOperationByBatchType(batch.BatchType) if err != nil { return nil, err } if err = appEnsureFeatureAccess(ctx, "card:"+operation.Code); err != nil { return nil, err } status := appLifecycleBatchStatus(batch.BatchStatus, batch.BatchAsyncStatus) return &model.AppCardLifecycleModel{ BatchNo: batch.BatchNo, BatchType: batch.BatchType, Operation: operation.Code, Status: status, Quantity: batch.TotalCount, SuccessCount: batch.SuccessCount, FailCount: batch.FailCount, FailReason: appLifecycleFailReasonForResponse(status, batch.FailReason), Cards: []*model.AppCardModel{}, }, nil } func appLifecycleFailReasonForResponse(status string, reason string) string { reason = strings.TrimSpace(reason) if reason == "" { return "" } lower := strings.ToLower(reason) internalMarkers := []string{ "save data service detail", "save lifecycle detail", "update `", "insert into", "delete from", "select ", "error 10", "sqlstate", "column '", "stack:", } for _, marker := range internalMarkers { if strings.Contains(lower, marker) { return "后台执行失败,请稍后重试;如持续失败,请联系管理员并提供批次号" } } if len([]rune(reason)) > 160 { return "后台执行失败,请稍后重试;如持续失败,请联系管理员并提供批次号" } if status == "failed" && (reason == "1 SIM cards failed" || reason == "1 SIM card failed") { return "卡片操作执行失败,请稍后重试" } return reason } func (s *sAppCard) prepareLifecycleCard(ctx context.Context, user *entity.SysUser, cardValue string, rowNo int) (*appCardLifecycleItem, error) { card, err := s.findVisibleCard(ctx, user, cardValue) if err != nil { return nil, err } if card == nil { return nil, gerror.Newf("卡%s不存在或不在当前企业可操作范围内", strings.TrimSpace(cardValue)) } if err = s.ensureOperateCard(ctx, user, card, "操作"); err != nil { return nil, err } return &appCardLifecycleItem{ Card: card, RowNo: rowNo, StatusBefore: card.BossCardStatus, }, nil } func (s *sAppCard) lockLifecycleCard(ctx context.Context, tx gdb.TX, user *entity.SysUser, cardValue string, rowNo int, operation appCardLifecycleOperation) (*appCardLifecycleItem, error) { card, err := s.findVisibleCard(ctx, user, cardValue) if err != nil { return nil, err } if card == nil { return nil, gerror.Newf("卡%s不存在或不在当前企业可操作范围内", strings.TrimSpace(cardValue)) } if err = s.ensureOperateCard(ctx, user, card, operation.Name); err != nil { return nil, err } channelInfo, err := s.resolveLifecycleCardChannelInfo(ctx, user.TenantId, card.Iccid, card.ChanKey) if err != nil { return nil, gerror.Wrapf(err, "解析卡%s通道失败", appFirstNotEmpty(card.Iccid, cardValue)) } if !appCardChannelAvailable(channelInfo) { return nil, gerror.Newf("此卡暂未收录,暂不能%s:%s", operation.Name, appFirstNotEmpty(card.Iccid, cardValue)) } var locked *entity.TSimCardInfo err = dao.TSimCardInfo.Ctx(ctx).TX(tx). Where(dao.TSimCardInfo.Columns().Id, card.Id). Where(dao.TSimCardInfo.Columns().TenantId, user.TenantId). LockUpdate(). Limit(1). Scan(&locked) if err != nil { return nil, gerror.Wrapf(err, "锁定卡%s失败", appFirstNotEmpty(card.Iccid, cardValue)) } if locked == nil || locked.Id == 0 { return nil, gerror.Newf("卡%s信息已变化,请刷新后重试", appFirstNotEmpty(card.Iccid, cardValue)) } if locked.DeptId != card.DeptId || strings.TrimSpace(locked.Iccid) != strings.TrimSpace(card.Iccid) { return nil, gerror.Newf("卡%s归属已变化,请刷新后重试", card.Iccid) } card.TSimCardInfo = *locked card.ChanKey = channelInfo.ChanKey card.ChanName = channelInfo.ChanName card.ChanAlias = channelInfo.ChanAlias card.TempKey = channelInfo.TempKey if err = s.ensureLifecycleCardStatus(card, operation); err != nil { return nil, err } statusBefore := locked.BossCardStatus if appCardLifecycleIsDataService(operation) { statusBefore = locked.DataServiceStatus } return &appCardLifecycleItem{ Card: card, LockedCard: locked, RowNo: rowNo, StatusBefore: statusBefore, }, nil } func (s *sAppCard) ensureLifecycleCardStatus(card *appCardQueryRow, operation appCardLifecycleOperation) error { if card == nil { return gerror.New("卡信息不存在") } cardLabel := appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei) switch operation.Code { case "stop": if card.BossCardStatus != consts.V3ApiSimBossCardStatusActivated { return gerror.Newf("仅已激活状态的卡可以停机:%s", cardLabel) } case "start": if card.BossCardStatus != consts.V3ApiSimBossCardStatusStopped { return gerror.Newf("仅停机状态的卡可以复机:%s", cardLabel) } case "restart": if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped { return gerror.Newf("停机卡不能重启,请先复机:%s", cardLabel) } case "dataStart": if card.DataServiceStatus == 1 { return gerror.Newf("卡片数据服务已经开启:%s", cardLabel) } case "dataStop": if card.DataServiceStatus == 0 { return gerror.Newf("卡片数据服务已经关闭:%s", cardLabel) } } return nil } func (s *sAppCard) applyMockLifecycleCards(ctx context.Context, tx gdb.TX, user *entity.SysUser, items []*appCardLifecycleItem, operation appCardLifecycleOperation, now *gtime.Time) error { if operation.Code == "restart" { return nil } for _, item := range items { result, err := dao.TSimCardInfo.Ctx(ctx).TX(tx). Where(dao.TSimCardInfo.Columns().Id, item.LockedCard.Id). Where(dao.TSimCardInfo.Columns().BossCardStatus, item.StatusBefore). Data(do.TSimCardInfo{ BossCardStatus: operation.TargetStatus, StatusSyncTime: now, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrapf(err, "更新卡%s状态失败", item.Card.Iccid) } affected, err := result.RowsAffected() if err != nil { return gerror.Wrap(err, "读取卡状态更新数量失败") } if affected != 1 { return gerror.Newf("卡%s状态已变化,请刷新后重试", item.Card.Iccid) } item.Card.BossCardStatus = operation.TargetStatus item.Card.StatusSyncTime = now } return nil } func (s *sAppCard) insertPendingLifecycleBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, items []*appCardLifecycleItem, operation appCardLifecycleOperation, batchNo string, remark string, executionMode int, now *gtime.Time) error { quantity := len(items) _, err := dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{ BatchNo: batchNo, BatchType: operation.BatchType, BatchStatus: consts.BatchAsyncStatusPending, BatchAsyncStatus: consts.BatchAsyncStatusPending, SourceType: consts.BatchSourceTypeBatchSelect, SourceChannel: batchmetadata.MiniProgram, ExecutionMode: batchmetadata.NormalizeMode(executionMode), DeptId: user.DeptId, TotalCount: quantity, ValidCount: 0, SuccessCount: 0, FailCount: 0, RunResult: consts.BatchRunResultPending, BatchInfo: appLifecycleBatchInfo(operation, items, batchNo, remark, now), Remark: gjson.New(map[string]any{"remark": remark}), TenantId: user.TenantId, CreatedDept: user.DeptId, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).Insert() if err != nil { return gerror.Wrap(err, "写入待处理卡片操作批次主表失败") } details := make([]do.BatchDetail, 0, quantity) for _, item := range items { details = append(details, do.BatchDetail{ BatchNo: batchNo, BatchType: operation.BatchType, DetailStatus: consts.BatchDetailStatusPending, BlankOutStatus: consts.BlankOutStatusNo, RowNo: item.RowNo, ItemId: item.Card.Iccid, Snapshot: appLifecyclePendingDetailSnapshot(operation, item, batchNo), TenantId: user.TenantId, CreatedDept: user.DeptId, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }) } if _, err = dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert(); err != nil { return gerror.Wrap(err, "写入待处理卡片操作批次明细失败") } return nil } func (s *sAppCard) executeMockLifecycleBatch(ctx context.Context, batchNo string) error { claimed, err := s.claimMockLifecycleBatch(ctx, batchNo) if err != nil { return err } if !claimed { return nil } batch, operation, rows, err := s.loadMockLifecycleTask(ctx, batchNo) if err != nil { _ = s.markLifecycleBatchFailed(ctx, batchNo, err.Error()) return err } validationOK, failureReason, err := s.validateMockLifecycleRows(ctx, batch, operation, rows) if err != nil { _ = s.markLifecycleBatchFailed(ctx, batchNo, err.Error()) return err } if !validationOK { if err = s.saveMockLifecycleRows(ctx, batch, operation, rows); err != nil { return err } return s.finalizeMockLifecycleBatch(ctx, batch, operation, rows, validationOK, failureReason) } var upstream appSimUpstreamProvider if operation.Code == "stop" || operation.Code == "start" { upstream, err = newAppSimUpstreamProvider(ctx) if err != nil { for _, row := range rows { if row != nil { row.Reason = err.Error() } } if saveErr := s.saveMockLifecycleRows(ctx, batch, operation, rows); saveErr != nil { return saveErr } return s.finalizeMockLifecycleBatch(ctx, batch, operation, rows, validationOK, "OneLink挡板配置不可用") } } for _, row := range rows { if err = s.applyMockLifecycleRow(ctx, batch, operation, row, upstream); err != nil { row.Reason = err.Error() } } if err = s.saveMockLifecycleRows(ctx, batch, operation, rows); err != nil { return err } return s.finalizeMockLifecycleBatch(ctx, batch, operation, rows, validationOK, "") } func (s *sAppCard) claimMockLifecycleBatch(ctx context.Context, batchNo string) (bool, error) { claimed := false err := dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { now := gtime.Now() result, err := dao.BatchMain.Ctx(ctx).TX(tx). Where(dao.BatchMain.Columns().BatchNo, batchNo). WhereIn(dao.BatchMain.Columns().BatchType, []int{consts.BatchTypeStart, consts.BatchTypeStop, consts.BatchTypeRestart, consts.BatchTypeDataStart, consts.BatchTypeDataStop}). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusPending). Data(do.BatchMain{ BatchStatus: consts.BatchAsyncStatusProcessing, BatchAsyncStatus: consts.BatchAsyncStatusProcessing, RunResult: consts.BatchRunResultRunning, StartTime: now, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrap(err, "抢占卡片操作批次失败") } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return nil } claimed = true _, err = dao.BatchDetail.Ctx(ctx).TX(tx). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusPending). Data(do.BatchDetail{ DetailStatus: consts.BatchDetailStatusRunning, UpdatedAt: now, }). Update() return gerror.Wrap(err, "更新卡片操作批次明细为处理中失败") }) return claimed, err } func (s *sAppCard) loadMockLifecycleTask(ctx context.Context, batchNo string) (*entity.BatchMain, appCardLifecycleOperation, []*appCardLifecycleExecutionRow, error) { var batch *entity.BatchMain if err := dao.BatchMain.Ctx(ctx). Where(dao.BatchMain.Columns().BatchNo, batchNo). Limit(1). Scan(&batch); err != nil { return nil, appCardLifecycleOperation{}, nil, gerror.Wrap(err, "读取卡片操作批次失败") } if batch == nil || batch.Id == 0 { return nil, appCardLifecycleOperation{}, nil, gerror.New("卡片操作批次不存在") } operation, err := appCardLifecycleOperationByBatchType(batch.BatchType) if err != nil { return nil, appCardLifecycleOperation{}, nil, err } var details []*entity.BatchDetail if err = dao.BatchDetail.Ctx(ctx). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().BatchType, batch.BatchType). OrderAsc(dao.BatchDetail.Columns().RowNo). Scan(&details); err != nil { return nil, appCardLifecycleOperation{}, nil, gerror.Wrap(err, "读取卡片操作批次明细失败") } if len(details) == 0 { return nil, appCardLifecycleOperation{}, nil, gerror.New("卡片操作批次没有明细") } rows := make([]*appCardLifecycleExecutionRow, 0, len(details)) for _, detail := range details { row := &appCardLifecycleExecutionRow{ DetailID: detail.Id, RowNo: detail.RowNo, Iccid: strings.TrimSpace(detail.ItemId), Reason: strings.TrimSpace(detail.ErrorReason), Authorized: true, } 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.Imei = strings.TrimSpace(detail.Snapshot.Get("IMEI").String()) row.Authorized = detail.Snapshot.Get("authorized").Bool() } rows = append(rows, row) } return batch, operation, rows, nil } func (s *sAppCard) validateMockLifecycleRows(ctx context.Context, batch *entity.BatchMain, operation appCardLifecycleOperation, rows []*appCardLifecycleExecutionRow) (bool, string, error) { iccids := make([]string, 0, len(rows)) for _, row := range rows { if row != nil && row.Iccid != "" { iccids = append(iccids, row.Iccid) } } cards := make(map[string]*entity.TSimCardInfo, len(iccids)) if len(iccids) > 0 { var cardList []*entity.TSimCardInfo if err := dao.TSimCardInfo.Ctx(ctx). Where(dao.TSimCardInfo.Columns().TenantId, batch.TenantId). WhereIn(dao.TSimCardInfo.Columns().Iccid, iccids). Scan(&cardList); err != nil { return false, "", err } for _, card := range cardList { if card != nil { cards[strings.TrimSpace(card.Iccid)] = card } } } hasFailure := false for _, row := range rows { if row == nil { hasFailure = true continue } card := cards[row.Iccid] legacyChanKey := strings.TrimSpace(row.ChanKey) if legacyChanKey == "" && card != nil { legacyChanKey = strings.TrimSpace(card.ChanKey) } channelInfo, err := s.loadCardChannelInfo(ctx, batch.TenantId, row.Iccid, legacyChanKey) if err != nil { row.Reason = err.Error() hasFailure = true continue } switch { case row.Reason != "": case card == nil: row.Reason = "ICCID不存在于正式卡记录" case !row.Authorized: row.Reason = "ICCID不在当前操作人的数据权限范围内" case channelInfo == nil || strings.TrimSpace(channelInfo.ChanKey) == "": row.Reason = "ICCID缺少通道标识chanKey" case row.Msisdn != "" && row.Msisdn != strings.TrimSpace(card.Msisdn): row.Reason = "MSISDN与正式卡记录不一致" case row.Imsi != "" && row.Imsi != strings.TrimSpace(card.Imsi): row.Reason = "IMSI与正式卡记录不一致" default: row.Card = card row.Msisdn = strings.TrimSpace(card.Msisdn) row.Imsi = strings.TrimSpace(card.Imsi) row.Imei = strings.TrimSpace(card.Imei) row.ChanKey = strings.TrimSpace(channelInfo.ChanKey) row.StatusBefore = card.BossCardStatus row.Reason = appLifecycleStatusError(operation, row, card) } if row.Reason != "" { hasFailure = true } } if !hasFailure { return true, "", nil } for _, row := range rows { if row != nil && row.Reason == "" { row.Reason = "同批次存在校验失败卡,本卡未请求上游" } } return false, "整批业务校验失败,未请求上游", nil } func appLifecycleStatusError(operation appCardLifecycleOperation, row *appCardLifecycleExecutionRow, card *entity.TSimCardInfo) string { cardLabel := row.Iccid if cardLabel == "" && card != nil { cardLabel = appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei) } switch operation.Code { case "stop": if card.BossCardStatus != consts.V3ApiSimBossCardStatusActivated { return "仅已激活状态的卡可以自主停机:" + cardLabel } case "start": if card.BossCardStatus != consts.V3ApiSimBossCardStatusStopped { return "仅停机状态的卡可以自主复机:" + cardLabel } case "restart": if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped { return "停机卡不能重启,请先复机:" + cardLabel } } return "" } func (s *sAppCard) applyMockLifecycleRow(ctx context.Context, batch *entity.BatchMain, operation appCardLifecycleOperation, row *appCardLifecycleExecutionRow, upstream appSimUpstreamProvider) error { if operation.Code == "restart" { row.UpstreamCode = 0 row.UpstreamMessage = "重启操作本地模拟成功" return nil } if upstream == nil { return gerror.New("OneLink挡板客户端未初始化") } outcome, err := upstream.ChangeSimStatus(ctx, operation, row) if err != nil { return err } row.OrderNum = outcome.OrderNum row.UpstreamCode = outcome.Code row.UpstreamMessage = outcome.Message if !outcome.Success { if strings.TrimSpace(row.UpstreamMessage) == "" { row.UpstreamMessage = "OneLink挡板处理失败" } row.Reason = row.UpstreamMessage return nil } now := gtime.Now() result, err := dao.TSimCardInfo.Ctx(ctx). Where(dao.TSimCardInfo.Columns().Id, row.Card.Id). Where(dao.TSimCardInfo.Columns().TenantId, batch.TenantId). Where(dao.TSimCardInfo.Columns().BossCardStatus, row.StatusBefore). Data(do.TSimCardInfo{ BossCardStatus: operation.TargetStatus, StatusSyncTime: now, UpdatedBy: batch.CreatedBy, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrap(err, "更新SIM卡本地状态失败") } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return gerror.New("SIM卡本地状态已变化") } return nil } func (s *sAppCard) saveMockLifecycleRows(ctx context.Context, batch *entity.BatchMain, operation appCardLifecycleOperation, rows []*appCardLifecycleExecutionRow) error { now := gtime.Now() return dao.BatchDetail.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { for _, row := range rows { if row == nil { continue } status := consts.BatchDetailStatusSuccess if row.Reason != "" { status = consts.BatchDetailStatusFailed } _, err := dao.BatchDetail.Ctx(ctx).TX(tx). Where(dao.BatchDetail.Columns().Id, row.DetailID). Where(dao.BatchDetail.Columns().BatchNo, batch.BatchNo). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning). Data(do.BatchDetail{ DetailStatus: status, ErrorReason: row.Reason, Snapshot: appLifecycleExecutionSnapshot(operation, row, batch.BatchNo), UpdatedBy: batch.CreatedBy, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrap(err, "保存卡片操作批次明细结果失败") } } return nil }) } func (s *sAppCard) finalizeMockLifecycleBatch(ctx context.Context, batch *entity.BatchMain, operation appCardLifecycleOperation, rows []*appCardLifecycleExecutionRow, validationOK bool, failureReason string) error { successCount := 0 for _, row := range rows { if row != nil && row.Reason == "" { successCount++ } } failCount := len(rows) - successCount validCount := 0 if validationOK { validCount = len(rows) } batchStatus := consts.BatchAsyncStatusSuccess runResult := consts.BatchRunResultSuccess if failCount > 0 { batchStatus = consts.BatchAsyncStatusFailed runResult = consts.BatchRunResultFailed if strings.TrimSpace(failureReason) == "" { failureReason = "存在卡片处理失败" } } now := gtime.Now() _, err := dao.BatchMain.Ctx(ctx). Where(dao.BatchMain.Columns().BatchNo, batch.BatchNo). Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing). Data(do.BatchMain{ BatchStatus: batchStatus, BatchAsyncStatus: batchStatus, ValidCount: validCount, SuccessCount: successCount, FailCount: failCount, RunResult: runResult, FailReason: strings.TrimSpace(failureReason), BatchInfo: appLifecycleFinalBatchInfo(batch.BatchInfo, operation, rows, validationOK, failureReason, now), EndTime: now, UpdatedBy: batch.CreatedBy, UpdatedAt: now, }). Update() return gerror.Wrap(err, "汇总卡片操作批次失败") } func (s *sAppCard) markLifecycleBatchFailed(ctx context.Context, batchNo, reason string) error { if strings.TrimSpace(reason) == "" { reason = "后台执行失败" } now := gtime.Now() return dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { if _, err := dao.BatchDetail.Ctx(ctx).TX(tx). Where(dao.BatchDetail.Columns().BatchNo, batchNo). WhereIn(dao.BatchDetail.Columns().DetailStatus, []int{consts.BatchDetailStatusPending, consts.BatchDetailStatusRunning}). Data(do.BatchDetail{ DetailStatus: consts.BatchDetailStatusFailed, ErrorReason: reason, UpdatedAt: now, }). Update(); err != nil { return err } _, err := dao.BatchMain.Ctx(ctx).TX(tx). Where(dao.BatchMain.Columns().BatchNo, batchNo). WhereIn(dao.BatchMain.Columns().BatchAsyncStatus, []int{consts.BatchAsyncStatusPending, consts.BatchAsyncStatusProcessing}). Data(do.BatchMain{ BatchStatus: consts.BatchAsyncStatusFailed, BatchAsyncStatus: consts.BatchAsyncStatusFailed, RunResult: consts.BatchRunResultFailed, FailReason: reason, EndTime: now, UpdatedAt: now, }). Update() return err }) } func (s *sAppCard) loadLifecycleBatchStatus(ctx context.Context, batchNo string) (string, error) { var batch *entity.BatchMain if err := dao.BatchMain.Ctx(ctx). Fields(dao.BatchMain.Columns().BatchStatus, dao.BatchMain.Columns().BatchAsyncStatus). Where(dao.BatchMain.Columns().BatchNo, batchNo). Limit(1). Scan(&batch); err != nil { return "", err } if batch == nil { return "", gerror.New("卡片操作批次不存在") } return appLifecycleBatchStatus(batch.BatchStatus, batch.BatchAsyncStatus), nil } func appLifecycleBatchStatus(batchStatus int, batchAsyncStatus int) string { status := batchAsyncStatus if status == consts.BatchAsyncStatusCreated && batchStatus != consts.BatchAsyncStatusCreated { status = batchStatus } switch status { case consts.BatchAsyncStatusPending: return "pending" case consts.BatchAsyncStatusProcessing: return "processing" case consts.BatchAsyncStatusSuccess: return "success" case consts.BatchAsyncStatusFailed: return "failed" case consts.BatchAsyncStatusPartialSuccess: return "partial_success" default: return "created" } } func (s *sAppCard) loadLifecycleCardsForResponse(ctx context.Context, user *entity.SysUser, items []*appCardLifecycleItem, ownerType string) ([]*model.AppCardModel, error) { cards := make([]*model.AppCardModel, 0, len(items)) for _, item := range items { if item == nil || item.Card == nil { continue } card, err := s.findVisibleCard(ctx, user, item.Card.Iccid) if err != nil { return nil, err } if card == nil { continue } cards = append(cards, s.cardRowToModel(ctx, card, user, ownerType, true)) } return cards, nil } func appCardLifecycleOperationByCode(code string) (appCardLifecycleOperation, error) { switch strings.TrimSpace(strings.ToLower(code)) { case "stop", "suspend": return appCardLifecycleOperation{ Code: "stop", Name: "自主停机", BatchType: consts.BatchTypeStop, BatchPrefix: consts.BatchNoPrefixStop, TargetStatus: consts.V3ApiSimBossCardStatusStopped, }, nil case "start", "resume": return appCardLifecycleOperation{ Code: "start", Name: "自主复机", BatchType: consts.BatchTypeStart, BatchPrefix: consts.BatchNoPrefixStart, TargetStatus: consts.V3ApiSimBossCardStatusActivated, }, nil case "restart": return appCardLifecycleOperation{ Code: "restart", Name: "重启卡", BatchType: consts.BatchTypeRestart, BatchPrefix: consts.BatchNoPrefixRestart, }, nil case "datastart", "data-start": return appCardLifecycleOperation{ Code: "dataStart", Name: "开通数据服务", BatchType: consts.BatchTypeDataStart, BatchPrefix: consts.BatchNoPrefixDataStart, TargetStatus: 1, }, nil case "datastop", "data-stop": return appCardLifecycleOperation{ Code: "dataStop", Name: "停用数据服务", BatchType: consts.BatchTypeDataStop, BatchPrefix: consts.BatchNoPrefixDataStop, TargetStatus: 0, }, nil default: return appCardLifecycleOperation{}, gerror.New("不支持的卡片操作") } } func appCardLifecycleOperationByBatchType(batchType int) (appCardLifecycleOperation, error) { switch batchType { case consts.BatchTypeStop: return appCardLifecycleOperationByCode("stop") case consts.BatchTypeStart: return appCardLifecycleOperationByCode("start") case consts.BatchTypeRestart: return appCardLifecycleOperationByCode("restart") case consts.BatchTypeDataStart: return appCardLifecycleOperationByCode("dataStart") case consts.BatchTypeDataStop: return appCardLifecycleOperationByCode("dataStop") default: return appCardLifecycleOperation{}, gerror.Newf("批次类型%d不是卡片操作批次", batchType) } } func appCardLifecycleIsDataService(operation appCardLifecycleOperation) bool { return operation.BatchType == consts.BatchTypeDataStart || operation.BatchType == consts.BatchTypeDataStop } func appLifecycleOperType(operation appCardLifecycleOperation) string { switch operation.Code { case "stop": return consts.SimCardStatusOperTypeStop case "start": return consts.SimCardStatusOperTypeStart default: return "" } } func appNormalizeLifecycleCardValues(cardValue string, cardValues []string) ([]string, int, error) { values := make([]string, 0, len(cardValues)+1) if strings.TrimSpace(cardValue) != "" { values = append(values, cardValue) } values = append(values, cardValues...) normalized := appNormalizeRenewCardValues(values) mode := batchmetadata.Bulk if strings.TrimSpace(cardValue) != "" && len(cardValues) == 0 { mode = batchmetadata.Single if len(normalized) != 1 { return nil, mode, gerror.New("单卡操作只能指定一张卡,请使用批量操作") } } return normalized, mode, nil } func appLifecycleBatchInfo(operation appCardLifecycleOperation, items []*appCardLifecycleItem, batchNo string, remark string, now *gtime.Time) *gjson.Json { info := map[string]any{ "source": "app", "batchNo": strings.TrimSpace(batchNo), "targetBatchType": operation.BatchType, "targetBatchName": operation.Name, "operation": operation.Code, "remark": strings.TrimSpace(remark), "quantity": len(items), "operateTime": now, "cards": appLifecycleCardSnapshotList(operation, items), } if operType := appLifecycleOperType(operation); operType != "" { info["operType"] = operType } if appCardLifecycleIsDataService(operation) { info["apnName"] = consts.SimCardDataApnName if operation.Code == "dataStart" { info["operType"] = consts.SimCardDataOperTypeStart } else { info["operType"] = consts.SimCardDataOperTypeStop } } if operation.Code == "restart" { delete(info, "operType") info["stopOperType"] = consts.SimCardStatusOperTypeStop info["resumeOperType"] = consts.SimCardStatusOperTypeStart info["resumeDelaySeconds"] = appLifecycleRestartDelaySeconds info["restart"] = map[string]any{"phase": appLifecycleRestartPhaseCreated} } return gjson.New(info) } func appLifecycleFinalBatchInfo(batchInfo *gjson.Json, operation appCardLifecycleOperation, rows []*appCardLifecycleExecutionRow, validationOK bool, failureReason string, now *gtime.Time) *gjson.Json { remark := "" if batchInfo != nil { remark = strings.TrimSpace(batchInfo.Get("remark").String()) } successCount := 0 for _, row := range rows { if row != nil && row.Reason == "" { successCount++ } } return gjson.New(map[string]any{ "source": "app", "onelinkMock": operation.Code != "restart", "localMock": operation.Code == "restart", "targetBatchType": operation.BatchType, "targetBatchName": operation.Name, "operation": operation.Code, "remark": remark, "quantity": len(rows), "successCount": successCount, "failCount": len(rows) - successCount, "validationOK": validationOK, "failureReason": strings.TrimSpace(failureReason), "finishTime": now, }) } func appLifecyclePendingDetailSnapshot(operation appCardLifecycleOperation, item *appCardLifecycleItem, batchNo string) *gjson.Json { card := item.Card snapshot := map[string]any{ "ICCID": card.Iccid, "MSISDN": card.Msisdn, "IMSI": card.Imsi, "IMEI": card.Imei, "rowNo": item.RowNo, "batchNo": strings.TrimSpace(batchNo), "operation": operation.Code, "authorized": true, "chanKey": card.ChanKey, "statusBefore": item.StatusBefore, "targetStatus": operation.TargetStatus, } if operType := appLifecycleOperType(operation); operType != "" { snapshot["operType"] = operType } return gjson.New(snapshot) } func appLifecycleDetailSnapshot(operation appCardLifecycleOperation, item *appCardLifecycleItem, batchNo string) *gjson.Json { card := item.Card return gjson.New(map[string]any{ "ICCID": card.Iccid, "MSISDN": card.Msisdn, "IMSI": card.Imsi, "IMEI": card.Imei, "rowNo": item.RowNo, "batchNo": strings.TrimSpace(batchNo), "operation": operation.Code, "onelinkMock": operation.Code != "restart", "localMock": operation.Code == "restart", "authorized": true, "chanKey": card.ChanKey, "statusBefore": item.StatusBefore, "targetStatus": operation.TargetStatus, "upstreamCode": 0, "upstreamMessage": "本地模拟成功", }) } func appLifecycleExecutionSnapshot(operation appCardLifecycleOperation, row *appCardLifecycleExecutionRow, batchNo string) *gjson.Json { if row == nil { return gjson.New(map[string]any{}) } return gjson.New(map[string]any{ "ICCID": row.Iccid, "MSISDN": row.Msisdn, "IMSI": row.Imsi, "IMEI": row.Imei, "rowNo": row.RowNo, "batchNo": strings.TrimSpace(batchNo), "operation": operation.Code, "onelinkMock": operation.Code != "restart", "localMock": operation.Code == "restart", "authorized": row.Authorized, "chanKey": row.ChanKey, "statusBefore": row.StatusBefore, "targetStatus": operation.TargetStatus, "orderNum": row.OrderNum, "upstreamCode": row.UpstreamCode, "upstreamMessage": row.UpstreamMessage, "errorReason": row.Reason, }) } func appLifecycleCardSnapshotList(operation appCardLifecycleOperation, items []*appCardLifecycleItem) []map[string]any { cards := make([]map[string]any, 0, len(items)) for _, item := range items { cards = append(cards, map[string]any{ "cardId": item.Card.Id, "iccid": item.Card.Iccid, "msisdn": item.Card.Msisdn, "operation": operation.Code, "statusBefore": item.StatusBefore, "targetStatus": operation.TargetStatus, }) } return cards }