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) }