package audit import ( "context" "strconv" "strings" "gorm.io/gorm" walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // WriteAgentWalletBalanceAdjustment 将人工余额调整写入统一 Audit Event。 func (w *Writer) WriteAgentWalletBalanceAdjustment(ctx context.Context, tx *gorm.DB, event walletapp.CreditedEvent) error { if event.WalletID == 0 || event.ReferenceType != constants.ReferenceTypeManualAdjustment || event.ReferenceID == 0 || event.TransactionType != constants.AgentTransactionTypeAdjustment || event.Amount <= 0 || strings.TrimSpace(event.Remark) == "" { return errors.New(errors.CodeInvalidParam, "代理主钱包人工调整审计事实不完整") } var wallet model.AgentWallet if err := tx.WithContext(ctx).Unscoped().First(&wallet, event.WalletID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询人工调整代理主钱包审计快照失败") } var transaction model.AgentWalletTransaction if err := tx.WithContext(ctx).Unscoped().Where( "agent_wallet_id = ? AND reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?", event.WalletID, event.ReferenceType, event.ReferenceID, event.TransactionType, constants.TransactionStatusSuccess, ).First(&transaction).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包人工调整流水失败") } walletResource := agentWalletAuditResource(&wallet, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleWalletTarget) walletResource.BeforeData = map[string]any{"balance": transaction.BalanceBefore, "frozen_balance": wallet.FrozenBalance} walletResource.AfterData = map[string]any{"balance": transaction.BalanceAfter, "frozen_balance": wallet.FrozenBalance} walletResource.SubjectVisibility = constants.AuditSubjectResult walletResource.SubjectSummary = "代理主钱包余额已人工调整" transactionResource := agentWalletTransactionResource(&transaction) transactionResource.Role = constants.AuditResourceRoleWalletTransaction return w.Append(ctx, tx, AppendInput{ ActionCode: constants.AuditActionAgentWalletBalanceAdjusted, Summary: "人工调整代理主钱包余额", ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess, CorrelationID: event.CorrelationID, Metadata: map[string]any{ "amount": event.Amount, "reason": strings.TrimSpace(event.Remark), "reference_type": event.ReferenceType, "reference_id": event.ReferenceID, }, Resources: []ResourceInput{walletResource, transactionResource}, }) } // WriteAgentWalletCreditChange 将实际信用额度变化写入统一 Audit Event。 func (w *Writer) WriteAgentWalletCreditChange(ctx context.Context, tx *gorm.DB, change walletapp.CreditChangeAudit) error { if change.Wallet == nil || change.Wallet.ID == 0 || change.Wallet.ShopID == 0 || change.Wallet.WalletType != constants.AgentWalletTypeMain { return errors.New(errors.CodeInvalidParam, "代理主钱包信用额度审计事实不完整") } walletResource := agentWalletAuditResource(change.Wallet, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleWalletTarget) walletResource.BeforeData = change.BeforeData walletResource.AfterData = change.AfterData walletResource.SubjectVisibility = constants.AuditSubjectResult var shop model.Shop if err := tx.WithContext(ctx).Unscoped().First(&shop, change.Wallet.ShopID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询信用额度关联店铺审计快照失败") } shopResource := ShopResource(&shop, constants.AuditResourceRelationReference, constants.AuditResourceRoleWalletShop) result := change.Result if result == "" { result = constants.AuditResultSuccess } walletResource.SubjectSummary = "代理主钱包信用额度已更新" if result != constants.AuditResultSuccess { walletResource.SubjectSummary = "代理主钱包信用额度更新未完成" } return w.Append(ctx, tx, AppendInput{ ActionCode: constants.AuditActionAgentWalletCreditChanged, Summary: "调整代理主钱包信用额度", ScopeType: constants.AuditScopePlatform, Result: result, ErrorCode: change.ErrorCode, ErrorSummary: change.ErrorSummary, Resources: []ResourceInput{walletResource, shopResource}, }) } // WriteAgentWalletDebit 将代理主钱包订单扣款写入统一 Audit Event。 func (w *Writer) WriteAgentWalletDebit(ctx context.Context, tx *gorm.DB, event walletapp.DebitedEvent) error { if event.WalletID == 0 || event.ReferenceType != constants.ReferenceTypeOrder || event.ReferenceID == 0 || event.Amount <= 0 { return errors.New(errors.CodeInvalidParam, "代理主钱包订单扣款审计事实不完整") } completed, err := completedReservationExists(ctx, tx, event.ReferenceID) if err != nil || completed { return err } order, wallet, transaction, err := loadAgentWalletDebitFacts(ctx, tx, event) if err != nil { return err } resources := []ResourceInput{ walletOrderResource(order, "代理主钱包订单扣款"), agentWalletResource(wallet, transaction.BalanceBefore, wallet.FrozenBalance, transaction.BalanceAfter, wallet.FrozenBalance), agentWalletTransactionResource(transaction), } return w.Append(ctx, tx, AppendInput{ ActionCode: constants.AuditActionAgentWalletOrderDebited, Summary: "代理主钱包完成订单扣款", ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess, CorrelationID: event.CorrelationID, Metadata: map[string]any{"amount": event.Amount}, Resources: resources, }) } // WriteAgentWalletReservation 将代理主钱包订单预占状态变化写入统一 Audit Event。 func (w *Writer) WriteAgentWalletReservation(ctx context.Context, tx *gorm.DB, event walletapp.ReservationEvent) error { actionCode, summary, err := reservationAuditAction(event.Status) if err != nil { return err } if event.ReservationID == 0 || event.WalletID == 0 || event.ReferenceType != constants.ReferenceTypeOrder || event.ReferenceID == 0 || event.Amount <= 0 { return errors.New(errors.CodeInvalidParam, "代理主钱包订单预占审计事实不完整") } order, wallet, reservation, transaction, err := loadAgentWalletReservationFacts(ctx, tx, event) if err != nil { return err } beforeBalance, beforeFrozen, afterBalance, afterFrozen := reservationWalletState(wallet, transaction, event) resources := []ResourceInput{ walletOrderResource(order, summary), agentWalletResource(wallet, beforeBalance, beforeFrozen, afterBalance, afterFrozen), agentWalletReservationResource(reservation, event.Status), } if transaction != nil { resources = append(resources, agentWalletTransactionResource(transaction)) } return w.Append(ctx, tx, AppendInput{ ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess, CorrelationID: event.CorrelationID, Metadata: map[string]any{"amount": event.Amount}, Resources: resources, }) } func completedReservationExists(ctx context.Context, tx *gorm.DB, orderID uint) (bool, error) { var count int64 err := tx.WithContext(ctx).Model(&model.AgentWalletReservation{}). Where("reference_type = ? AND reference_id = ? AND status = ?", constants.ReferenceTypeOrder, orderID, constants.AgentWalletReservationStatusCompleted). Count(&count).Error if err != nil { return false, errors.Wrap(errors.CodeDatabaseError, err, "查询订单钱包预占终态失败") } return count > 0, nil } func loadAgentWalletDebitFacts(ctx context.Context, tx *gorm.DB, event walletapp.DebitedEvent) (*model.Order, *model.AgentWallet, *model.AgentWalletTransaction, error) { var order model.Order if err := tx.WithContext(ctx).Unscoped().First(&order, event.ReferenceID).Error; err != nil { return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询钱包扣款订单审计快照失败") } var wallet model.AgentWallet if err := tx.WithContext(ctx).Unscoped().First(&wallet, event.WalletID).Error; err != nil { return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包审计快照失败") } transaction, err := loadOrderDebitTransaction(ctx, tx, event.WalletID, event.ReferenceID) if err != nil { return nil, nil, nil, err } return &order, &wallet, transaction, nil } func loadAgentWalletReservationFacts(ctx context.Context, tx *gorm.DB, event walletapp.ReservationEvent) (*model.Order, *model.AgentWallet, *model.AgentWalletReservation, *model.AgentWalletTransaction, error) { var order model.Order if err := tx.WithContext(ctx).Unscoped().First(&order, event.ReferenceID).Error; err != nil { return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询钱包预占订单审计快照失败") } var wallet model.AgentWallet if err := tx.WithContext(ctx).Unscoped().First(&wallet, event.WalletID).Error; err != nil { return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包审计快照失败") } var reservation model.AgentWalletReservation if err := tx.WithContext(ctx).First(&reservation, event.ReservationID).Error; err != nil { return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包预占审计快照失败") } var transaction *model.AgentWalletTransaction if event.Status == constants.AgentWalletReservationStatusCompleted { loaded, err := loadOrderDebitTransaction(ctx, tx, event.WalletID, event.ReferenceID) if err != nil { return nil, nil, nil, nil, err } transaction = loaded } return &order, &wallet, &reservation, transaction, nil } func loadOrderDebitTransaction(ctx context.Context, tx *gorm.DB, walletID, orderID uint) (*model.AgentWalletTransaction, error) { var transaction model.AgentWalletTransaction err := tx.WithContext(ctx).Unscoped().Where( "agent_wallet_id = ? AND reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?", walletID, constants.ReferenceTypeOrder, orderID, constants.AgentTransactionTypeDeduct, constants.TransactionStatusSuccess, ).First(&transaction).Error if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包订单扣款流水失败") } return &transaction, nil } func walletOrderResource(order *model.Order, summary string) ResourceInput { resource := OrderResource(order, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleOrderTarget) resource.SubjectVisibility = constants.AuditSubjectResult resource.SubjectSummary = summary return resource } func agentWalletResource(wallet *model.AgentWallet, beforeBalance, beforeFrozen, afterBalance, afterFrozen int64) ResourceInput { id := strconv.FormatUint(uint64(wallet.ID), 10) return ResourceInput{ Type: constants.AuditResourceAgentWallet, ID: &id, Key: id, DisplayName: "代理主钱包 " + id, Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleOrderWallet, IdentitySnapshot: map[string]any{ "id": wallet.ID, "shop_id": wallet.ShopID, "wallet_type": wallet.WalletType, "currency": wallet.Currency, "status": wallet.Status, }, BeforeData: map[string]any{"balance": beforeBalance, "frozen_balance": beforeFrozen}, AfterData: map[string]any{"balance": afterBalance, "frozen_balance": afterFrozen}, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "代理主钱包订单资金已更新", } } func agentWalletAuditResource(wallet *model.AgentWallet, relation, role string) ResourceInput { id := strconv.FormatUint(uint64(wallet.ID), 10) return ResourceInput{ Type: constants.AuditResourceAgentWallet, ID: &id, Key: id, DisplayName: "代理主钱包 " + id, Relation: relation, Role: role, IdentitySnapshot: map[string]any{ "id": wallet.ID, "shop_id": wallet.ShopID, "wallet_type": wallet.WalletType, "currency": wallet.Currency, "status": wallet.Status, "credit_enabled": wallet.CreditEnabled, "credit_limit": wallet.CreditLimit, }, } } func agentWalletReservationResource(reservation *model.AgentWalletReservation, status int) ResourceInput { id := strconv.FormatUint(uint64(reservation.ID), 10) return ResourceInput{ Type: constants.AuditResourceAgentWalletReservation, ID: &id, Key: reservation.ReferenceType + ":" + strconv.FormatUint(uint64(reservation.ReferenceID), 10), DisplayName: "订单钱包预占 " + id, Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleOrderWalletReservation, IdentitySnapshot: map[string]any{ "id": reservation.ID, "agent_wallet_id": reservation.AgentWalletID, "shop_id": reservation.ShopID, "amount": reservation.Amount, "status": status, "reference_type": reservation.ReferenceType, "reference_id": reservation.ReferenceID, }, BeforeData: map[string]any{"status": reservationStatusBefore(status)}, AfterData: map[string]any{"status": status}, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "订单钱包预占状态已更新", } } func agentWalletTransactionResource(transaction *model.AgentWalletTransaction) ResourceInput { id := strconv.FormatUint(uint64(transaction.ID), 10) return ResourceInput{ Type: constants.AuditResourceAgentWalletTransaction, ID: &id, Key: id, DisplayName: "代理钱包流水 " + id, Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleOrderWalletTransaction, IdentitySnapshot: map[string]any{ "id": transaction.ID, "agent_wallet_id": transaction.AgentWalletID, "shop_id": transaction.ShopID, "transaction_type": transaction.TransactionType, "transaction_subtype": transaction.TransactionSubtype, "reference_type": transaction.ReferenceType, "reference_id": transaction.ReferenceID, "status": transaction.Status, }, AfterData: map[string]any{"amount": transaction.Amount, "balance_before": transaction.BalanceBefore, "balance_after": transaction.BalanceAfter}, } } func reservationAuditAction(status int) (string, string, error) { switch status { case constants.AgentWalletReservationStatusFrozen: return constants.AuditActionAgentWalletOrderReserved, "代理主钱包已预占订单资金", nil case constants.AgentWalletReservationStatusReleased: return constants.AuditActionAgentWalletOrderReleased, "代理主钱包已释放订单预占", nil case constants.AgentWalletReservationStatusCompleted: return constants.AuditActionAgentWalletOrderCompleted, "代理主钱包已完成订单预占扣款", nil default: return "", "", errors.New(errors.CodeInvalidParam, "代理主钱包预占状态不受支持") } } func reservationWalletState(wallet *model.AgentWallet, transaction *model.AgentWalletTransaction, event walletapp.ReservationEvent) (int64, int64, int64, int64) { afterBalance, afterFrozen := wallet.Balance, wallet.FrozenBalance switch event.Status { case constants.AgentWalletReservationStatusFrozen: return afterBalance, afterFrozen - event.Amount, afterBalance, afterFrozen case constants.AgentWalletReservationStatusReleased: return afterBalance, afterFrozen + event.Amount, afterBalance, afterFrozen default: return transaction.BalanceBefore, afterFrozen + event.Amount, transaction.BalanceAfter, afterFrozen } } func reservationStatusBefore(status int) any { if status == constants.AgentWalletReservationStatusFrozen { return nil } return constants.AgentWalletReservationStatusFrozen }