All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m28s
992 lines
38 KiB
Go
992 lines
38 KiB
Go
package exchange
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
|
"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
|
|
iotCardStore *postgres.IotCardStore
|
|
deviceStore *postgres.DeviceStore
|
|
assetWalletStore *postgres.AssetWalletStore
|
|
assetWalletTransactionStore *postgres.AssetWalletTransactionStore
|
|
packageUsageStore *postgres.PackageUsageStore
|
|
packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore
|
|
resourceTagStore *postgres.ResourceTagStore
|
|
personalCustomerDeviceStore *postgres.PersonalCustomerDeviceStore
|
|
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,
|
|
personalCustomerDeviceStore *postgres.PersonalCustomerDeviceStore,
|
|
logger *zap.Logger,
|
|
) *Service {
|
|
return &Service{
|
|
db: db,
|
|
exchangeStore: exchangeStore,
|
|
iotCardStore: iotCardStore,
|
|
deviceStore: deviceStore,
|
|
assetWalletStore: assetWalletStore,
|
|
assetWalletTransactionStore: assetWalletTransactionStore,
|
|
packageUsageStore: packageUsageStore,
|
|
packageUsageDailyRecordStore: packageUsageDailyRecordStore,
|
|
resourceTagStore: resourceTagStore,
|
|
personalCustomerDeviceStore: personalCustomerDeviceStore,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
if asset.AssetStatus != constants.AssetStatusSold {
|
|
return nil, errors.New(errors.CodeExchangeStatusInvalid, "旧资产状态不允许换货")
|
|
}
|
|
|
|
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,
|
|
OldAssetType: asset.AssetType,
|
|
OldAssetID: asset.AssetID,
|
|
OldAssetIdentifier: asset.Identifier,
|
|
ExchangeReason: req.ExchangeReason,
|
|
Remark: req.Remark,
|
|
Status: constants.ExchangeStatusPendingInfo,
|
|
MigrationCompleted: false,
|
|
MigrationBalance: 0,
|
|
MigrateData: false,
|
|
BaseModel: model.BaseModel{Creator: creator, Updater: creator},
|
|
}
|
|
if asset.ShopID != nil {
|
|
order.ShopID = asset.ShopID
|
|
}
|
|
|
|
if err = s.exchangeStore.Create(ctx, order); err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建换货单失败")
|
|
}
|
|
|
|
return s.toExchangeOrderResponse(order), nil
|
|
}
|
|
|
|
func (s *Service) List(ctx context.Context, req *dto.ExchangeListRequest) (*dto.ExchangeListResponse, error) {
|
|
page := req.Page
|
|
page = max(page, 1)
|
|
pageSize := req.PageSize
|
|
if pageSize < 1 {
|
|
pageSize = constants.DefaultPageSize
|
|
}
|
|
if pageSize > constants.MaxPageSize {
|
|
pageSize = constants.MaxPageSize
|
|
}
|
|
|
|
filters := make(map[string]any)
|
|
if req.Status != nil {
|
|
filters["status"] = *req.Status
|
|
}
|
|
if req.FlowType != "" {
|
|
filters["flow_type"] = normalizeExchangeFlowType(req.FlowType)
|
|
}
|
|
if req.Identifier != "" {
|
|
filters["identifier"] = req.Identifier
|
|
}
|
|
if req.CreatedAtStart != nil {
|
|
filters["created_at_start"] = *req.CreatedAtStart
|
|
}
|
|
if req.CreatedAtEnd != nil {
|
|
filters["created_at_end"] = *req.CreatedAtEnd
|
|
}
|
|
|
|
orders, total, err := s.exchangeStore.List(ctx, filters, page, pageSize)
|
|
if err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货单列表失败")
|
|
}
|
|
|
|
list := make([]*dto.ExchangeOrderResponse, 0, len(orders))
|
|
for _, item := range orders {
|
|
list = append(list, s.toExchangeOrderResponse(item))
|
|
}
|
|
|
|
return &dto.ExchangeListResponse{List: list, Total: total, Page: page, PageSize: pageSize}, 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, "查询换货单详情失败")
|
|
}
|
|
return s.toExchangeOrderResponse(order), 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 {
|
|
return nil, errors.New(errors.CodeExchangeStatusInvalid)
|
|
}
|
|
if !isShippingExchangeFlow(order.FlowType) {
|
|
return nil, errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持发货")
|
|
}
|
|
|
|
if err = s.shipWithTx(ctx, order, req); err != nil {
|
|
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 {
|
|
return errors.New(errors.CodeExchangeStatusInvalid)
|
|
}
|
|
if !isShippingExchangeFlow(order.FlowType) {
|
|
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持确认完成")
|
|
}
|
|
|
|
return 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)
|
|
}
|
|
return s.completeExchangeWithTx(ctx, tx, lockedOrder, constants.ExchangeStatusShipped)
|
|
})
|
|
}
|
|
|
|
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 {
|
|
return errors.New(errors.CodeExchangeStatusInvalid)
|
|
}
|
|
if !isShippingExchangeFlow(order.FlowType) {
|
|
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持取消")
|
|
}
|
|
|
|
updates := map[string]any{
|
|
"updater": middleware.GetUserIDFromContext(ctx),
|
|
"updated_at": time.Now(),
|
|
}
|
|
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 {
|
|
return errors.New(errors.CodeExchangeStatusInvalid)
|
|
}
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "取消换货失败")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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 {
|
|
return errors.New(errors.CodeExchangeStatusInvalid)
|
|
}
|
|
|
|
return 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 {
|
|
return errors.New(errors.CodeAssetNotFound)
|
|
}
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询旧卡失败")
|
|
}
|
|
if card.AssetStatus != constants.AssetStatusExchanged {
|
|
return errors.New(errors.CodeExchangeAssetNotExchanged)
|
|
}
|
|
|
|
if err = 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, "重置旧卡转新状态失败")
|
|
}
|
|
|
|
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 err = tx.Where("resource_type = ? AND resource_id = ?", constants.ExchangeAssetTypeIotCard, card.ID).Delete(&model.AssetWallet{}).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "清理旧钱包失败")
|
|
}
|
|
|
|
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, "创建新钱包失败")
|
|
}
|
|
return 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)
|
|
}
|
|
|
|
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 = tx.Where("virtual_no = ?", deviceKey).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, 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, "创建新钱包失败")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
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) {
|
|
return errors.New(errors.CodeExchangeStatusInvalid, "该流程类型不支持填写收货信息")
|
|
}
|
|
if order.Status != constants.ExchangeStatusPendingInfo {
|
|
return errors.New(errors.CodeExchangeStatusInvalid)
|
|
}
|
|
oldAsset, err := s.resolveAssetByID(ctx, order.OldAssetType, order.OldAssetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !s.customerOwnsAsset(ctx, oldAsset) {
|
|
return errors.New(errors.CodeExchangeOrderNotFound)
|
|
}
|
|
|
|
updates := map[string]any{
|
|
"recipient_name": req.RecipientName,
|
|
"recipient_phone": req.RecipientPhone,
|
|
"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 {
|
|
return errors.New(errors.CodeExchangeStatusInvalid)
|
|
}
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "提交收货信息失败")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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 &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, AssetID: device.ID, Identifier: identifier, VirtualNo: device.VirtualNo, AssetStatus: device.AssetStatus, ShopID: device.ShopID, Device: 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 &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, AssetID: card.ID, Identifier: identifier, VirtualNo: card.VirtualNo, AssetStatus: card.AssetStatus, ShopID: card.ShopID, Card: card}, nil
|
|
} else if err != gorm.ErrRecordNotFound {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
|
}
|
|
}
|
|
|
|
return nil, errors.New(errors.CodeAssetNotFound)
|
|
}
|
|
|
|
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) (*dto.ExchangeOrderResponse, 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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if lockedOldAsset.AssetStatus != constants.AssetStatusSold {
|
|
return errors.New(errors.CodeExchangeStatusInvalid, "旧资产状态不允许换货")
|
|
}
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
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 nil, err
|
|
}
|
|
return s.Get(ctx, orderID)
|
|
}
|
|
|
|
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()
|
|
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,
|
|
"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)
|
|
}
|
|
return 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
|
|
}
|
|
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 migrationBalance int64
|
|
if order.MigrateData {
|
|
migrationBalance, err = s.executeMigrationWithTx(ctx, tx, order, oldAsset, newAsset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
updates := map[string]any{
|
|
"status": constants.ExchangeStatusCompleted,
|
|
"completed_at": now,
|
|
"updater": middleware.GetUserIDFromContext(ctx),
|
|
"updated_at": now,
|
|
}
|
|
if order.MigrateData {
|
|
updates["migration_completed"] = true
|
|
updates["migration_balance"] = migrationBalance
|
|
}
|
|
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)
|
|
}
|
|
return 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 &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, AssetID: card.ID, Identifier: card.VirtualNo, VirtualNo: card.VirtualNo, AssetStatus: card.AssetStatus, ShopID: card.ShopID, Card: 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 &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, AssetID: device.ID, Identifier: preferredDeviceIdentifier(device), VirtualNo: device.VirtualNo, AssetStatus: device.AssetStatus, ShopID: device.ShopID, Device: 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 &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, AssetID: card.ID, Identifier: card.VirtualNo, VirtualNo: card.VirtualNo, AssetStatus: card.AssetStatus, ShopID: card.ShopID, Card: &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 &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, AssetID: device.ID, Identifier: preferredDeviceIdentifier(&device), VirtualNo: device.VirtualNo, AssetStatus: device.AssetStatus, ShopID: device.ShopID, Device: &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 &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeDevice, AssetID: device.ID, Identifier: identifier, VirtualNo: device.VirtualNo, AssetStatus: device.AssetStatus, ShopID: device.ShopID, Device: &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 &resolvedExchangeAsset{AssetType: constants.ExchangeAssetTypeIotCard, AssetID: card.ID, Identifier: identifier, VirtualNo: card.VirtualNo, AssetStatus: card.AssetStatus, ShopID: card.ShopID, Card: &card}, nil
|
|
} else if err != gorm.ErrRecordNotFound {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
|
}
|
|
}
|
|
return nil, errors.New(errors.CodeAssetNotFound)
|
|
}
|
|
|
|
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 oldAsset.AssetStatus != constants.AssetStatusSold {
|
|
return errors.New(errors.CodeExchangeStatusInvalid, "旧资产状态不允许换货")
|
|
}
|
|
if newAsset.AssetStatus != constants.AssetStatusInStock {
|
|
return errors.New(errors.CodeExchangeNewAssetNotInStock)
|
|
}
|
|
if !sameShopID(oldAsset.ShopID, newAsset.ShopID) {
|
|
return errors.New(errors.CodeForbidden, "新旧资产归属不一致")
|
|
}
|
|
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 {
|
|
oldKey := exchangeAssetBindingKey(oldAsset)
|
|
newKey := exchangeAssetBindingKey(newAsset)
|
|
oldBindCount := int64(0)
|
|
if oldKey != "" {
|
|
if err := tx.WithContext(ctx).Model(&model.PersonalCustomerDevice{}).
|
|
Where("virtual_no = ? AND status = ?", oldKey, constants.StatusEnabled).
|
|
Count(&oldBindCount).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询旧资产客户绑定失败")
|
|
}
|
|
}
|
|
if oldBindCount > 0 && newKey == "" {
|
|
return errors.New(errors.CodeExchangeStatusInvalid, "新资产无法承接客户绑定")
|
|
}
|
|
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 {
|
|
oldKey := exchangeAssetBindingKey(oldAsset)
|
|
if oldKey == "" {
|
|
return nil
|
|
}
|
|
newKey := exchangeAssetBindingKey(newAsset)
|
|
if newKey == "" {
|
|
return errors.New(errors.CodeExchangeStatusInvalid, "新资产无法承接客户绑定")
|
|
}
|
|
if err := tx.WithContext(ctx).Model(&model.PersonalCustomerDevice{}).
|
|
Where("virtual_no = ? AND status = ?", oldKey, constants.StatusEnabled).
|
|
Updates(map[string]any{"virtual_no": newKey, "updated_at": time.Now()}).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新客户绑定关系失败")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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 = ?", oldAsset.AssetID, 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 = ? AND asset_status = ?", newAsset.AssetID, constants.AssetStatusInStock).
|
|
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.CodeExchangeNewAssetNotInStock)
|
|
}
|
|
return nil
|
|
}
|
|
result := tx.WithContext(ctx).Model(&model.Device{}).
|
|
Where("id = ? AND asset_status = ?", oldAsset.AssetID, 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 = ? AND asset_status = ?", newAsset.AssetID, constants.AssetStatusInStock).
|
|
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.CodeExchangeNewAssetNotInStock)
|
|
}
|
|
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 sameShopID(left, right *uint) bool {
|
|
if left == nil || right == nil {
|
|
return left == nil && right == nil
|
|
}
|
|
return *left == *right
|
|
}
|
|
|
|
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
|
|
}
|
|
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,
|
|
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,
|
|
Creator: order.Creator,
|
|
Updater: order.Updater,
|
|
}
|
|
}
|