收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
494
internal/service/exchange/audit.go
Normal file
494
internal/service/exchange/audit.go
Normal file
@@ -0,0 +1,494 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user