收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
363
internal/service/order/audit.go
Normal file
363
internal/service/order/audit.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// SetLifecycleAudit 注入订单生命周期统一审计 Writer。
|
||||
func (s *Service) SetLifecycleAudit(writer *audit.Writer) {
|
||||
s.auditWriter = writer
|
||||
}
|
||||
|
||||
func (s *Service) appendOrderAudit(ctx context.Context, tx *gorm.DB, actionCode, summary string, order *model.Order, beforeData, afterData map[string]any) error {
|
||||
if s.auditWriter == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "订单统一审计接缝未配置")
|
||||
}
|
||||
resources, err := orderAuditResources(ctx, tx, order, beforeData, afterData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
|
||||
Result: constants.AuditResultSuccess, CorrelationID: order.OrderNo,
|
||||
Metadata: map[string]any{
|
||||
"buyer_type": order.BuyerType, "buyer_id": order.BuyerID,
|
||||
"purchase_role": order.PurchaseRole, "payment_method": order.PaymentMethod,
|
||||
},
|
||||
Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordOrderFailure(ctx context.Context, actionCode, summary string, order *model.Order, businessErr error) {
|
||||
if businessErr == nil || order == nil || order.OrderNo == "" || s.auditWriter == nil || s.db == nil {
|
||||
return
|
||||
}
|
||||
resource := audit.OrderResource(order, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleOrderTarget)
|
||||
resource.BeforeData = orderStateData(order)
|
||||
correlationID := order.OrderNo
|
||||
if order.ID == 0 {
|
||||
correlationID = ""
|
||||
}
|
||||
s.auditWriter.RecordFailure(ctx, s.db, audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
|
||||
CorrelationID: correlationID, Resources: []audit.ResourceInput{resource},
|
||||
}, businessErr)
|
||||
}
|
||||
|
||||
func (s *Service) recordAgentWalletOrderFailure(ctx context.Context, actionCode, summary string, order *model.Order, shopID uint, businessErr error) {
|
||||
if businessErr == nil || order == nil || order.OrderNo == "" || s.auditWriter == nil || s.db == nil {
|
||||
return
|
||||
}
|
||||
orderSnapshot := *order
|
||||
if orderSnapshot.ID > 0 {
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).Unscoped().Model(&model.Order{}).Where("id = ?", orderSnapshot.ID).Count(&count).Error; err == nil && count == 0 {
|
||||
orderSnapshot.ID = 0
|
||||
}
|
||||
}
|
||||
primary := audit.OrderResource(&orderSnapshot, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleOrderTarget)
|
||||
primary.BeforeData = orderStateData(order)
|
||||
resources := []audit.ResourceInput{primary}
|
||||
|
||||
var reservation model.AgentWalletReservation
|
||||
if order.ID > 0 {
|
||||
if err := s.db.WithContext(ctx).Where("reference_type = ? AND reference_id = ?", constants.ReferenceTypeOrder, order.ID).First(&reservation).Error; err == nil {
|
||||
reservationID := strconv.FormatUint(uint64(reservation.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAgentWalletReservation, ID: &reservationID,
|
||||
Key: reservation.ReferenceType + ":" + strconv.FormatUint(uint64(reservation.ReferenceID), 10), DisplayName: "订单钱包预占 " + reservationID,
|
||||
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": reservation.Status,
|
||||
"reference_type": reservation.ReferenceType, "reference_id": reservation.ReferenceID,
|
||||
},
|
||||
BeforeData: map[string]any{"status": reservation.Status},
|
||||
})
|
||||
shopID = reservation.ShopID
|
||||
}
|
||||
}
|
||||
if shopID > 0 {
|
||||
var wallet model.AgentWallet
|
||||
if err := s.db.WithContext(ctx).Unscoped().Where("shop_id = ? AND wallet_type = ?", shopID, constants.AgentWalletTypeMain).First(&wallet).Error; err == nil {
|
||||
walletID := strconv.FormatUint(uint64(wallet.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAgentWallet, ID: &walletID, Key: walletID, DisplayName: "代理主钱包 " + walletID,
|
||||
Relation: constants.AuditResourceRelationReference, 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": wallet.Balance, "frozen_balance": wallet.FrozenBalance},
|
||||
})
|
||||
}
|
||||
}
|
||||
s.auditWriter.RecordFailure(ctx, s.db, audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
|
||||
CorrelationID: order.OrderNo, Metadata: map[string]any{"amount": order.TotalAmount}, Resources: resources,
|
||||
}, businessErr)
|
||||
}
|
||||
|
||||
// RecordCreateFailure 在订单创建调用方已识别资产后记录失败或拒绝事实。
|
||||
func (s *Service) RecordCreateFailure(ctx context.Context, order *model.Order, businessErr error) {
|
||||
s.recordOrderFailure(ctx, constants.AuditActionOrderCreated, "创建订单失败", order, businessErr)
|
||||
}
|
||||
|
||||
func orderAuditResources(ctx context.Context, tx *gorm.DB, order *model.Order, beforeData, afterData map[string]any) ([]audit.ResourceInput, error) {
|
||||
if order == nil || order.ID == 0 || order.OrderNo == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "订单审计资源不完整")
|
||||
}
|
||||
primary := audit.OrderResource(order, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleOrderTarget)
|
||||
primary.BeforeData, primary.AfterData = beforeData, afterData
|
||||
primary.SubjectVisibility = constants.AuditSubjectResult
|
||||
primary.SubjectSummary = "订单状态已更新"
|
||||
resources := []audit.ResourceInput{primary}
|
||||
|
||||
buyer, err := orderBuyerAuditResource(ctx, tx, order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if buyer != nil {
|
||||
resources = append(resources, *buyer)
|
||||
}
|
||||
asset, err := orderAssetAuditResource(ctx, tx, order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if asset != nil {
|
||||
resources = append(resources, *asset)
|
||||
}
|
||||
|
||||
packages, err := orderPackageAuditResources(ctx, tx, order.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, packages...)
|
||||
finance, err := orderFinanceAuditResources(ctx, tx, order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(resources, finance...), nil
|
||||
}
|
||||
|
||||
func orderBuyerAuditResource(ctx context.Context, tx *gorm.DB, order *model.Order) (*audit.ResourceInput, error) {
|
||||
if order.BuyerID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
switch order.BuyerType {
|
||||
case model.BuyerTypeAgent:
|
||||
var shop model.Shop
|
||||
if err := tx.WithContext(ctx).First(&shop, order.BuyerID).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单买家店铺审计快照失败")
|
||||
}
|
||||
resource := audit.ShopResource(&shop, constants.AuditResourceRelationReference, constants.AuditResourceRoleOrderBuyer)
|
||||
return &resource, nil
|
||||
case model.BuyerTypePersonal:
|
||||
var customer model.PersonalCustomer
|
||||
if err := tx.WithContext(ctx).First(&customer, order.BuyerID).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单买家审计快照失败")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(customer.ID), 10)
|
||||
resource := audit.ResourceInput{
|
||||
Type: constants.AuditResourcePersonalCustomer, ID: &id, Key: id, DisplayName: customer.Nickname,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleOrderBuyer,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": customer.ID, "nickname": customer.Nickname, "wx_open_id": customer.WxOpenID,
|
||||
"wx_union_id": customer.WxUnionID, "status": customer.Status,
|
||||
},
|
||||
}
|
||||
return &resource, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func orderAssetAuditResource(ctx context.Context, tx *gorm.DB, order *model.Order) (*audit.ResourceInput, error) {
|
||||
if order.IotCardID != nil {
|
||||
var card model.IotCard
|
||||
if err := tx.WithContext(ctx).First(&card, *order.IotCardID).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单关联卡审计快照失败")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(card.ID), 10)
|
||||
resource := audit.ResourceInput{
|
||||
Type: constants.AuditResourceIotCard, ID: &id, Key: audit.IotCardResourceKey(&card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleOrderAsset,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(&card), SubjectVisibility: constants.AuditSubjectResult,
|
||||
SubjectSummary: "关联订单状态已更新",
|
||||
}
|
||||
return &resource, nil
|
||||
}
|
||||
if order.DeviceID != nil {
|
||||
var device model.Device
|
||||
if err := tx.WithContext(ctx).First(&device, *order.DeviceID).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单关联设备审计快照失败")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(device.ID), 10)
|
||||
resource := audit.ResourceInput{
|
||||
Type: constants.AuditResourceDevice, ID: &id, Key: audit.DeviceResourceKey(&device), DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleOrderAsset,
|
||||
IdentitySnapshot: audit.DeviceIdentitySnapshot(&device), SubjectVisibility: constants.AuditSubjectResult,
|
||||
SubjectSummary: "关联订单状态已更新",
|
||||
}
|
||||
return &resource, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func orderPackageAuditResources(ctx context.Context, tx *gorm.DB, orderID uint) ([]audit.ResourceInput, error) {
|
||||
var packages []model.Package
|
||||
if err := tx.WithContext(ctx).Model(&model.Package{}).Distinct("tb_package.*").
|
||||
Joins("JOIN tb_order_item item ON item.package_id = tb_package.id AND item.deleted_at IS NULL").
|
||||
Where("item.order_id = ?", orderID).Order("tb_package.id ASC").Find(&packages).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单关联套餐审计快照失败")
|
||||
}
|
||||
resources := make([]audit.ResourceInput, 0, len(packages))
|
||||
for i := range packages {
|
||||
resources = append(resources, audit.PackageResource(&packages[i], constants.AuditResourceRelationReference, constants.AuditResourceRoleOrderPackage, nil, nil))
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func orderFinanceAuditResources(ctx context.Context, tx *gorm.DB, order *model.Order) ([]audit.ResourceInput, error) {
|
||||
resources := make([]audit.ResourceInput, 0, 5)
|
||||
agentResources, err := agentWalletOrderAuditResources(ctx, tx, order.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, agentResources...)
|
||||
assetResources, err := assetWalletOrderAuditResources(ctx, tx, order.OrderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, assetResources...)
|
||||
|
||||
var payments []model.Payment
|
||||
if err := tx.WithContext(ctx).Where("order_id = ?", order.ID).Order("id ASC").Find(&payments).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单支付记录审计快照失败")
|
||||
}
|
||||
for i := range payments {
|
||||
id := strconv.FormatUint(uint64(payments[i].ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourcePayment, ID: &id, Key: payments[i].PaymentNo, DisplayName: payments[i].PaymentNo,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleOrderPayment,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": payments[i].ID, "payment_no": payments[i].PaymentNo, "order_id": payments[i].OrderID,
|
||||
"order_type": payments[i].OrderType, "payment_method": payments[i].PaymentMethod,
|
||||
"amount": payments[i].Amount, "status": payments[i].Status,
|
||||
"third_party_trade_no": payments[i].ThirdPartyTradeNo, "payment_config_id": payments[i].PaymentConfigID,
|
||||
},
|
||||
})
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func agentWalletOrderAuditResources(ctx context.Context, tx *gorm.DB, orderID uint) ([]audit.ResourceInput, error) {
|
||||
var transactions []model.AgentWalletTransaction
|
||||
if err := tx.WithContext(ctx).Where("reference_type = ? AND reference_id = ?", constants.ReferenceTypeOrder, orderID).
|
||||
Order("id ASC").Find(&transactions).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单代理钱包流水审计快照失败")
|
||||
}
|
||||
resources := make([]audit.ResourceInput, 0, len(transactions)*2)
|
||||
wallets := make(map[uint]struct{}, len(transactions))
|
||||
for i := range transactions {
|
||||
transaction := &transactions[i]
|
||||
if _, ok := wallets[transaction.AgentWalletID]; !ok {
|
||||
var wallet model.AgentWallet
|
||||
if err := tx.WithContext(ctx).First(&wallet, transaction.AgentWalletID).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单代理钱包审计快照失败")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(wallet.ID), 10)
|
||||
resources = append(resources, audit.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": transaction.BalanceBefore},
|
||||
AfterData: map[string]any{"balance": transaction.BalanceAfter},
|
||||
})
|
||||
wallets[transaction.AgentWalletID] = struct{}{}
|
||||
}
|
||||
id := strconv.FormatUint(uint64(transaction.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAgentWalletTransaction, ID: &id, Key: id, DisplayName: "代理钱包流水 " + id,
|
||||
Relation: constants.AuditResourceRelationReference, 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func assetWalletOrderAuditResources(ctx context.Context, tx *gorm.DB, orderNo string) ([]audit.ResourceInput, error) {
|
||||
var transactions []model.AssetWalletTransaction
|
||||
if err := tx.WithContext(ctx).Where("reference_type = ? AND reference_no = ?", constants.ReferenceTypeOrder, orderNo).
|
||||
Order("id ASC").Find(&transactions).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单资产钱包流水审计快照失败")
|
||||
}
|
||||
resources := make([]audit.ResourceInput, 0, len(transactions)*2)
|
||||
wallets := make(map[uint]struct{}, len(transactions))
|
||||
for i := range transactions {
|
||||
transaction := &transactions[i]
|
||||
if _, ok := wallets[transaction.AssetWalletID]; !ok {
|
||||
var wallet model.AssetWallet
|
||||
if err := tx.WithContext(ctx).First(&wallet, transaction.AssetWalletID).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单资产钱包审计快照失败")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(wallet.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAssetWallet, ID: &id, Key: id, DisplayName: "资产钱包 " + id,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleOrderWallet,
|
||||
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: map[string]any{"balance": transaction.BalanceBefore},
|
||||
AfterData: map[string]any{"balance": transaction.BalanceAfter},
|
||||
})
|
||||
wallets[transaction.AssetWalletID] = struct{}{}
|
||||
}
|
||||
id := strconv.FormatUint(uint64(transaction.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAssetWalletTransaction, ID: &id, Key: id, DisplayName: "资产钱包流水 " + id,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleOrderWalletTransaction,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": transaction.ID, "asset_wallet_id": transaction.AssetWalletID,
|
||||
"resource_type": transaction.ResourceType, "resource_id": transaction.ResourceID,
|
||||
"transaction_type": transaction.TransactionType, "reference_type": transaction.ReferenceType,
|
||||
"reference_no": transaction.ReferenceNo, "status": transaction.Status,
|
||||
},
|
||||
AfterData: map[string]any{
|
||||
"amount": transaction.Amount, "balance_before": transaction.BalanceBefore, "balance_after": transaction.BalanceAfter,
|
||||
},
|
||||
})
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func orderStateData(order *model.Order) map[string]any {
|
||||
if order == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"payment_status": order.PaymentStatus, "payment_method": order.PaymentMethod,
|
||||
"total_amount": order.TotalAmount, "actual_paid_amount": order.ActualPaidAmount,
|
||||
"paid_at": order.PaidAt, "expires_at": order.ExpiresAt,
|
||||
"purchase_role": order.PurchaseRole,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user