All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
212 lines
7.7 KiB
Go
212 lines
7.7 KiB
Go
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}
|
|
}
|