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 "未知状态" } }