批量
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s

This commit is contained in:
2026-05-11 14:54:23 +08:00
parent 93200a9074
commit 1350a9ed79
8 changed files with 613 additions and 210 deletions

View File

@@ -1021,7 +1021,14 @@ func (s *Service) buildRecallRecords(devices []*model.Device, successDeviceIDs [
// BatchSetSeriesBinding 批量设置设备的套餐系列绑定
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 {
s.logDeviceOperation(
ctx,
@@ -1030,41 +1037,37 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
"operator_shop_id": operatorShopID,
},
len(req.DeviceIDs),
auditData,
batchTotal,
0,
len(req.DeviceIDs),
batchTotal,
err,
)
return nil, err
}
if len(devices) == 0 {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSeriesBinding,
"设备系列绑定被拒绝",
constants.AssetAuditResultDenied,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
"reason": "设备不存在",
},
len(req.DeviceIDs),
0,
len(req.DeviceIDs),
errors.New(errors.CodeNotFound, "设备不存在"),
)
failedItems := []dto.DeviceSeriesBindngFailedItem{}
if selectionType == dto.SelectionTypeList {
failedItems = s.buildDeviceNotFoundFailedItems(req.DeviceIDs)
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSeriesBinding,
"设备系列绑定被拒绝",
constants.AssetAuditResultDenied,
nil,
nil,
auditData,
batchTotal,
0,
len(failedItems),
errors.New(errors.CodeNotFound, "设备不存在"),
)
}
return &dto.BatchSetDeviceSeriesBindngResponse{
SuccessCount: 0,
FailCount: len(req.DeviceIDs),
FailedItems: s.buildDeviceNotFoundFailedItems(req.DeviceIDs),
FailCount: len(failedItems),
FailedItems: failedItems,
}, nil
}
@@ -1087,13 +1090,10 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
constants.AssetAuditResultDenied,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
},
len(req.DeviceIDs),
auditData,
batchTotal,
0,
len(req.DeviceIDs),
batchTotal,
appErr,
)
return nil, appErr
@@ -1105,13 +1105,10 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
},
len(req.DeviceIDs),
auditData,
batchTotal,
0,
len(req.DeviceIDs),
batchTotal,
err,
)
return nil, err
@@ -1125,13 +1122,10 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
constants.AssetAuditResultDenied,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
},
len(req.DeviceIDs),
auditData,
batchTotal,
0,
len(req.DeviceIDs),
batchTotal,
appErr,
)
return nil, appErr
@@ -1140,72 +1134,75 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
var successDeviceIDs []uint
var failedItems []dto.DeviceSeriesBindngFailedItem
successDeviceIDSet := make(map[uint]struct{})
for _, deviceID := range req.DeviceIDs {
device, exists := deviceMap[deviceID]
if !exists {
hasSeriesAllocation := true
if operatorShopID != nil && req.SeriesID > 0 {
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{
DeviceID: deviceID,
DeviceID: requestedDeviceID,
VirtualNo: "",
Reason: "设备不存在",
})
continue
return
}
// 验证操作者权限(仅代理用户)- 检查是否有该系列的套餐分配
if operatorShopID != nil && req.SeriesID > 0 {
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, *operatorShopID)
if err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSeriesBinding,
"设备系列绑定失败",
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
}
if !hasSeriesAllocation {
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
DeviceID: device.ID,
VirtualNo: device.VirtualNo,
Reason: "您没有权限分配该套餐系列",
})
return
}
// 验证设备权限(基于 device.ShopID
if operatorShopID != nil {
if device.ShopID == nil || *device.ShopID != *operatorShopID {
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
DeviceID: device.ID,
VirtualNo: device.VirtualNo,
Reason: "无权操作此设备",
})
continue
}
// 代理只能操作自己店铺名下的设备,保持旧接口权限语义不变。
if operatorShopID != nil && (device.ShopID == nil || *device.ShopID != *operatorShopID) {
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
DeviceID: device.ID,
VirtualNo: device.VirtualNo,
Reason: "无权操作此设备",
})
return
}
if _, exists := successDeviceIDSet[device.ID]; exists {
return
}
successDeviceIDSet[device.ID] = struct{}{}
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 {
var seriesIDPtr *uint
if req.SeriesID > 0 {
@@ -1221,10 +1218,11 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
nil,
map[string]any{
"series_id": req.SeriesID,
"selection_type": selectionType,
"success_device_ids": successDeviceIDs,
"failed_items": failedItems,
},
len(req.DeviceIDs),
batchTotal,
len(successDeviceIDs),
len(failedItems),
err,
@@ -1253,11 +1251,12 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
"devices": beforeData,
},
map[string]any{
"selection_type": selectionType,
"series_id": req.SeriesID,
"success_device_ids": successDeviceIDs,
"failed_items": failedItems,
},
len(req.DeviceIDs),
batchTotal,
len(successDeviceIDs),
len(failedItems),
resultErr,
@@ -1270,6 +1269,161 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
}, 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 {
items := make([]dto.DeviceSeriesBindngFailedItem, len(deviceIDs))
for i, id := range deviceIDs {