feat: 实现客户端换货系统(client-exchange-system)
新增完整换货生命周期管理:后台发起 → 客户端填收货信息 → 后台发货 → 确认完成(含可选全量迁移) → 旧资产转新再销售 后台接口(7个): - POST /api/admin/exchanges(发起换货) - GET /api/admin/exchanges(换货列表) - GET /api/admin/exchanges/:id(换货详情) - POST /api/admin/exchanges/:id/ship(发货) - POST /api/admin/exchanges/:id/complete(确认完成+可选迁移) - POST /api/admin/exchanges/:id/cancel(取消) - POST /api/admin/exchanges/:id/renew(旧资产转新) 客户端接口(2个): - GET /api/c/v1/exchange/pending(查询换货通知) - POST /api/c/v1/exchange/:id/shipping-info(填写收货信息) 核心能力: - ExchangeOrder 模型与状态机(1待填写→2待发货→3已发货→4已完成,1/2可取消→5) - 全量迁移事务(11张表:钱包、套餐、标签、客户绑定等) - 旧资产转新(generation+1、状态重置、新钱包、历史隔离) - 旧 CardReplacementRecord 表改名为 legacy,is_replaced 过滤改为查新表 - 数据库迁移:000085 新建 tb_exchange_order,000086 旧表改名
This commit is contained in:
243
internal/service/exchange/migration.go
Normal file
243
internal/service/exchange/migration.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (s *Service) executeMigration(ctx context.Context, order *model.ExchangeOrder) (int64, error) {
|
||||
var migrationBalance int64
|
||||
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if order.NewAssetID == nil || *order.NewAssetID == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "新资产信息缺失")
|
||||
}
|
||||
|
||||
oldAsset, err := s.resolveAssetByIdentifier(ctx, order.OldAssetType, order.OldAssetIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newAsset, err := s.resolveAssetByIdentifier(ctx, order.OldAssetType, order.NewAssetIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrationBalance, err = s.transferWalletBalanceWithTx(ctx, tx, order, oldAsset, newAsset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.migratePackageUsageWithTx(ctx, tx, oldAsset, newAsset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.copyAccumulatedFieldsWithTx(tx, oldAsset, newAsset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.copyResourceTagsWithTx(ctx, tx, oldAsset, newAsset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldAsset.VirtualNo != "" && newAsset.VirtualNo != "" {
|
||||
if err = tx.Model(&model.PersonalCustomerDevice{}).
|
||||
Where("virtual_no = ?", oldAsset.VirtualNo).
|
||||
Updates(map[string]any{"virtual_no": newAsset.VirtualNo, "updated_at": time.Now()}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新客户绑定关系失败")
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.updateOldAssetStatusWithTx(tx, oldAsset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Model(&model.ExchangeOrder{}).Where("id = ?", order.ID).Updates(map[string]any{
|
||||
"migration_completed": true,
|
||||
"migration_balance": migrationBalance,
|
||||
"updater": middleware.GetUserIDFromContext(ctx),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新换货单迁移状态失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errors.CodeExchangeMigrationFailed, err, "执行全量迁移失败")
|
||||
}
|
||||
|
||||
return migrationBalance, nil
|
||||
}
|
||||
|
||||
func (s *Service) transferWalletBalanceWithTx(ctx context.Context, tx *gorm.DB, order *model.ExchangeOrder, oldAsset, newAsset *resolvedExchangeAsset) (int64, error) {
|
||||
var oldWallet model.AssetWallet
|
||||
if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id = ?", oldAsset.AssetType, oldAsset.AssetID).First(&oldWallet).Error; err != nil {
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询旧资产钱包失败")
|
||||
}
|
||||
}
|
||||
|
||||
var newWallet model.AssetWallet
|
||||
if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id = ?", newAsset.AssetType, newAsset.AssetID).First(&newWallet).Error; err != nil {
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询新资产钱包失败")
|
||||
}
|
||||
|
||||
shopTag := uint(0)
|
||||
if newAsset.ShopID != nil {
|
||||
shopTag = *newAsset.ShopID
|
||||
}
|
||||
newWallet = model.AssetWallet{ResourceType: newAsset.AssetType, ResourceID: newAsset.AssetID, Balance: 0, FrozenBalance: 0, Currency: "CNY", Status: 1, Version: 0, ShopIDTag: shopTag}
|
||||
if err = tx.WithContext(ctx).Create(&newWallet).Error; err != nil {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "创建新资产钱包失败")
|
||||
}
|
||||
}
|
||||
|
||||
migrationBalance := oldWallet.Balance
|
||||
if migrationBalance <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
beforeBalance := newWallet.Balance
|
||||
if err := tx.WithContext(ctx).Model(&model.AssetWallet{}).Where("id = ?", oldWallet.ID).Updates(map[string]any{"balance": 0, "updated_at": time.Now()}).Error; err != nil {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "清空旧资产钱包余额失败")
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).Model(&model.AssetWallet{}).Where("id = ?", newWallet.ID).Updates(map[string]any{"balance": gorm.Expr("balance + ?", migrationBalance), "updated_at": time.Now()}).Error; err != nil {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "增加新资产钱包余额失败")
|
||||
}
|
||||
|
||||
refType := "exchange"
|
||||
if err := tx.WithContext(ctx).Create(&model.AssetWalletTransaction{
|
||||
AssetWalletID: newWallet.ID,
|
||||
ResourceType: newAsset.AssetType,
|
||||
ResourceID: newAsset.AssetID,
|
||||
UserID: middleware.GetUserIDFromContext(ctx),
|
||||
TransactionType: "refund",
|
||||
Amount: migrationBalance,
|
||||
BalanceBefore: beforeBalance,
|
||||
BalanceAfter: beforeBalance + migrationBalance,
|
||||
Status: 1,
|
||||
ReferenceType: &refType,
|
||||
ReferenceNo: &order.ExchangeNo,
|
||||
Creator: middleware.GetUserIDFromContext(ctx),
|
||||
ShopIDTag: newWallet.ShopIDTag,
|
||||
EnterpriseIDTag: newWallet.EnterpriseIDTag,
|
||||
}).Error; err != nil {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "写入迁移钱包流水失败")
|
||||
}
|
||||
|
||||
return migrationBalance, nil
|
||||
}
|
||||
|
||||
func (s *Service) migratePackageUsageWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||||
query := tx.WithContext(ctx).Model(&model.PackageUsage{}).Where("status IN ?", []int{constants.PackageUsageStatusPending, constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted})
|
||||
if oldAsset.AssetType == constants.ExchangeAssetTypeIotCard {
|
||||
query = query.Where("iot_card_id = ?", oldAsset.AssetID)
|
||||
} else {
|
||||
query = query.Where("device_id = ?", oldAsset.AssetID)
|
||||
}
|
||||
|
||||
var usageIDs []uint
|
||||
if err := query.Pluck("id", &usageIDs).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
}
|
||||
|
||||
if len(usageIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
updates := map[string]any{"updated_at": time.Now()}
|
||||
if oldAsset.AssetType == constants.ExchangeAssetTypeIotCard {
|
||||
updates["iot_card_id"] = newAsset.AssetID
|
||||
} else {
|
||||
updates["device_id"] = newAsset.AssetID
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).Model(&model.PackageUsage{}).Where("id IN ?", usageIDs).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "迁移套餐使用记录失败")
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).Model(&model.PackageUsageDailyRecord{}).Where("package_usage_id IN ?", usageIDs).Update("updated_at", gorm.Expr("updated_at")).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "迁移套餐日记录失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) copyAccumulatedFieldsWithTx(tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||||
if oldAsset.AssetType == constants.ExchangeAssetTypeIotCard {
|
||||
if oldAsset.Card == nil {
|
||||
return errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", newAsset.AssetID).Updates(map[string]any{
|
||||
"accumulated_recharge": oldAsset.Card.AccumulatedRecharge,
|
||||
"first_commission_paid": oldAsset.Card.FirstCommissionPaid,
|
||||
"accumulated_recharge_by_series": oldAsset.Card.AccumulatedRechargeBySeriesJSON,
|
||||
"first_recharge_triggered_by_series": oldAsset.Card.FirstRechargeTriggeredBySeriesJSON,
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "复制旧卡累计字段失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if oldAsset.Device == nil {
|
||||
return errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
if err := tx.Model(&model.Device{}).Where("id = ?", newAsset.AssetID).Updates(map[string]any{
|
||||
"accumulated_recharge": oldAsset.Device.AccumulatedRecharge,
|
||||
"first_commission_paid": oldAsset.Device.FirstCommissionPaid,
|
||||
"accumulated_recharge_by_series": oldAsset.Device.AccumulatedRechargeBySeriesJSON,
|
||||
"first_recharge_triggered_by_series": oldAsset.Device.FirstRechargeTriggeredBySeriesJSON,
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "复制旧设备累计字段失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) copyResourceTagsWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||||
var tags []*model.ResourceTag
|
||||
if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id = ?", oldAsset.AssetType, oldAsset.AssetID).Find(&tags).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询资源标签失败")
|
||||
}
|
||||
var creator = middleware.GetUserIDFromContext(ctx)
|
||||
for _, item := range tags {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
record := &model.ResourceTag{
|
||||
ResourceType: newAsset.AssetType,
|
||||
ResourceID: newAsset.AssetID,
|
||||
TagID: item.TagID,
|
||||
EnterpriseID: item.EnterpriseID,
|
||||
ShopID: item.ShopID,
|
||||
BaseModel: model.BaseModel{Creator: creator, Updater: creator},
|
||||
}
|
||||
if err := tx.WithContext(ctx).Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "resource_type"}, {Name: "resource_id"}, {Name: "tag_id"}}, DoNothing: true}).Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "复制资源标签失败")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) updateOldAssetStatusWithTx(tx *gorm.DB, oldAsset *resolvedExchangeAsset) error {
|
||||
if oldAsset.AssetType == constants.ExchangeAssetTypeIotCard {
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", oldAsset.AssetID).Updates(map[string]any{"asset_status": 3, "updated_at": time.Now()}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新旧卡状态失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := tx.Model(&model.Device{}).Where("id = ?", oldAsset.AssetID).Updates(map[string]any{"asset_status": 3, "updated_at": time.Now()}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新旧设备状态失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
487
internal/service/exchange/service.go
Normal file
487
internal/service/exchange/service.go
Normal file
@@ -0,0 +1,487 @@
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
)
|
||||
|
||||
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) {
|
||||
asset, err := s.resolveAssetByIdentifier(ctx, req.OldAssetType, req.OldIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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, "查询进行中换货单失败")
|
||||
}
|
||||
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
creator := middleware.GetUserIDFromContext(ctx)
|
||||
order := &model.ExchangeOrder{
|
||||
ExchangeNo: model.GenerateExchangeNo(),
|
||||
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 shopID > 0 {
|
||||
order.ShopID = &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.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)
|
||||
}
|
||||
|
||||
newAsset, err := s.resolveAssetByIdentifier(ctx, order.OldAssetType, req.NewIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if newAsset.AssetType != order.OldAssetType {
|
||||
return nil, errors.New(errors.CodeExchangeAssetTypeMismatch)
|
||||
}
|
||||
if newAsset.AssetStatus != 1 {
|
||||
return nil, errors.New(errors.CodeExchangeNewAssetNotInStock)
|
||||
}
|
||||
|
||||
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,
|
||||
"updater": middleware.GetUserIDFromContext(ctx),
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if err = s.exchangeStore.UpdateStatus(ctx, id, constants.ExchangeStatusPendingShip, constants.ExchangeStatusShipped, updates); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeExchangeStatusInvalid)
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, 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)
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
"updater": middleware.GetUserIDFromContext(ctx),
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if order.MigrateData {
|
||||
var migrationBalance int64
|
||||
migrationBalance, err = s.executeMigration(ctx, order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updates["migration_completed"] = true
|
||||
updates["migration_balance"] = migrationBalance
|
||||
}
|
||||
|
||||
if err = s.exchangeStore.UpdateStatus(ctx, id, constants.ExchangeStatusShipped, constants.ExchangeStatusCompleted, updates); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeExchangeStatusInvalid)
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "确认换货完成失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 != 3 {
|
||||
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": 1,
|
||||
"accumulated_recharge": 0,
|
||||
"first_commission_paid": false,
|
||||
"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, "重置旧卡转新状态失败")
|
||||
}
|
||||
|
||||
if err = tx.Where("virtual_no = ?", card.VirtualNo).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: 1, 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 != 3 {
|
||||
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": 1,
|
||||
"accumulated_recharge": 0,
|
||||
"first_commission_paid": false,
|
||||
"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, "重置旧设备转新状态失败")
|
||||
}
|
||||
|
||||
if err = tx.Where("virtual_no = ?", device.VirtualNo).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: 1, 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
|
||||
}
|
||||
|
||||
order, err := s.exchangeStore.FindActiveByOldAsset(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,
|
||||
Status: order.Status,
|
||||
StatusText: exchangeStatusText(order.Status),
|
||||
ExchangeReason: order.ExchangeReason,
|
||||
CreatedAt: order.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) SubmitShippingInfo(ctx context.Context, id uint, req *dto.ClientShippingInfoRequest) error {
|
||||
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 {
|
||||
var card model.IotCard
|
||||
query := s.db.WithContext(ctx).Where("virtual_no = ? OR iccid = ? OR msisdn = ?", 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) 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,
|
||||
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,
|
||||
ExchangeReason: order.ExchangeReason,
|
||||
Remark: order.Remark,
|
||||
Status: order.Status,
|
||||
StatusText: exchangeStatusText(order.Status),
|
||||
ShopID: order.ShopID,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
DeletedAt: deletedAt,
|
||||
Creator: order.Creator,
|
||||
Updater: order.Updater,
|
||||
}
|
||||
}
|
||||
|
||||
func exchangeStatusText(status int) string {
|
||||
switch status {
|
||||
case constants.ExchangeStatusPendingInfo:
|
||||
return "待填写信息"
|
||||
case constants.ExchangeStatusPendingShip:
|
||||
return "待发货"
|
||||
case constants.ExchangeStatusShipped:
|
||||
return "已发货待确认"
|
||||
case constants.ExchangeStatusCompleted:
|
||||
return "已完成"
|
||||
case constants.ExchangeStatusCancelled:
|
||||
return "已取消"
|
||||
default:
|
||||
return "未知状态"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user