package exchange import ( "context" "strconv" "strings" "gorm.io/gorm" "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/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) type deviceExchangeAuditBefore struct { Wallets map[uint]model.AssetWallet CustomerBindings []*model.PersonalCustomerDevice } func (s *Service) appendExchangeAudit( ctx context.Context, tx *gorm.DB, cardActionCode, summary, result string, order *model.ExchangeOrder, oldAsset, newAsset *resolvedExchangeAsset, orderBefore, orderAfter map[string]any, oldAssetBefore, oldAssetAfter map[string]any, newAssetBefore, newAssetAfter map[string]any, extra []audit.ResourceInput, businessErr error, ) error { if order != nil && order.OldAssetType == constants.ExchangeAssetTypeDevice { return s.appendDeviceExchangeAudit(ctx, tx, deviceExchangeActionCode(cardActionCode), strings.ReplaceAll(summary, "卡", "设备"), result, order, resolvedDevice(oldAsset), resolvedDevice(newAsset), orderBefore, orderAfter, oldAssetBefore, oldAssetAfter, newAssetBefore, newAssetAfter, extra, businessErr) } return s.appendCardExchangeAudit(ctx, tx, cardActionCode, summary, result, order, resolvedCard(oldAsset), resolvedCard(newAsset), orderBefore, orderAfter, oldAssetBefore, oldAssetAfter, newAssetBefore, newAssetAfter, extra, businessErr) } func resolvedCard(asset *resolvedExchangeAsset) *model.IotCard { if asset == nil { return nil } return asset.Card } func resolvedDevice(asset *resolvedExchangeAsset) *model.Device { if asset == nil { return nil } return asset.Device } func deviceExchangeActionCode(cardActionCode string) string { switch cardActionCode { case constants.AuditActionCardExchangeCreated: return constants.AuditActionDeviceExchangeCreated case constants.AuditActionCardExchangeShippingInfoSubmitted: return constants.AuditActionDeviceExchangeShippingInfoSubmitted case constants.AuditActionCardExchangeShipped: return constants.AuditActionDeviceExchangeShipped case constants.AuditActionCardExchangeCompleted: return constants.AuditActionDeviceExchangeCompleted case constants.AuditActionCardExchangeCancelled: return constants.AuditActionDeviceExchangeCancelled case constants.AuditActionCardExchangeRenewed: return constants.AuditActionDeviceExchangeRenewed default: return cardActionCode } } func (s *Service) appendDeviceExchangeAudit( ctx context.Context, tx *gorm.DB, actionCode, summary, result string, order *model.ExchangeOrder, oldDevice, newDevice *model.Device, orderBefore, orderAfter map[string]any, oldDeviceBefore, oldDeviceAfter map[string]any, newDeviceBefore, newDeviceAfter map[string]any, extra []audit.ResourceInput, businessErr error, ) error { if order == nil || order.OldAssetType != constants.ExchangeAssetTypeDevice { return nil } if s.auditWriter == nil { return errors.New(errors.CodeInvalidStatus, "设备换货统一审计接缝未配置") } internalOnly := actionCode == constants.AuditActionDeviceExchangeRenewed resources := []audit.ResourceInput{deviceExchangeOrderAuditResource(order, summary, internalOnly, orderBefore, orderAfter)} if oldDevice != nil { resources = append(resources, deviceExchangeDeviceAuditResource(oldDevice, constants.AuditResourceRoleDeviceExchangeOldDevice, summary, internalOnly, oldDeviceBefore, oldDeviceAfter)) } if newDevice != nil { resources = append(resources, deviceExchangeDeviceAuditResource(newDevice, constants.AuditResourceRoleDeviceExchangeNewDevice, summary, internalOnly, newDeviceBefore, newDeviceAfter)) } shopResource, err := loadDeviceExchangeShopAuditResource(ctx, tx, order.ShopID) if err != nil { return err } if shopResource != nil { resources = append(resources, *shopResource) } resources = append(resources, extra...) errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr) scopeType, scopeID := constants.AuditScopePlatform, "" if actionCode == constants.AuditActionDeviceExchangeShippingInfoSubmitted { scopeType = constants.AuditScopePersonalCustomer scopeID = auditcontext.From(ctx).ActorID } return s.auditWriter.Append(ctx, tx, audit.AppendInput{ ActionCode: actionCode, Summary: summary, ScopeType: scopeType, ScopeID: scopeID, Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary, Metadata: map[string]any{"flow_type": effectiveExchangeFlowType(order.FlowType), "migrate_data": order.MigrateData}, Resources: resources, }) } func deviceExchangeOrderAuditResource(order *model.ExchangeOrder, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput { resource := cardExchangeOrderAuditResource(order, summary, internalOnly, beforeData, afterData) resource.Role = constants.AuditResourceRoleDeviceExchangeOrder return resource } func deviceExchangeDeviceAuditResource(device *model.Device, role, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput { id := strconv.FormatUint(uint64(device.ID), 10) relation := constants.AuditResourceRelationReference if len(beforeData) > 0 || len(afterData) > 0 { relation = constants.AuditResourceRelationAffected } resource := audit.ResourceInput{ Type: constants.AuditResourceDevice, ID: &id, Key: audit.DeviceResourceKey(device), DisplayName: preferredDeviceIdentifier(device), Relation: relation, Role: role, IdentitySnapshot: audit.DeviceIdentitySnapshot(device), BeforeData: beforeData, AfterData: afterData, } if internalOnly { resource.SubjectVisibility = constants.AuditSubjectInternalOnly } else { resource.SubjectVisibility = constants.AuditSubjectResult resource.SubjectSummary = summary } return resource } func loadDeviceExchangeShopAuditResource(ctx context.Context, tx *gorm.DB, shopID *uint) (*audit.ResourceInput, error) { resource, err := loadCardExchangeShopAuditResource(ctx, tx, shopID) if resource != nil { resource.Role = constants.AuditResourceRoleDeviceExchangeShop } return resource, err } func (s *Service) captureDeviceExchangeAuditBefore(ctx context.Context, tx *gorm.DB, oldDevice, newDevice *model.Device) (*deviceExchangeAuditBefore, error) { state := &deviceExchangeAuditBefore{Wallets: make(map[uint]model.AssetWallet)} deviceIDs := make([]uint, 0, 2) if oldDevice != nil { deviceIDs = append(deviceIDs, oldDevice.ID) } if newDevice != nil { deviceIDs = append(deviceIDs, newDevice.ID) } if len(deviceIDs) > 0 { var wallets []model.AssetWallet if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id IN ?", constants.ExchangeAssetTypeDevice, deviceIDs).Find(&wallets).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货钱包审计快照失败") } for _, wallet := range wallets { state.Wallets[wallet.ResourceID] = wallet } } rows, err := loadDeviceExchangeCustomerBindings(ctx, tx, oldDevice) if err != nil { return nil, err } state.CustomerBindings = rows return state, nil } func loadDeviceExchangeCustomerBindings(ctx context.Context, tx *gorm.DB, device *model.Device) ([]*model.PersonalCustomerDevice, error) { if device == nil { return nil, nil } key := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: device, VirtualNo: device.VirtualNo}) if key == "" { return nil, nil } var rows []*model.PersonalCustomerDevice if err := tx.WithContext(ctx).Where("virtual_no = ? AND status = ?", key, constants.StatusEnabled).Find(&rows).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货客户绑定失败") } return rows, nil } func (s *Service) buildDeviceExchangeCompletionResources( ctx context.Context, tx *gorm.DB, order *model.ExchangeOrder, oldDevice, newDevice *model.Device, before *deviceExchangeAuditBefore, migration *exchangeMigrationResult, ) ([]audit.ResourceInput, error) { resources := deviceExchangeOldCustomerBindingResources(before) newBindings, err := loadDeviceExchangeCustomerBindings(ctx, tx, newDevice) if err != nil { return nil, err } resources = append(resources, deviceExchangeNewCustomerBindingResources(newBindings)...) simResources, err := loadDeviceExchangeSIMResources(ctx, tx, oldDevice, newDevice) if err != nil { return nil, err } resources = append(resources, simResources...) beforeWallets := map[uint]model.AssetWallet(nil) if before != nil { beforeWallets = before.Wallets } walletResources, err := loadDeviceExchangeWalletResources(ctx, tx, oldDevice.ID, newDevice.ID, beforeWallets) if err != nil { return nil, err } resources = append(resources, walletResources...) if migration == nil { return resources, nil } transactions, err := loadDeviceExchangeTransactionResources(ctx, tx, order.ExchangeNo) if err != nil { return nil, err } resources = append(resources, transactions...) usages, err := loadDeviceExchangePackageUsageResources(ctx, tx, migration.PackageUsageIDs, oldDevice.ID, newDevice.ID) if err != nil { return nil, err } return append(resources, usages...), nil } func deviceExchangeOldCustomerBindingResources(before *deviceExchangeAuditBefore) []audit.ResourceInput { if before == nil { return nil } return deviceExchangeCustomerBindingResources(before.CustomerBindings, constants.AuditResourceRoleDeviceExchangeOldCustomerBinding, true) } func deviceExchangeNewCustomerBindingResources(rows []*model.PersonalCustomerDevice) []audit.ResourceInput { return deviceExchangeCustomerBindingResources(rows, constants.AuditResourceRoleDeviceExchangeNewCustomerBinding, false) } func deviceExchangeCustomerBindingResources(rows []*model.PersonalCustomerDevice, role string, before bool) []audit.ResourceInput { resources := make([]audit.ResourceInput, 0, len(rows)) for _, row := range rows { if row == nil { continue } id := strconv.FormatUint(uint64(row.ID), 10) resource := audit.ResourceInput{ Type: constants.AuditResourcePersonalCustomerDevice, ID: &id, Key: id, DisplayName: row.VirtualNo, Relation: constants.AuditResourceRelationAffected, Role: role, IdentitySnapshot: map[string]any{"id": row.ID, "customer_id": row.CustomerID, "virtual_no": row.VirtualNo, "bind_at": row.BindAt, "last_used_at": row.LastUsedAt, "status": row.Status}, SubjectVisibility: constants.AuditSubjectInternalOnly, } if before { resource.BeforeData = map[string]any{"virtual_no": row.VirtualNo, "status": row.Status} } else { resource.AfterData = map[string]any{"virtual_no": row.VirtualNo, "status": row.Status} } resources = append(resources, resource) } return resources } func loadDeviceExchangeSIMResources(ctx context.Context, tx *gorm.DB, oldDevice, newDevice *model.Device) ([]audit.ResourceInput, error) { resources := make([]audit.ResourceInput, 0) for _, item := range []struct { device *model.Device cardRole string bindingRole string }{ {oldDevice, constants.AuditResourceRoleDeviceExchangeOldBoundCard, constants.AuditResourceRoleDeviceExchangeOldSIMBinding}, {newDevice, constants.AuditResourceRoleDeviceExchangeNewBoundCard, constants.AuditResourceRoleDeviceExchangeNewSIMBinding}, } { if item.device == nil { continue } var bindings []*model.DeviceSimBinding if err := tx.WithContext(ctx).Where("device_id = ? AND bind_status = ?", item.device.ID, constants.BindStatusBound). Order("slot_position ASC, id ASC").Find(&bindings).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货卡槽绑定失败") } cardIDs := make([]uint, 0, len(bindings)) for _, binding := range bindings { cardIDs = append(cardIDs, binding.IotCardID) } cards := make(map[uint]*model.IotCard, len(cardIDs)) if len(cardIDs) > 0 { var rows []*model.IotCard if err := tx.WithContext(ctx).Where("id IN ?", cardIDs).Find(&rows).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货绑定卡失败") } for _, card := range rows { cards[card.ID] = card } } for _, binding := range bindings { card := cards[binding.IotCardID] if card == nil { return nil, errors.New(errors.CodeAssetNotFound, "设备换货绑定卡不存在") } cardID := strconv.FormatUint(uint64(card.ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourceIotCard, ID: &cardID, Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID, Relation: constants.AuditResourceRelationReference, Role: item.cardRole, IdentitySnapshot: audit.IotCardIdentitySnapshot(card), SubjectVisibility: constants.AuditSubjectInternalOnly, }) bindingID := strconv.FormatUint(uint64(binding.ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID, Key: bindingID, DisplayName: preferredDeviceIdentifier(item.device), Relation: constants.AuditResourceRelationReference, Role: item.bindingRole, IdentitySnapshot: map[string]any{ "id": binding.ID, "device_id": binding.DeviceID, "device_virtual_no": item.device.VirtualNo, "slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID, "iccid": card.ICCID, "virtual_no": card.VirtualNo, "is_current": binding.IsCurrent, }, SubjectVisibility: constants.AuditSubjectInternalOnly, }) } } return resources, nil } func loadDeviceExchangeWalletResources(ctx context.Context, tx *gorm.DB, oldDeviceID, newDeviceID uint, before map[uint]model.AssetWallet) ([]audit.ResourceInput, error) { return loadExchangeWalletResources(ctx, tx, constants.ExchangeAssetTypeDevice, oldDeviceID, newDeviceID, before, constants.AuditResourceRoleDeviceExchangeOldWallet, constants.AuditResourceRoleDeviceExchangeNewWallet, "设备") } func loadDeviceExchangeRenewWalletResource(ctx context.Context, tx *gorm.DB, deviceID uint, before map[uint]model.AssetWallet) (*audit.ResourceInput, error) { return loadExchangeRenewWalletResource(ctx, tx, constants.ExchangeAssetTypeDevice, deviceID, before, constants.AuditResourceRoleDeviceExchangeOldWallet, "设备") } func loadDeviceExchangeTransactionResources(ctx context.Context, tx *gorm.DB, exchangeNo string) ([]audit.ResourceInput, error) { return loadExchangeTransactionResources(ctx, tx, exchangeNo, constants.AuditResourceRoleDeviceExchangeWalletTransaction, "设备") } func loadDeviceExchangePackageUsageResources(ctx context.Context, tx *gorm.DB, ids []uint, oldDeviceID, newDeviceID uint) ([]audit.ResourceInput, error) { return loadExchangePackageUsageResources(ctx, tx, ids, "device_id", oldDeviceID, newDeviceID, constants.AuditResourceRoleDeviceExchangePackageUsage, "设备") } func (s *Service) recordExchangeOrderFailure(ctx context.Context, cardActionCode, summary string, order *model.ExchangeOrder, businessErr error) { if order == nil || order.OldAssetType != constants.ExchangeAssetTypeDevice { s.recordCardExchangeOrderFailure(ctx, cardActionCode, summary, order, businessErr) return } oldDevice, newDevice := s.loadDeviceExchangeAuditDevices(ctx, order) if s.db == nil || s.auditWriter == nil { recordCardExchangeAuditSecondaryFailure(ctx, deviceExchangeActionCode(cardActionCode), order.ExchangeNo, businessErr, errors.New(errors.CodeInvalidStatus, "设备换货统一审计接缝未配置")) return } if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.appendDeviceExchangeAudit(ctx, tx, deviceExchangeActionCode(cardActionCode), strings.ReplaceAll(summary, "卡", "设备"), cardExchangeFailureResult(businessErr), order, oldDevice, newDevice, nil, nil, nil, nil, nil, nil, nil, businessErr) }); err != nil { recordCardExchangeAuditSecondaryFailure(ctx, deviceExchangeActionCode(cardActionCode), order.ExchangeNo, businessErr, err) } } func (s *Service) loadDeviceExchangeAuditDevices(ctx context.Context, order *model.ExchangeOrder) (*model.Device, *model.Device) { if order == nil || order.OldAssetType != constants.ExchangeAssetTypeDevice { return nil, nil } var oldDevice *model.Device if s.deviceStore != nil { oldDevice, _ = s.deviceStore.GetByID(ctx, order.OldAssetID) } if oldDevice == nil { oldDevice = &model.Device{Model: gorm.Model{ID: order.OldAssetID}, ShopID: order.ShopID} } var newDevice *model.Device if order.NewAssetID != nil && *order.NewAssetID > 0 { if s.deviceStore != nil { newDevice, _ = s.deviceStore.GetByID(ctx, *order.NewAssetID) } if newDevice == nil { newDevice = &model.Device{Model: gorm.Model{ID: *order.NewAssetID}, ShopID: order.ShopID} } } return oldDevice, newDevice }