七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
This commit is contained in:
242
internal/task/asset_package_batch_order.go
Normal file
242
internal/task/asset_package_batch_order.go
Normal file
@@ -0,0 +1,242 @@
|
||||
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) }
|
||||
211
internal/task/device_batch_allocation.go
Normal file
211
internal/task/device_batch_allocation.go
Normal file
@@ -0,0 +1,211 @@
|
||||
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}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/storage"
|
||||
@@ -27,6 +28,12 @@ type DeviceImportPayload struct {
|
||||
TaskID uint `json:"task_id"`
|
||||
}
|
||||
|
||||
// DeviceBatchAllocationExecutor 定义 Worker 复用现有设备分配与系列绑定规则的最小接口。
|
||||
type DeviceBatchAllocationExecutor interface {
|
||||
AllocateDevices(ctx context.Context, req *dto.AllocateDevicesRequest, operatorID uint, operatorShopID *uint) (*dto.AllocateDevicesResponse, error)
|
||||
BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDeviceSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetDeviceSeriesBindngResponse, error)
|
||||
}
|
||||
|
||||
type DeviceImportHandler struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
@@ -38,6 +45,7 @@ type DeviceImportHandler struct {
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
storageService *storage.Service
|
||||
logger *zap.Logger
|
||||
allocationExecutor DeviceBatchAllocationExecutor
|
||||
}
|
||||
|
||||
func NewDeviceImportHandler(
|
||||
@@ -51,6 +59,7 @@ func NewDeviceImportHandler(
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore,
|
||||
storageSvc *storage.Service,
|
||||
logger *zap.Logger,
|
||||
allocationExecutor DeviceBatchAllocationExecutor,
|
||||
) *DeviceImportHandler {
|
||||
return &DeviceImportHandler{
|
||||
db: db,
|
||||
@@ -63,6 +72,7 @@ func NewDeviceImportHandler(
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
storageService: storageSvc,
|
||||
logger: logger,
|
||||
allocationExecutor: allocationExecutor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +121,9 @@ func (h *DeviceImportHandler) HandleDeviceImport(ctx context.Context, task *asyn
|
||||
zap.String("task_no", importTask.TaskNo),
|
||||
zap.String("storage_key", importTask.StorageKey),
|
||||
)
|
||||
if importTask.OperationType != "" && importTask.OperationType != constants.DeviceImportOperationCreate {
|
||||
return h.handleDeviceBatchAllocation(ctx, importTask)
|
||||
}
|
||||
|
||||
parseResult, err := h.downloadAndParse(ctx, importTask)
|
||||
if err != nil {
|
||||
|
||||
31
internal/task/package_expiry_reminder.go
Normal file
31
internal/task/package_expiry_reminder.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
|
||||
packageexpiryapp "github.com/break/junhong_cmp_fiber/internal/application/packageexpiry"
|
||||
)
|
||||
|
||||
// PackageExpiryReminderHandler 处理每日套餐临期节点提醒任务。
|
||||
type PackageExpiryReminderHandler struct {
|
||||
service *packageexpiryapp.ReminderService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewPackageExpiryReminderHandler 创建套餐临期提醒任务处理器。
|
||||
func NewPackageExpiryReminderHandler(service *packageexpiryapp.ReminderService, logger *zap.Logger) *PackageExpiryReminderHandler {
|
||||
return &PackageExpiryReminderHandler{service: service, logger: logger}
|
||||
}
|
||||
|
||||
// Handle 扫描 15、7、3 天节点并发布个人客户站内通知。
|
||||
func (h *PackageExpiryReminderHandler) Handle(ctx context.Context, _ *asynq.Task) error {
|
||||
h.logger.Info("开始执行套餐临期节点提醒")
|
||||
if err := h.service.Run(ctx); err != nil {
|
||||
h.logger.Error("套餐临期节点提醒失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user