Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
495 lines
23 KiB
Go
495 lines
23 KiB
Go
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
|
|
}
|
|
}
|