package task import ( "bytes" "context" "encoding/csv" stderrors "errors" "io" "os" "strconv" "strings" "unicode/utf8" "github.com/bytedance/sonic" "github.com/hibiken/asynq" "go.uber.org/zap" "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/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" apperrors "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/break/junhong_cmp_fiber/pkg/storage" ) // AssetPackageBatchOrderCreator 定义 Worker 复用现有后台订单规则的最小接口。 type AssetPackageBatchOrderCreator interface { CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) } // AssetPackageBatchOrderPayload 资产套餐批量订购任务载荷。 type AssetPackageBatchOrderPayload struct { TaskID uint `json:"task_id"` } // AssetPackageBatchOrderHandler 资产套餐批量订购任务处理器。 type AssetPackageBatchOrderHandler struct { taskStore *postgres.AssetPackageBatchOrderTaskStore shopStore *postgres.ShopStore orderCreator AssetPackageBatchOrderCreator storageService *storage.Service logger *zap.Logger } // NewAssetPackageBatchOrderHandler 创建资产套餐批量订购任务处理器。 func NewAssetPackageBatchOrderHandler(taskStore *postgres.AssetPackageBatchOrderTaskStore, shopStore *postgres.ShopStore, orderCreator AssetPackageBatchOrderCreator, storageService *storage.Service, logger *zap.Logger) *AssetPackageBatchOrderHandler { return &AssetPackageBatchOrderHandler{taskStore: taskStore, shopStore: shopStore, orderCreator: orderCreator, storageService: storageService, logger: logger} } // Handle 处理资产套餐批量订购任务。 func (h *AssetPackageBatchOrderHandler) Handle(ctx context.Context, taskMessage *asynq.Task) error { var payload AssetPackageBatchOrderPayload if err := sonic.Unmarshal(taskMessage.Payload(), &payload); err != nil { h.logger.Error("解析资产套餐批量订购任务载荷失败", zap.Error(err)) return asynq.SkipRetry } taskRecord, err := h.taskStore.GetByID(ctx, payload.TaskID) if err != nil { h.logger.Error("查询资产套餐批量订购任务失败", zap.Uint("task_id", payload.TaskID), zap.Error(err)) return asynq.SkipRetry } claimed, err := h.taskStore.Claim(ctx, taskRecord.ID) if err != nil { return err } if !claimed { h.logger.Info("资产套餐批量订购任务已被领取或终结,跳过重复消费", zap.Uint("task_id", taskRecord.ID)) return nil } rows, err := h.downloadAndParse(ctx, taskRecord.StorageKey) if err != nil { h.logger.Error("下载或解析批量订购CSV失败", zap.Uint("task_id", taskRecord.ID), zap.Error(err)) _ = h.taskStore.MarkFailed(ctx, taskRecord.ID, err.Error()) return asynq.SkipRetry } items, successCount, failCount := h.processRows(ctx, taskRecord, rows) if err := h.taskStore.Complete(ctx, taskRecord.ID, items, successCount, failCount); err != nil { return err } h.logger.Info("资产套餐批量订购任务完成", zap.Uint("task_id", taskRecord.ID), zap.Int("success", successCount), zap.Int("fail", failCount)) return nil } type assetPackageBatchOrderRow struct { Line int Identifier string } func (h *AssetPackageBatchOrderHandler) downloadAndParse(ctx context.Context, key string) ([]assetPackageBatchOrderRow, error) { if h.storageService == nil { return nil, assetPackageBatchOrderError("对象存储服务未配置") } localPath, cleanup, err := h.storageService.DownloadToTemp(ctx, key) if err != nil { return nil, assetPackageBatchOrderError("下载批量订购CSV失败") } defer cleanup() info, err := os.Stat(localPath) if err != nil || info.Size() > constants.AssetPackageBatchOrderMaxFileSize { return nil, assetPackageBatchOrderError("批量订购CSV不存在或超过10MB") } data, err := os.ReadFile(localPath) if err != nil { return nil, assetPackageBatchOrderError("读取批量订购CSV失败") } return parseAssetPackageBatchOrderCSV(data) } func parseAssetPackageBatchOrderCSV(data []byte) ([]assetPackageBatchOrderRow, error) { data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}) if !utf8.Valid(data) { return nil, assetPackageBatchOrderError("批量订购CSV必须使用UTF-8编码") } reader := csv.NewReader(bytes.NewReader(data)) reader.TrimLeadingSpace = true rows := make([]assetPackageBatchOrderRow, 0) line := 0 for { record, err := reader.Read() if err == io.EOF { break } line++ if err != nil { return nil, assetPackageBatchOrderError("批量订购CSV格式错误") } if len(record) != 1 { return nil, assetPackageBatchOrderError("批量订购CSV必须只有一列资产标识") } identifier := strings.TrimSpace(record[0]) if line == 1 && isAssetIdentifierHeader(identifier) { continue } rows = append(rows, assetPackageBatchOrderRow{Line: line, Identifier: identifier}) if len(rows) > constants.AssetPackageBatchOrderMaxRows { return nil, assetPackageBatchOrderError("批量订购CSV最多包含1000行资产") } } if len(rows) == 0 { return nil, assetPackageBatchOrderError("批量订购CSV没有有效数据行") } return rows, nil } func (h *AssetPackageBatchOrderHandler) processRows(ctx context.Context, taskRecord *model.AssetPackageBatchOrderTask, rows []assetPackageBatchOrderRow) (model.AssetPackageBatchOrderResultItems, int, int) { subordinateShopIDs := h.resolveSubordinateShopIDs(ctx, taskRecord) workerCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{ UserID: taskRecord.Creator, UserType: taskRecord.CreatorUserType, Username: taskRecord.CreatorName, ShopID: taskRecord.CreatorShopID, SubordinateShopIDs: subordinateShopIDs, }) buyerType, buyerID := "", uint(0) if taskRecord.CreatorUserType == constants.UserTypeAgent { buyerType, buyerID = model.BuyerTypeAgent, taskRecord.CreatorShopID } items := make(model.AssetPackageBatchOrderResultItems, 0, len(rows)) seenInputs, seenAssets := make(map[string]int), make(map[string]int) successCount := 0 for _, row := range rows { item := h.processOne(workerCtx, taskRecord, row, buyerType, buyerID, seenInputs, seenAssets) items = append(items, item) if item.Status == constants.AssetPackageBatchOrderItemStatusSuccess { successCount++ } } return items, successCount, len(items) - successCount } func (h *AssetPackageBatchOrderHandler) resolveSubordinateShopIDs(ctx context.Context, taskRecord *model.AssetPackageBatchOrderTask) []uint { if taskRecord.CreatorUserType != constants.UserTypeAgent { return nil } fallback := []uint{taskRecord.CreatorShopID} if h.shopStore == nil { h.logger.Warn("批量订购任务未配置店铺存储,代理数据权限降级为仅自己店铺", zap.Uint("task_id", taskRecord.ID), zap.Uint("shop_id", taskRecord.CreatorShopID)) return fallback } shopIDs, err := h.shopStore.GetSubordinateShopIDs(ctx, taskRecord.CreatorShopID) if err != nil || len(shopIDs) == 0 { h.logger.Warn("查询批量订购任务代理数据权限失败,降级为仅自己店铺", zap.Uint("task_id", taskRecord.ID), zap.Uint("shop_id", taskRecord.CreatorShopID), zap.Error(err)) return fallback } return shopIDs } func (h *AssetPackageBatchOrderHandler) processOne(ctx context.Context, taskRecord *model.AssetPackageBatchOrderTask, row assetPackageBatchOrderRow, buyerType string, buyerID uint, seenInputs, seenAssets map[string]int) model.AssetPackageBatchOrderResultItem { item := model.AssetPackageBatchOrderResultItem{Line: row.Line, AssetIdentifier: row.Identifier} if row.Identifier == "" { return failedBatchOrderItem(item, "资产标识不能为空") } normalized := strings.ToLower(row.Identifier) if firstLine, exists := seenInputs[normalized]; exists { return failedBatchOrderItem(item, "资产标识与第"+strconv.Itoa(firstLine)+"行重复") } seenInputs[normalized] = row.Line order, err := h.orderCreator.CreateAdminOrder(ctx, &dto.CreateAdminOrderRequest{ Identifier: row.Identifier, PackageIDs: []uint{taskRecord.PackageID}, PaymentMethod: taskRecord.PaymentMethod, PaymentVoucherKey: []string(taskRecord.VoucherKeys), }, buyerType, buyerID) if err != nil { return failedBatchOrderItem(item, publicBatchOrderError(err)) } assetKey := order.AssetType + ":" if order.IotCardID != nil { assetKey += strconv.FormatUint(uint64(*order.IotCardID), 10) } else if order.DeviceID != nil { assetKey += strconv.FormatUint(uint64(*order.DeviceID), 10) } if firstLine, exists := seenAssets[assetKey]; assetKey != ":" && exists { return failedBatchOrderItem(item, "资产与第"+strconv.Itoa(firstLine)+"行解析为同一资产") } seenAssets[assetKey] = row.Line item.Status, item.OrderID, item.OrderNo, item.Amount = constants.AssetPackageBatchOrderItemStatusSuccess, order.ID, order.OrderNo, order.TotalAmount return item } func failedBatchOrderItem(item model.AssetPackageBatchOrderResultItem, reason string) model.AssetPackageBatchOrderResultItem { item.Status, item.Reason = constants.AssetPackageBatchOrderItemStatusFailed, reason return item } func publicBatchOrderError(err error) string { var appErr *apperrors.AppError if stderrors.As(err, &appErr) && appErr.Message != "" { return appErr.Message } return "创建订单失败" } func isAssetIdentifierHeader(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { case "资产标识", "identifier", "iccid", "iccid/虚拟号": return true default: return false } } type assetPackageBatchOrderError string func (e assetPackageBatchOrderError) Error() string { return string(e) }