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