收口审计治理与套餐任务进展

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -13,8 +13,9 @@ import (
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/gateway"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
"github.com/break/junhong_cmp_fiber/internal/model"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
@@ -25,6 +26,10 @@ import (
type StopResumeServiceInterface interface {
// EvaluateAndAct 停复机统一入口,根据卡的当前状态自动判断并执行停机或复机
EvaluateAndAct(ctx context.Context, card *model.IotCard) error
// ForceStopCard 强制停机单张卡,不执行正常停机条件判断。
ForceStopCard(ctx context.Context, card *model.IotCard, stopReason string) error
// ForceStartCard 强制复机单张卡,不执行正常复机条件判断。
ForceStartCard(ctx context.Context, card *model.IotCard) error
}
// 编译时验证 StopResumeService 实现了 StopResumeServiceInterface
@@ -43,6 +48,8 @@ type StopResumeService struct {
assetAuditService AssetAuditService
pollingCallback PollingCallback
observationSeriesEvents cardObservationApp.SeriesEventWriter
auditWriter *audit.Writer
integration *integrationlog.Repository
maxRetries int
retryInterval time.Duration
@@ -54,6 +61,12 @@ func (s *StopResumeService) SetObservationSeriesEventWriter(db *gorm.DB, writer
s.observationSeriesEvents = writer
}
// SetUnifiedAudit 注入停复机统一审计和外部交互日志接缝。
func (s *StopResumeService) SetUnifiedAudit(writer *audit.Writer, integration *integrationlog.Repository) {
s.auditWriter = writer
s.integration = integration
}
// NewStopResumeService 创建停复机服务
func NewStopResumeService(
redis *redis.Client,
@@ -364,7 +377,7 @@ func (s *StopResumeService) resumeDeviceCards(ctx context.Context, deviceID uint
var cardErrors []error
for _, card := range cards {
if !s.isRealnameOK(card) {
if updateErr := s.iotCardStore.UpdateStopReason(ctx, card.ID, constants.StopReasonNotRealname); updateErr != nil {
if updateErr := s.updateCardStopReasonWithAudit(ctx, card, constants.StopReasonNotRealname); updateErr != nil {
cardErrors = append(cardErrors, updateErr)
s.logger.Warn("更新未实名卡停机原因失败",
zap.Uint("card_id", card.ID), zap.Error(updateErr))
@@ -408,21 +421,48 @@ func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, carrierType
}
}
// ForceStopCard 强制停机单张卡,不执行正常停机条件判断。
func (s *StopResumeService) ForceStopCard(ctx context.Context, card *model.IotCard, stopReason string) error {
if card == nil || card.ID == 0 {
return errors.New(errors.CodeInvalidParam)
}
return s.stopCardWithRetry(ctx, card, stopReason)
}
// ForceStartCard 强制复机单张卡,不执行正常复机条件判断。
func (s *StopResumeService) ForceStartCard(ctx context.Context, card *model.IotCard) error {
if card == nil || card.ID == 0 {
return errors.New(errors.CodeInvalidParam)
}
actionCode, summary := constants.AuditActionIotCardAutoStarted, "自动恢复 IoT 卡网络"
attempt, err := s.resumeCardWithRetry(ctx, card, actionCode, summary)
if err != nil {
return err
}
if err := s.updateCardAndAppendNetworkSeries(ctx, card, map[string]any{
"network_status": constants.NetworkStatusOnline,
"resumed_at": time.Now(),
"stop_reason": "",
}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString(), actionCode, summary, attempt.log.IntegrationID); err != nil {
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, false); logErr != nil {
s.logger.Error("终结保护期复机 Integration Log 失败", zap.String("integration_id", attempt.log.IntegrationID), zap.Error(logErr))
}
s.recordCardCommandAudit(ctx, card, actionCode, summary+"结果待核对", constants.AuditResultUnknown,
attempt.log.IntegrationID, cardSnapshot(card), map[string]any{"requested_network_status": constants.NetworkStatusOnline}, err)
return err
}
s.reschedulePolling(ctx, card.ID)
if err := s.completeCardCommandAttempt(ctx, attempt, nil, true); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "终结保护期复机 Integration Log 失败")
}
return nil
}
// resumeSingleCard 对单张卡执行复机逻辑
// 依次检查:已开机则跳过 → 非轮询停机原因则跳过 → 不满足复机条件则跳过 → 加锁 → 调 Gateway → 更新 DB
func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) error {
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: assetAuditSvc.SystemOperator("系统任务"),
OperationType: constants.AssetAuditOpCardAutoStart,
OperationDesc: "自动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
})
return err
}
@@ -462,112 +502,54 @@ func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) e
}
defer s.redis.Del(ctx, lockKey)
if err := s.resumeCardWithRetry(ctx, card); err != nil {
actionCode, summary := constants.AuditActionIotCardAutoStarted, "自动恢复 IoT 卡网络"
attempt, err := s.resumeCardWithRetry(ctx, card, actionCode, summary)
if err != nil {
s.logger.Error("调用运营商复机接口失败",
zap.Uint("card_id", cardID),
zap.String("iccid", card.ICCID),
zap.Error(err))
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: assetAuditSvc.SystemOperator("系统任务"),
OperationType: constants.AssetAuditOpCardAutoStart,
OperationDesc: "自动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
})
return err
}
now := time.Now()
if err := s.updateCardAndAppendNetworkSeries(ctx, cardID, map[string]any{
if err := s.updateCardAndAppendNetworkSeries(ctx, card, map[string]any{
"network_status": constants.NetworkStatusOnline,
"resumed_at": now,
"stop_reason": "",
}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString()); err != nil {
}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString(), actionCode, summary, attempt.log.IntegrationID); err != nil {
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, false); logErr != nil {
s.logger.Error("终结复机 Integration Log 失败", zap.String("integration_id", attempt.log.IntegrationID), zap.Error(logErr))
}
s.logger.Error("复机 Gateway 成功但 DB 更新失败",
zap.Uint("card_id", cardID),
zap.String("iccid", card.ICCID),
zap.Error(err))
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: assetAuditSvc.SystemOperator("系统任务"),
OperationType: constants.AssetAuditOpCardAutoStart,
OperationDesc: "自动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
},
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"结果待核对", constants.AuditResultUnknown,
attempt.log.IntegrationID,
map[string]any{"network_status": card.NetworkStatus, "stop_reason": card.StopReason},
map[string]any{"requested_network_status": constants.NetworkStatusOnline}, err)
return err
}
s.reschedulePolling(ctx, card.ID)
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, true); logErr != nil {
return errors.Wrap(errors.CodeDatabaseError, logErr, "终结复机 Integration Log 失败")
}
s.logger.Info("卡已自动复机",
zap.Uint("card_id", cardID),
zap.String("iccid", card.ICCID))
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: assetAuditSvc.SystemOperator("系统任务"),
OperationType: constants.AssetAuditOpCardAutoStart,
OperationDesc: "自动复机",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
},
})
return nil
}
// stopCardWithRetry 调用运营商停机接口(带重试机制),并更新 DB 停机原因
func (s *StopResumeService) stopCardWithRetry(ctx context.Context, card *model.IotCard, stopReason string) error {
operator := assetAuditSvc.SystemOperator("系统任务")
operationType := constants.AssetAuditOpCardAutoStop
operationDesc := "自动停卡"
if stopReason == constants.StopReasonManual {
operator = assetAuditSvc.OperatorFromContext(ctx)
operationType = constants.AssetAuditOpCardManualStop
operationDesc = "手动停卡"
}
actionCode, summary := stopAuditAction(ctx, stopReason)
if s.gatewayClient == nil {
failErr := errors.New(errors.CodeInternalError, "Gateway 未配置,停复机操作不可用")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(failErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: operator,
OperationType: operationType,
OperationDesc: operationDesc + "执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"失败", constants.AuditResultFailed, "",
cardSnapshot(card), nil, failErr)
return failErr
}
@@ -576,7 +558,14 @@ func (s *StopResumeService) stopCardWithRetry(ctx context.Context, card *model.I
zap.String("iccid", card.ICCID),
zap.String("stop_reason", stopReason))
seriesKey := cardCommandSeriesKey(ctx)
attemptObserver := &cardCommandAttemptObserver{
service: s, card: card, operation: constants.IntegrationOperationGatewayStopCard,
scene: constants.CardObservationSceneBusinessStop, seriesKey: seriesKey,
}
gatewayCtx := gateway.WithAttemptObserver(ctx, attemptObserver)
var lastErr error
lastIntegrationID := ""
for i := 0; i < s.maxRetries; i++ {
if i > 0 {
s.logger.Debug("重试调用停机接口",
@@ -585,107 +574,83 @@ func (s *StopResumeService) stopCardWithRetry(ctx context.Context, card *model.I
time.Sleep(s.retryInterval)
}
err := s.gatewayClient.StopCard(ctx, &gateway.CardOperationReq{
CardNo: card.ICCID,
})
if err == nil {
callErr := s.gatewayClient.StopCard(gatewayCtx, &gateway.CardOperationReq{CardNo: card.ICCID})
lastIntegrationID = attemptObserver.lastIntegrationID
if attemptObserver.recordingErr != nil {
s.logger.Error("记录停机 Integration Log 失败", zap.String("integration_id", lastIntegrationID), zap.Error(attemptObserver.recordingErr))
lastErr = attemptObserver.lastCallErr
if lastErr == nil {
lastErr = attemptObserver.recordingErr
}
break
}
if callErr == nil {
attempt := attemptObserver.successful
s.logger.Info("网关停机成功",
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID))
now := time.Now()
if updateErr := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
if updateErr := s.updateCardAndAppendNetworkSeries(ctx, card, map[string]any{
"network_status": constants.NetworkStatusOffline,
"stopped_at": now,
"stop_reason": stopReason,
}, constants.CardObservationSceneBusinessStop, "offline", uuid.NewString()); updateErr != nil {
}, constants.CardObservationSceneBusinessStop, "offline", uuid.NewString(), actionCode, summary, lastIntegrationID); updateErr != nil {
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, false); logErr != nil {
s.logger.Error("终结停机 Integration Log 失败", zap.String("integration_id", lastIntegrationID), zap.Error(logErr))
}
s.logger.Error("停机 Gateway 成功但 DB 更新失败",
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID),
zap.Error(updateErr))
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(updateErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: operator,
OperationType: operationType,
OperationDesc: operationDesc + "执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": stopReason,
},
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"结果待核对", constants.AuditResultUnknown,
lastIntegrationID,
map[string]any{"network_status": card.NetworkStatus, "stop_reason": card.StopReason},
map[string]any{"requested_network_status": constants.NetworkStatusOffline, "stop_reason": stopReason}, updateErr)
return updateErr
}
s.reschedulePolling(ctx, card.ID)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: operator,
OperationType: operationType,
OperationDesc: operationDesc,
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": stopReason,
},
})
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, true); logErr != nil {
return errors.Wrap(errors.CodeDatabaseError, logErr, "终结停机 Integration Log 失败")
}
return nil
}
lastErr = err
lastErr = callErr
s.logger.Warn("调用停机接口失败,准备重试",
zap.Int("attempt", i+1),
zap.String("iccid", card.ICCID),
zap.Error(err))
zap.Error(callErr))
}
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(lastErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: operator,
OperationType: operationType,
OperationDesc: operationDesc + "执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": stopReason,
},
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"未完成", attemptObserver.auditResult(lastErr), lastIntegrationID,
map[string]any{"network_status": card.NetworkStatus, "stop_reason": card.StopReason},
map[string]any{"requested_network_status": constants.NetworkStatusOffline, "stop_reason": stopReason}, lastErr)
return lastErr
}
// resumeCardWithRetry 调用运营商复机接口(带重试机制)
func (s *StopResumeService) resumeCardWithRetry(ctx context.Context, card *model.IotCard) error {
// resumeCardWithRetry 调用运营商复机接口(带重试机制)
func (s *StopResumeService) resumeCardWithRetry(ctx context.Context, card *model.IotCard, actionCode, summary string) (*gatewayAttempt, error) {
if s.gatewayClient == nil {
return errors.New(errors.CodeInternalError, "Gateway 未配置,停复机操作不可用")
failErr := errors.New(errors.CodeInternalError, "Gateway 未配置,停复机操作不可用")
s.recordCardCommandAudit(ctx, card, actionCode, summary+"失败", constants.AuditResultFailed, "", cardSnapshot(card), nil, failErr)
return nil, failErr
}
s.logger.Info("调用网关复机",
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID))
seriesKey := cardCommandSeriesKey(ctx)
attemptObserver := &cardCommandAttemptObserver{
service: s, card: card, operation: constants.IntegrationOperationGatewayStartCard,
scene: constants.CardObservationSceneBusinessResume, seriesKey: seriesKey,
}
gatewayCtx := gateway.WithAttemptObserver(ctx, attemptObserver)
var lastErr error
lastIntegrationID := ""
for i := 0; i < s.maxRetries; i++ {
if i > 0 {
s.logger.Debug("重试调用复机接口",
@@ -698,22 +663,34 @@ func (s *StopResumeService) resumeCardWithRetry(ctx context.Context, card *model
if strings.TrimSpace(card.GatewayExtend) == constants.GatewayCardExtendMachineSeparated {
req.Extend = constants.GatewayCardStartExtendMachineSeparated
}
err := s.gatewayClient.StartCard(ctx, req)
if err == nil {
callErr := s.gatewayClient.StartCard(gatewayCtx, req)
lastIntegrationID = attemptObserver.lastIntegrationID
if attemptObserver.recordingErr != nil {
s.logger.Error("记录复机 Integration Log 失败", zap.String("integration_id", lastIntegrationID), zap.Error(attemptObserver.recordingErr))
lastErr = attemptObserver.lastCallErr
if lastErr == nil {
lastErr = attemptObserver.recordingErr
}
break
}
if callErr == nil {
s.logger.Info("网关复机成功",
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID))
return nil
return attemptObserver.successful, nil
}
lastErr = err
lastErr = callErr
s.logger.Warn("调用复机接口失败,准备重试",
zap.Int("attempt", i+1),
zap.String("iccid", card.ICCID),
zap.Error(err))
zap.Error(callErr))
}
return lastErr
s.recordCardCommandAudit(ctx, card, actionCode, summary+"未完成", attemptObserver.auditResult(lastErr), lastIntegrationID,
map[string]any{"network_status": card.NetworkStatus, "stop_reason": card.StopReason, "gateway_extend": card.GatewayExtend},
map[string]any{"requested_network_status": constants.NetworkStatusOnline}, lastErr)
return nil, lastErr
}
// StartMachineSeparatedCard 对机卡分离停机卡执行复机
@@ -722,8 +699,11 @@ func (s *StopResumeService) StartMachineSeparatedCard(ctx context.Context, card
if card == nil {
return errors.New(errors.CodeInvalidParam)
}
actionCode, summary := constants.AuditActionIotCardOpenAPIStarted, "OpenAPI 恢复 IoT 卡网络"
if s.gatewayClient == nil {
return errors.New(errors.CodeInternalError, "Gateway 未配置,停复机操作不可用")
failErr := errors.New(errors.CodeInternalError, "Gateway 未配置,停复机操作不可用")
s.recordCardCommandAudit(ctx, card, actionCode, summary+"失败", constants.AuditResultFailed, "", cardSnapshot(card), nil, failErr)
return failErr
}
gatewayExtend := strings.TrimSpace(card.GatewayExtend)
@@ -737,108 +717,43 @@ func (s *StopResumeService) StartMachineSeparatedCard(ctx context.Context, card
denyMsg = "该卡已被运营商销户,不允许复机"
}
denyErr := errors.New(errors.CodeForbidden, denyMsg)
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "机卡分离复机被拒绝(风险状态)",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"被拒绝", constants.AuditResultDenied, "", cardSnapshot(card), nil, denyErr)
return denyErr
}
if gatewayExtend != constants.GatewayCardExtendMachineSeparated {
denyErr := errors.New(errors.CodeForbidden, constants.AgentOpenAPIResumeOnlyMachineSeparatedMessage)
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "机卡分离复机被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
AfterData: map[string]any{
"gateway_extend": gatewayExtend,
},
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"被拒绝", constants.AuditResultDenied, "",
cardSnapshot(card), map[string]any{"gateway_extend": gatewayExtend}, denyErr)
return denyErr
}
if err := s.resumeCardWithRetry(ctx, card); err != nil {
attempt, err := s.resumeCardWithRetry(ctx, card, actionCode, summary)
if err != nil {
wrapErr := errors.Wrap(errors.CodeGatewayError, err, "调用运营商复机失败,请稍后重试")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "机卡分离复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
AfterData: map[string]any{
"gateway_extend": gatewayExtend,
},
})
return wrapErr
}
now := time.Now()
if err := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
if err := s.updateCardAndAppendNetworkSeries(ctx, card, map[string]any{
"network_status": constants.NetworkStatusOnline,
"resumed_at": now,
"stop_reason": "",
"gateway_extend": "",
}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString()); err != nil {
}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString(), actionCode, summary, attempt.log.IntegrationID); err != nil {
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, false); logErr != nil {
s.logger.Error("终结复机 Integration Log 失败", zap.String("integration_id", attempt.log.IntegrationID), zap.Error(logErr))
}
wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "机卡分离复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
"gateway_extend": gatewayExtend,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
"gateway_extend": "",
},
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"结果待核对", constants.AuditResultUnknown,
attempt.log.IntegrationID, cardSnapshot(card), map[string]any{"requested_network_status": constants.NetworkStatusOnline}, wrapErr)
return wrapErr
}
s.reschedulePolling(ctx, card.ID)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "机卡分离复机",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
"gateway_extend": gatewayExtend,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
"gateway_extend": "",
},
})
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, true); logErr != nil {
return errors.Wrap(errors.CodeDatabaseError, logErr, "终结 OpenAPI 复机 Integration Log 失败")
}
return nil
}
@@ -846,32 +761,13 @@ func (s *StopResumeService) StartMachineSeparatedCard(ctx context.Context, card
func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) error {
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
if err != nil {
denyErr := errors.New(errors.CodeNotFound, "卡不存在")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStop,
OperationDesc: "手动停卡被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetIdentifier: iccid,
})
return denyErr
return errors.New(errors.CodeNotFound, "卡不存在")
}
actionCode, summary := stopAuditAction(ctx, constants.StopReasonManual)
if card.RealNameStatus != constants.RealNameStatusVerified {
denyErr := errors.New(errors.CodeForbidden, "卡未实名,无法操作")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStop,
OperationDesc: "手动停卡被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"被拒绝", constants.AuditResultDenied, "", cardSnapshot(card), nil, denyErr)
return denyErr
}
@@ -882,41 +778,19 @@ func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) er
exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "start")).Result()
if exists > 0 {
denyErr := errors.New(errors.CodeForbidden, "设备复机保护期内,禁止停机")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStop,
OperationDesc: "手动停卡被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
AfterData: map[string]any{
"device_id": binding.DeviceID,
},
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"被拒绝", constants.AuditResultDenied, "",
cardSnapshot(card), map[string]any{"device_id": binding.DeviceID}, denyErr)
return denyErr
}
} else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) {
wrapErr := errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStop,
OperationDesc: "手动停卡执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"失败", constants.AuditResultFailed, "", cardSnapshot(card), nil, wrapErr)
return wrapErr
}
}
if err := s.stopCardWithRetry(ctx, card, constants.StopReasonManual); err != nil {
return errors.Wrap(errors.CodeGatewayError, err, "调用运营商停机失败,请稍后重试")
return err
}
return nil
@@ -926,18 +800,9 @@ func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) er
func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) error {
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
if err != nil {
denyErr := errors.New(errors.CodeNotFound, "卡不存在")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetIdentifier: iccid,
})
return denyErr
return errors.New(errors.CodeNotFound, "卡不存在")
}
actionCode, summary := constants.AuditActionIotCardManualStarted, "人工恢复 IoT 卡网络"
// 独立卡处于风险停机或已销户状态时,拒绝复机
if card.IsStandalone && isRiskGatewayExtend(card.GatewayExtend) {
@@ -948,33 +813,13 @@ func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) e
denyMsg = "该卡已被运营商销户,不允许复机"
}
denyErr := errors.New(errors.CodeForbidden, denyMsg)
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"被拒绝", constants.AuditResultDenied, "", cardSnapshot(card), nil, denyErr)
return denyErr
}
if card.RealNameStatus != constants.RealNameStatusVerified {
denyErr := errors.New(errors.CodeForbidden, "卡未实名,无法操作")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"被拒绝", constants.AuditResultDenied, "", cardSnapshot(card), nil, denyErr)
return denyErr
}
@@ -985,106 +830,52 @@ func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) e
exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "stop")).Result()
if exists > 0 {
denyErr := errors.New(errors.CodeForbidden, "设备停机保护期内,禁止复机")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
AfterData: map[string]any{
"device_id": binding.DeviceID,
},
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"被拒绝", constants.AuditResultDenied, "",
cardSnapshot(card), map[string]any{"device_id": binding.DeviceID}, denyErr)
return denyErr
}
} else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) {
wrapErr := errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"失败", constants.AuditResultFailed, "", cardSnapshot(card), nil, wrapErr)
return wrapErr
}
}
if err := s.resumeCardWithRetry(ctx, card); err != nil {
attempt, err := s.resumeCardWithRetry(ctx, card, actionCode, summary)
if err != nil {
wrapErr := errors.Wrap(errors.CodeGatewayError, err, "调用运营商复机失败,请稍后重试")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
return wrapErr
}
now := time.Now()
if err := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
if err := s.updateCardAndAppendNetworkSeries(ctx, card, map[string]any{
"network_status": constants.NetworkStatusOnline,
"resumed_at": now,
"stop_reason": "",
}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString()); err != nil {
}, constants.CardObservationSceneBusinessResume, "online", uuid.NewString(), actionCode, summary, attempt.log.IntegrationID); err != nil {
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, false); logErr != nil {
s.logger.Error("终结复机 Integration Log 失败", zap.String("integration_id", attempt.log.IntegrationID), zap.Error(logErr))
}
wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
},
})
s.recordCardCommandAudit(ctx, card, actionCode, summary+"结果待核对", constants.AuditResultUnknown,
attempt.log.IntegrationID, cardSnapshot(card), map[string]any{"requested_network_status": constants.NetworkStatusOnline}, wrapErr)
return wrapErr
}
s.reschedulePolling(ctx, card.ID)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
},
})
if logErr := s.completeCardCommandAttempt(ctx, attempt, nil, true); logErr != nil {
return errors.Wrap(errors.CodeDatabaseError, logErr, "终结人工复机 Integration Log 失败")
}
return nil
}
func (s *StopResumeService) updateCardAndAppendNetworkSeries(ctx context.Context, cardID uint, fields map[string]any, scene, expected, operationID string) error {
if s.db == nil || s.observationSeriesEvents == nil {
func (s *StopResumeService) updateCardAndAppendNetworkSeries(
ctx context.Context,
card *model.IotCard,
fields map[string]any,
scene, expected, operationID, actionCode, summary, integrationID string,
) error {
if s.db == nil || s.observationSeriesEvents == nil || card == nil || card.ID == 0 {
return errors.New(errors.CodeInternalError, "停复机观测 Outbox 能力未配置")
}
requestID := requestIDFromContext(ctx)
@@ -1092,15 +883,21 @@ func (s *StopResumeService) updateCardAndAppendNetworkSeries(ctx context.Context
requestID = operationID
}
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).Updates(fields).Error; err != nil {
if err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(fields).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡停复机状态失败")
}
if err := s.appendCardCommandAudit(ctx, tx, card, actionCode, summary, constants.AuditResultSuccess,
integrationID,
map[string]any{"network_status": card.NetworkStatus, "stop_reason": card.StopReason, "gateway_extend": card.GatewayExtend},
fields, nil); err != nil {
return err
}
if cardObservationApp.IsSeriesTriggerSuppressed(ctx) {
return nil
}
return s.observationSeriesEvents.AppendSeriesRequested(ctx, tx, cardObservationApp.SeriesRequestedEvent{
EventID: "card-observation:network-command:" + operationID,
Scene: scene, ResourceType: constants.CardObservationResourceTypeCard, ResourceID: cardID,
Scene: scene, ResourceType: constants.CardObservationResourceTypeCard, ResourceID: card.ID,
SyncTypes: []string{constants.CardObservationSyncTypeNetwork}, ExpectedValue: expected,
Source: constants.CardObservationSourceBusinessEvent, OccurredAt: time.Now().UTC(),
RequestID: requestID, CorrelationID: requestID,