Files
junhong_cmp_fiber/internal/infrastructure/audit/wallet.go
break 5e552d99bc 收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
2026-08-05 14:30:54 +08:00

294 lines
15 KiB
Go

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
}