Files
iotlcm/server/internal/logic/app/app_card_restart.go
T
MiFaZhan 2ed5dfa6a1 chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。

- app/:uni-app 小程序前端(基于 unibest,MIT)
  - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID
  - 内网后端地址 → admin.iotlcm.example.com
  - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明)
- server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0)
  - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件)
  - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化
  - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位)
  - 依赖号码尾部的断言同步更新
  - 补 server/LICENSE(Apache-2.0)
- 移除内部资产:CI 触发器
- 补根 README,LF 行尾 + .gitattributes

已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
2026-10-26 14:01:25 +08:00

1116 lines
36 KiB
Go

package app
import (
"context"
"strings"
"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 := appNormalizeLifecycleCardValues(param.CardValue, param.CardValues)
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), 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
}
return &model.AppCardLifecycleModel{
BatchNo: batch.BatchNo,
BatchType: batch.BatchType,
Operation: operation.Code,
Status: appLifecycleBatchStatus(batch.BatchStatus, batch.BatchAsyncStatus),
Quantity: batch.TotalCount,
SuccessCount: batch.SuccessCount,
FailCount: batch.FailCount,
FailReason: strings.TrimSpace(batch.FailReason),
Cards: []*model.AppCardModel{},
}, nil
}
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
}
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
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, 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,
SourceBatchNo: batchNo,
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 {
values := make([]string, 0, len(cardValues)+1)
if strings.TrimSpace(cardValue) != "" {
values = append(values, cardValue)
}
values = append(values, cardValues...)
return appNormalizeRenewCardValues(values)
}
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
}