package exchange import ( "context" "strconv" "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/auditfailure" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) type cardExchangeAuditBefore struct { Wallets map[uint]model.AssetWallet DeviceBindings []*model.PersonalCustomerDevice ICCIDBindings []*model.PersonalCustomerICCID } // SetAccessAudit 注入卡与设备换货完整用例的统一审计 Writer。 func (s *Service) SetAccessAudit(writer *audit.Writer) { s.auditWriter = writer } func (s *Service) appendCardExchangeAudit( ctx context.Context, tx *gorm.DB, actionCode, summary, result string, order *model.ExchangeOrder, oldCard, newCard *model.IotCard, orderBefore, orderAfter map[string]any, oldCardBefore, oldCardAfter map[string]any, newCardBefore, newCardAfter map[string]any, extra []audit.ResourceInput, businessErr error, ) error { if order == nil || order.OldAssetType != constants.ExchangeAssetTypeIotCard { return nil } if s.auditWriter == nil { return errors.New(errors.CodeInvalidStatus, "卡换货统一审计接缝未配置") } internalOnly := actionCode == constants.AuditActionCardExchangeRenewed resources := []audit.ResourceInput{cardExchangeOrderAuditResource(order, summary, internalOnly, orderBefore, orderAfter)} if oldCard != nil { resources = append(resources, cardExchangeCardAuditResource(oldCard, constants.AuditResourceRoleCardExchangeOldCard, summary, internalOnly, oldCardBefore, oldCardAfter)) } if newCard != nil { resources = append(resources, cardExchangeCardAuditResource(newCard, constants.AuditResourceRoleCardExchangeNewCard, summary, internalOnly, newCardBefore, newCardAfter)) } shopResource, err := loadCardExchangeShopAuditResource(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.AuditActionCardExchangeShippingInfoSubmitted { 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 cardExchangeOrderAuditResource(order *model.ExchangeOrder, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput { key := order.ExchangeNo if key == "" { key = "iot_card:" + strconv.FormatUint(uint64(order.OldAssetID), 10) + ":exchange" } var id *string if order.ID > 0 { value := strconv.FormatUint(uint64(order.ID), 10) id = &value } resource := audit.ResourceInput{ Type: constants.AuditResourceExchangeOrder, ID: id, Key: key, DisplayName: key, Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleCardExchangeOrder, IdentitySnapshot: map[string]any{ "id": order.ID, "exchange_no": order.ExchangeNo, "flow_type": effectiveExchangeFlowType(order.FlowType), "old_asset_type": order.OldAssetType, "old_asset_id": order.OldAssetID, "old_asset_identifier": order.OldAssetIdentifier, "new_asset_type": order.NewAssetType, "new_asset_id": order.NewAssetID, "new_asset_identifier": order.NewAssetIdentifier, "shop_id": order.ShopID, "status": order.Status, }, BeforeData: beforeData, AfterData: afterData, } if internalOnly { resource.SubjectVisibility = constants.AuditSubjectInternalOnly } else { resource.SubjectVisibility = constants.AuditSubjectResult resource.SubjectSummary = summary } return resource } func cardExchangeCardAuditResource(card *model.IotCard, role, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput { id := strconv.FormatUint(uint64(card.ID), 10) relation := constants.AuditResourceRelationReference if len(beforeData) > 0 || len(afterData) > 0 { relation = constants.AuditResourceRelationAffected } resource := audit.ResourceInput{ Type: constants.AuditResourceIotCard, ID: &id, Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID, Relation: relation, Role: role, IdentitySnapshot: audit.IotCardIdentitySnapshot(card), BeforeData: beforeData, AfterData: afterData, } if internalOnly { resource.SubjectVisibility = constants.AuditSubjectInternalOnly } else { resource.SubjectVisibility = constants.AuditSubjectResult resource.SubjectSummary = summary } return resource } func loadCardExchangeShopAuditResource(ctx context.Context, tx *gorm.DB, shopID *uint) (*audit.ResourceInput, error) { if shopID == nil || *shopID == 0 { return nil, nil } var shop model.Shop if err := tx.WithContext(ctx).Unscoped().Where("id = ?", *shopID).First(&shop).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货所属店铺失败") } id := strconv.FormatUint(uint64(shop.ID), 10) return &audit.ResourceInput{ Type: constants.AuditResourceShop, ID: &id, Key: id, DisplayName: shop.ShopName, Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleCardExchangeShop, IdentitySnapshot: map[string]any{"id": shop.ID, "shop_code": shop.ShopCode, "shop_name": shop.ShopName, "parent_id": shop.ParentID, "level": shop.Level}, SubjectVisibility: constants.AuditSubjectInternalOnly, }, nil } func (s *Service) captureCardExchangeAuditBefore(ctx context.Context, tx *gorm.DB, oldCard, newCard *model.IotCard) (*cardExchangeAuditBefore, error) { state := &cardExchangeAuditBefore{Wallets: make(map[uint]model.AssetWallet)} cardIDs := make([]uint, 0, 2) if oldCard != nil { cardIDs = append(cardIDs, oldCard.ID) } if newCard != nil { cardIDs = append(cardIDs, newCard.ID) } if len(cardIDs) > 0 { var wallets []model.AssetWallet if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id IN ?", constants.ExchangeAssetTypeIotCard, cardIDs).Find(&wallets).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货钱包审计快照失败") } for _, wallet := range wallets { state.Wallets[wallet.ResourceID] = wallet } } devices, iccids, err := loadCardExchangeBindings(ctx, tx, oldCard) if err != nil { return nil, err } state.DeviceBindings, state.ICCIDBindings = devices, iccids return state, nil } func loadCardExchangeBindings(ctx context.Context, tx *gorm.DB, card *model.IotCard) ([]*model.PersonalCustomerDevice, []*model.PersonalCustomerICCID, error) { if card == nil { return nil, nil, nil } if card.VirtualNo != "" { var rows []*model.PersonalCustomerDevice if err := tx.WithContext(ctx).Where("virtual_no = ? AND status = ?", card.VirtualNo, constants.StatusEnabled).Find(&rows).Error; err != nil { return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货客户绑定失败") } return rows, nil, nil } var rows []*model.PersonalCustomerICCID if err := tx.WithContext(ctx).Where("iccid = ? AND status = ?", card.ICCID, constants.StatusEnabled).Find(&rows).Error; err != nil { return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货 ICCID 绑定失败") } return nil, rows, nil } func (s *Service) buildCardExchangeCompletionResources( ctx context.Context, tx *gorm.DB, order *model.ExchangeOrder, oldCard, newCard *model.IotCard, before *cardExchangeAuditBefore, migration *exchangeMigrationResult, ) ([]audit.ResourceInput, error) { resources := cardExchangeOldBindingResources(before) devices, iccids, err := loadCardExchangeBindings(ctx, tx, newCard) if err != nil { return nil, err } resources = append(resources, cardExchangeNewBindingResources(devices, iccids)...) beforeWallets := map[uint]model.AssetWallet(nil) if before != nil { beforeWallets = before.Wallets } walletResources, err := loadCardExchangeWalletResources(ctx, tx, oldCard.ID, newCard.ID, beforeWallets) if err != nil { return nil, err } resources = append(resources, walletResources...) if migration == nil { return resources, nil } transactionResources, err := loadCardExchangeTransactionResources(ctx, tx, order.ExchangeNo) if err != nil { return nil, err } resources = append(resources, transactionResources...) usageResources, err := loadCardExchangePackageUsageResources(ctx, tx, migration.PackageUsageIDs, oldCard.ID, newCard.ID) if err != nil { return nil, err } return append(resources, usageResources...), nil } func cardExchangeOldBindingResources(before *cardExchangeAuditBefore) []audit.ResourceInput { if before == nil { return nil } resources := make([]audit.ResourceInput, 0, len(before.DeviceBindings)+len(before.ICCIDBindings)) for _, row := range before.DeviceBindings { if row == nil { continue } id := strconv.FormatUint(uint64(row.ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourcePersonalCustomerDevice, ID: &id, Key: id, DisplayName: row.VirtualNo, Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePersonalCustomerOldAssetBinding, 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}, BeforeData: map[string]any{"virtual_no": row.VirtualNo, "status": row.Status}, SubjectVisibility: constants.AuditSubjectInternalOnly, }) } for _, row := range before.ICCIDBindings { if row == nil { continue } id := strconv.FormatUint(uint64(row.ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourcePersonalCustomerICCID, ID: &id, Key: id, DisplayName: row.ICCID, Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePersonalCustomerOldAssetBinding, IdentitySnapshot: map[string]any{"id": row.ID, "customer_id": row.CustomerID, "iccid": row.ICCID, "iccid_19": row.ICCID19, "bind_at": row.BindAt, "last_used_at": row.LastUsedAt, "status": row.Status}, BeforeData: map[string]any{"iccid": row.ICCID, "status": row.Status}, SubjectVisibility: constants.AuditSubjectInternalOnly, }) } return resources } func cardExchangeNewBindingResources(devices []*model.PersonalCustomerDevice, iccids []*model.PersonalCustomerICCID) []audit.ResourceInput { resources := make([]audit.ResourceInput, 0, len(devices)+len(iccids)) for _, row := range devices { if row == nil { continue } id := strconv.FormatUint(uint64(row.ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourcePersonalCustomerDevice, ID: &id, Key: id, DisplayName: row.VirtualNo, Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePersonalCustomerNewAssetBinding, 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}, AfterData: map[string]any{"virtual_no": row.VirtualNo, "status": row.Status}, SubjectVisibility: constants.AuditSubjectInternalOnly, }) } for _, row := range iccids { if row == nil { continue } id := strconv.FormatUint(uint64(row.ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourcePersonalCustomerICCID, ID: &id, Key: id, DisplayName: row.ICCID, Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePersonalCustomerNewAssetBinding, IdentitySnapshot: map[string]any{"id": row.ID, "customer_id": row.CustomerID, "iccid": row.ICCID, "iccid_19": row.ICCID19, "bind_at": row.BindAt, "last_used_at": row.LastUsedAt, "status": row.Status}, AfterData: map[string]any{"iccid": row.ICCID, "status": row.Status}, SubjectVisibility: constants.AuditSubjectInternalOnly, }) } return resources } func loadCardExchangeWalletResources(ctx context.Context, tx *gorm.DB, oldCardID, newCardID uint, before map[uint]model.AssetWallet) ([]audit.ResourceInput, error) { return loadExchangeWalletResources(ctx, tx, constants.ExchangeAssetTypeIotCard, oldCardID, newCardID, before, constants.AuditResourceRoleCardExchangeOldWallet, constants.AuditResourceRoleCardExchangeNewWallet, "卡") } func loadExchangeWalletResources(ctx context.Context, tx *gorm.DB, assetType string, oldAssetID, newAssetID uint, before map[uint]model.AssetWallet, oldRole, newRole, assetName string) ([]audit.ResourceInput, error) { var wallets []model.AssetWallet if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id IN ?", assetType, []uint{oldAssetID, newAssetID}).Find(&wallets).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货迁移后钱包失败") } resources := make([]audit.ResourceInput, 0, len(wallets)) for _, wallet := range wallets { id := strconv.FormatUint(uint64(wallet.ID), 10) role := newRole if wallet.ResourceID == oldAssetID { role = oldRole } relation := constants.AuditResourceRelationAffected beforeData, afterData := map[string]any{"exists": false}, cardExchangeWalletData(wallet) if previous, ok := before[wallet.ResourceID]; ok { if !cardExchangeWalletChanged(previous, wallet) { relation, beforeData, afterData = constants.AuditResourceRelationReference, nil, nil } else { beforeData = cardExchangeWalletData(previous) } } resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourceAssetWallet, ID: &id, Key: id, DisplayName: id, Relation: relation, Role: role, IdentitySnapshot: map[string]any{"id": wallet.ID, "resource_type": wallet.ResourceType, "resource_id": wallet.ResourceID, "currency": wallet.Currency, "shop_id_tag": wallet.ShopIDTag, "enterprise_id_tag": wallet.EnterpriseIDTag}, BeforeData: beforeData, AfterData: afterData, SubjectVisibility: constants.AuditSubjectInternalOnly, }) } return resources, nil } func cardExchangeWalletData(wallet model.AssetWallet) map[string]any { return map[string]any{"balance": wallet.Balance, "frozen_balance": wallet.FrozenBalance, "status": wallet.Status, "version": wallet.Version, "shop_id_tag": wallet.ShopIDTag, "enterprise_id_tag": wallet.EnterpriseIDTag} } func cardExchangeWalletChanged(before, after model.AssetWallet) bool { return before.Balance != after.Balance || before.FrozenBalance != after.FrozenBalance || before.Status != after.Status || before.Version != after.Version || before.ShopIDTag != after.ShopIDTag || !sameOptionalUint(before.EnterpriseIDTag, after.EnterpriseIDTag) } func sameOptionalUint(left, right *uint) bool { return left == nil && right == nil || left != nil && right != nil && *left == *right } func loadCardExchangeRenewWalletResource(ctx context.Context, tx *gorm.DB, cardID uint, before map[uint]model.AssetWallet) (*audit.ResourceInput, error) { return loadExchangeRenewWalletResource(ctx, tx, constants.ExchangeAssetTypeIotCard, cardID, before, constants.AuditResourceRoleCardExchangeOldWallet, "卡") } func loadExchangeRenewWalletResource(ctx context.Context, tx *gorm.DB, assetType string, assetID uint, before map[uint]model.AssetWallet, role, assetName string) (*audit.ResourceInput, error) { var wallet model.AssetWallet if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id = ?", assetType, assetID).First(&wallet).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询旧"+assetName+"转新钱包失败") } id := strconv.FormatUint(uint64(wallet.ID), 10) beforeData := map[string]any{"exists": false} if previous, ok := before[assetID]; ok { beforeData = cardExchangeWalletData(previous) } return &audit.ResourceInput{ Type: constants.AuditResourceAssetWallet, ID: &id, Key: id, DisplayName: id, Relation: constants.AuditResourceRelationAffected, Role: role, IdentitySnapshot: map[string]any{"id": wallet.ID, "resource_type": wallet.ResourceType, "resource_id": wallet.ResourceID, "currency": wallet.Currency, "shop_id_tag": wallet.ShopIDTag, "enterprise_id_tag": wallet.EnterpriseIDTag}, BeforeData: beforeData, AfterData: cardExchangeWalletData(wallet), SubjectVisibility: constants.AuditSubjectInternalOnly, }, nil } func loadCardExchangeTransactionResources(ctx context.Context, tx *gorm.DB, exchangeNo string) ([]audit.ResourceInput, error) { return loadExchangeTransactionResources(ctx, tx, exchangeNo, constants.AuditResourceRoleCardExchangeWalletTransaction, "卡") } func loadExchangeTransactionResources(ctx context.Context, tx *gorm.DB, exchangeNo, role, assetName string) ([]audit.ResourceInput, error) { var rows []model.AssetWalletTransaction if err := tx.WithContext(ctx).Where("transaction_type = ? AND reference_type = ? AND reference_no = ?", constants.AssetTransactionTypeExchange, constants.ReferenceTypeExchange, exchangeNo).Find(&rows).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货钱包流水失败") } resources := make([]audit.ResourceInput, 0, len(rows)) for _, row := range rows { id := strconv.FormatUint(uint64(row.ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourceAssetWalletTransaction, ID: &id, Key: id, DisplayName: exchangeNo, Relation: constants.AuditResourceRelationAffected, Role: role, IdentitySnapshot: map[string]any{"id": row.ID, "asset_wallet_id": row.AssetWalletID, "resource_type": row.ResourceType, "resource_id": row.ResourceID, "transaction_type": row.TransactionType, "reference_type": row.ReferenceType, "reference_no": row.ReferenceNo, "status": row.Status}, AfterData: map[string]any{"amount": row.Amount, "balance_before": row.BalanceBefore, "balance_after": row.BalanceAfter}, SubjectVisibility: constants.AuditSubjectInternalOnly, }) } return resources, nil } func loadCardExchangePackageUsageResources(ctx context.Context, tx *gorm.DB, ids []uint, oldCardID, newCardID uint) ([]audit.ResourceInput, error) { return loadExchangePackageUsageResources(ctx, tx, ids, "iot_card_id", oldCardID, newCardID, constants.AuditResourceRoleCardExchangePackageUsage, "卡") } func loadExchangePackageUsageResources(ctx context.Context, tx *gorm.DB, ids []uint, assetIDField string, oldAssetID, newAssetID uint, role, assetName string) ([]audit.ResourceInput, error) { if len(ids) == 0 { return nil, nil } var rows []model.PackageUsage if err := tx.WithContext(ctx).Where("id IN ?", ids).Order("id ASC").Find(&rows).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货套餐权益失败") } orderIDs := make(map[uint]struct{}, len(rows)) packageIDs := make(map[uint]struct{}, len(rows)) resources := make([]audit.ResourceInput, 0, len(rows)*3) for _, row := range rows { resource := audit.PackageUsageResource(&row, constants.AuditResourceRelationAffected, role, map[string]any{assetIDField: oldAssetID}, map[string]any{assetIDField: newAssetID}) resource.SubjectVisibility = constants.AuditSubjectInternalOnly resources = append(resources, resource) orderIDs[row.OrderID] = struct{}{} packageIDs[row.PackageID] = struct{}{} } var orders []model.Order if err := tx.WithContext(ctx).Where("id IN ?", exchangeUintKeys(orderIDs)).Order("id ASC").Find(&orders).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货套餐权益关联订单失败") } for i := range orders { resources = append(resources, audit.OrderResource(&orders[i], constants.AuditResourceRelationReference, constants.AuditResourceRolePackageUsageOrder)) } var packages []model.Package if err := tx.WithContext(ctx).Where("id IN ?", exchangeUintKeys(packageIDs)).Order("id ASC").Find(&packages).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询"+assetName+"换货套餐权益关联套餐失败") } for i := range packages { resources = append(resources, audit.PackageResource(&packages[i], constants.AuditResourceRelationReference, constants.AuditResourceRolePackageUsagePackage, nil, nil)) } return resources, nil } func exchangeUintKeys(values map[uint]struct{}) []uint { result := make([]uint, 0, len(values)) for value := range values { if value > 0 { result = append(result, value) } } return result } func (s *Service) recordCardExchangeFailure(ctx context.Context, actionCode, summary, result string, order *model.ExchangeOrder, oldCard, newCard *model.IotCard, businessErr error) { if order == nil || order.OldAssetType != constants.ExchangeAssetTypeIotCard { return } if s.db == nil || s.auditWriter == nil { recordCardExchangeAuditSecondaryFailure(ctx, actionCode, order.ExchangeNo, businessErr, errors.New(errors.CodeInvalidStatus, "卡换货统一审计接缝未配置")) return } if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.appendCardExchangeAudit(ctx, tx, actionCode, summary, result, order, oldCard, newCard, nil, nil, nil, nil, nil, nil, nil, businessErr) }); err != nil { recordCardExchangeAuditSecondaryFailure(ctx, actionCode, order.ExchangeNo, businessErr, err) } } func recordCardExchangeAuditSecondaryFailure(ctx context.Context, actionCode, exchangeNo string, businessErr, auditErr error) { errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr) linkage := auditcontext.From(ctx) auditfailure.RecordSecondaryWriteFailure(actionCode, exchangeNo, linkage.RequestID, linkage.CorrelationID, errorCode, auditErr) } func (s *Service) recordCardExchangeOrderFailure(ctx context.Context, actionCode, summary string, order *model.ExchangeOrder, businessErr error) { if order == nil || order.OldAssetType != constants.ExchangeAssetTypeIotCard { return } oldCard, newCard := s.loadCardExchangeAuditCards(ctx, order) s.recordCardExchangeFailure(ctx, actionCode, summary, cardExchangeFailureResult(businessErr), order, oldCard, newCard, businessErr) } func (s *Service) loadCardExchangeAuditCards(ctx context.Context, order *model.ExchangeOrder) (*model.IotCard, *model.IotCard) { if order == nil || order.OldAssetType != constants.ExchangeAssetTypeIotCard { return nil, nil } var oldCard *model.IotCard if s.iotCardStore != nil { oldCard, _ = s.iotCardStore.GetByID(ctx, order.OldAssetID) } if oldCard == nil { oldCard = &model.IotCard{Model: gorm.Model{ID: order.OldAssetID}, ICCID: order.OldAssetIdentifier, ShopID: order.ShopID} } var newCard *model.IotCard if order.NewAssetID != nil && *order.NewAssetID > 0 { if s.iotCardStore != nil { newCard, _ = s.iotCardStore.GetByID(ctx, *order.NewAssetID) } if newCard == nil { newCard = &model.IotCard{Model: gorm.Model{ID: *order.NewAssetID}, ICCID: order.NewAssetIdentifier, ShopID: order.ShopID} } } return oldCard, newCard } func cardExchangeFailureResult(err error) string { appErr, ok := err.(*errors.AppError) if !ok { return constants.AuditResultFailed } switch appErr.Code { case errors.CodeDatabaseError, errors.CodeInternalError, errors.CodeExchangeMigrationFailed: return constants.AuditResultFailed default: return constants.AuditResultDenied } }