All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 11m42s
- 新增成对迁移 000222:tb_exchange_order 增加非空 migration_status 与 migration_failure_reason,按既有 migrate_data/migration_completed 回填历史, 并加四值 CHECK 约束,不新增索引 - 模型与常量定义四种迁移状态及中文名称,保留既有布尔字段兼容语义 - 物流换货创建恒 not_migrated,发货按请求落 pending/not_migrated, 完成成功写 migrated/not_migrated 并清空失败原因、同步兼容字段 - 直接换货创建即完成,任一步失败整体回滚,不持久化换货单、不产生 failed - 迁移失败回滚全部业务修改后,在独立短事务内条件更新 failed 与安全失败原因 并写失败审计,RowsAffected 为 0 时跳过状态写入但仍写审计 - failed 物流单重试仅限超级管理员或平台用户,授权以锁内 FOR UPDATE 判定为准, 重试从钱包余额起整表重跑;非 failed 单沿用既有完成门禁 - 列表与详情返回迁移状态与中文名称,仅 failed 返回失败原因;既有三字段保持兼容 - 换货导出在「状态」列后新增中文「迁移状态」列,不导出失败原因 - 同步 order-refund-exchange 主 spec 与验证证据,归档本 Change - 登记 KNOWN-ISSUE-001:既有标签复制 OnConflict 未声明部分索引谓词(42P10), 旧资产带标签时迁移最后一步失败,待另立变更修复
1314 lines
55 KiB
Go
1314 lines
55 KiB
Go
package exchange
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"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"
|
||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
type Service struct {
|
||
db *gorm.DB
|
||
exchangeStore *postgres.ExchangeOrderStore
|
||
refundStore *postgres.RefundStore
|
||
iotCardStore *postgres.IotCardStore
|
||
deviceStore *postgres.DeviceStore
|
||
assetWalletStore *postgres.AssetWalletStore
|
||
assetWalletTransactionStore *postgres.AssetWalletTransactionStore
|
||
packageUsageStore *postgres.PackageUsageStore
|
||
packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore
|
||
resourceTagStore *postgres.ResourceTagStore
|
||
customerBinding *customerBindingSvc.Service
|
||
shippingCreatedNotifier *exchangeapp.ShippingCreatedNotifier
|
||
auditWriter *audit.Writer
|
||
logger *zap.Logger
|
||
}
|
||
|
||
func New(
|
||
db *gorm.DB,
|
||
exchangeStore *postgres.ExchangeOrderStore,
|
||
iotCardStore *postgres.IotCardStore,
|
||
deviceStore *postgres.DeviceStore,
|
||
assetWalletStore *postgres.AssetWalletStore,
|
||
assetWalletTransactionStore *postgres.AssetWalletTransactionStore,
|
||
packageUsageStore *postgres.PackageUsageStore,
|
||
packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore,
|
||
resourceTagStore *postgres.ResourceTagStore,
|
||
customerBinding *customerBindingSvc.Service,
|
||
logger *zap.Logger,
|
||
) *Service {
|
||
return &Service{
|
||
db: db,
|
||
exchangeStore: exchangeStore,
|
||
refundStore: postgres.NewRefundStore(db),
|
||
iotCardStore: iotCardStore,
|
||
deviceStore: deviceStore,
|
||
assetWalletStore: assetWalletStore,
|
||
assetWalletTransactionStore: assetWalletTransactionStore,
|
||
packageUsageStore: packageUsageStore,
|
||
packageUsageDailyRecordStore: packageUsageDailyRecordStore,
|
||
resourceTagStore: resourceTagStore,
|
||
customerBinding: customerBinding,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
// SetShippingCreatedNotifier 注入物流换货创建后的可靠通知用例。
|
||
func (s *Service) SetShippingCreatedNotifier(notifier *exchangeapp.ShippingCreatedNotifier) {
|
||
s.shippingCreatedNotifier = notifier
|
||
}
|
||
|
||
func (s *Service) Create(ctx context.Context, req *dto.CreateExchangeRequest) (*dto.ExchangeOrderResponse, error) {
|
||
flowType := normalizeExchangeFlowType(req.FlowType)
|
||
if !isValidExchangeFlowType(flowType) {
|
||
return nil, errors.New(errors.CodeInvalidParam, "换货流程类型不合法")
|
||
}
|
||
if flowType == constants.ExchangeFlowTypeDirect && strings.TrimSpace(req.NewIdentifier) == "" {
|
||
return nil, errors.New(errors.CodeInvalidParam, "直接换货必须填写新资产标识")
|
||
}
|
||
|
||
asset, err := s.resolveAssetByIdentifier(ctx, req.OldAssetType, req.OldIdentifier)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
migrateData := false
|
||
if req.MigrateData != nil {
|
||
migrateData = *req.MigrateData
|
||
}
|
||
creator := middleware.GetUserIDFromContext(ctx)
|
||
order := &model.ExchangeOrder{
|
||
ExchangeNo: model.GenerateExchangeNo(),
|
||
FlowType: flowType,
|
||
OldAssetType: asset.AssetType,
|
||
OldAssetID: asset.AssetID,
|
||
OldAssetIdentifier: asset.Identifier,
|
||
ExchangeReason: req.ExchangeReason,
|
||
Remark: req.Remark,
|
||
Status: constants.ExchangeStatusPendingInfo,
|
||
MigrationCompleted: false,
|
||
MigrationBalance: 0,
|
||
MigrateData: flowType == constants.ExchangeFlowTypeDirect && migrateData,
|
||
// 创建阶段不写迁移意图:物流换货的唯一写入点是发货,直接换货在同一事务内完成。
|
||
MigrationStatus: constants.ExchangeMigrationStatusNotMigrated,
|
||
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.MigrationStatus = constants.ExchangeMigrationStatusNotMigrated
|
||
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 {
|
||
err = errors.New(errors.CodeInternalError, "物流换货通知服务未配置")
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCreated, "创建卡换货单失败", order, err)
|
||
return nil, err
|
||
}
|
||
requestID := ""
|
||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||
requestID = *value
|
||
}
|
||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if createErr := tx.WithContext(ctx).Create(order).Error; createErr != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, createErr, "创建换货单失败")
|
||
}
|
||
customerIDs, queryErr := s.customerBinding.ActiveCustomerIDsByAsset(ctx, tx, asset.AssetType, asset.AssetID)
|
||
if queryErr != nil {
|
||
return queryErr
|
||
}
|
||
for _, customerID := range customerIDs {
|
||
if notifyErr := s.shippingCreatedNotifier.Notify(ctx, tx, exchangeapp.ShippingCreatedEvent{
|
||
ExchangeID: order.ID, ExchangeNo: order.ExchangeNo, CustomerID: customerID,
|
||
AssetType: asset.AssetType, AssetID: asset.AssetID, AssetIdentifier: asset.Identifier,
|
||
RequestID: requestID, CorrelationID: requestID,
|
||
}); notifyErr != nil {
|
||
return notifyErr
|
||
}
|
||
}
|
||
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
|
||
}
|
||
|
||
resp := s.toExchangeOrderResponse(order)
|
||
resp.SubmitterName = s.loadExchangeSubmitterNameBestEffort(ctx, order.Creator)
|
||
return resp, nil
|
||
}
|
||
|
||
func (s *Service) Get(ctx context.Context, id uint) (*dto.ExchangeOrderResponse, error) {
|
||
order, err := s.exchangeStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeExchangeOrderNotFound)
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货单详情失败")
|
||
}
|
||
resp := s.toExchangeOrderResponse(order)
|
||
resp.SubmitterName = s.loadExchangeSubmitterNameBestEffort(ctx, order.Creator)
|
||
return resp, nil
|
||
}
|
||
|
||
func (s *Service) Ship(ctx context.Context, id uint, req *dto.ExchangeShipRequest) (*dto.ExchangeOrderResponse, error) {
|
||
order, err := s.exchangeStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeExchangeOrderNotFound)
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||
}
|
||
if order.Status != constants.ExchangeStatusPendingShip {
|
||
err = errors.New(errors.CodeExchangeStatusInvalid)
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShipped, "卡换货发货被拒绝", order, err)
|
||
return nil, err
|
||
}
|
||
if !isShippingExchangeFlow(order.FlowType) {
|
||
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
|
||
}
|
||
|
||
return s.Get(ctx, id)
|
||
}
|
||
|
||
func (s *Service) Complete(ctx context.Context, id uint) error {
|
||
order, err := s.exchangeStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeExchangeOrderNotFound)
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||
}
|
||
if order.Status != constants.ExchangeStatusShipped {
|
||
err = errors.New(errors.CodeExchangeStatusInvalid)
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货被拒绝", order, err)
|
||
return err
|
||
}
|
||
if !isShippingExchangeFlow(order.FlowType) {
|
||
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持确认完成")
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货被拒绝", order, err)
|
||
return err
|
||
}
|
||
// 事务外预读只做快速拒绝,授权判定以锁内读到的迁移状态为准。
|
||
if !isExchangeMigrationRetryAuthorized(ctx, order.MigrationStatus) {
|
||
err = errors.New(errors.CodeForbidden, "仅超级管理员或平台用户可重试迁移失败的换货单")
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货被拒绝", order, err)
|
||
return err
|
||
}
|
||
|
||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
lockedOrder, lockErr := s.lockExchangeOrderByID(ctx, tx, id)
|
||
if lockErr != nil {
|
||
return lockErr
|
||
}
|
||
if !isShippingExchangeFlow(lockedOrder.FlowType) {
|
||
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持确认完成")
|
||
}
|
||
if lockedOrder.Status != constants.ExchangeStatusShipped {
|
||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||
}
|
||
if !isExchangeMigrationRetryAuthorized(ctx, lockedOrder.MigrationStatus) {
|
||
return errors.New(errors.CodeForbidden, "仅超级管理员或平台用户可重试迁移失败的换货单")
|
||
}
|
||
return s.completeExchangeWithTx(ctx, tx, lockedOrder, constants.ExchangeStatusShipped)
|
||
})
|
||
if err != nil {
|
||
if isExchangeMigrationFailure(err) {
|
||
// 迁移失败已在主事务回滚,失败状态、安全化原因与失败审计在回滚后短事务内落库。
|
||
s.recordExchangeMigrationFailure(ctx, order, err)
|
||
} else {
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCompleted, "完成卡换货失败", order, err)
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
|
||
func (s *Service) Cancel(ctx context.Context, id uint, req *dto.ExchangeCancelRequest) error {
|
||
order, err := s.exchangeStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeExchangeOrderNotFound)
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||
}
|
||
if order.Status != constants.ExchangeStatusPendingInfo && order.Status != constants.ExchangeStatusPendingShip {
|
||
err = errors.New(errors.CodeExchangeStatusInvalid)
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货被拒绝", order, err)
|
||
return err
|
||
}
|
||
if !isShippingExchangeFlow(order.FlowType) {
|
||
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持取消")
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeCancelled, "取消卡换货被拒绝", order, err)
|
||
return err
|
||
}
|
||
|
||
updates := map[string]any{
|
||
"updater": middleware.GetUserIDFromContext(ctx),
|
||
"updated_at": time.Now(),
|
||
}
|
||
if req != nil {
|
||
updates["remark"] = req.Remark
|
||
}
|
||
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)
|
||
}
|
||
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 err
|
||
}
|
||
|
||
func (s *Service) Renew(ctx context.Context, id uint) error {
|
||
order, err := s.exchangeStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeExchangeOrderNotFound)
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||
}
|
||
if order.Status != constants.ExchangeStatusCompleted {
|
||
err = errors.New(errors.CodeExchangeStatusInvalid)
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeRenewed, "换出旧卡转新被拒绝", order, err)
|
||
return err
|
||
}
|
||
|
||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if order.OldAssetType == constants.ExchangeAssetTypeIotCard {
|
||
var card model.IotCard
|
||
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, 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 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; updateErr != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, updateErr, "重置旧卡转新状态失败")
|
||
}
|
||
|
||
cardKey := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, Card: &card, VirtualNo: card.VirtualNo})
|
||
if cardKey != "" {
|
||
if unbindErr := s.customerBinding.UnbindByVirtualNo(ctx, tx, constants.ExchangeAssetTypeIotCard, card.ID, cardKey); unbindErr != nil {
|
||
return unbindErr
|
||
}
|
||
}
|
||
|
||
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 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, "创建新钱包失败")
|
||
}
|
||
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
|
||
if err = tx.Where("id = ?", order.OldAssetID).First(&device).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询旧设备失败")
|
||
}
|
||
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,
|
||
"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, "重置旧设备转新状态失败")
|
||
}
|
||
|
||
deviceKey := exchangeAssetBindingKey(&resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, Device: &device, VirtualNo: device.VirtualNo})
|
||
if deviceKey != "" {
|
||
if err = s.customerBinding.UnbindByVirtualNo(ctx, tx, constants.ExchangeAssetTypeDevice, device.ID, deviceKey); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if err = tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeDevice, device.ID).Delete(&model.AssetWallet{}).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "清理旧钱包失败")
|
||
}
|
||
|
||
shopTag := uint(0)
|
||
if device.ShopID != nil {
|
||
shopTag = *device.ShopID
|
||
}
|
||
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, "创建新钱包失败")
|
||
}
|
||
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) {
|
||
asset, err := s.resolveAssetByIdentifier(ctx, "", identifier)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if !s.customerOwnsAsset(ctx, asset) {
|
||
return nil, errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
|
||
order, err := s.exchangeStore.FindShippingPendingByOldAsset(ctx, asset.AssetType, asset.AssetID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, nil
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询待处理换货单失败")
|
||
}
|
||
|
||
return &dto.ClientExchangePendingResponse{
|
||
ID: order.ID,
|
||
ExchangeNo: order.ExchangeNo,
|
||
FlowType: effectiveExchangeFlowType(order.FlowType),
|
||
Status: order.Status,
|
||
StatusName: constants.GetExchangeStatusName(order.Status),
|
||
StatusText: constants.GetExchangeStatusName(order.Status),
|
||
ExchangeReason: order.ExchangeReason,
|
||
CreatedAt: order.CreatedAt,
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) SubmitShippingInfo(ctx context.Context, id uint, req *dto.ClientShippingInfoRequest) error {
|
||
order, err := s.exchangeStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeExchangeOrderNotFound)
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||
}
|
||
if !isShippingExchangeFlow(order.FlowType) {
|
||
err = errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持填写收货信息")
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息被拒绝", order, err)
|
||
return err
|
||
}
|
||
if order.Status != constants.ExchangeStatusPendingInfo {
|
||
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) {
|
||
err = errors.New(errors.CodeExchangeOrderNotFound)
|
||
s.recordExchangeOrderFailure(ctx, constants.AuditActionCardExchangeShippingInfoSubmitted, "提交卡换货收货信息被拒绝", order, err)
|
||
return err
|
||
}
|
||
|
||
updates := map[string]any{
|
||
"recipient_name": req.RecipientName,
|
||
"recipient_phone": req.RecipientPhone,
|
||
"recipient_address": req.RecipientAddress,
|
||
"updated_at": time.Now(),
|
||
}
|
||
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)
|
||
}
|
||
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 err
|
||
}
|
||
|
||
type resolvedExchangeAsset struct {
|
||
AssetType string
|
||
AssetID uint
|
||
Identifier string
|
||
VirtualNo string
|
||
AssetStatus int
|
||
ShopID *uint
|
||
Card *model.IotCard
|
||
Device *model.Device
|
||
}
|
||
|
||
func (s *Service) resolveAssetByIdentifier(ctx context.Context, expectedAssetType, identifier string) (*resolvedExchangeAsset, error) {
|
||
if expectedAssetType == "" || expectedAssetType == constants.ExchangeAssetTypeDevice {
|
||
device, err := s.deviceStore.GetByIdentifier(ctx, identifier)
|
||
if err == nil {
|
||
if expectedAssetType != "" && expectedAssetType != constants.ExchangeAssetTypeDevice {
|
||
return nil, errors.New(errors.CodeExchangeAssetTypeMismatch)
|
||
}
|
||
return newResolvedDeviceAsset(device), nil
|
||
}
|
||
if err != gorm.ErrRecordNotFound {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||
}
|
||
}
|
||
|
||
if expectedAssetType == "" || expectedAssetType == constants.ExchangeAssetTypeIotCard {
|
||
card, err := s.iotCardStore.GetByIdentifier(ctx, identifier)
|
||
if err == nil {
|
||
if expectedAssetType != "" && expectedAssetType != constants.ExchangeAssetTypeIotCard {
|
||
return nil, errors.New(errors.CodeExchangeAssetTypeMismatch)
|
||
}
|
||
return newResolvedIotCardAsset(card), nil
|
||
} else if err != gorm.ErrRecordNotFound {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||
}
|
||
}
|
||
|
||
return nil, errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
|
||
// isExchangeableAssetStatus 判断旧资产状态是否允许发起换货(在库和已销售均可换货)
|
||
func isExchangeableAssetStatus(status int) bool {
|
||
return status == constants.AssetStatusInStock || status == constants.AssetStatusSold
|
||
}
|
||
|
||
// oldAssetStatusError 根据旧资产的实际状态返回具体的错误原因
|
||
func oldAssetStatusError(status int) error {
|
||
switch status {
|
||
case constants.AssetStatusExchanged:
|
||
return errors.New(errors.CodeExchangeStatusInvalid, "该资产已完成过换货,如需再次换货请使用换货后的新资产")
|
||
case constants.AssetStatusDeactivated:
|
||
return errors.New(errors.CodeExchangeStatusInvalid, "该资产已停用,无法发起换货")
|
||
default:
|
||
return errors.New(errors.CodeExchangeStatusInvalid, "该资产当前状态不允许换货")
|
||
}
|
||
}
|
||
|
||
func normalizeExchangeFlowType(flowType string) string {
|
||
flowType = strings.TrimSpace(flowType)
|
||
if flowType == "" {
|
||
return constants.ExchangeFlowTypeShipping
|
||
}
|
||
return flowType
|
||
}
|
||
|
||
func effectiveExchangeFlowType(flowType string) string {
|
||
return normalizeExchangeFlowType(flowType)
|
||
}
|
||
|
||
func isValidExchangeFlowType(flowType string) bool {
|
||
return flowType == constants.ExchangeFlowTypeShipping || flowType == constants.ExchangeFlowTypeDirect
|
||
}
|
||
|
||
func isShippingExchangeFlow(flowType string) bool {
|
||
return effectiveExchangeFlowType(flowType) == constants.ExchangeFlowTypeShipping
|
||
}
|
||
|
||
func (s *Service) createDirectExchange(ctx context.Context, req *dto.CreateExchangeRequest, oldAsset *resolvedExchangeAsset, order *model.ExchangeOrder) (uint, error) {
|
||
var orderID uint
|
||
|
||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
lockedOldAsset, err := s.resolveAssetByIDWithTx(ctx, tx, oldAsset.AssetType, oldAsset.AssetID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !isExchangeableAssetStatus(lockedOldAsset.AssetStatus) {
|
||
return oldAssetStatusError(lockedOldAsset.AssetStatus)
|
||
}
|
||
if err = s.ensureNoActiveExchangeWithTx(ctx, tx, lockedOldAsset.AssetType, lockedOldAsset.AssetID); err != nil {
|
||
return err
|
||
}
|
||
|
||
newAsset, err := s.resolveAssetByIdentifierWithTx(ctx, tx, "", req.NewIdentifier)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if newAsset.AssetType != lockedOldAsset.AssetType {
|
||
return errors.New(errors.CodeExchangeAssetTypeMismatch)
|
||
}
|
||
|
||
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, "创建直接换货单失败")
|
||
}
|
||
if err = s.completeExchangeWithTx(ctx, tx, order, constants.ExchangeStatusPendingInfo); err != nil {
|
||
return err
|
||
}
|
||
orderID = order.ID
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return orderID, nil
|
||
}
|
||
|
||
func (s *Service) shipWithTx(ctx context.Context, order *model.ExchangeOrder, req *dto.ExchangeShipRequest) error {
|
||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
lockedOrder, err := s.lockExchangeOrderByID(ctx, tx, order.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !isShippingExchangeFlow(lockedOrder.FlowType) {
|
||
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持发货")
|
||
}
|
||
if lockedOrder.Status != constants.ExchangeStatusPendingShip {
|
||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||
}
|
||
|
||
oldAsset, err := s.resolveAssetByIDWithTx(ctx, tx, lockedOrder.OldAssetType, lockedOrder.OldAssetID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
newAsset, err := s.resolveAssetByIdentifierWithTx(ctx, tx, "", req.NewIdentifier)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if newAsset.AssetType != lockedOrder.OldAssetType {
|
||
return errors.New(errors.CodeExchangeAssetTypeMismatch)
|
||
}
|
||
if err = s.validateExchangeAssetsWithTx(ctx, tx, lockedOrder.ID, oldAsset, newAsset); err != nil {
|
||
return err
|
||
}
|
||
|
||
now := time.Now()
|
||
// 迁移意图在发货时确定:选择迁移进入待迁移,否则保持不迁移。
|
||
migrationStatus := constants.ExchangeMigrationStatusNotMigrated
|
||
if req.MigrateData {
|
||
migrationStatus = constants.ExchangeMigrationStatusPending
|
||
}
|
||
result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
|
||
Where("id = ? AND status = ?", lockedOrder.ID, constants.ExchangeStatusPendingShip).
|
||
Updates(map[string]any{
|
||
"new_asset_type": newAsset.AssetType,
|
||
"new_asset_id": newAsset.AssetID,
|
||
"new_asset_identifier": newAsset.Identifier,
|
||
"express_company": req.ExpressCompany,
|
||
"express_no": req.ExpressNo,
|
||
"migrate_data": req.MigrateData,
|
||
"migration_status": migrationStatus,
|
||
"shipped_at": now,
|
||
"status": constants.ExchangeStatusShipped,
|
||
"updater": middleware.GetUserIDFromContext(ctx),
|
||
"updated_at": now,
|
||
})
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新换货单发货状态失败")
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||
}
|
||
lockedOrder.NewAssetType = newAsset.AssetType
|
||
lockedOrder.NewAssetID = &newAsset.AssetID
|
||
lockedOrder.NewAssetIdentifier = newAsset.Identifier
|
||
lockedOrder.MigrateData = req.MigrateData
|
||
lockedOrder.MigrationStatus = migrationStatus
|
||
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)
|
||
})
|
||
}
|
||
|
||
func (s *Service) completeExchangeWithTx(ctx context.Context, tx *gorm.DB, order *model.ExchangeOrder, fromStatus int) error {
|
||
if order.NewAssetID == nil || *order.NewAssetID == 0 || order.NewAssetIdentifier == "" {
|
||
return errors.New(errors.CodeInvalidParam, "新资产信息缺失")
|
||
}
|
||
|
||
oldAsset, err := s.resolveAssetByIDWithTx(ctx, tx, order.OldAssetType, order.OldAssetID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
newAsset, err := s.resolveAssetByIDWithTx(ctx, tx, order.NewAssetType, *order.NewAssetID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
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
|
||
}
|
||
if err = s.switchCustomerBindingWithTx(ctx, tx, oldAsset, newAsset); err != nil {
|
||
return err
|
||
}
|
||
if err = s.updateAssetStatusesForCompletion(ctx, tx, oldAsset, newAsset); err != nil {
|
||
return err
|
||
}
|
||
|
||
var migration *exchangeMigrationResult
|
||
if order.MigrateData {
|
||
migration, err = s.executeMigrationWithTx(ctx, tx, order, oldAsset, newAsset)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
now := time.Now()
|
||
// 成功后迁移状态落到 migrated 或 not_migrated,并清空上一次失败原因。
|
||
migrationStatus := constants.ExchangeMigrationStatusNotMigrated
|
||
if order.MigrateData {
|
||
migrationStatus = constants.ExchangeMigrationStatusMigrated
|
||
}
|
||
beforeMigrationStatus := order.MigrationStatus
|
||
updates := map[string]any{
|
||
"status": constants.ExchangeStatusCompleted,
|
||
"migration_status": migrationStatus,
|
||
"migration_failure_reason": "",
|
||
"completed_at": now,
|
||
"updater": middleware.GetUserIDFromContext(ctx),
|
||
"updated_at": now,
|
||
}
|
||
if order.MigrateData {
|
||
updates["migration_completed"] = true
|
||
updates["migration_balance"] = migration.Balance
|
||
}
|
||
result := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
|
||
Where("id = ? AND status = ?", order.ID, fromStatus).
|
||
Updates(updates)
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "确认换货完成失败")
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||
}
|
||
orderBefore := map[string]any{"status": fromStatus, "migration_completed": order.MigrationCompleted, "migration_balance": order.MigrationBalance, "migration_status": beforeMigrationStatus}
|
||
order.Status = constants.ExchangeStatusCompleted
|
||
order.CompletedAt = &now
|
||
order.MigrationStatus = migrationStatus
|
||
order.MigrationFailureReason = ""
|
||
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, "migration_status": order.MigrationStatus},
|
||
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, "migration_status": order.MigrationStatus},
|
||
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) {
|
||
var order model.ExchangeOrder
|
||
query := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", id)
|
||
query = middleware.ApplyShopFilter(ctx, query)
|
||
if err := query.First(&order).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeExchangeOrderNotFound)
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货单失败")
|
||
}
|
||
return &order, nil
|
||
}
|
||
|
||
func (s *Service) resolveAssetByID(ctx context.Context, assetType string, assetID uint) (*resolvedExchangeAsset, error) {
|
||
if assetType == constants.ExchangeAssetTypeIotCard {
|
||
card, err := s.iotCardStore.GetByID(ctx, assetID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||
}
|
||
return newResolvedIotCardAsset(card), nil
|
||
}
|
||
if assetType == constants.ExchangeAssetTypeDevice {
|
||
device, err := s.deviceStore.GetByID(ctx, assetID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||
}
|
||
return newResolvedDeviceAsset(device), nil
|
||
}
|
||
return nil, errors.New(errors.CodeInvalidParam, "资产类型不合法")
|
||
}
|
||
|
||
func (s *Service) resolveAssetByIDWithTx(ctx context.Context, tx *gorm.DB, assetType string, assetID uint) (*resolvedExchangeAsset, error) {
|
||
if assetType == constants.ExchangeAssetTypeIotCard {
|
||
var card model.IotCard
|
||
query := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", assetID)
|
||
query = middleware.ApplyShopFilter(ctx, query)
|
||
if err := query.First(&card).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||
}
|
||
return newResolvedIotCardAsset(&card), nil
|
||
}
|
||
if assetType == constants.ExchangeAssetTypeDevice {
|
||
var device model.Device
|
||
query := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", assetID)
|
||
query = middleware.ApplyShopFilter(ctx, query)
|
||
if err := query.First(&device).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||
}
|
||
return newResolvedDeviceAsset(&device), nil
|
||
}
|
||
return nil, errors.New(errors.CodeInvalidParam, "资产类型不合法")
|
||
}
|
||
|
||
func (s *Service) resolveAssetByIdentifierWithTx(ctx context.Context, tx *gorm.DB, expectedAssetType, identifier string) (*resolvedExchangeAsset, error) {
|
||
if expectedAssetType == "" || expectedAssetType == constants.ExchangeAssetTypeDevice {
|
||
var device model.Device
|
||
query := tx.WithContext(ctx).
|
||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||
Where("virtual_no = ? OR imei = ? OR sn = ?", identifier, identifier, identifier)
|
||
query = middleware.ApplyShopFilter(ctx, query)
|
||
if err := query.First(&device).Error; err == nil {
|
||
if expectedAssetType != "" && expectedAssetType != constants.ExchangeAssetTypeDevice {
|
||
return nil, errors.New(errors.CodeExchangeAssetTypeMismatch)
|
||
}
|
||
return newResolvedDeviceAsset(&device), nil
|
||
} else if err != gorm.ErrRecordNotFound {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||
}
|
||
}
|
||
if expectedAssetType == "" || expectedAssetType == constants.ExchangeAssetTypeIotCard {
|
||
var card model.IotCard
|
||
query := tx.WithContext(ctx).
|
||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||
Where("virtual_no = ? OR iccid = ? OR msisdn = ? OR iccid_19 = ? OR iccid_20 = ?", identifier, identifier, identifier, identifier, identifier)
|
||
query = middleware.ApplyShopFilter(ctx, query)
|
||
if err := query.First(&card).Error; err == nil {
|
||
if expectedAssetType != "" && expectedAssetType != constants.ExchangeAssetTypeIotCard {
|
||
return nil, errors.New(errors.CodeExchangeAssetTypeMismatch)
|
||
}
|
||
return newResolvedIotCardAsset(&card), nil
|
||
} else if err != gorm.ErrRecordNotFound {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||
}
|
||
}
|
||
return nil, errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
|
||
// newResolvedIotCardAsset 将卡的权威 ICCID 固化为换货快照,避免请求标识污染历史记录。
|
||
func newResolvedIotCardAsset(card *model.IotCard) *resolvedExchangeAsset {
|
||
return &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, AssetID: card.ID, Identifier: card.ICCID, VirtualNo: card.VirtualNo, AssetStatus: card.AssetStatus, ShopID: card.ShopID, Card: card}
|
||
}
|
||
|
||
// newResolvedDeviceAsset 按虚拟号、IMEI、SN 的稳定优先级生成设备换货快照。
|
||
func newResolvedDeviceAsset(device *model.Device) *resolvedExchangeAsset {
|
||
return &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, AssetID: device.ID, Identifier: preferredDeviceIdentifier(device), VirtualNo: device.VirtualNo, AssetStatus: device.AssetStatus, ShopID: device.ShopID, Device: device}
|
||
}
|
||
|
||
func (s *Service) ensureNoActiveExchangeWithTx(ctx context.Context, tx *gorm.DB, assetType string, assetID uint) error {
|
||
var count int64
|
||
query := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
|
||
Where("old_asset_type = ? AND old_asset_id = ?", assetType, assetID).
|
||
Where("status IN ?", []int{constants.ExchangeStatusPendingInfo, constants.ExchangeStatusPendingShip, constants.ExchangeStatusShipped})
|
||
query = middleware.ApplyShopFilter(ctx, query)
|
||
if err := query.Count(&count).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询进行中换货单失败")
|
||
}
|
||
if count > 0 {
|
||
return errors.New(errors.CodeExchangeInProgress)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) validateExchangeAssetsWithTx(ctx context.Context, tx *gorm.DB, orderID uint, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||
if oldAsset.AssetType != newAsset.AssetType {
|
||
return errors.New(errors.CodeExchangeAssetTypeMismatch)
|
||
}
|
||
if !isExchangeableAssetStatus(oldAsset.AssetStatus) {
|
||
return oldAssetStatusError(oldAsset.AssetStatus)
|
||
}
|
||
if err := s.ensureNewAssetBindingAvailableWithTx(ctx, tx, oldAsset, newAsset); err != nil {
|
||
return err
|
||
}
|
||
occupied, err := s.hasShippingNewAssetOccupiedWithTx(ctx, tx, newAsset.AssetType, newAsset.AssetID, orderID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if occupied {
|
||
return errors.New(errors.CodeExchangeStatusInvalid, "新资产已被其他换货单占用")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) ensureNewAssetBindingAvailableWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||
newKey := exchangeAssetBindingKey(newAsset)
|
||
// 新资产无虚拟号时不再拦截:Migrate() 已能正确处理无绑定跳过、有绑定迁移到 pci 的情形
|
||
if newKey == "" {
|
||
return nil
|
||
}
|
||
var newBindCount int64
|
||
if err := tx.WithContext(ctx).Model(&model.PersonalCustomerDevice{}).
|
||
Where("virtual_no = ? AND status = ?", newKey, constants.StatusEnabled).
|
||
Count(&newBindCount).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询新资产客户绑定失败")
|
||
}
|
||
if newBindCount > 0 {
|
||
return errors.New(errors.CodeExchangeStatusInvalid, "新资产存在有效客户绑定,不可用于换货")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) switchCustomerBindingWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||
return s.customerBinding.Migrate(ctx, tx, oldAsset.AssetType, oldAsset.AssetID, newAsset.AssetType, newAsset.AssetID)
|
||
}
|
||
|
||
// syncNewAssetOwnershipWithTx 将新资产归属同步为旧资产当前归属。
|
||
// 归属继承不受 migrate_data 控制,避免平台库存资产换入店铺后仍处于平台租户范围。
|
||
func (s *Service) syncNewAssetOwnershipWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||
ownershipStatus := constants.IotCardStatusInStock
|
||
if oldAsset.ShopID != nil {
|
||
ownershipStatus = constants.IotCardStatusDistributed
|
||
}
|
||
|
||
modelValue := any(&model.IotCard{})
|
||
if newAsset.AssetType == constants.ExchangeAssetTypeDevice {
|
||
modelValue = &model.Device{}
|
||
ownershipStatus = constants.DeviceStatusInStock
|
||
if oldAsset.ShopID != nil {
|
||
ownershipStatus = constants.DeviceStatusDistributed
|
||
}
|
||
}
|
||
|
||
result := tx.WithContext(ctx).Model(modelValue).
|
||
Where("id = ?", newAsset.AssetID).
|
||
Updates(map[string]any{
|
||
"shop_id": oldAsset.ShopID,
|
||
"status": ownershipStatus,
|
||
"updated_at": time.Now(),
|
||
})
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "同步新资产店铺归属失败")
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
|
||
shopIDTag := uint(0)
|
||
if oldAsset.ShopID != nil {
|
||
shopIDTag = *oldAsset.ShopID
|
||
}
|
||
if err := tx.WithContext(ctx).Model(&model.AssetWallet{}).
|
||
Where("resource_type = ? AND resource_id = ?", newAsset.AssetType, newAsset.AssetID).
|
||
Updates(map[string]any{"shop_id_tag": shopIDTag, "updated_at": time.Now()}).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "同步新资产钱包店铺标签失败")
|
||
}
|
||
|
||
newAsset.ShopID = cloneShopID(oldAsset.ShopID)
|
||
return nil
|
||
}
|
||
|
||
func cloneShopID(shopID *uint) *uint {
|
||
if shopID == nil {
|
||
return nil
|
||
}
|
||
value := *shopID
|
||
return &value
|
||
}
|
||
|
||
func (s *Service) updateAssetStatusesForCompletion(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||
now := time.Now()
|
||
if oldAsset.AssetType == constants.ExchangeAssetTypeIotCard {
|
||
result := tx.WithContext(ctx).Model(&model.IotCard{}).
|
||
Where("id = ? AND asset_status IN ?", oldAsset.AssetID, []int{constants.AssetStatusInStock, constants.AssetStatusSold}).
|
||
Updates(map[string]any{"asset_status": constants.AssetStatusExchanged, "updated_at": now})
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新旧卡状态失败")
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New(errors.CodeExchangeStatusInvalid, "旧资产状态已被修改,请重试")
|
||
}
|
||
result = tx.WithContext(ctx).Model(&model.IotCard{}).
|
||
Where("id = ?", newAsset.AssetID).
|
||
Updates(map[string]any{"asset_status": constants.AssetStatusSold, "updated_at": now})
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新新卡状态失败")
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
return nil
|
||
}
|
||
result := tx.WithContext(ctx).Model(&model.Device{}).
|
||
Where("id = ? AND asset_status IN ?", oldAsset.AssetID, []int{constants.AssetStatusInStock, constants.AssetStatusSold}).
|
||
Updates(map[string]any{"asset_status": constants.AssetStatusExchanged, "updated_at": now})
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新旧设备状态失败")
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New(errors.CodeExchangeStatusInvalid, "旧资产状态已被修改,请重试")
|
||
}
|
||
result = tx.WithContext(ctx).Model(&model.Device{}).
|
||
Where("id = ?", newAsset.AssetID).
|
||
Updates(map[string]any{"asset_status": constants.AssetStatusSold, "updated_at": now})
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新新设备状态失败")
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) hasShippingNewAssetOccupiedWithTx(ctx context.Context, tx *gorm.DB, assetType string, assetID uint, excludeOrderID uint) (bool, error) {
|
||
var count int64
|
||
query := tx.WithContext(ctx).Model(&model.ExchangeOrder{}).
|
||
Where("new_asset_type = ? AND new_asset_id = ?", assetType, assetID).
|
||
Where("status = ?", constants.ExchangeStatusShipped).
|
||
Where("COALESCE(NULLIF(flow_type, ''), ?) = ?", constants.ExchangeFlowTypeShipping, constants.ExchangeFlowTypeShipping)
|
||
if excludeOrderID > 0 {
|
||
query = query.Where("id <> ?", excludeOrderID)
|
||
}
|
||
query = middleware.ApplyShopFilter(ctx, query)
|
||
if err := query.Count(&count).Error; err != nil {
|
||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询新资产占用失败")
|
||
}
|
||
return count > 0, nil
|
||
}
|
||
|
||
func (s *Service) customerOwnsAsset(ctx context.Context, asset *resolvedExchangeAsset) bool {
|
||
customerID := middleware.GetCustomerIDFromContext(ctx)
|
||
if customerID == 0 || asset == nil {
|
||
return false
|
||
}
|
||
key := exchangeAssetBindingKey(asset)
|
||
if key == "" {
|
||
return false
|
||
}
|
||
var count int64
|
||
if err := s.db.WithContext(ctx).Model(&model.PersonalCustomerDevice{}).
|
||
Where("customer_id = ? AND virtual_no = ? AND status = ?", customerID, key, constants.StatusEnabled).
|
||
Count(&count).Error; err != nil {
|
||
return false
|
||
}
|
||
return count > 0
|
||
}
|
||
|
||
func exchangeAssetBindingKey(asset *resolvedExchangeAsset) string {
|
||
if asset == nil {
|
||
return ""
|
||
}
|
||
if asset.AssetType == constants.ExchangeAssetTypeIotCard {
|
||
if asset.Card != nil {
|
||
return asset.Card.VirtualNo
|
||
}
|
||
return asset.VirtualNo
|
||
}
|
||
if asset.Device == nil {
|
||
return asset.VirtualNo
|
||
}
|
||
if asset.Device.VirtualNo != "" {
|
||
return asset.Device.VirtualNo
|
||
}
|
||
return asset.Device.IMEI
|
||
}
|
||
|
||
func preferredDeviceIdentifier(device *model.Device) string {
|
||
if device == nil {
|
||
return ""
|
||
}
|
||
if device.VirtualNo != "" {
|
||
return device.VirtualNo
|
||
}
|
||
if device.IMEI != "" {
|
||
return device.IMEI
|
||
}
|
||
return device.SN
|
||
}
|
||
|
||
func (s *Service) toExchangeOrderResponse(order *model.ExchangeOrder) *dto.ExchangeOrderResponse {
|
||
if order == nil {
|
||
return nil
|
||
}
|
||
var deletedAt *time.Time
|
||
if order.DeletedAt.Valid {
|
||
deletedAt = &order.DeletedAt.Time
|
||
}
|
||
// 失败原因只在迁移失败时对外可见,其他状态一律不返回,避免把历史原因误读为当前状态。
|
||
failureReason := ""
|
||
if order.MigrationStatus == constants.ExchangeMigrationStatusFailed {
|
||
failureReason = order.MigrationFailureReason
|
||
}
|
||
return &dto.ExchangeOrderResponse{
|
||
ID: order.ID,
|
||
ExchangeNo: order.ExchangeNo,
|
||
FlowType: effectiveExchangeFlowType(order.FlowType),
|
||
FlowTypeName: constants.GetExchangeFlowTypeName(order.FlowType),
|
||
OldAssetType: order.OldAssetType,
|
||
OldAssetID: order.OldAssetID,
|
||
OldAssetIdentifier: order.OldAssetIdentifier,
|
||
NewAssetType: order.NewAssetType,
|
||
NewAssetID: order.NewAssetID,
|
||
NewAssetIdentifier: order.NewAssetIdentifier,
|
||
RecipientName: order.RecipientName,
|
||
RecipientPhone: order.RecipientPhone,
|
||
RecipientAddress: order.RecipientAddress,
|
||
ExpressCompany: order.ExpressCompany,
|
||
ExpressNo: order.ExpressNo,
|
||
MigrateData: order.MigrateData,
|
||
MigrationCompleted: order.MigrationCompleted,
|
||
MigrationBalance: order.MigrationBalance,
|
||
MigrationStatus: order.MigrationStatus,
|
||
MigrationStatusName: constants.GetExchangeMigrationStatusName(order.MigrationStatus),
|
||
MigrationFailureReason: failureReason,
|
||
ShippedAt: order.ShippedAt,
|
||
CompletedAt: order.CompletedAt,
|
||
ExchangeReason: order.ExchangeReason,
|
||
Remark: order.Remark,
|
||
Status: order.Status,
|
||
StatusName: constants.GetExchangeStatusName(order.Status),
|
||
StatusText: constants.GetExchangeStatusName(order.Status),
|
||
ShopID: order.ShopID,
|
||
CreatedAt: order.CreatedAt,
|
||
UpdatedAt: order.UpdatedAt,
|
||
DeletedAt: deletedAt,
|
||
SubmitterID: order.Creator,
|
||
Creator: order.Creator,
|
||
Updater: order.Updater,
|
||
}
|
||
}
|
||
|
||
func (s *Service) loadExchangeSubmitterNameBestEffort(ctx context.Context, id uint) string {
|
||
accounts, err := postgres.NewAccountStore(s.db, nil).GetDisplayAccountsByIDs(ctx, []uint{id})
|
||
if err != nil {
|
||
s.logger.Warn("查询换货提交人失败", zap.Uint("submitter_id", id), zap.Error(err))
|
||
return ""
|
||
}
|
||
if len(accounts) == 0 {
|
||
return ""
|
||
}
|
||
return accounts[0].Username
|
||
}
|