package device import ( "context" "strconv" "strings" "gorm.io/gorm" "gorm.io/gorm/clause" "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/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) type deviceBindingAuditItem struct { Card *model.IotCard Binding *model.DeviceSimBinding CardRole string BindingRole string CardBefore map[string]any CardAfter map[string]any BindingBefore map[string]any BindingAfter map[string]any } type deviceBindingState struct { bindings []*model.DeviceSimBinding cards map[uint]*model.IotCard target *model.DeviceSimBinding current *model.DeviceSimBinding } func (s *Service) appendDeviceBindingAudit( ctx context.Context, tx *gorm.DB, actionCode, summary, result string, device *model.Device, deviceBefore, deviceAfter map[string]any, items []deviceBindingAuditItem, metadata map[string]any, businessErr error, ) error { if s.auditWriter == nil || device == nil || device.ID == 0 { return errors.New(errors.CodeInvalidStatus, "设备卡槽统一审计接缝未配置或资源不完整") } deviceID := strconv.FormatUint(uint64(device.ID), 10) resources := []audit.ResourceInput{{ Type: constants.AuditResourceDevice, ID: &deviceID, Key: audit.DeviceResourceKey(device), DisplayName: device.VirtualNo, Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget, IdentitySnapshot: audit.DeviceIdentitySnapshot(device), BeforeData: deviceBefore, AfterData: deviceAfter, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary, }} for index, item := range items { if item.Card != nil && item.Card.ID > 0 { cardID := strconv.FormatUint(uint64(item.Card.ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourceIotCard, ID: &cardID, Key: audit.IotCardResourceKey(item.Card), DisplayName: item.Card.ICCID, Relation: constants.AuditResourceRelationAffected, Role: item.CardRole, IdentitySnapshot: audit.IotCardIdentitySnapshot(item.Card), BeforeData: item.CardBefore, AfterData: item.CardAfter, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary, SortOrder: index*2 + 1, }) } if item.Binding != nil && item.Binding.ID > 0 { bindingID := strconv.FormatUint(uint64(item.Binding.ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID, Key: bindingID, DisplayName: device.VirtualNo, Relation: constants.AuditResourceRelationAffected, Role: item.BindingRole, IdentitySnapshot: deviceBindingIdentity(device, item.Card, item.Binding), BeforeData: item.BindingBefore, AfterData: item.BindingAfter, SubjectVisibility: constants.AuditSubjectInternalOnly, SortOrder: index*2 + 2, }) } } errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr) return s.auditWriter.Append(ctx, tx, audit.AppendInput{ ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform, Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary, Metadata: metadata, Resources: resources, }) } func (s *Service) recordDeviceBindingAuditFailure( ctx context.Context, actionCode, summary, result string, device *model.Device, deviceBefore map[string]any, items []deviceBindingAuditItem, metadata map[string]any, businessErr error, ) { deviceID := uint(0) if device != nil { deviceID = device.ID } if s.db == nil || s.auditWriter == nil || deviceID == 0 { recordDeviceAuditSecondaryFailure(ctx, actionCode, deviceID, businessErr, errors.New(errors.CodeInvalidStatus, "设备卡槽统一审计接缝未配置或资源不完整")) return } if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.appendDeviceBindingAudit(ctx, tx, actionCode, summary, result, device, deviceBefore, nil, items, metadata, businessErr) }); err != nil { recordDeviceAuditSecondaryFailure(ctx, actionCode, deviceID, businessErr, err) } } func deviceBindingIdentity(device *model.Device, card *model.IotCard, binding *model.DeviceSimBinding) map[string]any { identity := map[string]any{ "id": binding.ID, "device_id": binding.DeviceID, "slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID, "is_current": binding.IsCurrent, } if device != nil { identity["device_virtual_no"] = device.VirtualNo } if card != nil { identity["iccid"] = card.ICCID identity["virtual_no"] = card.VirtualNo } return identity } func bindingStateData(binding *model.DeviceSimBinding, bindStatus int, isCurrent bool) map[string]any { return map[string]any{ "slot_position": binding.SlotPosition, "bind_status": bindStatus, "is_current": isCurrent, } } func loadDeviceBindingState(ctx context.Context, db *gorm.DB, deviceID uint, targetICCID string, lock bool) (*deviceBindingState, error) { query := db.WithContext(ctx).Where("device_id = ? AND bind_status = ?", deviceID, constants.BindStatusBound).Order("slot_position ASC") if lock { query = query.Clauses(clause.Locking{Strength: "UPDATE"}) } state := &deviceBindingState{cards: make(map[uint]*model.IotCard)} if err := query.Find(&state.bindings).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备卡槽关系失败") } cardIDs := make([]uint, 0, len(state.bindings)) for _, binding := range state.bindings { cardIDs = append(cardIDs, binding.IotCardID) if binding.IsCurrent { state.current = binding } } if len(cardIDs) > 0 { var cards []*model.IotCard if err := db.WithContext(ctx).Where("id IN ?", cardIDs).Find(&cards).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备绑定卡失败") } for _, card := range cards { state.cards[card.ID] = card } } targetICCID = strings.TrimSpace(targetICCID) for _, binding := range state.bindings { if card := state.cards[binding.IotCardID]; card != nil && cardMatchesICCID(card, targetICCID) { state.target = binding break } } return state, nil } func switchCardAuditItems(state *deviceBindingState) []deviceBindingAuditItem { items := make([]deviceBindingAuditItem, 0, 2) if state.current != nil { oldCurrentAfter := false if state.target != nil && state.current.ID == state.target.ID { oldCurrentAfter = true } items = append(items, deviceBindingAuditItem{ Card: state.cards[state.current.IotCardID], Binding: state.current, CardRole: constants.AuditResourceRoleDeviceOldCurrentCard, BindingRole: constants.AuditResourceRoleDeviceOldCurrentBinding, CardBefore: map[string]any{"is_current": true}, CardAfter: map[string]any{"is_current": oldCurrentAfter}, BindingBefore: bindingStateData(state.current, constants.BindStatusBound, true), BindingAfter: bindingStateData(state.current, constants.BindStatusBound, oldCurrentAfter), }) } if state.target != nil { wasCurrent := state.target.IsCurrent items = append(items, deviceBindingAuditItem{ Card: state.cards[state.target.IotCardID], Binding: state.target, CardRole: constants.AuditResourceRoleDeviceNewCurrentCard, BindingRole: constants.AuditResourceRoleDeviceNewCurrentBinding, CardBefore: map[string]any{"is_current": wasCurrent}, CardAfter: map[string]any{"is_current": true}, BindingBefore: bindingStateData(state.target, constants.BindStatusBound, wasCurrent), BindingAfter: bindingStateData(state.target, constants.BindStatusBound, true), }) } return items } func currentCardID(state *deviceBindingState) uint { if state == nil || state.current == nil { return 0 } return state.current.IotCardID } func loadDeviceUnbindAuditReferences(ctx context.Context, tx *gorm.DB, device *model.Device) ([]audit.ResourceInput, error) { referencesByDevice, _, err := loadDeviceCardAuditReferences(ctx, tx, []*model.Device{device}, nil) if err != nil { return nil, err } references := referencesByDevice[device.ID] for index := range references { resource := &references[index] resource.Relation = constants.AuditResourceRelationAffected switch resource.Type { case constants.AuditResourceIotCard: resource.Role = constants.AuditResourceRoleDeviceBindingTargetCard resource.BeforeData = map[string]any{"device_id": device.ID} resource.AfterData = map[string]any{"device_id": nil} resource.SubjectVisibility = constants.AuditSubjectResult resource.SubjectSummary = "设备删除并解绑 IoT 卡" case constants.AuditResourceDeviceSIMBinding: resource.Role = constants.AuditResourceRoleDeviceRemovedBinding resource.BeforeData = map[string]any{ "slot_position": resource.IdentitySnapshot["slot_position"], "bind_status": constants.BindStatusBound, "is_current": resource.IdentitySnapshot["is_current"], } resource.AfterData = map[string]any{ "slot_position": resource.IdentitySnapshot["slot_position"], "bind_status": constants.BindStatusUnbound, "is_current": false, } } } return references, nil }