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