This commit is contained in:
@@ -145,8 +145,23 @@ type RecallDevicesResponse struct {
|
|||||||
|
|
||||||
// BatchSetDeviceSeriesBindngRequest 批量设置设备的套餐系列绑定请求
|
// BatchSetDeviceSeriesBindngRequest 批量设置设备的套餐系列绑定请求
|
||||||
type BatchSetDeviceSeriesBindngRequest struct {
|
type BatchSetDeviceSeriesBindngRequest struct {
|
||||||
DeviceIDs []uint `json:"device_ids" validate:"required,min=1,max=500,dive,required" required:"true" minItems:"1" maxItems:"500" description:"设备ID列表"`
|
SelectionType string `json:"selection_type" validate:"omitempty,oneof=list range filter" enum:"list,range,filter" description:"选设备方式 (list:设备ID列表, range:设备虚拟号范围, filter:筛选条件;不传时兼容旧版设备ID列表)"`
|
||||||
SeriesID uint `json:"series_id" validate:"required,min=0" required:"true" minimum:"0" description:"套餐系列ID(0表示清除关联)"`
|
DeviceIDs []uint `json:"device_ids" validate:"omitempty,max=1000,dive,required" maxItems:"1000" description:"设备ID列表(selection_type=list时使用,最多1000个)"`
|
||||||
|
VirtualNoStart string `json:"virtual_no_start" validate:"omitempty,max=100" maxLength:"100" description:"起始设备虚拟号(selection_type=range时使用)"`
|
||||||
|
VirtualNoEnd string `json:"virtual_no_end" validate:"omitempty,max=100" maxLength:"100" description:"结束设备虚拟号(selection_type=range时使用)"`
|
||||||
|
VirtualNo string `json:"virtual_no" validate:"omitempty,max=100" maxLength:"100" description:"设备虚拟号模糊查询(selection_type=filter时可选)"`
|
||||||
|
DeviceName string `json:"device_name" validate:"omitempty,max=255" maxLength:"255" description:"设备名称模糊查询(selection_type=filter时可选)"`
|
||||||
|
Status *int `json:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)(selection_type=filter时可选)"`
|
||||||
|
ShopID *uint `json:"shop_id" description:"店铺ID(selection_type=filter时可选,兼容旧参数,单选)"`
|
||||||
|
ShopIDs []uint `json:"shop_ids" validate:"omitempty,dive,min=1" description:"店铺ID列表(selection_type=filter时可选,多选)"`
|
||||||
|
FilterSeriesID *uint `json:"filter_series_id" description:"当前绑定套餐系列ID(selection_type=filter时可选;目标套餐系列仍使用series_id)"`
|
||||||
|
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号(selection_type=filter时可选)"`
|
||||||
|
DeviceType string `json:"device_type" validate:"omitempty,max=50" maxLength:"50" description:"设备类型(selection_type=filter时可选)"`
|
||||||
|
Manufacturer string `json:"manufacturer" validate:"omitempty,max=255" maxLength:"255" description:"制造商模糊查询(selection_type=filter时可选)"`
|
||||||
|
CreatedAtStart *time.Time `json:"created_at_start" description:"创建时间起始(selection_type=filter时可选)"`
|
||||||
|
CreatedAtEnd *time.Time `json:"created_at_end" description:"创建时间结束(selection_type=filter时可选)"`
|
||||||
|
|
||||||
|
SeriesID uint `json:"series_id" validate:"required,min=0" required:"true" minimum:"0" description:"目标套餐系列ID(0表示清除关联)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceSeriesBindngFailedItem 设备系列绑定失败项
|
// DeviceSeriesBindngFailedItem 设备系列绑定失败项
|
||||||
|
|||||||
@@ -148,8 +148,26 @@ type IotCardDetailResponse struct {
|
|||||||
|
|
||||||
// BatchSetCardSeriesBindngRequest 批量设置卡的套餐系列绑定请求
|
// BatchSetCardSeriesBindngRequest 批量设置卡的套餐系列绑定请求
|
||||||
type BatchSetCardSeriesBindngRequest struct {
|
type BatchSetCardSeriesBindngRequest struct {
|
||||||
ICCIDs []string `json:"iccids" validate:"required,min=1,max=500,dive,required" required:"true" minItems:"1" maxItems:"500" description:"ICCID列表"`
|
SelectionType string `json:"selection_type" validate:"omitempty,oneof=list range filter" enum:"list,range,filter" description:"选卡方式 (list:ICCID列表, range:号段范围, filter:筛选条件;不传时兼容旧版ICCID列表)"`
|
||||||
SeriesID uint `json:"series_id" validate:"required,min=0" required:"true" minimum:"0" description:"套餐系列ID(0表示清除关联)"`
|
ICCIDs []string `json:"iccids" validate:"omitempty,max=1000,dive,required,max=20" maxItems:"1000" description:"ICCID列表(selection_type=list时使用,最多1000个)"`
|
||||||
|
ICCIDStart string `json:"iccid_start" validate:"omitempty,max=20" maxLength:"20" description:"起始ICCID(selection_type=range时使用)"`
|
||||||
|
ICCIDEnd string `json:"iccid_end" validate:"omitempty,max=20" maxLength:"20" description:"结束ICCID(selection_type=range时使用)"`
|
||||||
|
|
||||||
|
Status *int `json:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)(selection_type=filter时可选)"`
|
||||||
|
CarrierID *uint `json:"carrier_id" description:"运营商ID(selection_type=filter时可选)"`
|
||||||
|
ShopID *uint `json:"shop_id" description:"分销商ID(selection_type=filter时可选,兼容旧参数,单选)"`
|
||||||
|
ShopIDs []uint `json:"shop_ids" validate:"omitempty,dive,min=1" description:"分销商ID列表(selection_type=filter时可选,多选)"`
|
||||||
|
FilterSeriesID *uint `json:"filter_series_id" description:"当前绑定套餐系列ID(selection_type=filter时可选;目标套餐系列仍使用series_id)"`
|
||||||
|
ICCID string `json:"iccid" validate:"omitempty,max=20" maxLength:"20" description:"ICCID模糊查询(selection_type=filter时可选)"`
|
||||||
|
MSISDN string `json:"msisdn" validate:"omitempty,max=20" maxLength:"20" description:"卡接入号模糊查询(selection_type=filter时可选)"`
|
||||||
|
IsStandalone *bool `json:"is_standalone" description:"是否为独立卡(selection_type=filter时可选)"`
|
||||||
|
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号(selection_type=filter时可选)"`
|
||||||
|
PackageID *uint `json:"package_id" description:"套餐ID(selection_type=filter时可选)"`
|
||||||
|
IsDistributed *bool `json:"is_distributed" description:"是否已分销(selection_type=filter时可选)"`
|
||||||
|
IsReplaced *bool `json:"is_replaced" description:"是否有换卡记录(selection_type=filter时可选)"`
|
||||||
|
CarrierName string `json:"carrier_name" validate:"omitempty,max=100" maxLength:"100" description:"运营商名称模糊查询(selection_type=filter时可选)"`
|
||||||
|
|
||||||
|
SeriesID uint `json:"series_id" validate:"required,min=0" required:"true" minimum:"0" description:"目标套餐系列ID(0表示清除关联)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CardSeriesBindngFailedItem 卡系列绑定失败项
|
// CardSeriesBindngFailedItem 卡系列绑定失败项
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp
|
|||||||
|
|
||||||
Register(devices, doc, groupPath, "PATCH", "/series-binding", handler.BatchSetSeriesBinding, RouteSpec{
|
Register(devices, doc, groupPath, "PATCH", "/series-binding", handler.BatchSetSeriesBinding, RouteSpec{
|
||||||
Summary: "批量设置设备的套餐系列绑定",
|
Summary: "批量设置设备的套餐系列绑定",
|
||||||
Description: "批量设置或清除设备与套餐系列分配的关联关系。参数:series_id(套餐系列ID,0表示清除关联)。",
|
Description: "批量设置或清除设备与套餐系列分配的关联关系。series_id 为目标套餐系列ID,0表示清除关联。支持 selection_type=list/range/filter:list 传 device_ids(兼容旧请求,不传 selection_type 时默认按 device_ids 处理),range 传 virtual_no_start/virtual_no_end,filter 按筛选条件选择完整结果,不受列表分页限制。",
|
||||||
Tags: []string{"设备管理"},
|
Tags: []string{"设备管理"},
|
||||||
Input: new(dto.BatchSetDeviceSeriesBindngRequest),
|
Input: new(dto.BatchSetDeviceSeriesBindngRequest),
|
||||||
Output: new(dto.BatchSetDeviceSeriesBindngResponse),
|
Output: new(dto.BatchSetDeviceSeriesBindngResponse),
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func registerIotCardRoutes(router fiber.Router, handler *admin.IotCardHandler, i
|
|||||||
|
|
||||||
Register(iotCards, doc, groupPath, "PATCH", "/series-binding", handler.BatchSetSeriesBinding, RouteSpec{
|
Register(iotCards, doc, groupPath, "PATCH", "/series-binding", handler.BatchSetSeriesBinding, RouteSpec{
|
||||||
Summary: "批量设置卡的套餐系列绑定",
|
Summary: "批量设置卡的套餐系列绑定",
|
||||||
Description: "批量设置或清除卡与套餐系列分配的关联关系。参数:series_id(套餐系列ID,0表示清除关联)。",
|
Description: "批量设置或清除卡与套餐系列分配的关联关系。series_id 为目标套餐系列ID,0表示清除关联。支持 selection_type=list/range/filter:list 传 iccids(兼容旧请求,不传 selection_type 时默认按 iccids 处理),range 传 iccid_start/iccid_end,filter 按筛选条件选择完整结果,不受列表分页限制。",
|
||||||
Tags: []string{"IoT卡管理"},
|
Tags: []string{"IoT卡管理"},
|
||||||
Input: new(dto.BatchSetCardSeriesBindngRequest),
|
Input: new(dto.BatchSetCardSeriesBindngRequest),
|
||||||
Output: new(dto.BatchSetCardSeriesBindngResponse),
|
Output: new(dto.BatchSetCardSeriesBindngResponse),
|
||||||
|
|||||||
@@ -1021,7 +1021,14 @@ func (s *Service) buildRecallRecords(devices []*model.Device, successDeviceIDs [
|
|||||||
|
|
||||||
// BatchSetSeriesBinding 批量设置设备的套餐系列绑定
|
// BatchSetSeriesBinding 批量设置设备的套餐系列绑定
|
||||||
func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDeviceSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetDeviceSeriesBindngResponse, error) {
|
func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDeviceSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetDeviceSeriesBindngResponse, error) {
|
||||||
devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
|
selectionType, err := normalizeDeviceSeriesBindingSelection(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := s.getDevicesForSeriesBinding(ctx, req, selectionType)
|
||||||
|
batchTotal := deviceSeriesBindingBatchTotal(req, selectionType, devices)
|
||||||
|
auditData := deviceSeriesBindingAuditData(req, selectionType, operatorShopID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logDeviceOperation(
|
s.logDeviceOperation(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -1030,41 +1037,37 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
|
|||||||
constants.AssetAuditResultFailed,
|
constants.AssetAuditResultFailed,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
map[string]any{
|
auditData,
|
||||||
"series_id": req.SeriesID,
|
batchTotal,
|
||||||
"device_ids": req.DeviceIDs,
|
|
||||||
"operator_shop_id": operatorShopID,
|
|
||||||
},
|
|
||||||
len(req.DeviceIDs),
|
|
||||||
0,
|
0,
|
||||||
len(req.DeviceIDs),
|
batchTotal,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(devices) == 0 {
|
if len(devices) == 0 {
|
||||||
s.logDeviceOperation(
|
failedItems := []dto.DeviceSeriesBindngFailedItem{}
|
||||||
ctx,
|
if selectionType == dto.SelectionTypeList {
|
||||||
constants.AssetAuditOpDeviceSeriesBinding,
|
failedItems = s.buildDeviceNotFoundFailedItems(req.DeviceIDs)
|
||||||
"设备系列绑定被拒绝",
|
s.logDeviceOperation(
|
||||||
constants.AssetAuditResultDenied,
|
ctx,
|
||||||
nil,
|
constants.AssetAuditOpDeviceSeriesBinding,
|
||||||
nil,
|
"设备系列绑定被拒绝",
|
||||||
map[string]any{
|
constants.AssetAuditResultDenied,
|
||||||
"series_id": req.SeriesID,
|
nil,
|
||||||
"device_ids": req.DeviceIDs,
|
nil,
|
||||||
"reason": "设备不存在",
|
auditData,
|
||||||
},
|
batchTotal,
|
||||||
len(req.DeviceIDs),
|
0,
|
||||||
0,
|
len(failedItems),
|
||||||
len(req.DeviceIDs),
|
errors.New(errors.CodeNotFound, "设备不存在"),
|
||||||
errors.New(errors.CodeNotFound, "设备不存在"),
|
)
|
||||||
)
|
}
|
||||||
return &dto.BatchSetDeviceSeriesBindngResponse{
|
return &dto.BatchSetDeviceSeriesBindngResponse{
|
||||||
SuccessCount: 0,
|
SuccessCount: 0,
|
||||||
FailCount: len(req.DeviceIDs),
|
FailCount: len(failedItems),
|
||||||
FailedItems: s.buildDeviceNotFoundFailedItems(req.DeviceIDs),
|
FailedItems: failedItems,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1087,13 +1090,10 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
|
|||||||
constants.AssetAuditResultDenied,
|
constants.AssetAuditResultDenied,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
map[string]any{
|
auditData,
|
||||||
"series_id": req.SeriesID,
|
batchTotal,
|
||||||
"device_ids": req.DeviceIDs,
|
|
||||||
},
|
|
||||||
len(req.DeviceIDs),
|
|
||||||
0,
|
0,
|
||||||
len(req.DeviceIDs),
|
batchTotal,
|
||||||
appErr,
|
appErr,
|
||||||
)
|
)
|
||||||
return nil, appErr
|
return nil, appErr
|
||||||
@@ -1105,13 +1105,10 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
|
|||||||
constants.AssetAuditResultFailed,
|
constants.AssetAuditResultFailed,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
map[string]any{
|
auditData,
|
||||||
"series_id": req.SeriesID,
|
batchTotal,
|
||||||
"device_ids": req.DeviceIDs,
|
|
||||||
},
|
|
||||||
len(req.DeviceIDs),
|
|
||||||
0,
|
0,
|
||||||
len(req.DeviceIDs),
|
batchTotal,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1125,13 +1122,10 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
|
|||||||
constants.AssetAuditResultDenied,
|
constants.AssetAuditResultDenied,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
map[string]any{
|
auditData,
|
||||||
"series_id": req.SeriesID,
|
batchTotal,
|
||||||
"device_ids": req.DeviceIDs,
|
|
||||||
},
|
|
||||||
len(req.DeviceIDs),
|
|
||||||
0,
|
0,
|
||||||
len(req.DeviceIDs),
|
batchTotal,
|
||||||
appErr,
|
appErr,
|
||||||
)
|
)
|
||||||
return nil, appErr
|
return nil, appErr
|
||||||
@@ -1140,72 +1134,75 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
|
|||||||
|
|
||||||
var successDeviceIDs []uint
|
var successDeviceIDs []uint
|
||||||
var failedItems []dto.DeviceSeriesBindngFailedItem
|
var failedItems []dto.DeviceSeriesBindngFailedItem
|
||||||
|
successDeviceIDSet := make(map[uint]struct{})
|
||||||
|
|
||||||
for _, deviceID := range req.DeviceIDs {
|
hasSeriesAllocation := true
|
||||||
device, exists := deviceMap[deviceID]
|
if operatorShopID != nil && req.SeriesID > 0 {
|
||||||
if !exists {
|
hasSeriesAllocation, err = s.hasAvailableSeriesAllocation(ctx, *operatorShopID, req.SeriesID)
|
||||||
|
if err != nil {
|
||||||
|
s.logDeviceOperation(
|
||||||
|
ctx,
|
||||||
|
constants.AssetAuditOpDeviceSeriesBinding,
|
||||||
|
"设备系列绑定失败",
|
||||||
|
constants.AssetAuditResultFailed,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
auditData,
|
||||||
|
batchTotal,
|
||||||
|
0,
|
||||||
|
batchTotal,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addDevice := func(device *model.Device, requestedDeviceID uint) {
|
||||||
|
if device == nil {
|
||||||
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
||||||
DeviceID: deviceID,
|
DeviceID: requestedDeviceID,
|
||||||
VirtualNo: "",
|
VirtualNo: "",
|
||||||
Reason: "设备不存在",
|
Reason: "设备不存在",
|
||||||
})
|
})
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证操作者权限(仅代理用户)- 检查是否有该系列的套餐分配
|
if !hasSeriesAllocation {
|
||||||
if operatorShopID != nil && req.SeriesID > 0 {
|
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
||||||
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, *operatorShopID)
|
DeviceID: device.ID,
|
||||||
if err != nil {
|
VirtualNo: device.VirtualNo,
|
||||||
s.logDeviceOperation(
|
Reason: "您没有权限分配该套餐系列",
|
||||||
ctx,
|
})
|
||||||
constants.AssetAuditOpDeviceSeriesBinding,
|
return
|
||||||
"设备系列绑定失败",
|
|
||||||
constants.AssetAuditResultFailed,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
map[string]any{
|
|
||||||
"series_id": req.SeriesID,
|
|
||||||
"device_ids": req.DeviceIDs,
|
|
||||||
},
|
|
||||||
len(req.DeviceIDs),
|
|
||||||
0,
|
|
||||||
len(req.DeviceIDs),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
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,
|
|
||||||
VirtualNo: device.VirtualNo,
|
|
||||||
Reason: "您没有权限分配该套餐系列",
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证设备权限(基于 device.ShopID)
|
// 代理只能操作自己店铺名下的设备,保持旧接口权限语义不变。
|
||||||
if operatorShopID != nil {
|
if operatorShopID != nil && (device.ShopID == nil || *device.ShopID != *operatorShopID) {
|
||||||
if device.ShopID == nil || *device.ShopID != *operatorShopID {
|
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
||||||
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
DeviceID: device.ID,
|
||||||
DeviceID: device.ID,
|
VirtualNo: device.VirtualNo,
|
||||||
VirtualNo: device.VirtualNo,
|
Reason: "无权操作此设备",
|
||||||
Reason: "无权操作此设备",
|
})
|
||||||
})
|
return
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, exists := successDeviceIDSet[device.ID]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
successDeviceIDSet[device.ID] = struct{}{}
|
||||||
successDeviceIDs = append(successDeviceIDs, device.ID)
|
successDeviceIDs = append(successDeviceIDs, device.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if selectionType == dto.SelectionTypeList {
|
||||||
|
for _, deviceID := range req.DeviceIDs {
|
||||||
|
addDevice(deviceMap[deviceID], deviceID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, device := range devices {
|
||||||
|
addDevice(device, device.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(successDeviceIDs) > 0 {
|
if len(successDeviceIDs) > 0 {
|
||||||
var seriesIDPtr *uint
|
var seriesIDPtr *uint
|
||||||
if req.SeriesID > 0 {
|
if req.SeriesID > 0 {
|
||||||
@@ -1221,10 +1218,11 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
|
|||||||
nil,
|
nil,
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"series_id": req.SeriesID,
|
"series_id": req.SeriesID,
|
||||||
|
"selection_type": selectionType,
|
||||||
"success_device_ids": successDeviceIDs,
|
"success_device_ids": successDeviceIDs,
|
||||||
"failed_items": failedItems,
|
"failed_items": failedItems,
|
||||||
},
|
},
|
||||||
len(req.DeviceIDs),
|
batchTotal,
|
||||||
len(successDeviceIDs),
|
len(successDeviceIDs),
|
||||||
len(failedItems),
|
len(failedItems),
|
||||||
err,
|
err,
|
||||||
@@ -1253,11 +1251,12 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
|
|||||||
"devices": beforeData,
|
"devices": beforeData,
|
||||||
},
|
},
|
||||||
map[string]any{
|
map[string]any{
|
||||||
|
"selection_type": selectionType,
|
||||||
"series_id": req.SeriesID,
|
"series_id": req.SeriesID,
|
||||||
"success_device_ids": successDeviceIDs,
|
"success_device_ids": successDeviceIDs,
|
||||||
"failed_items": failedItems,
|
"failed_items": failedItems,
|
||||||
},
|
},
|
||||||
len(req.DeviceIDs),
|
batchTotal,
|
||||||
len(successDeviceIDs),
|
len(successDeviceIDs),
|
||||||
len(failedItems),
|
len(failedItems),
|
||||||
resultErr,
|
resultErr,
|
||||||
@@ -1270,6 +1269,161 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeDeviceSeriesBindingSelection(req *dto.BatchSetDeviceSeriesBindngRequest) (string, error) {
|
||||||
|
switch req.SelectionType {
|
||||||
|
case dto.SelectionTypeList:
|
||||||
|
if len(req.DeviceIDs) == 0 {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "selection_type=list时device_ids不能为空")
|
||||||
|
}
|
||||||
|
return dto.SelectionTypeList, nil
|
||||||
|
case dto.SelectionTypeRange:
|
||||||
|
if req.VirtualNoStart == "" || req.VirtualNoEnd == "" {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "selection_type=range时virtual_no_start和virtual_no_end不能为空")
|
||||||
|
}
|
||||||
|
if req.VirtualNoStart > req.VirtualNoEnd {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "设备虚拟号起始值不能大于结束值")
|
||||||
|
}
|
||||||
|
return dto.SelectionTypeRange, nil
|
||||||
|
case dto.SelectionTypeFilter:
|
||||||
|
return dto.SelectionTypeFilter, nil
|
||||||
|
case "":
|
||||||
|
if len(req.DeviceIDs) > 0 {
|
||||||
|
return dto.SelectionTypeList, nil
|
||||||
|
}
|
||||||
|
if req.VirtualNoStart != "" || req.VirtualNoEnd != "" {
|
||||||
|
if req.VirtualNoStart == "" || req.VirtualNoEnd == "" {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "virtual_no_start和virtual_no_end必须同时传入")
|
||||||
|
}
|
||||||
|
if req.VirtualNoStart > req.VirtualNoEnd {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "设备虚拟号起始值不能大于结束值")
|
||||||
|
}
|
||||||
|
return dto.SelectionTypeRange, nil
|
||||||
|
}
|
||||||
|
if hasDeviceSeriesBindingFilters(req) {
|
||||||
|
return dto.SelectionTypeFilter, nil
|
||||||
|
}
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "请选择要设置套餐系列的设备")
|
||||||
|
default:
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "无效的选设备方式")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasDeviceSeriesBindingFilters(req *dto.BatchSetDeviceSeriesBindngRequest) bool {
|
||||||
|
return req.VirtualNo != "" ||
|
||||||
|
req.DeviceName != "" ||
|
||||||
|
req.Status != nil ||
|
||||||
|
req.ShopID != nil ||
|
||||||
|
len(req.ShopIDs) > 0 ||
|
||||||
|
req.FilterSeriesID != nil ||
|
||||||
|
req.BatchNo != "" ||
|
||||||
|
req.DeviceType != "" ||
|
||||||
|
req.Manufacturer != "" ||
|
||||||
|
req.CreatedAtStart != nil ||
|
||||||
|
req.CreatedAtEnd != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getDevicesForSeriesBinding(ctx context.Context, req *dto.BatchSetDeviceSeriesBindngRequest, selectionType string) ([]*model.Device, error) {
|
||||||
|
switch selectionType {
|
||||||
|
case dto.SelectionTypeList:
|
||||||
|
return s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
|
||||||
|
case dto.SelectionTypeRange:
|
||||||
|
return s.deviceStore.GetByFilters(ctx, map[string]any{
|
||||||
|
"virtual_no_start": req.VirtualNoStart,
|
||||||
|
"virtual_no_end": req.VirtualNoEnd,
|
||||||
|
})
|
||||||
|
case dto.SelectionTypeFilter:
|
||||||
|
return s.deviceStore.GetByFilters(ctx, buildDeviceSeriesBindingFilters(req))
|
||||||
|
default:
|
||||||
|
return nil, errors.New(errors.CodeInvalidParam, "无效的选设备方式")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDeviceSeriesBindingFilters(req *dto.BatchSetDeviceSeriesBindngRequest) map[string]any {
|
||||||
|
filters := make(map[string]any)
|
||||||
|
if req.VirtualNo != "" {
|
||||||
|
filters["virtual_no"] = req.VirtualNo
|
||||||
|
}
|
||||||
|
if req.VirtualNoStart != "" {
|
||||||
|
filters["virtual_no_start"] = req.VirtualNoStart
|
||||||
|
}
|
||||||
|
if req.VirtualNoEnd != "" {
|
||||||
|
filters["virtual_no_end"] = req.VirtualNoEnd
|
||||||
|
}
|
||||||
|
if req.DeviceName != "" {
|
||||||
|
filters["device_name"] = req.DeviceName
|
||||||
|
}
|
||||||
|
if req.Status != nil {
|
||||||
|
filters["status"] = *req.Status
|
||||||
|
}
|
||||||
|
shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs)
|
||||||
|
if hasShopIDs {
|
||||||
|
filters["shop_ids"] = shopIDs
|
||||||
|
} else if req.ShopID != nil {
|
||||||
|
if *req.ShopID == 0 {
|
||||||
|
filters["shop_ids"] = []uint{}
|
||||||
|
} else {
|
||||||
|
filters["shop_id"] = req.ShopID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.FilterSeriesID != nil {
|
||||||
|
filters["series_id"] = *req.FilterSeriesID
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) hasAvailableSeriesAllocation(ctx context.Context, shopID uint, seriesID uint) (bool, error) {
|
||||||
|
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, shopID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, alloc := range seriesAllocations {
|
||||||
|
if alloc.SeriesID == seriesID && alloc.Status == 1 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deviceSeriesBindingBatchTotal(req *dto.BatchSetDeviceSeriesBindngRequest, selectionType string, devices []*model.Device) int {
|
||||||
|
if selectionType == dto.SelectionTypeList {
|
||||||
|
return len(req.DeviceIDs)
|
||||||
|
}
|
||||||
|
return len(devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deviceSeriesBindingAuditData(req *dto.BatchSetDeviceSeriesBindngRequest, selectionType string, operatorShopID *uint) map[string]any {
|
||||||
|
data := map[string]any{
|
||||||
|
"selection_type": selectionType,
|
||||||
|
"series_id": req.SeriesID,
|
||||||
|
"operator_shop_id": operatorShopID,
|
||||||
|
}
|
||||||
|
switch selectionType {
|
||||||
|
case dto.SelectionTypeList:
|
||||||
|
data["device_ids"] = req.DeviceIDs
|
||||||
|
case dto.SelectionTypeRange:
|
||||||
|
data["virtual_no_start"] = req.VirtualNoStart
|
||||||
|
data["virtual_no_end"] = req.VirtualNoEnd
|
||||||
|
case dto.SelectionTypeFilter:
|
||||||
|
data["filters"] = buildDeviceSeriesBindingFilters(req)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) buildDeviceNotFoundFailedItems(deviceIDs []uint) []dto.DeviceSeriesBindngFailedItem {
|
func (s *Service) buildDeviceNotFoundFailedItems(deviceIDs []uint) []dto.DeviceSeriesBindngFailedItem {
|
||||||
items := make([]dto.DeviceSeriesBindngFailedItem, len(deviceIDs))
|
items := make([]dto.DeviceSeriesBindngFailedItem, len(deviceIDs))
|
||||||
for i, id := range deviceIDs {
|
for i, id := range deviceIDs {
|
||||||
|
|||||||
@@ -982,7 +982,14 @@ func (s *Service) buildRecallRecords(successCards []*model.IotCard, toShopID *ui
|
|||||||
|
|
||||||
// BatchSetSeriesBinding 批量设置卡的套餐系列绑定
|
// BatchSetSeriesBinding 批量设置卡的套餐系列绑定
|
||||||
func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetCardSeriesBindngResponse, error) {
|
func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetCardSeriesBindngResponse, error) {
|
||||||
cards, err := s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
|
selectionType, err := normalizeCardSeriesBindingSelection(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cards, err := s.getCardsForSeriesBinding(ctx, req, selectionType)
|
||||||
|
batchTotal := cardSeriesBindingBatchTotal(req, selectionType, cards)
|
||||||
|
auditData := cardSeriesBindingAuditData(req, selectionType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||||
@@ -991,44 +998,38 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
|
|||||||
ResultStatus: constants.AssetAuditResultFailed,
|
ResultStatus: constants.AssetAuditResultFailed,
|
||||||
ErrorCode: errorCode,
|
ErrorCode: errorCode,
|
||||||
ErrorMsg: errorMsg,
|
ErrorMsg: errorMsg,
|
||||||
BatchTotal: len(req.ICCIDs),
|
BatchTotal: batchTotal,
|
||||||
AfterData: map[string]any{
|
AfterData: auditData,
|
||||||
"series_id": req.SeriesID,
|
|
||||||
"iccids": req.ICCIDs,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cards) == 0 {
|
if len(cards) == 0 {
|
||||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
failedItems := []dto.CardSeriesBindngFailedItem{}
|
||||||
OperationType: constants.AssetAuditOpCardSeriesBinding,
|
if selectionType == dto.SelectionTypeList {
|
||||||
OperationDesc: "卡系列绑定被拒绝",
|
failedItems = s.buildCardNotFoundFailedItems(req.ICCIDs)
|
||||||
ResultStatus: constants.AssetAuditResultDenied,
|
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||||
ErrorMsg: "卡不存在",
|
OperationType: constants.AssetAuditOpCardSeriesBinding,
|
||||||
BatchTotal: len(req.ICCIDs),
|
OperationDesc: "卡系列绑定被拒绝",
|
||||||
FailCount: len(req.ICCIDs),
|
ResultStatus: constants.AssetAuditResultDenied,
|
||||||
AfterData: map[string]any{
|
ErrorMsg: "卡不存在",
|
||||||
"series_id": req.SeriesID,
|
BatchTotal: batchTotal,
|
||||||
"iccids": req.ICCIDs,
|
FailCount: len(failedItems),
|
||||||
},
|
AfterData: auditData,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return &dto.BatchSetCardSeriesBindngResponse{
|
return &dto.BatchSetCardSeriesBindngResponse{
|
||||||
SuccessCount: 0,
|
SuccessCount: 0,
|
||||||
FailCount: len(req.ICCIDs),
|
FailCount: len(failedItems),
|
||||||
FailedItems: s.buildCardNotFoundFailedItems(req.ICCIDs),
|
FailedItems: failedItems,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cardMap := make(map[string]*model.IotCard)
|
cardMap := indexCardsByICCID(cards)
|
||||||
for _, card := range cards {
|
|
||||||
cardMap[card.ICCID] = card
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证系列存在(仅当 SeriesID > 0 时)
|
// 验证系列存在(仅当 SeriesID > 0 时)
|
||||||
var packageSeries *model.PackageSeries
|
|
||||||
if req.SeriesID > 0 {
|
if req.SeriesID > 0 {
|
||||||
packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID)
|
packageSeries, err := s.packageSeriesStore.GetByID(ctx, req.SeriesID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
denyErr := errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
|
denyErr := errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
|
||||||
@@ -1039,11 +1040,8 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
|
|||||||
ResultStatus: constants.AssetAuditResultDenied,
|
ResultStatus: constants.AssetAuditResultDenied,
|
||||||
ErrorCode: errorCode,
|
ErrorCode: errorCode,
|
||||||
ErrorMsg: errorMsg,
|
ErrorMsg: errorMsg,
|
||||||
BatchTotal: len(req.ICCIDs),
|
BatchTotal: batchTotal,
|
||||||
AfterData: map[string]any{
|
AfterData: auditData,
|
||||||
"series_id": req.SeriesID,
|
|
||||||
"iccids": req.ICCIDs,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return nil, denyErr
|
return nil, denyErr
|
||||||
}
|
}
|
||||||
@@ -1054,11 +1052,8 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
|
|||||||
ResultStatus: constants.AssetAuditResultFailed,
|
ResultStatus: constants.AssetAuditResultFailed,
|
||||||
ErrorCode: errorCode,
|
ErrorCode: errorCode,
|
||||||
ErrorMsg: errorMsg,
|
ErrorMsg: errorMsg,
|
||||||
BatchTotal: len(req.ICCIDs),
|
BatchTotal: batchTotal,
|
||||||
AfterData: map[string]any{
|
AfterData: auditData,
|
||||||
"series_id": req.SeriesID,
|
|
||||||
"iccids": req.ICCIDs,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1071,11 +1066,8 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
|
|||||||
ResultStatus: constants.AssetAuditResultDenied,
|
ResultStatus: constants.AssetAuditResultDenied,
|
||||||
ErrorCode: errorCode,
|
ErrorCode: errorCode,
|
||||||
ErrorMsg: errorMsg,
|
ErrorMsg: errorMsg,
|
||||||
BatchTotal: len(req.ICCIDs),
|
BatchTotal: batchTotal,
|
||||||
AfterData: map[string]any{
|
AfterData: auditData,
|
||||||
"series_id": req.SeriesID,
|
|
||||||
"iccids": req.ICCIDs,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return nil, denyErr
|
return nil, denyErr
|
||||||
}
|
}
|
||||||
@@ -1083,53 +1075,59 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
|
|||||||
|
|
||||||
var successCardIDs []uint
|
var successCardIDs []uint
|
||||||
var failedItems []dto.CardSeriesBindngFailedItem
|
var failedItems []dto.CardSeriesBindngFailedItem
|
||||||
|
successCardIDSet := make(map[uint]struct{})
|
||||||
|
|
||||||
for _, iccid := range req.ICCIDs {
|
hasSeriesAllocation := true
|
||||||
card, exists := cardMap[iccid]
|
if operatorShopID != nil && req.SeriesID > 0 {
|
||||||
if !exists {
|
hasSeriesAllocation, err = s.hasAvailableSeriesAllocation(ctx, *operatorShopID, req.SeriesID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCard := func(card *model.IotCard, requestedICCID string) {
|
||||||
|
if card == nil {
|
||||||
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
||||||
ICCID: iccid,
|
ICCID: requestedICCID,
|
||||||
Reason: "卡不存在",
|
Reason: "卡不存在",
|
||||||
})
|
})
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证操作者权限(仅代理用户)
|
if !hasSeriesAllocation {
|
||||||
if operatorShopID != nil && req.SeriesID > 0 {
|
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
||||||
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, *operatorShopID)
|
ICCID: card.ICCID,
|
||||||
if err != nil {
|
Reason: "您没有权限分配该套餐系列",
|
||||||
return nil, err
|
})
|
||||||
}
|
return
|
||||||
hasSeriesAllocation := false
|
|
||||||
for _, alloc := range seriesAllocations {
|
|
||||||
if alloc.SeriesID == req.SeriesID && alloc.Status == 1 {
|
|
||||||
hasSeriesAllocation = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasSeriesAllocation {
|
|
||||||
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
|
||||||
ICCID: iccid,
|
|
||||||
Reason: "您没有权限分配该套餐系列",
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证卡权限(基于 card.ShopID)
|
// 代理只能操作自己店铺名下的卡,保持旧接口权限语义不变。
|
||||||
if operatorShopID != nil {
|
if operatorShopID != nil && (card.ShopID == nil || *card.ShopID != *operatorShopID) {
|
||||||
if card.ShopID == nil || *card.ShopID != *operatorShopID {
|
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
||||||
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
ICCID: card.ICCID,
|
||||||
ICCID: iccid,
|
Reason: "无权操作此卡",
|
||||||
Reason: "无权操作此卡",
|
})
|
||||||
})
|
return
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, exists := successCardIDSet[card.ID]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
successCardIDSet[card.ID] = struct{}{}
|
||||||
successCardIDs = append(successCardIDs, card.ID)
|
successCardIDs = append(successCardIDs, card.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if selectionType == dto.SelectionTypeList {
|
||||||
|
for _, iccid := range req.ICCIDs {
|
||||||
|
addCard(cardMap[iccid], iccid)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, card := range cards {
|
||||||
|
addCard(card, card.ICCID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(successCardIDs) > 0 {
|
if len(successCardIDs) > 0 {
|
||||||
var seriesIDPtr *uint
|
var seriesIDPtr *uint
|
||||||
if req.SeriesID > 0 {
|
if req.SeriesID > 0 {
|
||||||
@@ -1143,13 +1141,14 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
|
|||||||
ResultStatus: constants.AssetAuditResultFailed,
|
ResultStatus: constants.AssetAuditResultFailed,
|
||||||
ErrorCode: errorCode,
|
ErrorCode: errorCode,
|
||||||
ErrorMsg: errorMsg,
|
ErrorMsg: errorMsg,
|
||||||
BatchTotal: len(req.ICCIDs),
|
BatchTotal: batchTotal,
|
||||||
SuccessCount: len(successCardIDs),
|
SuccessCount: len(successCardIDs),
|
||||||
FailCount: len(failedItems),
|
FailCount: len(failedItems),
|
||||||
AfterData: map[string]any{
|
AfterData: map[string]any{
|
||||||
"series_id": req.SeriesID,
|
"selection_type": selectionType,
|
||||||
"success_ids": successCardIDs,
|
"series_id": req.SeriesID,
|
||||||
"failed_items": failedItems,
|
"success_ids": successCardIDs,
|
||||||
|
"failed_items": failedItems,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1184,16 +1183,17 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
|
|||||||
OperationDesc: operationDesc,
|
OperationDesc: operationDesc,
|
||||||
ResultStatus: resultStatus,
|
ResultStatus: resultStatus,
|
||||||
ErrorMsg: errorMsg,
|
ErrorMsg: errorMsg,
|
||||||
BatchTotal: len(req.ICCIDs),
|
BatchTotal: batchTotal,
|
||||||
SuccessCount: len(successCardIDs),
|
SuccessCount: len(successCardIDs),
|
||||||
FailCount: len(failedItems),
|
FailCount: len(failedItems),
|
||||||
BeforeData: map[string]any{
|
BeforeData: map[string]any{
|
||||||
"cards": beforeCards,
|
"cards": beforeCards,
|
||||||
},
|
},
|
||||||
AfterData: map[string]any{
|
AfterData: map[string]any{
|
||||||
"series_id": req.SeriesID,
|
"selection_type": selectionType,
|
||||||
"success_ids": successCardIDs,
|
"series_id": req.SeriesID,
|
||||||
"failed_items": failedItems,
|
"success_ids": successCardIDs,
|
||||||
|
"failed_items": failedItems,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1204,6 +1204,185 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeCardSeriesBindingSelection(req *dto.BatchSetCardSeriesBindngRequest) (string, error) {
|
||||||
|
switch req.SelectionType {
|
||||||
|
case dto.SelectionTypeList:
|
||||||
|
if len(req.ICCIDs) == 0 {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "selection_type=list时iccids不能为空")
|
||||||
|
}
|
||||||
|
return dto.SelectionTypeList, nil
|
||||||
|
case dto.SelectionTypeRange:
|
||||||
|
if req.ICCIDStart == "" || req.ICCIDEnd == "" {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "selection_type=range时iccid_start和iccid_end不能为空")
|
||||||
|
}
|
||||||
|
if req.ICCIDStart > req.ICCIDEnd {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "ICCID起始号不能大于结束号")
|
||||||
|
}
|
||||||
|
return dto.SelectionTypeRange, nil
|
||||||
|
case dto.SelectionTypeFilter:
|
||||||
|
return dto.SelectionTypeFilter, nil
|
||||||
|
case "":
|
||||||
|
if len(req.ICCIDs) > 0 {
|
||||||
|
return dto.SelectionTypeList, nil
|
||||||
|
}
|
||||||
|
if req.ICCIDStart != "" || req.ICCIDEnd != "" {
|
||||||
|
if req.ICCIDStart == "" || req.ICCIDEnd == "" {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "iccid_start和iccid_end必须同时传入")
|
||||||
|
}
|
||||||
|
if req.ICCIDStart > req.ICCIDEnd {
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "ICCID起始号不能大于结束号")
|
||||||
|
}
|
||||||
|
return dto.SelectionTypeRange, nil
|
||||||
|
}
|
||||||
|
if hasCardSeriesBindingFilters(req) {
|
||||||
|
return dto.SelectionTypeFilter, nil
|
||||||
|
}
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "请选择要设置套餐系列的卡")
|
||||||
|
default:
|
||||||
|
return "", errors.New(errors.CodeInvalidParam, "无效的选卡方式")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasCardSeriesBindingFilters(req *dto.BatchSetCardSeriesBindngRequest) bool {
|
||||||
|
return req.Status != nil ||
|
||||||
|
req.CarrierID != nil ||
|
||||||
|
req.ShopID != nil ||
|
||||||
|
len(req.ShopIDs) > 0 ||
|
||||||
|
req.FilterSeriesID != nil ||
|
||||||
|
req.ICCID != "" ||
|
||||||
|
req.MSISDN != "" ||
|
||||||
|
req.IsStandalone != nil ||
|
||||||
|
req.BatchNo != "" ||
|
||||||
|
req.PackageID != nil ||
|
||||||
|
req.IsDistributed != nil ||
|
||||||
|
req.IsReplaced != nil ||
|
||||||
|
req.CarrierName != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getCardsForSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, selectionType string) ([]*model.IotCard, error) {
|
||||||
|
switch selectionType {
|
||||||
|
case dto.SelectionTypeList:
|
||||||
|
return s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
|
||||||
|
case dto.SelectionTypeRange:
|
||||||
|
filters := map[string]any{
|
||||||
|
"iccid_start": req.ICCIDStart,
|
||||||
|
"iccid_end": req.ICCIDEnd,
|
||||||
|
}
|
||||||
|
return s.iotCardStore.GetBySeriesBindingFilters(ctx, filters)
|
||||||
|
case dto.SelectionTypeFilter:
|
||||||
|
return s.iotCardStore.GetBySeriesBindingFilters(ctx, buildCardSeriesBindingFilters(req))
|
||||||
|
default:
|
||||||
|
return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCardSeriesBindingFilters(req *dto.BatchSetCardSeriesBindngRequest) map[string]any {
|
||||||
|
filters := make(map[string]any)
|
||||||
|
if req.Status != nil {
|
||||||
|
filters["status"] = *req.Status
|
||||||
|
}
|
||||||
|
if req.CarrierID != nil {
|
||||||
|
filters["carrier_id"] = *req.CarrierID
|
||||||
|
}
|
||||||
|
shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs)
|
||||||
|
if hasShopIDs {
|
||||||
|
filters["shop_ids"] = shopIDs
|
||||||
|
} else if req.ShopID != nil {
|
||||||
|
if *req.ShopID == 0 {
|
||||||
|
filters["shop_ids"] = []uint{}
|
||||||
|
} else {
|
||||||
|
filters["shop_id"] = *req.ShopID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.FilterSeriesID != nil {
|
||||||
|
filters["series_id"] = *req.FilterSeriesID
|
||||||
|
}
|
||||||
|
if req.ICCID != "" {
|
||||||
|
filters["iccid"] = req.ICCID
|
||||||
|
}
|
||||||
|
if req.MSISDN != "" {
|
||||||
|
filters["msisdn"] = req.MSISDN
|
||||||
|
}
|
||||||
|
if req.IsStandalone != nil {
|
||||||
|
filters["is_standalone"] = req.IsStandalone
|
||||||
|
}
|
||||||
|
if req.BatchNo != "" {
|
||||||
|
filters["batch_no"] = req.BatchNo
|
||||||
|
}
|
||||||
|
if req.PackageID != nil {
|
||||||
|
filters["package_id"] = *req.PackageID
|
||||||
|
}
|
||||||
|
if req.IsDistributed != nil {
|
||||||
|
filters["is_distributed"] = *req.IsDistributed
|
||||||
|
}
|
||||||
|
if req.ICCIDStart != "" {
|
||||||
|
filters["iccid_start"] = req.ICCIDStart
|
||||||
|
}
|
||||||
|
if req.ICCIDEnd != "" {
|
||||||
|
filters["iccid_end"] = req.ICCIDEnd
|
||||||
|
}
|
||||||
|
if req.IsReplaced != nil {
|
||||||
|
filters["is_replaced"] = *req.IsReplaced
|
||||||
|
}
|
||||||
|
if req.CarrierName != "" {
|
||||||
|
filters["carrier_name"] = req.CarrierName
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) hasAvailableSeriesAllocation(ctx context.Context, shopID uint, seriesID uint) (bool, error) {
|
||||||
|
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, shopID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, alloc := range seriesAllocations {
|
||||||
|
if alloc.SeriesID == seriesID && alloc.Status == 1 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexCardsByICCID(cards []*model.IotCard) map[string]*model.IotCard {
|
||||||
|
cardMap := make(map[string]*model.IotCard, len(cards))
|
||||||
|
for _, card := range cards {
|
||||||
|
if card.ICCID != "" {
|
||||||
|
cardMap[card.ICCID] = card
|
||||||
|
}
|
||||||
|
if card.ICCID19 != "" {
|
||||||
|
cardMap[card.ICCID19] = card
|
||||||
|
}
|
||||||
|
if card.ICCID20 != nil && *card.ICCID20 != "" {
|
||||||
|
cardMap[*card.ICCID20] = card
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cardMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func cardSeriesBindingBatchTotal(req *dto.BatchSetCardSeriesBindngRequest, selectionType string, cards []*model.IotCard) int {
|
||||||
|
if selectionType == dto.SelectionTypeList {
|
||||||
|
return len(req.ICCIDs)
|
||||||
|
}
|
||||||
|
return len(cards)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cardSeriesBindingAuditData(req *dto.BatchSetCardSeriesBindngRequest, selectionType string) map[string]any {
|
||||||
|
data := map[string]any{
|
||||||
|
"selection_type": selectionType,
|
||||||
|
"series_id": req.SeriesID,
|
||||||
|
}
|
||||||
|
switch selectionType {
|
||||||
|
case dto.SelectionTypeList:
|
||||||
|
data["iccids"] = req.ICCIDs
|
||||||
|
case dto.SelectionTypeRange:
|
||||||
|
data["iccid_start"] = req.ICCIDStart
|
||||||
|
data["iccid_end"] = req.ICCIDEnd
|
||||||
|
case dto.SelectionTypeFilter:
|
||||||
|
data["filters"] = buildCardSeriesBindingFilters(req)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) buildCardNotFoundFailedItems(iccids []string) []dto.CardSeriesBindngFailedItem {
|
func (s *Service) buildCardNotFoundFailedItems(iccids []string) []dto.CardSeriesBindngFailedItem {
|
||||||
items := make([]dto.CardSeriesBindngFailedItem, len(iccids))
|
items := make([]dto.CardSeriesBindngFailedItem, len(iccids))
|
||||||
for i, iccid := range iccids {
|
for i, iccid := range iccids {
|
||||||
|
|||||||
@@ -99,10 +99,44 @@ func (s *DeviceStore) List(ctx context.Context, opts *store.QueryOptions, filter
|
|||||||
query := s.db.WithContext(ctx).Model(&model.Device{})
|
query := s.db.WithContext(ctx).Model(&model.Device{})
|
||||||
// 应用数据权限过滤(NULL shop_id 对代理用户不可见)
|
// 应用数据权限过滤(NULL shop_id 对代理用户不可见)
|
||||||
query = middleware.ApplyShopFilter(ctx, query)
|
query = middleware.ApplyShopFilter(ctx, query)
|
||||||
|
query = s.applyDeviceFilters(query, filters)
|
||||||
|
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts == nil {
|
||||||
|
opts = &store.QueryOptions{
|
||||||
|
Page: 1,
|
||||||
|
PageSize: constants.DefaultPageSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset := (opts.Page - 1) * opts.PageSize
|
||||||
|
query = query.Offset(offset).Limit(opts.PageSize)
|
||||||
|
|
||||||
|
if opts.OrderBy != "" {
|
||||||
|
query = query.Order(opts.OrderBy)
|
||||||
|
} else {
|
||||||
|
query = query.Order("created_at DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Find(&devices).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DeviceStore) applyDeviceFilters(query *gorm.DB, filters map[string]any) *gorm.DB {
|
||||||
if virtualNo, ok := filters["virtual_no"].(string); ok && virtualNo != "" {
|
if virtualNo, ok := filters["virtual_no"].(string); ok && virtualNo != "" {
|
||||||
query = query.Where("virtual_no LIKE ?", "%"+virtualNo+"%")
|
query = query.Where("virtual_no LIKE ?", "%"+virtualNo+"%")
|
||||||
}
|
}
|
||||||
|
if virtualNoStart, ok := filters["virtual_no_start"].(string); ok && virtualNoStart != "" {
|
||||||
|
query = query.Where("virtual_no >= ?", virtualNoStart)
|
||||||
|
}
|
||||||
|
if virtualNoEnd, ok := filters["virtual_no_end"].(string); ok && virtualNoEnd != "" {
|
||||||
|
query = query.Where("virtual_no <= ?", virtualNoEnd)
|
||||||
|
}
|
||||||
if deviceName, ok := filters["device_name"].(string); ok && deviceName != "" {
|
if deviceName, ok := filters["device_name"].(string); ok && deviceName != "" {
|
||||||
query = query.Where("device_name LIKE ?", "%"+deviceName+"%")
|
query = query.Where("device_name LIKE ?", "%"+deviceName+"%")
|
||||||
}
|
}
|
||||||
@@ -140,31 +174,20 @@ func (s *DeviceStore) List(ctx context.Context, opts *store.QueryOptions, filter
|
|||||||
if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 {
|
if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 {
|
||||||
query = query.Where("series_id = ?", seriesID)
|
query = query.Where("series_id = ?", seriesID)
|
||||||
}
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
if err := query.Count(&total).Error; err != nil {
|
// GetByFilters 根据筛选条件查询设备
|
||||||
return nil, 0, err
|
// 用于批量设置套餐系列时选择完整筛选结果,不受列表分页限制。
|
||||||
}
|
func (s *DeviceStore) GetByFilters(ctx context.Context, filters map[string]any) ([]*model.Device, error) {
|
||||||
|
var devices []*model.Device
|
||||||
if opts == nil {
|
query := s.db.WithContext(ctx).Model(&model.Device{})
|
||||||
opts = &store.QueryOptions{
|
query = middleware.ApplyShopFilter(ctx, query)
|
||||||
Page: 1,
|
query = s.applyDeviceFilters(query, filters)
|
||||||
PageSize: constants.DefaultPageSize,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
offset := (opts.Page - 1) * opts.PageSize
|
|
||||||
query = query.Offset(offset).Limit(opts.PageSize)
|
|
||||||
|
|
||||||
if opts.OrderBy != "" {
|
|
||||||
query = query.Order(opts.OrderBy)
|
|
||||||
} else {
|
|
||||||
query = query.Order("created_at DESC")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Find(&devices).Error; err != nil {
|
if err := query.Find(&devices).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return devices, nil
|
||||||
return devices, total, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DeviceStore) UpdateShopID(ctx context.Context, id uint, shopID *uint) error {
|
func (s *DeviceStore) UpdateShopID(ctx context.Context, id uint, shopID *uint) error {
|
||||||
|
|||||||
@@ -1036,6 +1036,20 @@ func (s *IotCardStore) GetDistributedStandaloneByFilters(ctx context.Context, fi
|
|||||||
return cards, nil
|
return cards, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBySeriesBindingFilters 根据系列绑定筛选条件查询卡
|
||||||
|
// 用于批量设置套餐系列时选择完整筛选结果,不受列表分页限制。
|
||||||
|
func (s *IotCardStore) GetBySeriesBindingFilters(ctx context.Context, filters map[string]any) ([]*model.IotCard, error) {
|
||||||
|
query := s.db.WithContext(ctx).Model(&model.IotCard{})
|
||||||
|
query = middleware.ApplyShopFilter(ctx, query)
|
||||||
|
query = s.applyStandaloneFilters(ctx, query, filters)
|
||||||
|
|
||||||
|
var cards []*model.IotCard
|
||||||
|
if err := query.Find(&cards).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cards, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *IotCardStore) BatchUpdateShopIDAndStatus(ctx context.Context, cardIDs []uint, shopID *uint, status int) error {
|
func (s *IotCardStore) BatchUpdateShopIDAndStatus(ctx context.Context, cardIDs []uint, shopID *uint, status int) error {
|
||||||
if len(cardIDs) == 0 {
|
if len(cardIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user