package device_import import ( "context" "path/filepath" "strings" "time" "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" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/break/junhong_cmp_fiber/pkg/queue" "github.com/hibiken/asynq" "gorm.io/gorm" ) type Service struct { db *gorm.DB importTaskStore *postgres.DeviceImportTaskStore queueClient *queue.Client assetAudit AssetAuditService } type DeviceImportPayload struct { TaskID uint `json:"task_id"` } func New( db *gorm.DB, importTaskStore *postgres.DeviceImportTaskStore, queueClient *queue.Client, assetAudit AssetAuditService, ) *Service { return &Service{ db: db, importTaskStore: importTaskStore, queueClient: queueClient, assetAudit: assetAudit, } } func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportDeviceRequest) (*dto.ImportDeviceResponse, error) { userID := middleware.GetUserIDFromContext(ctx) if userID == 0 { appErr := errors.New(errors.CodeUnauthorized, "未授权访问") s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(0, "", req, constants.AssetAuditResultDenied, appErr)) return nil, appErr } taskNo := s.importTaskStore.GenerateTaskNo(ctx) fileName := filepath.Base(req.FileKey) task := &model.DeviceImportTask{ TaskNo: taskNo, OperationType: constants.DeviceImportOperationCreate, Status: model.ImportTaskStatusPending, BatchNo: req.BatchNo, FileName: fileName, StorageKey: req.FileKey, RealnamePolicy: req.RealnamePolicy, CreatorName: middleware.GetUsernameFromContext(ctx), } task.Creator = userID task.Updater = userID if err := s.importTaskStore.Create(ctx, task); err != nil { appErr := errors.Wrap(errors.CodeInternalError, err, "创建导入任务失败") s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(0, taskNo, req, constants.AssetAuditResultFailed, appErr)) return nil, appErr } payload := DeviceImportPayload{TaskID: task.ID} err := s.queueClient.EnqueueTask( ctx, constants.TaskTypeDeviceImport, payload, asynq.Queue(constants.QueueForTaskType(constants.TaskTypeDeviceImport)), ) if err != nil { s.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "任务入队失败: "+err.Error()) appErr := errors.Wrap(errors.CodeInternalError, err, "任务入队失败") s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(task.ID, taskNo, req, constants.AssetAuditResultFailed, appErr)) return nil, appErr } s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(task.ID, taskNo, req, constants.AssetAuditResultSuccess, nil)) return &dto.ImportDeviceResponse{ TaskID: task.ID, TaskNo: taskNo, Message: "导入任务已创建,Worker 将异步处理文件", }, nil } // CreateBatchAllocationTask 复用设备导入任务创建单列 CSV 批量分配任务。 func (s *Service) CreateBatchAllocationTask(ctx context.Context, req *dto.CreateDeviceBatchAllocationRequest) (*dto.CreateDeviceBatchAllocationResponse, error) { userID := middleware.GetUserIDFromContext(ctx) userType := middleware.GetUserTypeFromContext(ctx) if userID == 0 || (userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform && userType != constants.UserTypeAgent) { appErr := errors.New(errors.CodeForbidden, "仅平台和代理后台账号可创建设备批量分配任务") s.logDeviceImportAudit(ctx, newDeviceBatchAllocationAuditParams(0, "", req, constants.AssetAuditResultDenied, appErr)) return nil, appErr } if req == nil || !constants.IsDeviceImportOperation(req.OperationType) || req.OperationType == constants.DeviceImportOperationCreate || req.TargetID == 0 { return nil, errors.New(errors.CodeInvalidParam, "设备批量分配参数不合法") } if !strings.HasPrefix(req.FileKey, constants.DeviceBatchAllocationStoragePrefix+"/") || !strings.EqualFold(filepath.Ext(req.FileKey), ".csv") { return nil, errors.New(errors.CodeInvalidParam, "设备批量分配文件必须是指定目录下的CSV文件") } taskNo := s.importTaskStore.GenerateTaskNo(ctx) var operatorShopID *uint if userType == constants.UserTypeAgent { shopID := middleware.GetShopIDFromContext(ctx) if shopID == 0 { return nil, errors.New(errors.CodeForbidden, "代理账号缺少店铺归属") } operatorShopID = &shopID } targetID := req.TargetID task := &model.DeviceImportTask{ TaskNo: taskNo, OperationType: req.OperationType, TargetID: &targetID, OperatorType: userType, OperatorShopID: operatorShopID, Status: model.ImportTaskStatusPending, StorageKey: req.FileKey, FileName: filepath.Base(req.FileKey), CreatorName: middleware.GetUsernameFromContext(ctx), } task.Creator, task.Updater = userID, userID if err := s.importTaskStore.Create(ctx, task); err != nil { appErr := errors.Wrap(errors.CodeDatabaseError, err, "创建设备批量分配任务失败") s.logDeviceImportAudit(ctx, newDeviceBatchAllocationAuditParams(0, taskNo, req, constants.AssetAuditResultFailed, appErr)) return nil, appErr } if err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeDeviceImport, DeviceImportPayload{TaskID: task.ID}, asynq.Queue(constants.QueueForTaskType(constants.TaskTypeDeviceImport)), asynq.Timeout(constants.DeviceBatchAllocationTaskTimeout)); err != nil { _ = s.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "任务入队失败") appErr := errors.Wrap(errors.CodeInternalError, err, "设备批量分配任务入队失败") s.logDeviceImportAudit(ctx, newDeviceBatchAllocationAuditParams(task.ID, taskNo, req, constants.AssetAuditResultFailed, appErr)) return nil, appErr } s.logDeviceImportAudit(ctx, newDeviceBatchAllocationAuditParams(task.ID, taskNo, req, constants.AssetAuditResultSuccess, nil)) return &dto.CreateDeviceBatchAllocationResponse{ TaskID: task.ID, TaskNo: task.TaskNo, Message: "设备批量分配任务已创建,Worker 将异步处理CSV文件", }, nil } func (s *Service) List(ctx context.Context, req *dto.ListDeviceImportTaskRequest) (*dto.ListDeviceImportTaskResponse, 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.Status != nil { filters["status"] = *req.Status } if req.OperationType != "" { filters["operation_type"] = req.OperationType } if req.BatchNo != "" { filters["batch_no"] = req.BatchNo } if req.StartTime != nil { filters["start_time"] = *req.StartTime } if req.EndTime != nil { filters["end_time"] = *req.EndTime } tasks, total, err := s.importTaskStore.List(ctx, opts, filters) if err != nil { return nil, err } list := make([]*dto.DeviceImportTaskResponse, 0, len(tasks)) for _, task := range tasks { list = append(list, s.toTaskResponse(task)) } return &dto.ListDeviceImportTaskResponse{ List: list, Total: total, Page: page, PageSize: pageSize, }, nil } func (s *Service) GetByID(ctx context.Context, id uint) (*dto.DeviceImportTaskDetailResponse, error) { task, err := s.importTaskStore.GetByID(ctx, id) if err != nil { return nil, errors.New(errors.CodeNotFound, "导入任务不存在") } resp := &dto.DeviceImportTaskDetailResponse{ DeviceImportTaskResponse: *s.toTaskResponse(task), SkippedItems: make([]*dto.DeviceImportResultItemDTO, 0), FailedItems: make([]*dto.DeviceImportResultItemDTO, 0), WarningItems: make([]*dto.DeviceImportResultItemDTO, 0), } for _, item := range task.SkippedItems { resp.SkippedItems = append(resp.SkippedItems, &dto.DeviceImportResultItemDTO{ Line: item.Line, VirtualNo: item.ICCID, DeviceIdentifier: item.ICCID, Reason: item.Reason, }) } for _, item := range task.FailedItems { resp.FailedItems = append(resp.FailedItems, &dto.DeviceImportResultItemDTO{ Line: item.Line, VirtualNo: item.ICCID, DeviceIdentifier: item.ICCID, Reason: item.Reason, }) } for _, item := range task.WarningItems { resp.WarningItems = append(resp.WarningItems, &dto.DeviceImportResultItemDTO{ Line: item.Line, VirtualNo: item.ICCID, DeviceIdentifier: item.ICCID, Reason: item.Reason, }) } return resp, nil } func (s *Service) toTaskResponse(task *model.DeviceImportTask) *dto.DeviceImportTaskResponse { var startedAt, completedAt *time.Time if task.StartedAt != nil { startedAt = task.StartedAt } if task.CompletedAt != nil { completedAt = task.CompletedAt } return &dto.DeviceImportTaskResponse{ ID: task.ID, TaskNo: task.TaskNo, OperationType: task.OperationType, OperationName: constants.GetDeviceImportOperationName(task.OperationType), TargetID: task.TargetID, Status: task.Status, StatusName: getStatusText(task.Status), StatusText: getStatusText(task.Status), BatchNo: task.BatchNo, RealnamePolicy: task.RealnamePolicy, FileName: task.FileName, TotalCount: task.TotalCount, SuccessCount: task.SuccessCount, SkipCount: task.SkipCount, FailCount: task.FailCount, WarningCount: task.WarningCount, StartedAt: startedAt, CompletedAt: completedAt, ErrorMessage: task.ErrorMessage, CreatorName: task.CreatorName, CreatedAt: task.CreatedAt, } } func getStatusText(status int) string { switch status { case model.ImportTaskStatusPending: return "待处理" case model.ImportTaskStatusProcessing: return "处理中" case model.ImportTaskStatusCompleted: return "已完成" case model.ImportTaskStatusFailed: return "失败" default: return "未知" } }