All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
主要变更: - 新增 tb_shop_series_allocation 表,存储系列级别的一次性佣金配置 - ShopPackageAllocation 移除 one_time_commission_amount 字段 - PackageSeries 新增 enable_one_time_commission 字段控制是否启用一次性佣金 - 新增 /api/admin/shop-series-allocations CRUD 接口 - 佣金计算逻辑改为从 ShopSeriesAllocation 获取一次性佣金金额 - 删除废弃的 ShopSeriesOneTimeCommissionTier 模型 - OpenAPI Tag '系列分配' 和 '单套餐分配' 合并为 '套餐分配' 迁移脚本: - 000042: 重构佣金套餐模型 - 000043: 简化佣金分配 - 000044: 一次性佣金分配重构 - 000045: PackageSeries 添加 enable_one_time_commission 字段 测试: - 新增验收测试 (shop_series_allocation, commission_calculation) - 新增流程测试 (one_time_commission_chain) - 删除过时的单元测试(已被验收测试覆盖)
702 lines
19 KiB
Go
702 lines
19 KiB
Go
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
|
||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||
packageSeriesStore *postgres.PackageSeriesStore
|
||
}
|
||
|
||
func New(
|
||
db *gorm.DB,
|
||
deviceStore *postgres.DeviceStore,
|
||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||
iotCardStore *postgres.IotCardStore,
|
||
shopStore *postgres.ShopStore,
|
||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore,
|
||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||
packageSeriesStore *postgres.PackageSeriesStore,
|
||
) *Service {
|
||
return &Service{
|
||
db: db,
|
||
deviceStore: deviceStore,
|
||
deviceSimBindingStore: deviceSimBindingStore,
|
||
iotCardStore: iotCardStore,
|
||
shopStore: shopStore,
|
||
assetAllocationRecordStore: assetAllocationRecordStore,
|
||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||
packageSeriesStore: packageSeriesStore,
|
||
}
|
||
}
|
||
|
||
// 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 {
|
||
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, *operatorShopID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
hasSeriesAllocation := false
|
||
for _, alloc := range seriesAllocations {
|
||
if alloc.SeriesID == req.SeriesID && alloc.Status == 1 {
|
||
hasSeriesAllocation = true
|
||
break
|
||
}
|
||
}
|
||
if !hasSeriesAllocation {
|
||
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
||
DeviceID: deviceID,
|
||
DeviceNo: device.DeviceNo,
|
||
Reason: "您没有权限分配该套餐系列",
|
||
})
|
||
continue
|
||
}
|
||
}
|
||
|
||
// 验证设备权限(基于 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
|
||
}
|