收口审计治理与套餐任务进展

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -6,6 +6,7 @@ import (
"time"
exchangeapp "github.com/break/junhong_cmp_fiber/internal/application/exchange"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
customerBindingSvc "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
@@ -31,6 +32,7 @@ type Service struct {
resourceTagStore *postgres.ResourceTagStore
customerBinding *customerBindingSvc.Service
shippingCreatedNotifier *exchangeapp.ShippingCreatedNotifier
auditWriter *audit.Writer
logger *zap.Logger
}
@@ -81,31 +83,14 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateExchangeRequest) (*
if err != nil {
return nil, err
}
if !isExchangeableAssetStatus(asset.AssetStatus) {
return nil, oldAssetStatusError(asset.AssetStatus)
migrateData := false
if req.MigrateData != nil {
migrateData = *req.MigrateData
}
hasUnfinishedRefund, err := s.refundStore.HasUnfinishedByAsset(ctx, asset.AssetType, asset.AssetID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产退款申请失败")
}
if hasUnfinishedRefund {
return nil, errors.New(errors.CodeExchangeActiveRefund)
}
if _, err = s.exchangeStore.FindActiveByOldAsset(ctx, asset.AssetType, asset.AssetID); err == nil {
return nil, errors.New(errors.CodeExchangeInProgress)
} else if err != gorm.ErrRecordNotFound {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询进行中换货单失败")
}
if flowType == constants.ExchangeFlowTypeDirect {
return s.createDirectExchange(ctx, req, asset)
}
creator := middleware.GetUserIDFromContext(ctx)
order := &model.ExchangeOrder{
ExchangeNo: model.GenerateExchangeNo(),
FlowType: constants.ExchangeFlowTypeShipping,
FlowType: flowType,
OldAssetType: asset.AssetType,
OldAssetID: asset.AssetID,
OldAssetIdentifier: asset.Identifier,
@@ -114,15 +99,57 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateExchangeRequest) (*
Status: constants.ExchangeStatusPendingInfo,
MigrationCompleted: false,
MigrationBalance: 0,
MigrateData: false,
MigrateData: flowType == constants.ExchangeFlowTypeDirect && migrateData,
BaseModel: model.BaseModel{Creator: creator, Updater: creator},
}
if asset.ShopID != nil {
order.ShopID = asset.ShopID
}
if !isExchangeableAssetStatus(asset.AssetStatus) {
err = oldAssetStatusError(asset.AssetStatus)
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单被拒绝", order, err)
return nil, err
}
hasUnfinishedRefund, err := s.refundStore.HasUnfinishedByAsset(ctx, asset.AssetType, asset.AssetID)
if err != nil {
err = errors.Wrap(errors.CodeDatabaseError, err, "查询资产退款申请失败")
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单失败", order, err)
return nil, err
}
if hasUnfinishedRefund {
err = errors.New(errors.CodeExchangeActiveRefund)
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单被拒绝", order, err)
return nil, err
}
if _, err = s.exchangeStore.FindActiveByOldAsset(ctx, asset.AssetType, asset.AssetID); err == nil {
err = errors.New(errors.CodeExchangeInProgress)
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单被拒绝", order, err)
return nil, err
} else if err != gorm.ErrRecordNotFound {
err = errors.Wrap(errors.CodeDatabaseError, err, "查询进行中换货单失败")
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单失败", order, err)
return nil, err
}
if flowType == constants.ExchangeFlowTypeDirect {
orderID, directErr := s.createDirectExchange(ctx, req, asset, order)
if directErr != nil {
order.ID = 0
order.Status = constants.ExchangeStatusPendingInfo
order.MigrationCompleted = false
order.MigrationBalance = 0
order.CompletedAt = nil
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡直接换货失败", order, directErr)
return nil, directErr
}
return s.Get(ctx, orderID)
}
if s.shippingCreatedNotifier == nil {
return nil, errors.New(errors.CodeInternalError, "物流换货通知服务未配置")
err = errors.New(errors.CodeInternalError, "物流换货通知服务未配置")
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单失败", order, err)
return nil, err
}
requestID := ""
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
@@ -145,9 +172,14 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateExchangeRequest) (*
return notifyErr
}
}
return nil
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeCreated, "已创建卡换货单", constants.AuditResultSuccess,
order, asset, nil,
map[string]any{"exists": false}, map[string]any{"status": constants.ExchangeStatusPendingInfo},
nil, nil, nil, nil, nil, nil)
})
if err != nil {
order.ID = 0
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单失败", order, err)
return nil, err
}
@@ -178,13 +210,18 @@ func (s *Service) Ship(ctx context.Context, id uint, req *dto.ExchangeShipReques
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
}
if order.Status != constants.ExchangeStatusPendingShip {
return nil, errors.New(errors.CodeExchangeStatusInvalid)
err = errors.New(errors.CodeExchangeStatusInvalid)
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShipped, "卡换货发货被拒绝", order, err)
return nil, err
}
if !isShippingExchangeFlow(order.FlowType) {
return nil, errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持发货")
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持发货")
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShipped, "卡换货发货被拒绝", order, err)
return nil, err
}
if err = s.shipWithTx(ctx, order, req); err != nil {
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShipped, "卡换货发货失败", order, err)
return nil, err
}
@@ -200,13 +237,17 @@ func (s *Service) Complete(ctx context.Context, id uint) error {
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
}
if order.Status != constants.ExchangeStatusShipped {
return errors.New(errors.CodeExchangeStatusInvalid)
err = errors.New(errors.CodeExchangeStatusInvalid)
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货被拒绝", order, err)
return err
}
if !isShippingExchangeFlow(order.FlowType) {
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持确认完成")
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持确认完成")
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货被拒绝", order, err)
return err
}
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
lockedOrder, lockErr := s.lockExchangeOrderByID(ctx, tx, id)
if lockErr != nil {
return lockErr
@@ -219,6 +260,10 @@ func (s *Service) Complete(ctx context.Context, id uint) error {
}
return s.completeExchangeWithTx(ctx, tx, lockedOrder, constants.ExchangeStatusShipped)
})
if err != nil {
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货失败", order, err)
}
return err
}
func (s *Service) Cancel(ctx context.Context, id uint, req *dto.ExchangeCancelRequest) error {
@@ -230,10 +275,14 @@ func (s *Service) Cancel(ctx context.Context, id uint, req *dto.ExchangeCancelRe
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
}
if order.Status != constants.ExchangeStatusPendingInfo && order.Status != constants.ExchangeStatusPendingShip {
return errors.New(errors.CodeExchangeStatusInvalid)
err = errors.New(errors.CodeExchangeStatusInvalid)
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货被拒绝", order, err)
return err
}
if !isShippingExchangeFlow(order.FlowType) {
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持取消")
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持取消")
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货被拒绝", order, err)
return err
}
updates := map[string]any{
@@ -243,13 +292,36 @@ func (s *Service) Cancel(ctx context.Context, id uint, req *dto.ExchangeCancelRe
if req != nil {
updates["remark"] = req.Remark
}
if err = s.exchangeStore.UpdateStatus(ctx, id, order.Status, constants.ExchangeStatusCancelled, updates); err != nil {
if err == gorm.ErrRecordNotFound {
oldAsset, resolveErr := s.resolveAssetByID(ctx, order.OldAssetType, order.OldAssetID)
if resolveErr != nil {
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货失败", order, resolveErr)
return resolveErr
}
fromStatus := order.Status
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
values := make(map[string]any, len(updates)+1)
for key, value := range updates {
values[key] = value
}
values["status"] = constants.ExchangeStatusCancelled
result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).Where("id = ? AND status = ?", id, fromStatus).Updates(values)
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "取消换货失败")
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeExchangeStatusInvalid)
}
return errors.Wrap(errors.CodeDatabaseError, err, "取消换货失败")
order.Status = constants.ExchangeStatusCancelled
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeCancelled, "已取消卡换货单", constants.AuditResultSuccess,
order, oldAsset, nil,
map[string]any{"status": fromStatus}, map[string]any{"status": constants.ExchangeStatusCancelled},
nil, nil, nil, nil, nil, nil)
})
if err != nil {
order.Status = fromStatus
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货失败", order, err)
}
return nil
return err
}
func (s *Service) Renew(ctx context.Context, id uint) error {
@@ -261,52 +333,84 @@ func (s *Service) Renew(ctx context.Context, id uint) error {
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
}
if order.Status != constants.ExchangeStatusCompleted {
return errors.New(errors.CodeExchangeStatusInvalid)
err = errors.New(errors.CodeExchangeStatusInvalid)
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeRenewed, "换出旧卡转新被拒绝", order, err)
return err
}
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if order.OldAssetType == constants.ExchangeAssetTypeIotCard {
var card model.IotCard
if err = tx.Where("id = ?", order.OldAssetID).First(&card).Error; err != nil {
if err == gorm.ErrRecordNotFound {
if queryErr := tx.WithContext(ctx).Where("id = ?", order.OldAssetID).First(&card).Error; queryErr != nil {
if queryErr == gorm.ErrRecordNotFound {
return errors.New(errors.CodeAssetNotFound)
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询旧卡失败")
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询旧卡失败")
}
if card.AssetStatus != constants.AssetStatusExchanged {
return errors.New(errors.CodeExchangeAssetNotExchanged)
}
var newCard *model.IotCard
if order.NewAssetID != nil && *order.NewAssetID > 0 {
var value model.IotCard
if queryErr := tx.WithContext(ctx).Where("id = ?", *order.NewAssetID).First(&value).Error; queryErr != nil {
if queryErr == gorm.ErrRecordNotFound {
return errors.New(errors.CodeAssetNotFound)
}
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询换货新卡失败")
}
newCard = &value
}
auditBefore, auditErr := s.captureCardExchangeAuditBefore(ctx, tx, &card, newCard)
if auditErr != nil {
return auditErr
}
cardBefore := map[string]any{"generation": card.Generation, "asset_status": card.AssetStatus}
if err = tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(map[string]any{
if updateErr := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(map[string]any{
"generation": card.Generation + 1,
"asset_status": constants.AssetStatusInStock,
"accumulated_recharge_by_series": "{}",
"first_recharge_triggered_by_series": "{}",
"updater": middleware.GetUserIDFromContext(ctx),
"updated_at": time.Now(),
}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "重置旧卡转新状态失败")
}).Error; updateErr != nil {
return errors.Wrap(errors.CodeDatabaseError, updateErr, "重置旧卡转新状态失败")
}
cardKey := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, Card: &card, VirtualNo: card.VirtualNo})
if cardKey != "" {
if err = tx.Where("virtual_no = ?", cardKey).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "清理个人客户绑定失败")
if unbindErr := s.customerBinding.UnbindByVirtualNo(ctx, tx, constants.ExchangeAssetTypeIotCard, card.ID, cardKey); unbindErr != nil {
return unbindErr
}
}
if err = tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeIotCard, card.ID).Delete(&model.AssetWallet{}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "清理旧钱包失败")
if deleteErr := tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeIotCard, card.ID).Delete(&model.AssetWallet{}).Error; deleteErr != nil {
return errors.Wrap(errors.CodeDatabaseError, deleteErr, "清理旧钱包失败")
}
shopTag := uint(0)
if card.ShopID != nil {
shopTag = *card.ShopID
}
if err = tx.Create(&model.AssetWallet{ResourceType: constants.ExchangeAssetTypeIotCard, ResourceID: card.ID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: constants.AssetWalletStatusNormal, Version: 0, ShopIDTag: shopTag}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建新钱包失败")
if createErr := tx.Create(&model.AssetWallet{ResourceType: constants.ExchangeAssetTypeIotCard, ResourceID: card.ID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: constants.AssetWalletStatusNormal, Version: 0, ShopIDTag: shopTag}).Error; createErr != nil {
return errors.Wrap(errors.CodeDatabaseError, createErr, "创建新钱包失败")
}
return nil
var renewedCard model.IotCard
if queryErr := tx.WithContext(ctx).Where("id = ?", card.ID).First(&renewedCard).Error; queryErr != nil {
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询旧卡转新结果失败")
}
walletResource, resourceErr := loadCardExchangeRenewWalletResource(ctx, tx, card.ID, auditBefore.Wallets)
if resourceErr != nil {
return resourceErr
}
extra := cardExchangeOldBindingResources(auditBefore)
extra = append(extra, *walletResource)
return s.appendCardExchangeAudit(ctx, tx, constants.AuditActionCardExchangeRenewed, "换出旧卡已转为新卡状态", constants.AuditResultSuccess,
order, &renewedCard, newCard,
nil, nil,
cardBefore, map[string]any{"generation": renewedCard.Generation, "asset_status": renewedCard.AssetStatus},
nil, nil, extra, nil)
}
var device model.Device
@@ -319,6 +423,22 @@ func (s *Service) Renew(ctx context.Context, id uint) error {
if device.AssetStatus != constants.AssetStatusExchanged {
return errors.New(errors.CodeExchangeAssetNotExchanged)
}
var newDevice *model.Device
if order.NewAssetID != nil && *order.NewAssetID > 0 {
var value model.Device
if queryErr := tx.WithContext(ctx).Where("id = ?", *order.NewAssetID).First(&value).Error; queryErr != nil {
if queryErr == gorm.ErrRecordNotFound {
return errors.New(errors.CodeAssetNotFound)
}
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询换货新设备失败")
}
newDevice = &value
}
deviceAuditBefore, auditErr := s.captureDeviceExchangeAuditBefore(ctx, tx, &device, newDevice)
if auditErr != nil {
return auditErr
}
deviceBefore := map[string]any{"generation": device.Generation, "asset_status": device.AssetStatus}
if err = tx.Model(&model.Device{}).Where("id = ?", device.ID).Updates(map[string]any{
"generation": device.Generation + 1,
@@ -333,8 +453,8 @@ func (s *Service) Renew(ctx context.Context, id uint) error {
deviceKey := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &device, VirtualNo: device.VirtualNo})
if deviceKey != "" {
if err = tx.Where("virtual_no = ?", deviceKey).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "清理个人客户绑定失败")
if err = s.customerBinding.UnbindByVirtualNo(ctx, tx, constants.ExchangeAssetTypeDevice, device.ID, deviceKey); err != nil {
return err
}
}
@@ -349,8 +469,31 @@ func (s *Service) Renew(ctx context.Context, id uint) error {
if err = tx.Create(&model.AssetWallet{ResourceType: constants.ExchangeAssetTypeDevice, ResourceID: device.ID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: constants.AssetWalletStatusNormal, Version: 0, ShopIDTag: shopTag}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建新钱包失败")
}
return nil
var renewedDevice model.Device
if queryErr := tx.WithContext(ctx).Where("id = ?", device.ID).First(&renewedDevice).Error; queryErr != nil {
return errors.Wrap(errors.CodeDatabaseError, queryErr, "查询旧设备转新结果失败")
}
walletResource, resourceErr := loadDeviceExchangeRenewWalletResource(ctx, tx, device.ID, deviceAuditBefore.Wallets)
if resourceErr != nil {
return resourceErr
}
extra := deviceExchangeOldCustomerBindingResources(deviceAuditBefore)
simResources, resourceErr := loadDeviceExchangeSIMResources(ctx, tx, &renewedDevice, newDevice)
if resourceErr != nil {
return resourceErr
}
extra = append(extra, simResources...)
extra = append(extra, *walletResource)
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeRenewed, "换出旧卡已转为新卡状态", constants.AuditResultSuccess,
order, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &renewedDevice}, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: newDevice},
nil, nil,
deviceBefore, map[string]any{"generation": renewedDevice.Generation, "asset_status": renewedDevice.AssetStatus},
nil, nil, extra, nil)
})
if err != nil {
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeRenewed, "换出旧卡转新失败", order, err)
}
return err
}
func (s *Service) GetPending(ctx context.Context, identifier string) (*dto.ClientExchangePendingResponse, error) {
@@ -392,17 +535,24 @@ func (s *Service) SubmitShippingInfo(ctx context.Context, id uint, req *dto.Clie
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
}
if !isShippingExchangeFlow(order.FlowType) {
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持填写收货信息")
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持填写收货信息")
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息被拒绝", order, err)
return err
}
if order.Status != constants.ExchangeStatusPendingInfo {
return errors.New(errors.CodeExchangeStatusInvalid)
err = errors.New(errors.CodeExchangeStatusInvalid)
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息被拒绝", order, err)
return err
}
oldAsset, err := s.resolveAssetByID(ctx, order.OldAssetType, order.OldAssetID)
if err != nil {
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息失败", order, err)
return err
}
if !s.customerOwnsAsset(ctx, oldAsset) {
return errors.New(errors.CodeExchangeOrderNotFound)
err = errors.New(errors.CodeExchangeOrderNotFound)
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息被拒绝", order, err)
return err
}
updates := map[string]any{
@@ -411,13 +561,32 @@ func (s *Service) SubmitShippingInfo(ctx context.Context, id uint, req *dto.Clie
"recipient_address": req.RecipientAddress,
"updated_at": time.Now(),
}
if err := s.exchangeStore.UpdateStatus(ctx, id, constants.ExchangeStatusPendingInfo, constants.ExchangeStatusPendingShip, updates); err != nil {
if err == gorm.ErrRecordNotFound {
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
values := make(map[string]any, len(updates)+1)
for key, value := range updates {
values[key] = value
}
values["status"] = constants.ExchangeStatusPendingShip
result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
Where("id = ? AND status = ?", id, constants.ExchangeStatusPendingInfo).
Updates(values)
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "提交收货信息失败")
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeExchangeStatusInvalid)
}
return errors.Wrap(errors.CodeDatabaseError, err, "提交收货信息失败")
order.Status = constants.ExchangeStatusPendingShip
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeShippingInfoSubmitted, "已提交卡换货收货信息", constants.AuditResultSuccess,
order, oldAsset, nil,
map[string]any{"status": constants.ExchangeStatusPendingInfo}, map[string]any{"status": constants.ExchangeStatusPendingShip},
nil, nil, nil, nil, nil, nil)
})
if err != nil {
order.Status = constants.ExchangeStatusPendingInfo
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息失败", order, err)
}
return nil
return err
}
type resolvedExchangeAsset struct {
@@ -497,12 +666,8 @@ func isShippingExchangeFlow(flowType string) bool {
return effectiveExchangeFlowType(flowType) == constants.ExchangeFlowTypeShipping
}
func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExchangeRequest, oldAsset *resolvedExchangeAsset) (*dto.ExchangeOrderResponse, error) {
func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExchangeRequest, oldAsset *resolvedExchangeAsset, order *model.ExchangeOrder) (uint, error) {
var orderID uint
migrateData := false
if req.MigrateData != nil {
migrateData = *req.MigrateData
}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
lockedOldAsset, err := s.resolveAssetByIDWithTx(ctx, tx, oldAsset.AssetType, oldAsset.AssetID)
@@ -524,27 +689,13 @@ func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExcha
return errors.New(errors.CodeExchangeAssetTypeMismatch)
}
creator := middleware.GetUserIDFromContext(ctx)
order := &model.ExchangeOrder{
ExchangeNo: model.GenerateExchangeNo(),
FlowType: constants.ExchangeFlowTypeDirect,
OldAssetType: lockedOldAsset.AssetType,
OldAssetID: lockedOldAsset.AssetID,
OldAssetIdentifier: lockedOldAsset.Identifier,
NewAssetType: newAsset.AssetType,
NewAssetID: &newAsset.AssetID,
NewAssetIdentifier: newAsset.Identifier,
ExchangeReason: req.ExchangeReason,
Remark: req.Remark,
Status: constants.ExchangeStatusPendingInfo,
MigrationCompleted: false,
MigrationBalance: 0,
MigrateData: migrateData,
BaseModel: model.BaseModel{Creator: creator, Updater: creator},
}
if lockedOldAsset.ShopID != nil {
order.ShopID = lockedOldAsset.ShopID
}
order.OldAssetType = lockedOldAsset.AssetType
order.OldAssetID = lockedOldAsset.AssetID
order.OldAssetIdentifier = lockedOldAsset.Identifier
order.NewAssetType = newAsset.AssetType
order.NewAssetID = &newAsset.AssetID
order.NewAssetIdentifier = newAsset.Identifier
order.ShopID = cloneShopID(lockedOldAsset.ShopID)
if err = tx.WithContext(ctx).Create(order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建直接换货单失败")
}
@@ -555,9 +706,9 @@ func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExcha
return nil
})
if err != nil {
return nil, err
return 0, err
}
return s.Get(ctx, orderID)
return orderID, nil
}
func (s *Service) shipWithTx(ctx context.Context, order *model.ExchangeOrder, req *dto.ExchangeShipRequest) error {
@@ -609,7 +760,16 @@ func (s *Service) shipWithTx(ctx context.Context, order *model.ExchangeOrder, re
if result.RowsAffected == 0 {
return errors.New(errors.CodeExchangeStatusInvalid)
}
return nil
lockedOrder.NewAssetType = newAsset.AssetType
lockedOrder.NewAssetID = &newAsset.AssetID
lockedOrder.NewAssetIdentifier = newAsset.Identifier
lockedOrder.MigrateData = req.MigrateData
lockedOrder.ShippedAt = &now
lockedOrder.Status = constants.ExchangeStatusShipped
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeShipped, "卡换货单已发货", constants.AuditResultSuccess,
lockedOrder, oldAsset, newAsset,
map[string]any{"status": constants.ExchangeStatusPendingShip}, map[string]any{"status": constants.ExchangeStatusShipped},
nil, nil, nil, nil, nil, nil)
})
}
@@ -629,6 +789,19 @@ func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order
if err = s.validateExchangeAssetsWithTx(ctx, tx, order.ID, oldAsset, newAsset); err != nil {
return err
}
var auditBefore *cardExchangeAuditBefore
var deviceAuditBefore *deviceExchangeAuditBefore
if order.OldAssetType == constants.ExchangeAssetTypeIotCard {
auditBefore, err = s.captureCardExchangeAuditBefore(ctx, tx, oldAsset.Card, newAsset.Card)
if err != nil {
return err
}
} else {
deviceAuditBefore, err = s.captureDeviceExchangeAuditBefore(ctx, tx, oldAsset.Device, newAsset.Device)
if err != nil {
return err
}
}
if err = s.syncNewAssetOwnershipWithTx(ctx, tx, oldAsset, newAsset); err != nil {
return err
}
@@ -639,9 +812,9 @@ func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order
return err
}
var migrationBalance int64
var migration *exchangeMigrationResult
if order.MigrateData {
migrationBalance, err = s.executeMigrationWithTx(ctx, tx, order, oldAsset, newAsset)
migration, err = s.executeMigrationWithTx(ctx, tx, order, oldAsset, newAsset)
if err != nil {
return err
}
@@ -656,7 +829,7 @@ func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order
}
if order.MigrateData {
updates["migration_completed"] = true
updates["migration_balance"] = migrationBalance
updates["migration_balance"] = migration.Balance
}
result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
Where("id = ? AND status = ?", order.ID, fromStatus).
@@ -667,7 +840,49 @@ func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order
if result.RowsAffected == 0 {
return errors.New(errors.CodeExchangeStatusInvalid)
}
return nil
orderBefore := map[string]any{"status": fromStatus, "migration_completed": order.MigrationCompleted, "migration_balance": order.MigrationBalance}
order.Status = constants.ExchangeStatusCompleted
order.CompletedAt = &now
if migration != nil {
order.MigrationCompleted = true
order.MigrationBalance = migration.Balance
}
if order.OldAssetType == constants.ExchangeAssetTypeDevice {
var oldDeviceAfter, newDeviceAfter model.Device
if err = tx.WithContext(ctx).Where("id = ?", oldAsset.AssetID).First(&oldDeviceAfter).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货旧设备结果失败")
}
if err = tx.WithContext(ctx).Where("id = ?", newAsset.AssetID).First(&newDeviceAfter).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备换货新设备结果失败")
}
extra, resourceErr := s.buildDeviceExchangeCompletionResources(ctx, tx, order, &oldDeviceAfter, &newDeviceAfter, deviceAuditBefore, migration)
if resourceErr != nil {
return resourceErr
}
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeCompleted, "卡换货已完成", constants.AuditResultSuccess,
order, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &oldDeviceAfter}, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &newDeviceAfter},
orderBefore, map[string]any{"status": constants.ExchangeStatusCompleted, "migration_completed": order.MigrationCompleted, "migration_balance": order.MigrationBalance},
map[string]any{"asset_status": oldAsset.Device.AssetStatus, "shop_id": oldAsset.Device.ShopID}, map[string]any{"asset_status": oldDeviceAfter.AssetStatus, "shop_id": oldDeviceAfter.ShopID},
map[string]any{"asset_status": newAsset.Device.AssetStatus, "shop_id": newAsset.Device.ShopID}, map[string]any{"asset_status": newDeviceAfter.AssetStatus, "shop_id": newDeviceAfter.ShopID},
extra, nil)
}
var oldCardAfter, newCardAfter model.IotCard
if err = tx.WithContext(ctx).Where("id = ?", oldAsset.AssetID).First(&oldCardAfter).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货旧卡结果失败")
}
if err = tx.WithContext(ctx).Where("id = ?", newAsset.AssetID).First(&newCardAfter).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询卡换货新卡结果失败")
}
extra, err := s.buildCardExchangeCompletionResources(ctx, tx, order, &oldCardAfter, &newCardAfter, auditBefore, migration)
if err != nil {
return err
}
return s.appendExchangeAudit(ctx, tx, constants.AuditActionCardExchangeCompleted, "卡换货已完成", constants.AuditResultSuccess,
order, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, Card: &oldCardAfter}, &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, Card: &newCardAfter},
orderBefore, map[string]any{"status": constants.ExchangeStatusCompleted, "migration_completed": order.MigrationCompleted, "migration_balance": order.MigrationBalance},
map[string]any{"asset_status": oldAsset.Card.AssetStatus, "shop_id": oldAsset.Card.ShopID}, map[string]any{"asset_status": oldCardAfter.AssetStatus, "shop_id": oldCardAfter.ShopID},
map[string]any{"asset_status": newAsset.Card.AssetStatus, "shop_id": newAsset.Card.ShopID}, map[string]any{"asset_status": newCardAfter.AssetStatus, "shop_id": newCardAfter.ShopID},
extra, nil)
}
func (s *Service) lockExchangeOrderByID(ctx context.Context, tx *gorm.DB, id uint) (*model.ExchangeOrder, error) {