收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
117
internal/service/iot_card/gateway_integration.go
Normal file
117
internal/service/iot_card/gateway_integration.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package iot_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
type gatewayAttempt struct {
|
||||
log *model.IntegrationLog
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
func (s *Service) startGatewayCardAttempt(ctx context.Context, card *model.IotCard, operation, scene, seriesKey string, attempt int) (*gatewayAttempt, error) {
|
||||
if s == nil || s.speedTierIntegration == nil {
|
||||
return nil, pkgerrors.New(pkgerrors.CodeInvalidStatus, "Gateway Integration Log 接缝未配置")
|
||||
}
|
||||
resourceID := strconv.FormatUint(uint64(card.ID), 10)
|
||||
triggerSource := auditcontext.From(ctx).Source
|
||||
if triggerSource == "" {
|
||||
triggerSource = "service"
|
||||
}
|
||||
triggerScene := scene
|
||||
triggerSeries := uuid.NewSHA1(uuid.NameSpaceOID, []byte("gateway-card:"+seriesKey+":"+operation)).String()
|
||||
requestID := requestIDFromContext(ctx)
|
||||
var requestIDPtr *string
|
||||
if requestID != "" {
|
||||
requestIDPtr = &requestID
|
||||
}
|
||||
log, err := s.speedTierIntegration.Start(ctx, integrationlog.Attempt{
|
||||
Provider: constants.IntegrationProviderGateway, Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: operation, ExternalID: &card.ICCID,
|
||||
ResourceType: constants.AssetTypeIotCard, ResourceID: &resourceID, ResourceKey: &card.ICCID,
|
||||
TriggerSource: &triggerSource, TriggerScene: &triggerScene, TriggerSeries: &triggerSeries,
|
||||
Attempt: attempt, RequestID: requestIDPtr, CorrelationID: requestIDPtr,
|
||||
RequestSummary: map[string]any{"iot_card_id": card.ID, "iccid": card.ICCID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &gatewayAttempt{log: log, startedAt: time.Now()}, nil
|
||||
}
|
||||
|
||||
func (s *Service) completeGatewayCardAttempt(ctx context.Context, attempt *gatewayAttempt, callErr error, stateChanged bool) error {
|
||||
if attempt == nil || attempt.log == nil {
|
||||
return nil
|
||||
}
|
||||
completion := integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, DurationMS: time.Since(attempt.startedAt).Milliseconds(),
|
||||
StateChanged: stateChanged, ResponseSummary: map[string]any{"result": "success"},
|
||||
}
|
||||
if callErr != nil {
|
||||
completion.Result = constants.IntegrationResultFailed
|
||||
completion.SafeProviderMessage = "Gateway 请求失败"
|
||||
completion.ResponseSummary = map[string]any{"result": "failed"}
|
||||
if isGatewayTimeout(callErr) {
|
||||
completion.Result = constants.IntegrationResultUnknown
|
||||
completion.SafeProviderMessage = "Gateway 请求结果未知"
|
||||
completion.ResponseSummary = map[string]any{"result": "unknown"}
|
||||
completion.RecoveryStrategy = constants.GatewayQueryUnknownRecoveryStrategy
|
||||
}
|
||||
}
|
||||
_, err := s.speedTierIntegration.Complete(ctx, attempt.log.IntegrationID, completion)
|
||||
return err
|
||||
}
|
||||
|
||||
type gatewayCardAttemptObserver struct {
|
||||
service *Service
|
||||
card *model.IotCard
|
||||
operation string
|
||||
scene string
|
||||
seriesKey string
|
||||
nextAttempt int
|
||||
current *gatewayAttempt
|
||||
successful *gatewayAttempt
|
||||
lastCallErr error
|
||||
recordingErr error
|
||||
unknown bool
|
||||
}
|
||||
|
||||
func (o *gatewayCardAttemptObserver) BeforeAttempt(ctx context.Context, _ int) error {
|
||||
o.nextAttempt++
|
||||
o.lastCallErr = nil
|
||||
attempt, err := o.service.startGatewayCardAttempt(ctx, o.card, o.operation, o.scene, o.seriesKey, o.nextAttempt)
|
||||
if err != nil {
|
||||
o.recordingErr = err
|
||||
return err
|
||||
}
|
||||
o.current = attempt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *gatewayCardAttemptObserver) AfterAttempt(ctx context.Context, _ int, callErr error) error {
|
||||
o.lastCallErr = callErr
|
||||
if isGatewayTimeout(callErr) {
|
||||
o.unknown = true
|
||||
}
|
||||
if callErr == nil {
|
||||
o.successful = o.current
|
||||
o.current = nil
|
||||
return nil
|
||||
}
|
||||
err := o.service.completeGatewayCardAttempt(ctx, o.current, callErr, false)
|
||||
o.current = nil
|
||||
if err != nil {
|
||||
o.recordingErr = err
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
@@ -22,8 +21,8 @@ func (s *Service) BatchUpdateRealnamePolicy(ctx context.Context, req *dto.BatchU
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cards []*model.IotCard
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var cards []model.IotCard
|
||||
query := middleware.ApplyShopFilter(ctx, tx.Model(&model.IotCard{})).Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
if err := query.Where("id IN ?", ids).Find(&cards).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询批量卡资产失败")
|
||||
@@ -31,29 +30,31 @@ func (s *Service) BatchUpdateRealnamePolicy(ctx context.Context, req *dto.BatchU
|
||||
if len(cards) != len(ids) {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
result := tx.Model(&model.IotCard{}).Where("id IN ?", ids).Update("realname_policy", req.RealnamePolicy)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "批量更新卡实名认证策略失败")
|
||||
changedIDs := make([]uint, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if card != nil && card.RealnamePolicy != req.RealnamePolicy {
|
||||
changedIDs = append(changedIDs, card.ID)
|
||||
}
|
||||
}
|
||||
if result.RowsAffected != int64(len(ids)) {
|
||||
return errors.New(errors.CodeConflict, "卡资产状态已变化,请刷新后重试")
|
||||
if len(changedIDs) > 0 {
|
||||
result := tx.Model(&model.IotCard{}).Where("id IN ?", changedIDs).Update("realname_policy", req.RealnamePolicy)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "批量更新卡实名认证策略失败")
|
||||
}
|
||||
if result.RowsAffected != int64(len(changedIDs)) {
|
||||
return errors.New(errors.CodeConflict, "卡资产状态已变化,请刷新后重试")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return s.appendCardRealnamePolicyBatchAudit(ctx, tx, cards, req.RealnamePolicy)
|
||||
})
|
||||
if err != nil {
|
||||
result := constants.AuditResultFailed
|
||||
if appErr, ok := err.(*errors.AppError); ok && appErr.Code == errors.CodeForbidden {
|
||||
result = constants.AuditResultDenied
|
||||
}
|
||||
s.recordCardRealnamePolicyBatchFailure(ctx, cards, req.RealnamePolicy, result, err)
|
||||
return nil, err
|
||||
}
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
OperationType: constants.AssetAuditOpCardRealnamePolicy,
|
||||
OperationDesc: "批量更新卡实名认证策略",
|
||||
ResultStatus: constants.AssetAuditResultSuccess,
|
||||
BatchTotal: len(ids),
|
||||
SuccessCount: len(ids),
|
||||
AfterData: map[string]any{
|
||||
"asset_ids": ids,
|
||||
"realname_policy": req.RealnamePolicy,
|
||||
},
|
||||
})
|
||||
return &dto.BatchUpdateAssetRealnamePolicyResponse{SuccessCount: len(ids), RealnamePolicy: req.RealnamePolicy}, nil
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,11 +10,11 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type speedTierIntegrationLog interface {
|
||||
@@ -30,7 +30,7 @@ func (s *Service) SetSpeedTier(ctx context.Context, iccid string, code *int) (*d
|
||||
if code == nil || !constants.IsGatewaySpeedTier(*code) {
|
||||
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "固定限速档位不合法")
|
||||
}
|
||||
if s == nil || s.iotCardStore == nil || s.gatewayClient == nil || s.speedTierIntegration == nil {
|
||||
if s == nil || s.iotCardStore == nil || s.gatewayClient == nil || s.speedTierIntegration == nil || s.db == nil || s.auditWriter == nil {
|
||||
return nil, pkgerrors.New(pkgerrors.CodeServiceUnavailable, "卡限速服务未完整配置")
|
||||
}
|
||||
|
||||
@@ -58,11 +58,10 @@ func (s *Service) SetSpeedTier(ctx context.Context, iccid string, code *int) (*d
|
||||
"iccid": card.ICCID,
|
||||
"tier_code": *code,
|
||||
"tier_name": tierName,
|
||||
"operator_id": middleware.GetUserIDFromContext(ctx),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.logSpeedTierAudit(ctx, card, *code, "", constants.AssetAuditResultFailed, err)
|
||||
s.recordSpeedTierAudit(ctx, card, *code, "", false, constants.AuditResultFailed, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -87,18 +86,22 @@ func (s *Service) SetSpeedTier(ctx context.Context, iccid string, code *int) (*d
|
||||
zap.Error(completeErr),
|
||||
)
|
||||
}
|
||||
s.logSpeedTierAudit(ctx, card, *code, attempt.IntegrationID, constants.AssetAuditResultFailed, completeErr)
|
||||
auditErr := gatewayErr
|
||||
if auditErr == nil {
|
||||
auditErr = completeErr
|
||||
}
|
||||
s.recordSpeedTierAudit(ctx, card, *code, attempt.IntegrationID, false, speedTierAuditResult(gatewayErr), auditErr)
|
||||
return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, completeErr, "终结卡限速外部交互记录失败")
|
||||
}
|
||||
if gatewayErr != nil {
|
||||
s.logSpeedTierAudit(ctx, card, *code, attempt.IntegrationID, constants.AssetAuditResultFailed, gatewayErr)
|
||||
s.recordSpeedTierAudit(ctx, card, *code, attempt.IntegrationID, true, speedTierAuditResult(gatewayErr), gatewayErr)
|
||||
if isGatewayTimeout(gatewayErr) {
|
||||
return nil, pkgerrors.New(pkgerrors.CodeGatewayTimeout, "Gateway 卡限速请求结果未知,请核对实际档位后再操作")
|
||||
}
|
||||
return nil, gatewayErr
|
||||
}
|
||||
|
||||
s.logSpeedTierAudit(ctx, card, *code, attempt.IntegrationID, constants.AssetAuditResultSuccess, nil)
|
||||
s.recordSpeedTierAudit(ctx, card, *code, attempt.IntegrationID, true, constants.AuditResultSuccess, nil)
|
||||
return &dto.SetIotCardSpeedTierResponse{
|
||||
IotCardID: card.ID, ICCID: card.ICCID, Code: *code,
|
||||
SpeedTierName: tierName, IntegrationID: attempt.IntegrationID,
|
||||
@@ -118,7 +121,7 @@ func speedTierCompletion(err error, duration time.Duration) integrationlog.Compl
|
||||
completion := integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess,
|
||||
DurationMS: duration.Milliseconds(),
|
||||
StateChanged: true,
|
||||
StateChanged: false,
|
||||
ResponseSummary: map[string]any{
|
||||
"result": "success",
|
||||
},
|
||||
@@ -132,6 +135,7 @@ func speedTierCompletion(err error, duration time.Duration) integrationlog.Compl
|
||||
completion.ResponseSummary = map[string]any{"result": "failed"}
|
||||
if isGatewayTimeout(err) {
|
||||
completion.Result = constants.IntegrationResultUnknown
|
||||
completion.SafeProviderMessage = "Gateway 卡限速请求结果未知"
|
||||
completion.ResponseSummary = map[string]any{"result": "unknown"}
|
||||
completion.RecoveryStrategy = constants.GatewaySpeedTierUnknownRecoveryStrategy
|
||||
}
|
||||
@@ -143,24 +147,34 @@ func isGatewayTimeout(err error) bool {
|
||||
return stderrors.As(err, &appErr) && appErr != nil && appErr.Code == pkgerrors.CodeGatewayTimeout
|
||||
}
|
||||
|
||||
func (s *Service) logSpeedTierAudit(ctx context.Context, card *model.IotCard, code int, integrationID, result string, err error) {
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeIotCard,
|
||||
AssetID: card.ID,
|
||||
AssetIdentifier: card.ICCID,
|
||||
OperationType: constants.AssetAuditOpCardSpeedTier,
|
||||
OperationDesc: "设置 IoT 卡固定限速档位",
|
||||
BeforeData: map[string]any{"card": cardSnapshot(card)},
|
||||
AfterData: map[string]any{
|
||||
"tier_code": code,
|
||||
"tier_name": constants.GetGatewaySpeedTierName(code),
|
||||
"integration_id": integrationID,
|
||||
},
|
||||
ResultStatus: result,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
})
|
||||
func speedTierAuditResult(err error) string {
|
||||
if err == nil {
|
||||
return constants.AuditResultSuccess
|
||||
}
|
||||
if isGatewayTimeout(err) {
|
||||
return constants.AuditResultUnknown
|
||||
}
|
||||
return constants.AuditResultFailed
|
||||
}
|
||||
|
||||
func (s *Service) recordSpeedTierAudit(ctx context.Context, card *model.IotCard, code int, integrationID string, integrationLogCompleted bool, result string, businessErr error) {
|
||||
afterData := map[string]any{
|
||||
"requested_tier_code": code,
|
||||
"requested_tier_name": constants.GetGatewaySpeedTierName(code),
|
||||
"integration_id": integrationID,
|
||||
"integration_log_completed": integrationLogCompleted,
|
||||
}
|
||||
if s.db == nil || s.auditWriter == nil {
|
||||
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardSpeedTierSet, card.ID, businessErr,
|
||||
pkgerrors.New(pkgerrors.CodeInvalidStatus, "IoT 卡统一审计接缝未配置"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendCardLifecycleAudit(ctx, tx, constants.AuditActionIotCardSpeedTierSet,
|
||||
"设置 IoT 卡固定限速档位为"+constants.GetGatewaySpeedTierName(code), result, card, nil, afterData, businessErr)
|
||||
}); err != nil {
|
||||
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardSpeedTierSet, card.ID, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
var _ speedTierIntegrationLog = (*integrationlog.Repository)(nil)
|
||||
|
||||
239
internal/service/iot_card/stop_resume_audit.go
Normal file
239
internal/service/iot_card/stop_resume_audit.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package iot_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"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/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *StopResumeService) appendCardCommandAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
card *model.IotCard,
|
||||
actionCode, summary, result, integrationID string,
|
||||
beforeData, afterData map[string]any,
|
||||
businessErr error,
|
||||
) error {
|
||||
if s.auditWriter == nil || card == nil || card.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "IoT 卡停复机统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
resourcesByCard, err := loadCardDeviceAuditReferences(ctx, tx, []*model.IotCard{card})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cardID := strconv.FormatUint(uint64(card.ID), 10)
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCard, ID: &cardID,
|
||||
Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(card), BeforeData: beforeData, AfterData: afterData,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
||||
}}
|
||||
resources = append(resources, resourcesByCard[card.ID]...)
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
input := audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
|
||||
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
Metadata: map[string]any{"integration_id": integrationID}, Resources: resources,
|
||||
}
|
||||
if actionCode == constants.AuditActionIotCardAutoStopped || actionCode == constants.AuditActionIotCardAutoStarted ||
|
||||
actionCode == constants.AuditActionIotCardAutoStopReasonUpdated {
|
||||
input.Actor = audit.ActorInput{Kind: constants.AuditActorSystemTask, ID: "iot-card-stop-resume", Name: "IoT 卡停复机服务"}
|
||||
input.Source = constants.AuditSourceWorker
|
||||
}
|
||||
return s.auditWriter.Append(ctx, tx, input)
|
||||
}
|
||||
|
||||
func (s *StopResumeService) recordCardCommandAudit(
|
||||
ctx context.Context,
|
||||
card *model.IotCard,
|
||||
actionCode, summary, result, integrationID string,
|
||||
beforeData, afterData map[string]any,
|
||||
businessErr error,
|
||||
) {
|
||||
if s.db == nil || s.auditWriter == nil || card == nil || card.ID == 0 {
|
||||
recordCardAuditSecondaryFailure(ctx, actionCode, cardID(card), businessErr,
|
||||
errors.New(errors.CodeInvalidStatus, "IoT 卡停复机统一审计接缝未配置"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendCardCommandAudit(ctx, tx, card, actionCode, summary, result, integrationID, beforeData, afterData, businessErr)
|
||||
}); err != nil {
|
||||
recordCardAuditSecondaryFailure(ctx, actionCode, card.ID, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func cardID(card *model.IotCard) uint {
|
||||
if card == nil {
|
||||
return 0
|
||||
}
|
||||
return card.ID
|
||||
}
|
||||
|
||||
func stopAuditAction(ctx context.Context, stopReason string) (string, string) {
|
||||
if stopReason == constants.StopReasonManual && auditcontext.From(ctx).ActorKind == constants.AuditActorAccount {
|
||||
return constants.AuditActionIotCardManualStopped, "人工停用 IoT 卡网络"
|
||||
}
|
||||
return constants.AuditActionIotCardAutoStopped, "自动停用 IoT 卡网络"
|
||||
}
|
||||
|
||||
func cardCommandSeriesKey(ctx context.Context) string {
|
||||
linkage := auditcontext.From(ctx)
|
||||
if linkage.CorrelationID != "" {
|
||||
return linkage.CorrelationID
|
||||
}
|
||||
if linkage.RequestID != "" {
|
||||
return linkage.RequestID
|
||||
}
|
||||
return uuid.NewString()
|
||||
}
|
||||
|
||||
func cardCommandAuditResult(err error) string {
|
||||
if err == nil {
|
||||
return constants.AuditResultSuccess
|
||||
}
|
||||
if isGatewayTimeout(err) {
|
||||
return constants.AuditResultUnknown
|
||||
}
|
||||
return constants.AuditResultFailed
|
||||
}
|
||||
|
||||
func (s *StopResumeService) updateCardStopReasonWithAudit(ctx context.Context, card *model.IotCard, stopReason string) error {
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Update("stop_reason", stopReason).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡停机原因失败")
|
||||
}
|
||||
return s.appendCardCommandAudit(ctx, tx, card, constants.AuditActionIotCardAutoStopReasonUpdated,
|
||||
"自动更新 IoT 卡停机原因", constants.AuditResultSuccess, "",
|
||||
map[string]any{"stop_reason": card.StopReason}, map[string]any{"stop_reason": stopReason}, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StopResumeService) startCardCommandAttempt(
|
||||
ctx context.Context,
|
||||
card *model.IotCard,
|
||||
operation, scene, seriesKey string,
|
||||
attempt int,
|
||||
) (*gatewayAttempt, error) {
|
||||
if s.integration == nil {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "停复机 Integration Log 接缝未配置")
|
||||
}
|
||||
resourceID := strconv.FormatUint(uint64(card.ID), 10)
|
||||
triggerSource := auditcontext.From(ctx).Source
|
||||
if triggerSource == "" {
|
||||
triggerSource = constants.AuditSourceWorker
|
||||
}
|
||||
triggerScene := scene
|
||||
triggerSeries := uuid.NewSHA1(uuid.NameSpaceOID, []byte("gateway-card-command:"+seriesKey+":"+operation)).String()
|
||||
requestID := requestIDFromContext(ctx)
|
||||
correlationID := auditcontext.From(ctx).CorrelationID
|
||||
if correlationID == "" {
|
||||
correlationID = requestID
|
||||
}
|
||||
var requestIDPtr, correlationIDPtr *string
|
||||
if requestID != "" {
|
||||
requestIDPtr = &requestID
|
||||
}
|
||||
if correlationID != "" {
|
||||
correlationIDPtr = &correlationID
|
||||
}
|
||||
log, err := s.integration.Start(ctx, integrationlog.Attempt{
|
||||
Provider: constants.IntegrationProviderGateway, Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: operation, ExternalID: &card.ICCID,
|
||||
ResourceType: constants.AssetTypeIotCard, ResourceID: &resourceID, ResourceKey: &card.ICCID,
|
||||
TriggerSource: &triggerSource, TriggerScene: &triggerScene, TriggerSeries: &triggerSeries,
|
||||
Attempt: attempt, RequestID: requestIDPtr, CorrelationID: correlationIDPtr,
|
||||
RequestSummary: map[string]any{"iot_card_id": card.ID, "iccid": card.ICCID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &gatewayAttempt{log: log, startedAt: time.Now()}, nil
|
||||
}
|
||||
|
||||
func (s *StopResumeService) completeCardCommandAttempt(ctx context.Context, attempt *gatewayAttempt, callErr error, stateChanged bool) error {
|
||||
if attempt == nil || attempt.log == nil {
|
||||
return nil
|
||||
}
|
||||
completion := integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, DurationMS: time.Since(attempt.startedAt).Milliseconds(),
|
||||
StateChanged: stateChanged, ResponseSummary: map[string]any{"result": "success"},
|
||||
}
|
||||
if callErr != nil {
|
||||
completion.Result = constants.IntegrationResultFailed
|
||||
completion.SafeProviderMessage = "Gateway 停复机请求失败"
|
||||
completion.ResponseSummary = map[string]any{"result": "failed"}
|
||||
if isGatewayTimeout(callErr) {
|
||||
completion.Result = constants.IntegrationResultUnknown
|
||||
completion.SafeProviderMessage = "Gateway 停复机请求结果未知"
|
||||
completion.ResponseSummary = map[string]any{"result": "unknown"}
|
||||
completion.RecoveryStrategy = constants.GatewayCardCommandUnknownRecoveryStrategy
|
||||
}
|
||||
}
|
||||
_, err := s.integration.Complete(ctx, attempt.log.IntegrationID, completion)
|
||||
return err
|
||||
}
|
||||
|
||||
type cardCommandAttemptObserver struct {
|
||||
service *StopResumeService
|
||||
card *model.IotCard
|
||||
operation string
|
||||
scene string
|
||||
seriesKey string
|
||||
nextAttempt int
|
||||
current *gatewayAttempt
|
||||
successful *gatewayAttempt
|
||||
lastIntegrationID string
|
||||
lastCallErr error
|
||||
recordingErr error
|
||||
unknown bool
|
||||
}
|
||||
|
||||
func (o *cardCommandAttemptObserver) BeforeAttempt(ctx context.Context, _ int) error {
|
||||
o.nextAttempt++
|
||||
o.lastCallErr = nil
|
||||
attempt, err := o.service.startCardCommandAttempt(ctx, o.card, o.operation, o.scene, o.seriesKey, o.nextAttempt)
|
||||
if err != nil {
|
||||
o.recordingErr = err
|
||||
return err
|
||||
}
|
||||
o.current = attempt
|
||||
o.lastIntegrationID = attempt.log.IntegrationID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *cardCommandAttemptObserver) AfterAttempt(ctx context.Context, _ int, callErr error) error {
|
||||
o.lastCallErr = callErr
|
||||
if isGatewayTimeout(callErr) {
|
||||
o.unknown = true
|
||||
}
|
||||
if callErr == nil {
|
||||
o.successful = o.current
|
||||
o.current = nil
|
||||
return nil
|
||||
}
|
||||
err := o.service.completeCardCommandAttempt(ctx, o.current, callErr, false)
|
||||
o.current = nil
|
||||
if err != nil {
|
||||
o.recordingErr = err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *cardCommandAttemptObserver) auditResult(lastErr error) string {
|
||||
if o.unknown {
|
||||
return constants.AuditResultUnknown
|
||||
}
|
||||
return cardCommandAuditResult(lastErr)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
751
internal/service/iot_card/unified_audit.go
Normal file
751
internal/service/iot_card/unified_audit.go
Normal file
@@ -0,0 +1,751 @@
|
||||
package iot_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"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/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// SetAccessAudit 注入 IoT 卡身份生命周期的统一审计 Writer。
|
||||
func (s *Service) SetAccessAudit(writer *audit.Writer) {
|
||||
s.auditWriter = writer
|
||||
}
|
||||
|
||||
// WriteCardStateAudit 将卡观测事务中的人工状态操作写入统一 Audit Event。
|
||||
func (s *Service) WriteCardStateAudit(ctx context.Context, tx *gorm.DB, input cardapp.StateAudit) error {
|
||||
extraResources := make([]audit.ResourceInput, 0, 1)
|
||||
if input.IntegrationID != "" {
|
||||
extraResources = append(extraResources, callbackIntegrationAuditResource(ctx, input.IntegrationID))
|
||||
}
|
||||
return s.appendCardLifecycleAudit(ctx, tx, input.ActionCode, input.Summary, constants.AuditResultSuccess,
|
||||
input.Card, input.BeforeData, input.AfterData, nil, extraResources...)
|
||||
}
|
||||
|
||||
// WriteCardStateFailure 使用独立短事务记录已解析卡资源后的回调失败。
|
||||
func (s *Service) WriteCardStateFailure(ctx context.Context, input cardapp.StateAudit, businessErr error) {
|
||||
extraResources := make([]audit.ResourceInput, 0, 1)
|
||||
if input.IntegrationID != "" {
|
||||
extraResources = append(extraResources, callbackIntegrationAuditResource(ctx, input.IntegrationID))
|
||||
}
|
||||
s.recordCardLifecycleFailure(ctx, input.ActionCode, input.Summary, constants.AuditResultFailed,
|
||||
input.Card, input.Card.ID, businessErr, extraResources...)
|
||||
}
|
||||
|
||||
func callbackIntegrationAuditResource(ctx context.Context, integrationID string) audit.ResourceInput {
|
||||
linkage := auditcontext.From(ctx)
|
||||
correlationID := linkage.CorrelationID
|
||||
if correlationID == "" {
|
||||
correlationID = linkage.RequestID
|
||||
}
|
||||
return audit.ResourceInput{
|
||||
Type: constants.AuditResourceIntegrationLog, Key: integrationID, DisplayName: integrationID,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleCallbackIntegration,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"integration_id": integrationID, "provider": linkage.ActorID,
|
||||
"direction": constants.IntegrationDirectionInbound, "correlation_id": correlationID,
|
||||
},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}
|
||||
}
|
||||
|
||||
func cardRefreshAuditAction(ctx context.Context) (string, bool) {
|
||||
switch auditcontext.From(ctx).ActorKind {
|
||||
case constants.AuditActorAccount:
|
||||
return constants.AuditActionIotCardManualRefreshed, true
|
||||
case constants.AuditActorPersonalCustomer:
|
||||
return constants.AuditActionIotCardPersonalRefreshed, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) updateCardRefreshCompletion(ctx context.Context, card *model.IotCard, syncTime time.Time, result, summary string) error {
|
||||
actionCode, audited := cardRefreshAuditAction(ctx)
|
||||
if !audited {
|
||||
return s.iotCardStore.UpdateFields(ctx, card.ID, map[string]any{"last_sync_time": syncTime})
|
||||
}
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Update("last_sync_time", syncTime).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡刷新时间失败")
|
||||
}
|
||||
return s.appendCardLifecycleAudit(ctx, tx, actionCode, summary, result,
|
||||
card, map[string]any{"last_sync_time": card.LastSyncTime}, map[string]any{"last_sync_time": syncTime}, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordCardRefreshFailure(ctx context.Context, card *model.IotCard, result string, businessErr error) {
|
||||
actionCode, audited := cardRefreshAuditAction(ctx)
|
||||
if !audited || card == nil {
|
||||
return
|
||||
}
|
||||
s.recordCardLifecycleFailure(ctx, actionCode, "人工刷新 IoT 卡未完成", result, card, card.ID, businessErr)
|
||||
}
|
||||
|
||||
func (s *Service) completeCardRefreshAttempt(
|
||||
ctx context.Context,
|
||||
card *model.IotCard,
|
||||
attempt *gatewayAttempt,
|
||||
callErr error,
|
||||
stateChanged bool,
|
||||
message string,
|
||||
) error {
|
||||
if err := s.completeGatewayCardAttempt(ctx, attempt, callErr, stateChanged); err != nil {
|
||||
wrapped := errors.Wrap(errors.CodeDatabaseError, err, message)
|
||||
s.recordCardRefreshFailure(ctx, card, constants.AuditResultFailed, wrapped)
|
||||
return wrapped
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) appendCardLifecycleAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
actionCode, summary, result string,
|
||||
card *model.IotCard,
|
||||
beforeData, afterData map[string]any,
|
||||
businessErr error,
|
||||
extraResources ...audit.ResourceInput,
|
||||
) error {
|
||||
if s.auditWriter == nil || card == nil || card.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "IoT 卡统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
resourceID := strconv.FormatUint(uint64(card.ID), 10)
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCard, ID: &resourceID,
|
||||
Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(card), BeforeData: beforeData, AfterData: afterData,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
||||
}}
|
||||
resources = append(resources, extraResources...)
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
|
||||
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordCardLifecycleFailure(ctx context.Context, actionCode, summary, result string, card *model.IotCard, cardID uint, businessErr error, extraResources ...audit.ResourceInput) {
|
||||
if card == nil {
|
||||
card = &model.IotCard{}
|
||||
card.ID = cardID
|
||||
}
|
||||
if s.db == nil || s.auditWriter == nil || card.ID == 0 {
|
||||
recordCardAuditSecondaryFailure(ctx, actionCode, cardID, businessErr, errors.New(errors.CodeInvalidStatus, "IoT 卡统一审计接缝未配置"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendCardLifecycleAudit(ctx, tx, actionCode, summary, result, card, nil, nil, businessErr, extraResources...)
|
||||
}); err != nil {
|
||||
recordCardAuditSecondaryFailure(ctx, actionCode, card.ID, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) appendBatchDeleteAudit(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, batchTotal int) error {
|
||||
linkage := auditcontext.From(ctx)
|
||||
if s.auditWriter == nil || linkage.RequestID == "" {
|
||||
return errors.New(errors.CodeInvalidStatus, "IoT 卡批量删除审计上下文不完整")
|
||||
}
|
||||
rootEventID := stableCardBatchEventID("delete", linkage.RequestID)
|
||||
result := constants.AuditResultSuccess
|
||||
if len(cards) < batchTotal {
|
||||
result = constants.AuditResultPartial
|
||||
}
|
||||
children := make([]audit.AppendInput, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if card == nil || card.ID == 0 {
|
||||
continue
|
||||
}
|
||||
resourceID := strconv.FormatUint(uint64(card.ID), 10)
|
||||
children = append(children, audit.AppendInput{
|
||||
EventID: stableCardBatchEventID("delete-card", linkage.RequestID+":"+resourceID),
|
||||
ActionCode: constants.AuditActionIotCardDeleted, Summary: "批量删除 IoT 卡", Result: constants.AuditResultSuccess,
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCard, ID: &resourceID,
|
||||
Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(card), BeforeData: cardSnapshot(card),
|
||||
AfterData: map[string]any{"deleted": true},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "IoT 卡已删除",
|
||||
}},
|
||||
})
|
||||
}
|
||||
return s.auditWriter.AppendBatch(ctx, tx, audit.BatchInput{
|
||||
Root: audit.AppendInput{
|
||||
EventID: rootEventID, ActionCode: constants.AuditActionIotCardBatchDeleted,
|
||||
Summary: "批量删除 IoT 卡", Result: result,
|
||||
BatchTotal: batchTotal, SuccessCount: len(cards), FailCount: batchTotal - len(cards),
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCardBatch, Key: linkage.RequestID, DisplayName: linkage.RequestID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardBatch,
|
||||
IdentitySnapshot: map[string]any{"request_id": linkage.RequestID, "card_count": len(cards)},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}},
|
||||
},
|
||||
Children: children,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordBatchDeleteFailure(ctx context.Context, cardIDs []uint, businessErr error) {
|
||||
linkage := auditcontext.From(ctx)
|
||||
if s.db == nil || s.auditWriter == nil || linkage.RequestID == "" {
|
||||
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardBatchDeleted, 0, businessErr, errors.New(errors.CodeInvalidStatus, "IoT 卡批量删除审计接缝未配置"))
|
||||
return
|
||||
}
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
EventID: stableCardBatchEventID("delete-failed", linkage.RequestID),
|
||||
ActionCode: constants.AuditActionIotCardBatchDeleted, Summary: "批量删除 IoT 卡失败",
|
||||
Result: constants.AuditResultFailed, ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
BatchTotal: len(cardIDs), FailCount: len(cardIDs),
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCardBatch, Key: linkage.RequestID, DisplayName: linkage.RequestID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardBatch,
|
||||
IdentitySnapshot: map[string]any{"request_id": linkage.RequestID, "card_count": len(cardIDs)},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}},
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardBatchDeleted, 0, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
type cardAuditOutcome struct {
|
||||
Result string
|
||||
Summary string
|
||||
}
|
||||
|
||||
func cardAuditOutcomes(cards []*model.IotCard, result, summary string) map[uint]cardAuditOutcome {
|
||||
outcomes := make(map[uint]cardAuditOutcome, len(cards))
|
||||
for _, card := range cards {
|
||||
if card != nil && card.ID > 0 {
|
||||
outcomes[card.ID] = cardAuditOutcome{Result: result, Summary: summary}
|
||||
}
|
||||
}
|
||||
return outcomes
|
||||
}
|
||||
|
||||
func setCardAuditOutcomes(outcomes map[uint]cardAuditOutcome, cardIDs []uint, result, summary string) {
|
||||
for _, cardID := range cardIDs {
|
||||
outcomes[cardID] = cardAuditOutcome{Result: result, Summary: summary}
|
||||
}
|
||||
}
|
||||
|
||||
func setCardAuditOutcomeByICCID(outcomes map[uint]cardAuditOutcome, cards []*model.IotCard, iccid, result, summary string) {
|
||||
for _, card := range cards {
|
||||
if card != nil && (card.ICCID == iccid || card.ICCID19 == iccid || card.ICCID20 != nil && *card.ICCID20 == iccid) {
|
||||
outcomes[card.ID] = cardAuditOutcome{Result: result, Summary: summary}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) appendCardTransferAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
rootAction, itemAction, kind, summary, result string,
|
||||
cards []*model.IotCard,
|
||||
outcomes map[uint]cardAuditOutcome,
|
||||
records []*model.AssetAllocationRecord,
|
||||
newShopID *uint,
|
||||
newStatus, batchTotal, successCount, failCount int,
|
||||
businessErr error,
|
||||
) error {
|
||||
shops, err := loadCardTransferAuditShops(ctx, tx, cards, records, newShopID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deviceReferences, err := loadCardDeviceAuditReferences(ctx, tx, cards)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordByCardID := make(map[uint]*model.AssetAllocationRecord, len(records))
|
||||
for _, record := range records {
|
||||
if record != nil {
|
||||
recordByCardID[record.AssetID] = record
|
||||
}
|
||||
}
|
||||
items := make([]cardBatchAuditItem, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if card == nil || card.ID == 0 {
|
||||
continue
|
||||
}
|
||||
outcome, ok := outcomes[card.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
beforeData := map[string]any{"shop_id": card.ShopID, "status": card.Status}
|
||||
var afterData map[string]any
|
||||
if outcome.Result == constants.AuditResultSuccess {
|
||||
afterData = map[string]any{"shop_id": newShopID, "status": newStatus}
|
||||
}
|
||||
references := cardTransferAuditReferences(card, recordByCardID[card.ID], newShopID, shops)
|
||||
references = append(references, deviceReferences[card.ID]...)
|
||||
items = append(items, cardBatchAuditItem{
|
||||
Card: card, Result: outcome.Result, Summary: outcome.Summary,
|
||||
BeforeData: beforeData, AfterData: afterData,
|
||||
References: references,
|
||||
})
|
||||
}
|
||||
allocationNo := ""
|
||||
if len(records) > 0 && records[0] != nil {
|
||||
allocationNo = records[0].AllocationNo
|
||||
}
|
||||
return s.appendCardBatchAudit(ctx, tx, rootAction, itemAction, kind, summary, result,
|
||||
batchTotal, successCount, failCount, items,
|
||||
map[string]any{"allocation_no": allocationNo, "to_shop_id": newShopID, "new_status": newStatus}, businessErr)
|
||||
}
|
||||
|
||||
func loadCardTransferAuditShops(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, records []*model.AssetAllocationRecord, newShopID *uint) (map[uint]*model.Shop, error) {
|
||||
shopIDs := make(map[uint]struct{})
|
||||
if newShopID != nil && *newShopID > 0 {
|
||||
shopIDs[*newShopID] = struct{}{}
|
||||
}
|
||||
for _, card := range cards {
|
||||
if card != nil && card.ShopID != nil && *card.ShopID > 0 {
|
||||
shopIDs[*card.ShopID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
if record.FromOwnerType == constants.OwnerTypeShop && record.FromOwnerID != nil {
|
||||
shopIDs[*record.FromOwnerID] = struct{}{}
|
||||
}
|
||||
if record.ToOwnerType == constants.OwnerTypeShop && record.ToOwnerID > 0 {
|
||||
shopIDs[record.ToOwnerID] = struct{}{}
|
||||
}
|
||||
}
|
||||
ids := make([]uint, 0, len(shopIDs))
|
||||
for id := range shopIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var rows []*model.Shop
|
||||
if len(ids) > 0 {
|
||||
if err := tx.WithContext(ctx).Unscoped().Where("id IN ?", ids).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
shops := make(map[uint]*model.Shop, len(rows))
|
||||
for _, shop := range rows {
|
||||
shops[shop.ID] = shop
|
||||
}
|
||||
return shops, nil
|
||||
}
|
||||
|
||||
func cardTransferAuditReferences(card *model.IotCard, record *model.AssetAllocationRecord, targetShopID *uint, shops map[uint]*model.Shop) []audit.ResourceInput {
|
||||
resources := make([]audit.ResourceInput, 0, 3)
|
||||
if record != nil && record.ID > 0 {
|
||||
recordID := strconv.FormatUint(uint64(record.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAssetAllocationRecord, ID: &recordID,
|
||||
Key: recordID, DisplayName: record.AllocationNo,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleAssetAllocationRecord,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": record.ID, "allocation_no": record.AllocationNo, "asset_type": record.AssetType,
|
||||
"asset_id": record.AssetID, "asset_identifier": record.AssetIdentifier,
|
||||
"from_owner_type": record.FromOwnerType, "from_owner_id": record.FromOwnerID,
|
||||
"to_owner_type": record.ToOwnerType, "to_owner_id": record.ToOwnerID,
|
||||
},
|
||||
AfterData: map[string]any{"created": true}, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
sourceShopID := card.ShopID
|
||||
if record != nil && record.FromOwnerType == constants.OwnerTypeShop {
|
||||
sourceShopID = record.FromOwnerID
|
||||
}
|
||||
if sourceShopID != nil && *sourceShopID > 0 {
|
||||
resources = appendShopAuditReference(resources, shops[*sourceShopID], *sourceShopID, constants.AuditResourceRoleTransferSourceShop)
|
||||
}
|
||||
if record != nil && record.ToOwnerType == constants.OwnerTypeShop && record.ToOwnerID > 0 {
|
||||
resources = appendShopAuditReference(resources, shops[record.ToOwnerID], record.ToOwnerID, constants.AuditResourceRoleTransferTargetShop)
|
||||
} else if targetShopID != nil && *targetShopID > 0 {
|
||||
resources = appendShopAuditReference(resources, shops[*targetShopID], *targetShopID, constants.AuditResourceRoleTransferTargetShop)
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
func appendShopAuditReference(resources []audit.ResourceInput, shop *model.Shop, shopID uint, role string) []audit.ResourceInput {
|
||||
id := strconv.FormatUint(uint64(shopID), 10)
|
||||
name := id
|
||||
identity := map[string]any{"id": shopID}
|
||||
if shop != nil {
|
||||
name = shop.ShopName
|
||||
identity = map[string]any{"id": shop.ID, "shop_code": shop.ShopCode, "shop_name": shop.ShopName, "parent_id": shop.ParentID, "level": shop.Level}
|
||||
}
|
||||
return append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceShop, ID: &id, Key: id, DisplayName: name,
|
||||
Relation: constants.AuditResourceRelationReference, Role: role,
|
||||
IdentitySnapshot: identity, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
|
||||
type cardBatchAuditItem struct {
|
||||
Card *model.IotCard
|
||||
PrimaryRole string
|
||||
Result string
|
||||
Summary string
|
||||
BeforeData map[string]any
|
||||
AfterData map[string]any
|
||||
References []audit.ResourceInput
|
||||
}
|
||||
|
||||
func (s *Service) appendCardBatchAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
rootAction, itemAction, kind, summary, result string,
|
||||
batchTotal, successCount, failCount int,
|
||||
items []cardBatchAuditItem,
|
||||
metadata map[string]any,
|
||||
businessErr error,
|
||||
) error {
|
||||
linkage := auditcontext.From(ctx)
|
||||
if s.auditWriter == nil || linkage.RequestID == "" {
|
||||
return errors.New(errors.CodeInvalidStatus, "IoT 卡批量审计上下文不完整")
|
||||
}
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
children := make([]audit.AppendInput, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Card == nil || item.Card.ID == 0 {
|
||||
continue
|
||||
}
|
||||
cardID := strconv.FormatUint(uint64(item.Card.ID), 10)
|
||||
primaryRole := item.PrimaryRole
|
||||
if primaryRole == "" {
|
||||
primaryRole = constants.AuditResourceRoleIotCardTransferTarget
|
||||
}
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCard, ID: &cardID,
|
||||
Key: audit.IotCardResourceKey(item.Card), DisplayName: item.Card.ICCID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: primaryRole,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(item.Card), BeforeData: item.BeforeData, AfterData: item.AfterData,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: item.Summary,
|
||||
}}
|
||||
resources = append(resources, item.References...)
|
||||
childErrorCode, childErrorSummary := "", ""
|
||||
if item.Result == constants.AuditResultFailed || item.Result == constants.AuditResultDenied {
|
||||
childErrorCode, childErrorSummary = errorCode, errorSummary
|
||||
}
|
||||
children = append(children, audit.AppendInput{
|
||||
EventID: stableCardBatchEventID(kind+"-"+item.Result+"-card", linkage.RequestID+":"+cardID),
|
||||
ActionCode: itemAction, Summary: item.Summary, ScopeType: constants.AuditScopePlatform, Result: item.Result,
|
||||
ErrorCode: childErrorCode, ErrorSummary: childErrorSummary, Resources: resources,
|
||||
})
|
||||
}
|
||||
return s.auditWriter.AppendBatch(ctx, tx, audit.BatchInput{
|
||||
Root: audit.AppendInput{
|
||||
EventID: stableCardBatchEventID(kind+"-"+result, linkage.RequestID),
|
||||
ActionCode: rootAction, Summary: summary, ScopeType: constants.AuditScopePlatform, Result: result,
|
||||
ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
BatchTotal: batchTotal, SuccessCount: successCount, FailCount: failCount, Metadata: metadata,
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCardBatch, Key: linkage.RequestID, DisplayName: linkage.RequestID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardBatch,
|
||||
IdentitySnapshot: map[string]any{"request_id": linkage.RequestID, "card_count": len(items)},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}},
|
||||
},
|
||||
Children: children,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordCardTransferAuditFailure(
|
||||
ctx context.Context,
|
||||
rootAction, itemAction, kind, summary, result string,
|
||||
cards []*model.IotCard,
|
||||
outcomes map[uint]cardAuditOutcome,
|
||||
newShopID *uint,
|
||||
newStatus, batchTotal, successCount, failCount int,
|
||||
businessErr error,
|
||||
) {
|
||||
if s.db == nil || s.auditWriter == nil || len(cards) == 0 {
|
||||
recordCardAuditSecondaryFailure(ctx, rootAction, 0, businessErr, errors.New(errors.CodeInvalidStatus, "IoT 卡批量审计接缝未配置"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendCardTransferAudit(ctx, tx, rootAction, itemAction, kind, summary, result,
|
||||
cards, outcomes, nil, newShopID, newStatus, batchTotal, successCount, failCount, businessErr)
|
||||
}); err != nil {
|
||||
recordCardAuditSecondaryFailure(ctx, rootAction, 0, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) appendCardSeriesBindingAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
cards []*model.IotCard,
|
||||
outcomes map[uint]cardAuditOutcome,
|
||||
seriesID *uint,
|
||||
result string,
|
||||
batchTotal, successCount, failCount int,
|
||||
metadata map[string]any,
|
||||
businessErr error,
|
||||
) error {
|
||||
series, err := loadCardSeriesAuditResources(ctx, tx, cards, seriesID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deviceReferences, err := loadCardDeviceAuditReferences(ctx, tx, cards)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]cardBatchAuditItem, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if card == nil || card.ID == 0 {
|
||||
continue
|
||||
}
|
||||
outcome, ok := outcomes[card.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var afterData map[string]any
|
||||
if outcome.Result == constants.AuditResultSuccess {
|
||||
afterData = map[string]any{"series_id": seriesID}
|
||||
}
|
||||
references := cardSeriesAuditReferences(card.SeriesID, seriesID, series)
|
||||
references = append(references, deviceReferences[card.ID]...)
|
||||
items = append(items, cardBatchAuditItem{
|
||||
Card: card, PrimaryRole: constants.AuditResourceRoleIotCardSeriesTarget,
|
||||
Result: outcome.Result, Summary: outcome.Summary,
|
||||
BeforeData: map[string]any{"series_id": card.SeriesID}, AfterData: afterData,
|
||||
References: references,
|
||||
})
|
||||
}
|
||||
return s.appendCardBatchAudit(ctx, tx,
|
||||
constants.AuditActionIotCardSeriesBindingBatch,
|
||||
constants.AuditActionIotCardSeriesBound,
|
||||
"series-binding", "批量设置 IoT 卡系列绑定", result,
|
||||
batchTotal, successCount, failCount, items, metadata, businessErr)
|
||||
}
|
||||
|
||||
func (s *Service) appendCardRealnamePolicyBatchAudit(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, policy string) error {
|
||||
deviceReferences, err := loadCardDeviceAuditReferences(ctx, tx, cards)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]cardBatchAuditItem, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if card == nil || card.ID == 0 || card.RealnamePolicy == policy {
|
||||
continue
|
||||
}
|
||||
items = append(items, cardBatchAuditItem{
|
||||
Card: card, PrimaryRole: constants.AuditResourceRoleIotCardTarget,
|
||||
Result: constants.AuditResultSuccess, Summary: "更新 IoT 卡实名策略",
|
||||
BeforeData: map[string]any{"realname_policy": card.RealnamePolicy},
|
||||
AfterData: map[string]any{"realname_policy": policy},
|
||||
References: deviceReferences[card.ID],
|
||||
})
|
||||
}
|
||||
return s.appendCardBatchAudit(ctx, tx,
|
||||
constants.AuditActionIotCardRealnamePolicyBatchUpdated,
|
||||
constants.AuditActionIotCardRealnamePolicyUpdated,
|
||||
"realname-policy", "批量更新 IoT 卡实名策略", constants.AuditResultSuccess,
|
||||
len(items), len(items), 0, items, map[string]any{"realname_policy": policy, "requested_count": len(cards)}, nil)
|
||||
}
|
||||
|
||||
func (s *Service) recordCardRealnamePolicyBatchFailure(ctx context.Context, cards []*model.IotCard, policy, result string, businessErr error) {
|
||||
if s.db == nil || s.auditWriter == nil || len(cards) == 0 {
|
||||
return
|
||||
}
|
||||
items := make([]cardBatchAuditItem, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if card == nil || card.ID == 0 {
|
||||
continue
|
||||
}
|
||||
items = append(items, cardBatchAuditItem{
|
||||
Card: card, PrimaryRole: constants.AuditResourceRoleIotCardTarget,
|
||||
Result: result, Summary: "更新 IoT 卡实名策略未完成",
|
||||
BeforeData: map[string]any{"realname_policy": card.RealnamePolicy},
|
||||
AfterData: map[string]any{"requested_realname_policy": policy},
|
||||
})
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendCardBatchAudit(ctx, tx,
|
||||
constants.AuditActionIotCardRealnamePolicyBatchUpdated,
|
||||
constants.AuditActionIotCardRealnamePolicyUpdated,
|
||||
"realname-policy", "批量更新 IoT 卡实名策略未完成", result,
|
||||
len(cards), 0, len(cards), items, map[string]any{"realname_policy": policy}, businessErr)
|
||||
}); err != nil {
|
||||
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardRealnamePolicyBatchUpdated, 0, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadCardSeriesAuditResources(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, targetSeriesID *uint) (map[uint]*model.PackageSeries, error) {
|
||||
seriesIDs := make(map[uint]struct{})
|
||||
if targetSeriesID != nil && *targetSeriesID > 0 {
|
||||
seriesIDs[*targetSeriesID] = struct{}{}
|
||||
}
|
||||
for _, card := range cards {
|
||||
if card != nil && card.SeriesID != nil && *card.SeriesID > 0 {
|
||||
seriesIDs[*card.SeriesID] = struct{}{}
|
||||
}
|
||||
}
|
||||
ids := make([]uint, 0, len(seriesIDs))
|
||||
for id := range seriesIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var rows []*model.PackageSeries
|
||||
if len(ids) > 0 {
|
||||
if err := tx.WithContext(ctx).Unscoped().Where("id IN ?", ids).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
series := make(map[uint]*model.PackageSeries, len(rows))
|
||||
for _, item := range rows {
|
||||
series[item.ID] = item
|
||||
}
|
||||
return series, nil
|
||||
}
|
||||
|
||||
func cardSeriesAuditReferences(previousID, targetID *uint, series map[uint]*model.PackageSeries) []audit.ResourceInput {
|
||||
resources := make([]audit.ResourceInput, 0, 2)
|
||||
if previousID != nil && *previousID > 0 {
|
||||
resources = appendPackageSeriesAuditReference(resources, series[*previousID], *previousID, constants.AuditResourceRolePreviousPackageSeries)
|
||||
}
|
||||
if targetID != nil && *targetID > 0 {
|
||||
resources = appendPackageSeriesAuditReference(resources, series[*targetID], *targetID, constants.AuditResourceRoleTargetPackageSeries)
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
func appendPackageSeriesAuditReference(resources []audit.ResourceInput, series *model.PackageSeries, seriesID uint, role string) []audit.ResourceInput {
|
||||
id := strconv.FormatUint(uint64(seriesID), 10)
|
||||
name := id
|
||||
identity := map[string]any{"id": seriesID}
|
||||
if series != nil {
|
||||
name = series.SeriesName
|
||||
identity = map[string]any{"id": series.ID, "series_code": series.SeriesCode, "series_name": series.SeriesName, "status": series.Status}
|
||||
}
|
||||
return append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourcePackageSeries, ID: &id, Key: id, DisplayName: name,
|
||||
Relation: constants.AuditResourceRelationReference, Role: role,
|
||||
IdentitySnapshot: identity, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
|
||||
func loadCardDeviceAuditReferences(ctx context.Context, tx *gorm.DB, cards []*model.IotCard) (map[uint][]audit.ResourceInput, error) {
|
||||
cardByID := make(map[uint]*model.IotCard, len(cards))
|
||||
cardIDs := make([]uint, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if card != nil && card.ID > 0 {
|
||||
cardByID[card.ID] = card
|
||||
cardIDs = append(cardIDs, card.ID)
|
||||
}
|
||||
}
|
||||
result := make(map[uint][]audit.ResourceInput)
|
||||
if len(cardIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
var bindings []*model.DeviceSimBinding
|
||||
if err := tx.WithContext(ctx).Where("iot_card_id IN ? AND bind_status = ?", cardIDs, 1).Find(&bindings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deviceIDs := make([]uint, 0, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
deviceIDs = append(deviceIDs, binding.DeviceID)
|
||||
}
|
||||
var devices []*model.Device
|
||||
if len(deviceIDs) > 0 {
|
||||
if err := tx.WithContext(ctx).Unscoped().Where("id IN ?", deviceIDs).Find(&devices).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
deviceByID := make(map[uint]*model.Device, len(devices))
|
||||
for _, device := range devices {
|
||||
deviceByID[device.ID] = device
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
card := cardByID[binding.IotCardID]
|
||||
device := deviceByID[binding.DeviceID]
|
||||
bindingID := strconv.FormatUint(uint64(binding.ID), 10)
|
||||
deviceID := strconv.FormatUint(uint64(binding.DeviceID), 10)
|
||||
deviceKey, deviceName := deviceID, deviceID
|
||||
deviceIdentity := map[string]any{"id": binding.DeviceID}
|
||||
deviceVirtualNo := ""
|
||||
if device != nil {
|
||||
deviceVirtualNo = device.VirtualNo
|
||||
if device.VirtualNo != "" {
|
||||
deviceKey = device.VirtualNo
|
||||
}
|
||||
deviceName = device.DeviceName
|
||||
if deviceName == "" {
|
||||
deviceName = device.VirtualNo
|
||||
}
|
||||
deviceIdentity = map[string]any{"id": device.ID, "virtual_no": device.VirtualNo, "imei": device.IMEI, "sn": device.SN, "generation": device.Generation}
|
||||
}
|
||||
cardICCID, cardVirtualNo := "", ""
|
||||
if card != nil {
|
||||
cardICCID, cardVirtualNo = card.ICCID, card.VirtualNo
|
||||
}
|
||||
result[binding.IotCardID] = append(result[binding.IotCardID],
|
||||
audit.ResourceInput{
|
||||
Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID,
|
||||
Key: bindingID, DisplayName: deviceVirtualNo,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleIotCardDeviceBinding,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": binding.ID, "device_id": binding.DeviceID, "device_virtual_no": deviceVirtualNo,
|
||||
"slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID,
|
||||
"iccid": cardICCID, "virtual_no": cardVirtualNo, "is_current": binding.IsCurrent,
|
||||
},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
},
|
||||
audit.ResourceInput{
|
||||
Type: constants.AuditResourceDevice, ID: &deviceID,
|
||||
Key: deviceKey, DisplayName: deviceName,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleIotCardRelatedDevice,
|
||||
IdentitySnapshot: deviceIdentity, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
},
|
||||
)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) recordCardSeriesBindingAuditFailure(
|
||||
ctx context.Context,
|
||||
cards []*model.IotCard,
|
||||
outcomes map[uint]cardAuditOutcome,
|
||||
seriesID *uint,
|
||||
result string,
|
||||
batchTotal, successCount, failCount int,
|
||||
metadata map[string]any,
|
||||
businessErr error,
|
||||
) {
|
||||
if s.db == nil || s.auditWriter == nil || len(cards) == 0 {
|
||||
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardSeriesBindingBatch, 0, businessErr, errors.New(errors.CodeInvalidStatus, "IoT 卡系列绑定审计接缝未配置"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendCardSeriesBindingAudit(ctx, tx, cards, outcomes, seriesID, result,
|
||||
batchTotal, successCount, failCount, metadata, businessErr)
|
||||
}); err != nil {
|
||||
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardSeriesBindingBatch, 0, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func stableCardBatchEventID(kind, key string) string {
|
||||
return "evt_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte("iot-card:"+kind+":"+key)).String()
|
||||
}
|
||||
|
||||
func recordCardAuditSecondaryFailure(ctx context.Context, actionCode string, cardID uint, businessErr, auditErr error) {
|
||||
errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
linkage := auditcontext.From(ctx)
|
||||
auditfailure.RecordSecondaryWriteFailure(
|
||||
actionCode, strconv.FormatUint(uint64(cardID), 10), linkage.RequestID, linkage.CorrelationID, errorCode, auditErr,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user