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, } }