Files
iotlcm/server/internal/logic/app/app_card_restart.go
T

1173 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}