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 将卡观测事务中的人工、回调或 Worker 状态变化写入统一 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, cardStateIntegrationAuditResource(ctx, input.IntegrationID)) } return s.appendCardLifecycleAudit(ctx, tx, input.ActionCode, input.Summary, constants.AuditResultSuccess, input.Card, input.BeforeData, input.AfterData, nil, extraResources...) } // WriteCardStateFailure 使用独立短事务记录已解析卡资源后的回调或 Worker 失败。 func (s *Service) WriteCardStateFailure(ctx context.Context, input cardapp.StateAudit, businessErr error) { extraResources := make([]audit.ResourceInput, 0, 1) if input.IntegrationID != "" { extraResources = append(extraResources, cardStateIntegrationAuditResource(ctx, input.IntegrationID)) } s.recordCardLifecycleFailure(ctx, input.ActionCode, input.Summary, constants.AuditResultFailed, input.Card, input.Card.ID, businessErr, extraResources...) } func cardStateIntegrationAuditResource(ctx context.Context, integrationID string) audit.ResourceInput { linkage := auditcontext.From(ctx) correlationID := linkage.CorrelationID if correlationID == "" { correlationID = linkage.RequestID } direction := constants.IntegrationDirectionInbound role := constants.AuditResourceRoleCallbackIntegration provider := linkage.ActorID if linkage.Source == constants.AuditSourceWorker { direction = constants.IntegrationDirectionOutbound role = constants.AuditResourceRoleWorkerIntegration provider = constants.IntegrationProviderGateway } return audit.ResourceInput{ Type: constants.AuditResourceIntegrationLog, Key: integrationID, DisplayName: integrationID, Relation: constants.AuditResourceRelationReference, Role: role, IdentitySnapshot: map[string]any{ "integration_id": integrationID, "provider": provider, "direction": direction, "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, ) }