收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user