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

394 lines
16 KiB
Go

package exchange
import (
"context"
"strconv"
"strings"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
type deviceExchangeAuditBefore struct {
Wallets map[uint]model.AssetWallet
CustomerBindings []*model.PersonalCustomerDevice
}
func (s *Service) appendExchangeAudit(
ctx context.Context,
tx *gorm.DB,
cardActionCode, summary, result string,
order *model.ExchangeOrder,
oldAsset, newAsset *resolvedExchangeAsset,
orderBefore, orderAfter map[string]any,
oldAssetBefore, oldAssetAfter map[string]any,
newAssetBefore, newAssetAfter map[string]any,
extra []audit.ResourceInput,
businessErr error,
) error {
if order != nil && order.OldAssetType == constants.ExchangeAssetTypeDevice {
return s.appendDeviceExchangeAudit(ctx, tx, deviceExchangeActionCode(cardActionCode), strings.ReplaceAll(summary, "卡", "设备"), result,
order, resolvedDevice(oldAsset), resolvedDevice(newAsset), orderBefore, orderAfter,
oldAssetBefore, oldAssetAfter, newAssetBefore, newAssetAfter, extra, businessErr)
}
return s.appendCardExchangeAudit(ctx, tx, cardActionCode, summary, result, order, resolvedCard(oldAsset), resolvedCard(newAsset),
orderBefore, orderAfter, oldAssetBefore, oldAssetAfter, newAssetBefore, newAssetAfter, extra, businessErr)
}
func resolvedCard(asset *resolvedExchangeAsset) *model.IotCard {
if asset == nil {
return nil
}
return asset.Card
}
func resolvedDevice(asset *resolvedExchangeAsset) *model.Device {
if asset == nil {
return nil
}
return asset.Device
}
func deviceExchangeActionCode(cardActionCode string) string {
switch cardActionCode {
case constants.AuditActionCardExchangeCreated:
return constants.AuditActionDeviceExchangeCreated
case constants.AuditActionCardExchangeShippingInfoSubmitted:
return constants.AuditActionDeviceExchangeShippingInfoSubmitted
case constants.AuditActionCardExchangeShipped:
return constants.AuditActionDeviceExchangeShipped
case constants.AuditActionCardExchangeCompleted:
return constants.AuditActionDeviceExchangeCompleted
case constants.AuditActionCardExchangeCancelled:
return constants.AuditActionDeviceExchangeCancelled
case constants.AuditActionCardExchangeRenewed:
return constants.AuditActionDeviceExchangeRenewed
default:
return cardActionCode
}
}
func (s *Service) appendDeviceExchangeAudit(
ctx context.Context,
tx *gorm.DB,
actionCode, summary, result string,
order *model.ExchangeOrder,
oldDevice, newDevice *model.Device,
orderBefore, orderAfter map[string]any,
oldDeviceBefore, oldDeviceAfter map[string]any,
newDeviceBefore, newDeviceAfter map[string]any,
extra []audit.ResourceInput,
businessErr error,
) error {
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeDevice {
return nil
}
if s.auditWriter == nil {
return errors.New(errors.CodeInvalidStatus, "设备换货统一审计接缝未配置")
}
internalOnly := actionCode == constants.AuditActionDeviceExchangeRenewed
resources := []audit.ResourceInput{deviceExchangeOrderAuditResource(order, summary, internalOnly, orderBefore, orderAfter)}
if oldDevice != nil {
resources = append(resources, deviceExchangeDeviceAuditResource(oldDevice, constants.AuditResourceRoleDeviceExchangeOldDevice, summary, internalOnly, oldDeviceBefore, oldDeviceAfter))
}
if newDevice != nil {
resources = append(resources, deviceExchangeDeviceAuditResource(newDevice, constants.AuditResourceRoleDeviceExchangeNewDevice, summary, internalOnly, newDeviceBefore, newDeviceAfter))
}
shopResource, err := loadDeviceExchangeShopAuditResource(ctx, tx, order.ShopID)
if err != nil {
return err
}
if shopResource != nil {
resources = append(resources, *shopResource)
}
resources = append(resources, extra...)
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
scopeType, scopeID := constants.AuditScopePlatform, ""
if actionCode == constants.AuditActionDeviceExchangeShippingInfoSubmitted {
scopeType = constants.AuditScopePersonalCustomer
scopeID = auditcontext.From(ctx).ActorID
}
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
ActionCode: actionCode, Summary: summary, ScopeType: scopeType, ScopeID: scopeID,
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
Metadata: map[string]any{"flow_type": effectiveExchangeFlowType(order.FlowType), "migrate_data": order.MigrateData},
Resources: resources,
})
}
func deviceExchangeOrderAuditResource(order *model.ExchangeOrder, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput {
resource := cardExchangeOrderAuditResource(order, summary, internalOnly, beforeData, afterData)
resource.Role = constants.AuditResourceRoleDeviceExchangeOrder
return resource
}
func deviceExchangeDeviceAuditResource(device *model.Device, role, summary string, internalOnly bool, beforeData, afterData map[string]any) audit.ResourceInput {
id := strconv.FormatUint(uint64(device.ID), 10)
relation := constants.AuditResourceRelationReference
if len(beforeData) > 0 || len(afterData) > 0 {
relation = constants.AuditResourceRelationAffected
}
resource := audit.ResourceInput{
Type: constants.AuditResourceDevice, ID: &id, Key: audit.DeviceResourceKey(device), DisplayName: preferredDeviceIdentifier(device),
Relation: relation, Role: role, IdentitySnapshot: audit.DeviceIdentitySnapshot(device),
BeforeData: beforeData, AfterData: afterData,
}
if internalOnly {
resource.SubjectVisibility = constants.AuditSubjectInternalOnly
} else {
resource.SubjectVisibility = constants.AuditSubjectResult
resource.SubjectSummary = summary
}
return resource
}
func loadDeviceExchangeShopAuditResource(ctx context.Context, tx *gorm.DB, shopID *uint) (*audit.ResourceInput, error) {
resource, err := loadCardExchangeShopAuditResource(ctx, tx, shopID)
if resource != nil {
resource.Role = constants.AuditResourceRoleDeviceExchangeShop
}
return resource, err
}
func (s *Service) captureDeviceExchangeAuditBefore(ctx context.Context, tx *gorm.DB, oldDevice, newDevice *model.Device) (*deviceExchangeAuditBefore, error) {
state := &deviceExchangeAuditBefore{Wallets: make(map[uint]model.AssetWallet)}
deviceIDs := make([]uint, 0, 2)
if oldDevice != nil {
deviceIDs = append(deviceIDs, oldDevice.ID)
}
if newDevice != nil {
deviceIDs = append(deviceIDs, newDevice.ID)
}
if len(deviceIDs) > 0 {
var wallets []model.AssetWallet
if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id IN ?", constants.ExchangeAssetTypeDevice, deviceIDs).Find(&wallets).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货钱包审计快照失败")
}
for _, wallet := range wallets {
state.Wallets[wallet.ResourceID] = wallet
}
}
rows, err := loadDeviceExchangeCustomerBindings(ctx, tx, oldDevice)
if err != nil {
return nil, err
}
state.CustomerBindings = rows
return state, nil
}
func loadDeviceExchangeCustomerBindings(ctx context.Context, tx *gorm.DB, device *model.Device) ([]*model.PersonalCustomerDevice, error) {
if device == nil {
return nil, nil
}
key := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: device, VirtualNo: device.VirtualNo})
if key == "" {
return nil, nil
}
var rows []*model.PersonalCustomerDevice
if err := tx.WithContext(ctx).Where("virtual_no = ? AND status = ?", key, constants.StatusEnabled).Find(&rows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货客户绑定失败")
}
return rows, nil
}
func (s *Service) buildDeviceExchangeCompletionResources(
ctx context.Context,
tx *gorm.DB,
order *model.ExchangeOrder,
oldDevice, newDevice *model.Device,
before *deviceExchangeAuditBefore,
migration *exchangeMigrationResult,
) ([]audit.ResourceInput, error) {
resources := deviceExchangeOldCustomerBindingResources(before)
newBindings, err := loadDeviceExchangeCustomerBindings(ctx, tx, newDevice)
if err != nil {
return nil, err
}
resources = append(resources, deviceExchangeNewCustomerBindingResources(newBindings)...)
simResources, err := loadDeviceExchangeSIMResources(ctx, tx, oldDevice, newDevice)
if err != nil {
return nil, err
}
resources = append(resources, simResources...)
beforeWallets := map[uint]model.AssetWallet(nil)
if before != nil {
beforeWallets = before.Wallets
}
walletResources, err := loadDeviceExchangeWalletResources(ctx, tx, oldDevice.ID, newDevice.ID, beforeWallets)
if err != nil {
return nil, err
}
resources = append(resources, walletResources...)
if migration == nil {
return resources, nil
}
transactions, err := loadDeviceExchangeTransactionResources(ctx, tx, order.ExchangeNo)
if err != nil {
return nil, err
}
resources = append(resources, transactions...)
usages, err := loadDeviceExchangePackageUsageResources(ctx, tx, migration.PackageUsageIDs, oldDevice.ID, newDevice.ID)
if err != nil {
return nil, err
}
return append(resources, usages...), nil
}
func deviceExchangeOldCustomerBindingResources(before *deviceExchangeAuditBefore) []audit.ResourceInput {
if before == nil {
return nil
}
return deviceExchangeCustomerBindingResources(before.CustomerBindings, constants.AuditResourceRoleDeviceExchangeOldCustomerBinding, true)
}
func deviceExchangeNewCustomerBindingResources(rows []*model.PersonalCustomerDevice) []audit.ResourceInput {
return deviceExchangeCustomerBindingResources(rows, constants.AuditResourceRoleDeviceExchangeNewCustomerBinding, false)
}
func deviceExchangeCustomerBindingResources(rows []*model.PersonalCustomerDevice, role string, before bool) []audit.ResourceInput {
resources := make([]audit.ResourceInput, 0, len(rows))
for _, row := range rows {
if row == nil {
continue
}
id := strconv.FormatUint(uint64(row.ID), 10)
resource := audit.ResourceInput{
Type: constants.AuditResourcePersonalCustomerDevice, ID: &id, Key: id, DisplayName: row.VirtualNo,
Relation: constants.AuditResourceRelationAffected, Role: role,
IdentitySnapshot: map[string]any{"id": row.ID, "customer_id": row.CustomerID, "virtual_no": row.VirtualNo, "bind_at": row.BindAt, "last_used_at": row.LastUsedAt, "status": row.Status},
SubjectVisibility: constants.AuditSubjectInternalOnly,
}
if before {
resource.BeforeData = map[string]any{"virtual_no": row.VirtualNo, "status": row.Status}
} else {
resource.AfterData = map[string]any{"virtual_no": row.VirtualNo, "status": row.Status}
}
resources = append(resources, resource)
}
return resources
}
func loadDeviceExchangeSIMResources(ctx context.Context, tx *gorm.DB, oldDevice, newDevice *model.Device) ([]audit.ResourceInput, error) {
resources := make([]audit.ResourceInput, 0)
for _, item := range []struct {
device *model.Device
cardRole string
bindingRole string
}{
{oldDevice, constants.AuditResourceRoleDeviceExchangeOldBoundCard, constants.AuditResourceRoleDeviceExchangeOldSIMBinding},
{newDevice, constants.AuditResourceRoleDeviceExchangeNewBoundCard, constants.AuditResourceRoleDeviceExchangeNewSIMBinding},
} {
if item.device == nil {
continue
}
var bindings []*model.DeviceSimBinding
if err := tx.WithContext(ctx).Where("device_id = ? AND bind_status = ?", item.device.ID, constants.BindStatusBound).
Order("slot_position ASC, id ASC").Find(&bindings).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货卡槽绑定失败")
}
cardIDs := make([]uint, 0, len(bindings))
for _, binding := range bindings {
cardIDs = append(cardIDs, binding.IotCardID)
}
cards := make(map[uint]*model.IotCard, len(cardIDs))
if len(cardIDs) > 0 {
var rows []*model.IotCard
if err := tx.WithContext(ctx).Where("id IN ?", cardIDs).Find(&rows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货绑定卡失败")
}
for _, card := range rows {
cards[card.ID] = card
}
}
for _, binding := range bindings {
card := cards[binding.IotCardID]
if card == nil {
return nil, errors.New(errors.CodeAssetNotFound, "设备换货绑定卡不存在")
}
cardID := strconv.FormatUint(uint64(card.ID), 10)
resources = append(resources, audit.ResourceInput{
Type: constants.AuditResourceIotCard, ID: &cardID, Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
Relation: constants.AuditResourceRelationReference, Role: item.cardRole,
IdentitySnapshot: audit.IotCardIdentitySnapshot(card), SubjectVisibility: constants.AuditSubjectInternalOnly,
})
bindingID := strconv.FormatUint(uint64(binding.ID), 10)
resources = append(resources, audit.ResourceInput{
Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID, Key: bindingID, DisplayName: preferredDeviceIdentifier(item.device),
Relation: constants.AuditResourceRelationReference, Role: item.bindingRole,
IdentitySnapshot: map[string]any{
"id": binding.ID, "device_id": binding.DeviceID, "device_virtual_no": item.device.VirtualNo,
"slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID,
"iccid": card.ICCID, "virtual_no": card.VirtualNo, "is_current": binding.IsCurrent,
}, SubjectVisibility: constants.AuditSubjectInternalOnly,
})
}
}
return resources, nil
}
func loadDeviceExchangeWalletResources(ctx context.Context, tx *gorm.DB, oldDeviceID, newDeviceID uint, before map[uint]model.AssetWallet) ([]audit.ResourceInput, error) {
return loadExchangeWalletResources(ctx, tx, constants.ExchangeAssetTypeDevice, oldDeviceID, newDeviceID, before,
constants.AuditResourceRoleDeviceExchangeOldWallet, constants.AuditResourceRoleDeviceExchangeNewWallet, "设备")
}
func loadDeviceExchangeRenewWalletResource(ctx context.Context, tx *gorm.DB, deviceID uint, before map[uint]model.AssetWallet) (*audit.ResourceInput, error) {
return loadExchangeRenewWalletResource(ctx, tx, constants.ExchangeAssetTypeDevice, deviceID, before, constants.AuditResourceRoleDeviceExchangeOldWallet, "设备")
}
func loadDeviceExchangeTransactionResources(ctx context.Context, tx *gorm.DB, exchangeNo string) ([]audit.ResourceInput, error) {
return loadExchangeTransactionResources(ctx, tx, exchangeNo, constants.AuditResourceRoleDeviceExchangeWalletTransaction, "设备")
}
func loadDeviceExchangePackageUsageResources(ctx context.Context, tx *gorm.DB, ids []uint, oldDeviceID, newDeviceID uint) ([]audit.ResourceInput, error) {
return loadExchangePackageUsageResources(ctx, tx, ids, "device_id", oldDeviceID, newDeviceID, constants.AuditResourceRoleDeviceExchangePackageUsage, "设备")
}
func (s *Service) recordExchangeOrderFailure(ctx context.Context, cardActionCode, summary string, order *model.ExchangeOrder, businessErr error) {
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeDevice {
s.recordCardExchangeOrderFailure(ctx, cardActionCode, summary, order, businessErr)
return
}
oldDevice, newDevice := s.loadDeviceExchangeAuditDevices(ctx, order)
if s.db == nil || s.auditWriter == nil {
recordCardExchangeAuditSecondaryFailure(ctx, deviceExchangeActionCode(cardActionCode), order.ExchangeNo, businessErr,
errors.New(errors.CodeInvalidStatus, "设备换货统一审计接缝未配置"))
return
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return s.appendDeviceExchangeAudit(ctx, tx, deviceExchangeActionCode(cardActionCode), strings.ReplaceAll(summary, "卡", "设备"),
cardExchangeFailureResult(businessErr), order, oldDevice, newDevice,
nil, nil, nil, nil, nil, nil, nil, businessErr)
}); err != nil {
recordCardExchangeAuditSecondaryFailure(ctx, deviceExchangeActionCode(cardActionCode), order.ExchangeNo, businessErr, err)
}
}
func (s *Service) loadDeviceExchangeAuditDevices(ctx context.Context, order *model.ExchangeOrder) (*model.Device, *model.Device) {
if order == nil || order.OldAssetType != constants.ExchangeAssetTypeDevice {
return nil, nil
}
var oldDevice *model.Device
if s.deviceStore != nil {
oldDevice, _ = s.deviceStore.GetByID(ctx, order.OldAssetID)
}
if oldDevice == nil {
oldDevice = &model.Device{Model: gorm.Model{ID: order.OldAssetID}, ShopID: order.ShopID}
}
var newDevice *model.Device
if order.NewAssetID != nil && *order.NewAssetID > 0 {
if s.deviceStore != nil {
newDevice, _ = s.deviceStore.GetByID(ctx, *order.NewAssetID)
}
if newDevice == nil {
newDevice = &model.Device{Model: gorm.Model{ID: *order.NewAssetID}, ShopID: order.ShopID}
}
}
return oldDevice, newDevice
}