package task import ( "bytes" "context" "encoding/csv" "io" "os" "strings" "github.com/hibiken/asynq" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) type deviceBatchAllocationRow struct { line int identifier string } func (h *DeviceImportHandler) handleDeviceBatchAllocation(ctx context.Context, task *model.DeviceImportTask) error { if h.allocationExecutor == nil || task.TargetID == nil || *task.TargetID == 0 { _ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "设备批量分配执行器或目标未配置") return asynq.SkipRetry } rows, err := h.downloadDeviceBatchAllocationCSV(ctx, task) if err != nil { _ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, err.Error()) return asynq.SkipRetry } workerCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{ UserID: task.Creator, UserType: task.OperatorType, Username: task.CreatorName, ShopID: valueOrZero(task.OperatorShopID), SubordinateShopIDs: operatorDeviceShopScope(task.OperatorShopID), }) result, err := h.executeDeviceBatchAllocation(workerCtx, task, rows) if err != nil { _ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, err.Error()) return asynq.SkipRetry } _ = h.importTaskStore.UpdateResult(ctx, task.ID, len(rows), result.successCount, result.skipCount, result.failCount, 0, result.skippedItems, result.failedItems, nil) if result.successCount == 0 && result.failCount > 0 { _ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "所有设备分配均失败") } else { _ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusCompleted, "") } return nil } func (h *DeviceImportHandler) downloadDeviceBatchAllocationCSV(ctx context.Context, task *model.DeviceImportTask) ([]deviceBatchAllocationRow, error) { if h.storageService == nil || task.StorageKey == "" { return nil, errors.New(errors.CodeServiceUnavailable, "设备批量分配对象存储未配置") } path, cleanup, err := h.storageService.DownloadToTemp(ctx, task.StorageKey) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "下载设备批量分配CSV失败") } defer cleanup() info, err := os.Stat(path) if err != nil || info.Size() > constants.DeviceBatchAllocationMaxFileSize { return nil, errors.New(errors.CodeInvalidParam, "设备批量分配CSV不存在或超过10MB") } data, err := os.ReadFile(path) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "读取设备批量分配CSV失败") } return parseDeviceBatchAllocationCSV(data) } func parseDeviceBatchAllocationCSV(data []byte) ([]deviceBatchAllocationRow, error) { data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}) reader := csv.NewReader(bytes.NewReader(data)) reader.FieldsPerRecord = -1 rows := make([]deviceBatchAllocationRow, 0) for line := 1; ; line++ { record, err := reader.Read() if err == io.EOF { break } if err != nil || len(record) != 1 { return nil, errors.New(errors.CodeInvalidParam, "设备批量分配CSV必须只有一列设备标识") } identifier := strings.TrimSpace(record[0]) if line == 1 && (identifier == "device_identifier" || identifier == "设备标识") { continue } if identifier == "" { return nil, errors.New(errors.CodeInvalidParam, "设备批量分配CSV存在空设备标识") } rows = append(rows, deviceBatchAllocationRow{line: line, identifier: identifier}) if len(rows) > constants.DeviceBatchAllocationMaxRows { return nil, errors.New(errors.CodeInvalidParam, "设备批量分配CSV最多包含1000行设备") } } if len(rows) == 0 { return nil, errors.New(errors.CodeInvalidParam, "设备批量分配CSV没有有效数据行") } return rows, nil } func (h *DeviceImportHandler) executeDeviceBatchAllocation(ctx context.Context, task *model.DeviceImportTask, rows []deviceBatchAllocationRow) (*deviceImportResult, error) { result := &deviceImportResult{skippedItems: model.ImportResultItems{}, failedItems: model.ImportResultItems{}} identifiers := make([]string, 0, len(rows)) for _, row := range rows { identifiers = append(identifiers, row.identifier) } devices, err := h.deviceStore.GetByIdentifiers(ctx, identifiers) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询设备失败") } lookup := make(map[string]*model.Device, len(devices)*3) for _, device := range devices { for _, key := range []string{device.VirtualNo, device.IMEI, device.SN} { if key != "" { lookup[key] = device } } } seen := make(map[uint]struct{}) pending := make([]uint, 0, len(rows)) lineByID := make(map[uint]deviceBatchAllocationRow) for _, row := range rows { device := lookup[row.identifier] if device == nil { result.failedItems = append(result.failedItems, deviceAllocationResultItem(row, "设备不存在或无权限")) result.failCount++ continue } if _, exists := seen[device.ID]; exists { result.skippedItems = append(result.skippedItems, deviceAllocationResultItem(row, "同一文件重复设备")) result.skipCount++ continue } seen[device.ID] = struct{}{} if deviceAlreadyAtAllocationTarget(device, task.OperationType, *task.TargetID) { result.successCount++ continue } pending = append(pending, device.ID) lineByID[device.ID] = row } if len(pending) == 0 { return result, nil } failed, err := h.applyDeviceBatchAllocation(ctx, task, pending) if err != nil { return nil, err } for _, id := range pending { if reason, exists := failed[id]; exists { result.failedItems = append(result.failedItems, deviceAllocationResultItem(lineByID[id], reason)) result.failCount++ } else { result.successCount++ } } return result, nil } func (h *DeviceImportHandler) applyDeviceBatchAllocation(ctx context.Context, task *model.DeviceImportTask, deviceIDs []uint) (map[uint]string, error) { failed := make(map[uint]string) switch task.OperationType { case constants.DeviceImportOperationAssignShop: response, err := h.allocationExecutor.AllocateDevices(ctx, &dto.AllocateDevicesRequest{TargetShopID: *task.TargetID, DeviceIDs: deviceIDs, Remark: "CSV批量分配任务 " + task.TaskNo}, task.Creator, task.OperatorShopID) if err != nil { return nil, err } for _, item := range response.FailedItems { failed[item.DeviceID] = item.Reason } case constants.DeviceImportOperationAssignSeries: response, err := h.allocationExecutor.BatchSetSeriesBinding(ctx, &dto.BatchSetDeviceSeriesBindngRequest{SelectionType: dto.SelectionTypeList, DeviceIDs: deviceIDs, SeriesID: *task.TargetID}, task.OperatorShopID) if err != nil { return nil, err } for _, item := range response.FailedItems { failed[item.DeviceID] = item.Reason } default: return nil, errors.New(errors.CodeInvalidParam, "设备批量分配任务类型不支持") } return failed, nil } func deviceAlreadyAtAllocationTarget(device *model.Device, operation string, targetID uint) bool { if operation == constants.DeviceImportOperationAssignShop { return device.ShopID != nil && *device.ShopID == targetID } return operation == constants.DeviceImportOperationAssignSeries && device.SeriesID != nil && *device.SeriesID == targetID } func deviceAllocationResultItem(row deviceBatchAllocationRow, reason string) model.ImportResultItem { return model.ImportResultItem{Line: row.line, ICCID: row.identifier, Reason: reason} } func valueOrZero(value *uint) uint { if value == nil { return 0 } return *value } func operatorDeviceShopScope(shopID *uint) []uint { if shopID == nil { return nil } return []uint{*shopID} }