package device import ( "context" "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" "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" "gorm.io/gorm" ) type Service struct { db *gorm.DB deviceStore *postgres.DeviceStore deviceSimBindingStore *postgres.DeviceSimBindingStore iotCardStore *postgres.IotCardStore shopStore *postgres.ShopStore assetAllocationRecordStore *postgres.AssetAllocationRecordStore seriesAllocationStore *postgres.ShopSeriesAllocationStore packageSeriesStore *postgres.PackageSeriesStore shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore } func New( db *gorm.DB, deviceStore *postgres.DeviceStore, deviceSimBindingStore *postgres.DeviceSimBindingStore, iotCardStore *postgres.IotCardStore, shopStore *postgres.ShopStore, assetAllocationRecordStore *postgres.AssetAllocationRecordStore, seriesAllocationStore *postgres.ShopSeriesAllocationStore, packageSeriesStore *postgres.PackageSeriesStore, ) *Service { return &Service{ db: db, deviceStore: deviceStore, deviceSimBindingStore: deviceSimBindingStore, iotCardStore: iotCardStore, shopStore: shopStore, assetAllocationRecordStore: assetAllocationRecordStore, seriesAllocationStore: seriesAllocationStore, packageSeriesStore: packageSeriesStore, shopSeriesAllocationStore: seriesAllocationStore, } } // List 获取设备列表 func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.ListDeviceResponse, error) { page := req.Page pageSize := req.PageSize if page == 0 { page = 1 } if pageSize == 0 { pageSize = constants.DefaultPageSize } opts := &store.QueryOptions{ Page: page, PageSize: pageSize, } filters := make(map[string]interface{}) if req.DeviceNo != "" { filters["device_no"] = req.DeviceNo } if req.DeviceName != "" { filters["device_name"] = req.DeviceName } if req.Status != nil { filters["status"] = *req.Status } if req.ShopID != nil { filters["shop_id"] = req.ShopID } if req.BatchNo != "" { filters["batch_no"] = req.BatchNo } if req.DeviceType != "" { filters["device_type"] = req.DeviceType } if req.Manufacturer != "" { filters["manufacturer"] = req.Manufacturer } if req.CreatedAtStart != nil { filters["created_at_start"] = *req.CreatedAtStart } if req.CreatedAtEnd != nil { filters["created_at_end"] = *req.CreatedAtEnd } if req.SeriesID != nil { filters["series_id"] = *req.SeriesID } devices, total, err := s.deviceStore.List(ctx, opts, filters) if err != nil { return nil, err } shopMap := s.loadShopData(ctx, devices) bindingCounts, err := s.getBindingCounts(ctx, s.extractDeviceIDs(devices)) if err != nil { return nil, err } list := make([]*dto.DeviceResponse, 0, len(devices)) for _, device := range devices { item := s.toDeviceResponse(device, shopMap, bindingCounts) list = append(list, item) } totalPages := int(total) / pageSize if int(total)%pageSize > 0 { totalPages++ } return &dto.ListDeviceResponse{ List: list, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, }, nil } // Get 获取设备详情 func (s *Service) Get(ctx context.Context, id uint) (*dto.DeviceResponse, error) { device, err := s.deviceStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "设备不存在") } return nil, err } shopMap := s.loadShopData(ctx, []*model.Device{device}) bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID}) if err != nil { return nil, err } return s.toDeviceResponse(device, shopMap, bindingCounts), nil } // GetByDeviceNo 通过设备号获取设备详情 func (s *Service) GetByDeviceNo(ctx context.Context, deviceNo string) (*dto.DeviceResponse, error) { device, err := s.deviceStore.GetByDeviceNo(ctx, deviceNo) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "设备不存在") } return nil, err } shopMap := s.loadShopData(ctx, []*model.Device{device}) bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID}) if err != nil { return nil, err } return s.toDeviceResponse(device, shopMap, bindingCounts), nil } func (s *Service) Delete(ctx context.Context, id uint) error { device, err := s.deviceStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "设备不存在") } return err } if err := s.deviceSimBindingStore.UnbindByDeviceID(ctx, device.ID); err != nil { return err } return s.deviceStore.Delete(ctx, id) } // AllocateDevices 批量分配设备 func (s *Service) AllocateDevices(ctx context.Context, req *dto.AllocateDevicesRequest, operatorID uint, operatorShopID *uint) (*dto.AllocateDevicesResponse, error) { // 验证目标店铺是否为直属下级 if err := s.validateDirectSubordinate(ctx, operatorShopID, req.TargetShopID); err != nil { return nil, err } devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs) if err != nil { return nil, err } if len(devices) == 0 { return &dto.AllocateDevicesResponse{ SuccessCount: 0, FailCount: 0, FailedItems: []dto.AllocationDeviceFailedItem{}, }, nil } var deviceIDs []uint var failedItems []dto.AllocationDeviceFailedItem isPlatform := operatorShopID == nil for _, device := range devices { // 平台只能分配 shop_id=NULL 的设备 if isPlatform && device.ShopID != nil { failedItems = append(failedItems, dto.AllocationDeviceFailedItem{ DeviceID: device.ID, DeviceNo: device.DeviceNo, Reason: "平台只能分配库存设备", }) continue } // 代理只能分配自己店铺的设备 if !isPlatform && (device.ShopID == nil || *device.ShopID != *operatorShopID) { failedItems = append(failedItems, dto.AllocationDeviceFailedItem{ DeviceID: device.ID, DeviceNo: device.DeviceNo, Reason: "设备不属于当前店铺", }) continue } deviceIDs = append(deviceIDs, device.ID) } if len(deviceIDs) == 0 { return &dto.AllocateDevicesResponse{ SuccessCount: 0, FailCount: len(failedItems), FailedItems: failedItems, }, nil } newStatus := constants.DeviceStatusDistributed targetShopID := req.TargetShopID err = s.db.Transaction(func(tx *gorm.DB) error { txDeviceStore := postgres.NewDeviceStore(tx, nil) txCardStore := postgres.NewIotCardStore(tx, nil) txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil) if err := txDeviceStore.BatchUpdateShopIDAndStatus(ctx, deviceIDs, &targetShopID, newStatus); err != nil { return err } boundCardIDs, err := s.deviceSimBindingStore.GetBoundCardIDsByDeviceIDs(ctx, deviceIDs) if err != nil { return err } if len(boundCardIDs) > 0 { if err := txCardStore.BatchUpdateShopIDAndStatus(ctx, boundCardIDs, &targetShopID, constants.IotCardStatusDistributed); err != nil { return err } } allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate) records := s.buildAllocationRecords(devices, deviceIDs, operatorShopID, targetShopID, operatorID, allocationNo, req.Remark) return txRecordStore.BatchCreate(ctx, records) }) if err != nil { return nil, err } return &dto.AllocateDevicesResponse{ SuccessCount: len(deviceIDs), FailCount: len(failedItems), FailedItems: failedItems, }, nil } // RecallDevices 批量回收设备 func (s *Service) RecallDevices(ctx context.Context, req *dto.RecallDevicesRequest, operatorID uint, operatorShopID *uint) (*dto.RecallDevicesResponse, error) { devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs) if err != nil { return nil, err } if len(devices) == 0 { return &dto.RecallDevicesResponse{ SuccessCount: 0, FailCount: 0, FailedItems: []dto.AllocationDeviceFailedItem{}, }, nil } var deviceIDs []uint var failedItems []dto.AllocationDeviceFailedItem isPlatform := operatorShopID == nil for _, device := range devices { // 验证设备所属店铺是否为直属下级 if device.ShopID == nil { failedItems = append(failedItems, dto.AllocationDeviceFailedItem{ DeviceID: device.ID, DeviceNo: device.DeviceNo, Reason: "设备已在平台库存中", }) continue } // 验证直属下级关系 if err := s.validateDirectSubordinate(ctx, operatorShopID, *device.ShopID); err != nil { failedItems = append(failedItems, dto.AllocationDeviceFailedItem{ DeviceID: device.ID, DeviceNo: device.DeviceNo, Reason: "只能回收直属下级店铺的设备", }) continue } deviceIDs = append(deviceIDs, device.ID) } if len(deviceIDs) == 0 { return &dto.RecallDevicesResponse{ SuccessCount: 0, FailCount: len(failedItems), FailedItems: failedItems, }, nil } var newShopID *uint var newStatus int if isPlatform { newShopID = nil newStatus = constants.DeviceStatusInStock } else { newShopID = operatorShopID newStatus = constants.DeviceStatusDistributed } err = s.db.Transaction(func(tx *gorm.DB) error { txDeviceStore := postgres.NewDeviceStore(tx, nil) txCardStore := postgres.NewIotCardStore(tx, nil) txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil) if err := txDeviceStore.BatchUpdateShopIDAndStatus(ctx, deviceIDs, newShopID, newStatus); err != nil { return err } boundCardIDs, err := s.deviceSimBindingStore.GetBoundCardIDsByDeviceIDs(ctx, deviceIDs) if err != nil { return err } if len(boundCardIDs) > 0 { var cardStatus int if isPlatform { cardStatus = constants.IotCardStatusInStock } else { cardStatus = constants.IotCardStatusDistributed } if err := txCardStore.BatchUpdateShopIDAndStatus(ctx, boundCardIDs, newShopID, cardStatus); err != nil { return err } } allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall) records := s.buildRecallRecords(devices, deviceIDs, operatorShopID, newShopID, operatorID, allocationNo, req.Remark) return txRecordStore.BatchCreate(ctx, records) }) if err != nil { return nil, err } return &dto.RecallDevicesResponse{ SuccessCount: len(deviceIDs), FailCount: len(failedItems), FailedItems: failedItems, }, nil } // 辅助方法 func (s *Service) validateDirectSubordinate(ctx context.Context, operatorShopID *uint, targetShopID uint) error { if operatorShopID != nil && *operatorShopID == targetShopID { return errors.ErrCannotAllocateToSelf } targetShop, err := s.shopStore.GetByID(ctx, targetShopID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeShopNotFound) } return err } if operatorShopID == nil { if targetShop.ParentID != nil { return errors.ErrNotDirectSubordinate } } else { if targetShop.ParentID == nil || *targetShop.ParentID != *operatorShopID { return errors.ErrNotDirectSubordinate } } return nil } func (s *Service) loadShopData(ctx context.Context, devices []*model.Device) map[uint]string { shopIDs := make([]uint, 0) shopIDSet := make(map[uint]bool) for _, device := range devices { if device.ShopID != nil && *device.ShopID > 0 && !shopIDSet[*device.ShopID] { shopIDs = append(shopIDs, *device.ShopID) shopIDSet[*device.ShopID] = true } } shopMap := make(map[uint]string) if len(shopIDs) > 0 { var shops []model.Shop s.db.WithContext(ctx).Where("id IN ?", shopIDs).Find(&shops) for _, shop := range shops { shopMap[shop.ID] = shop.ShopName } } return shopMap } func (s *Service) getBindingCounts(ctx context.Context, deviceIDs []uint) (map[uint]int64, error) { result := make(map[uint]int64) if len(deviceIDs) == 0 { return result, nil } bindings, err := s.deviceSimBindingStore.ListByDeviceIDs(ctx, deviceIDs) if err != nil { return nil, err } for _, binding := range bindings { result[binding.DeviceID]++ } return result, nil } func (s *Service) extractDeviceIDs(devices []*model.Device) []uint { ids := make([]uint, len(devices)) for i, device := range devices { ids[i] = device.ID } return ids } func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string, bindingCounts map[uint]int64) *dto.DeviceResponse { resp := &dto.DeviceResponse{ ID: device.ID, DeviceNo: device.DeviceNo, DeviceName: device.DeviceName, DeviceModel: device.DeviceModel, DeviceType: device.DeviceType, MaxSimSlots: device.MaxSimSlots, Manufacturer: device.Manufacturer, BatchNo: device.BatchNo, ShopID: device.ShopID, Status: device.Status, StatusName: s.getDeviceStatusName(device.Status), BoundCardCount: int(bindingCounts[device.ID]), SeriesID: device.SeriesID, FirstCommissionPaid: device.FirstCommissionPaid, AccumulatedRecharge: device.AccumulatedRecharge, ActivatedAt: device.ActivatedAt, CreatedAt: device.CreatedAt, UpdatedAt: device.UpdatedAt, } if device.ShopID != nil && *device.ShopID > 0 { resp.ShopName = shopMap[*device.ShopID] } return resp } func (s *Service) getDeviceStatusName(status int) string { switch status { case constants.DeviceStatusInStock: return "在库" case constants.DeviceStatusDistributed: return "已分销" case constants.DeviceStatusActivated: return "已激活" case constants.DeviceStatusSuspended: return "已停用" default: return "未知" } } func (s *Service) buildAllocationRecords(devices []*model.Device, successDeviceIDs []uint, fromShopID *uint, toShopID uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord { successIDSet := make(map[uint]bool) for _, id := range successDeviceIDs { successIDSet[id] = true } var records []*model.AssetAllocationRecord for _, device := range devices { if !successIDSet[device.ID] { continue } record := &model.AssetAllocationRecord{ AllocationNo: allocationNo, AllocationType: constants.AssetAllocationTypeAllocate, AssetType: constants.AssetTypeDevice, AssetID: device.ID, AssetIdentifier: device.DeviceNo, ToOwnerType: constants.OwnerTypeShop, ToOwnerID: toShopID, OperatorID: operatorID, Remark: remark, } if fromShopID == nil { record.FromOwnerType = constants.OwnerTypePlatform record.FromOwnerID = nil } else { record.FromOwnerType = constants.OwnerTypeShop record.FromOwnerID = fromShopID } records = append(records, record) } return records } func (s *Service) buildRecallRecords(devices []*model.Device, successDeviceIDs []uint, fromShopID *uint, toShopID *uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord { successIDSet := make(map[uint]bool) for _, id := range successDeviceIDs { successIDSet[id] = true } var records []*model.AssetAllocationRecord for _, device := range devices { if !successIDSet[device.ID] { continue } record := &model.AssetAllocationRecord{ AllocationNo: allocationNo, AllocationType: constants.AssetAllocationTypeRecall, AssetType: constants.AssetTypeDevice, AssetID: device.ID, AssetIdentifier: device.DeviceNo, OperatorID: operatorID, Remark: remark, } if fromShopID == nil { record.FromOwnerType = constants.OwnerTypePlatform record.FromOwnerID = nil } else { record.FromOwnerType = constants.OwnerTypeShop record.FromOwnerID = fromShopID } if toShopID == nil { record.ToOwnerType = constants.OwnerTypePlatform record.ToOwnerID = 0 } else { record.ToOwnerType = constants.OwnerTypeShop record.ToOwnerID = *toShopID } records = append(records, record) } return records } // BatchSetSeriesBinding 批量设置设备的套餐系列绑定 func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDeviceSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetDeviceSeriesBindngResponse, error) { devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs) if err != nil { return nil, err } if len(devices) == 0 { return &dto.BatchSetDeviceSeriesBindngResponse{ SuccessCount: 0, FailCount: len(req.DeviceIDs), FailedItems: s.buildDeviceNotFoundFailedItems(req.DeviceIDs), }, nil } deviceMap := make(map[uint]*model.Device) for _, device := range devices { deviceMap[device.ID] = device } // 验证系列存在(仅当 SeriesID > 0 时) var packageSeries *model.PackageSeries if req.SeriesID > 0 { packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用") } return nil, err } if packageSeries.Status != 1 { return nil, errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用") } } var successDeviceIDs []uint var failedItems []dto.DeviceSeriesBindngFailedItem for _, deviceID := range req.DeviceIDs { device, exists := deviceMap[deviceID] if !exists { failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{ DeviceID: deviceID, DeviceNo: "", Reason: "设备不存在", }) continue } // 验证操作者权限(仅代理用户) if operatorShopID != nil && req.SeriesID > 0 { allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *operatorShopID, req.SeriesID) if err != nil { if err == gorm.ErrRecordNotFound || allocation.Status != 1 { failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{ DeviceID: deviceID, DeviceNo: device.DeviceNo, Reason: "您没有权限分配该套餐系列", }) continue } return nil, err } } // 验证设备权限(基于 device.ShopID) if operatorShopID != nil { if device.ShopID == nil || *device.ShopID != *operatorShopID { failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{ DeviceID: device.ID, DeviceNo: device.DeviceNo, Reason: "无权操作此设备", }) continue } } successDeviceIDs = append(successDeviceIDs, device.ID) } if len(successDeviceIDs) > 0 { var seriesIDPtr *uint if req.SeriesID > 0 { seriesIDPtr = &req.SeriesID } if err := s.deviceStore.BatchUpdateSeriesID(ctx, successDeviceIDs, seriesIDPtr); err != nil { return nil, err } } return &dto.BatchSetDeviceSeriesBindngResponse{ SuccessCount: len(successDeviceIDs), FailCount: len(failedItems), FailedItems: failedItems, }, nil } func (s *Service) buildDeviceNotFoundFailedItems(deviceIDs []uint) []dto.DeviceSeriesBindngFailedItem { items := make([]dto.DeviceSeriesBindngFailedItem, len(deviceIDs)) for i, id := range deviceIDs { items[i] = dto.DeviceSeriesBindngFailedItem{ DeviceID: id, DeviceNo: "", Reason: "设备不存在", } } return items }