feat: 实现设备管理和设备导入功能,修复测试问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s

主要变更:
- 实现设备管理模块(创建、查询、列表、更新状态、删除)
- 实现设备批量导入功能(CSV 解析、ICCID 绑定、异步任务处理)
- 添加设备-SIM 卡绑定约束(部分唯一索引防止并发问题)
- 修复 fee_rate 数据库字段类型(numeric -> bigint)
- 修复测试数据隔离问题(基于增量断言)
- 修复集成测试中间件顺序问题
- 清理无用测试文件(PersonalCustomer、Email 相关)
- 归档 enterprise-card-authorization 变更
This commit is contained in:
2026-01-26 18:05:12 +08:00
parent fdcff33058
commit ce0783f96e
68 changed files with 6400 additions and 1482 deletions

View File

@@ -0,0 +1,172 @@
package device
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"gorm.io/gorm"
)
func (s *Service) ListBindings(ctx context.Context, deviceID uint) (*dto.ListDeviceCardsResponse, error) {
device, err := s.deviceStore.GetByID(ctx, deviceID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "设备不存在")
}
return nil, err
}
bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, device.ID)
if err != nil {
return nil, err
}
cardIDs := make([]uint, 0, len(bindings))
for _, binding := range bindings {
cardIDs = append(cardIDs, binding.IotCardID)
}
var cards []*model.IotCard
if len(cardIDs) > 0 {
cards, err = s.iotCardStore.GetByIDs(ctx, cardIDs)
if err != nil {
return nil, err
}
}
cardMap := make(map[uint]*model.IotCard)
for _, card := range cards {
cardMap[card.ID] = card
}
carrierMap := s.loadCarrierData(ctx, cards)
responses := make([]*dto.DeviceCardBindingResponse, 0, len(bindings))
for _, binding := range bindings {
card := cardMap[binding.IotCardID]
if card == nil {
continue
}
resp := &dto.DeviceCardBindingResponse{
ID: binding.ID,
SlotPosition: binding.SlotPosition,
IotCardID: binding.IotCardID,
ICCID: card.ICCID,
MSISDN: card.MSISDN,
CarrierName: carrierMap[card.CarrierID],
Status: card.Status,
BindTime: binding.BindTime,
}
responses = append(responses, resp)
}
return &dto.ListDeviceCardsResponse{
Bindings: responses,
}, nil
}
func (s *Service) BindCard(ctx context.Context, deviceID uint, req *dto.BindCardToDeviceRequest) (*dto.BindCardToDeviceResponse, error) {
device, err := s.deviceStore.GetByID(ctx, deviceID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "设备不存在")
}
return nil, err
}
if req.SlotPosition > device.MaxSimSlots {
return nil, errors.New(errors.CodeInvalidParam, "插槽位置超出设备最大插槽数")
}
existingBinding, err := s.deviceSimBindingStore.GetByDeviceAndSlot(ctx, device.ID, req.SlotPosition)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if existingBinding != nil {
return nil, errors.New(errors.CodeConflict, "该插槽已有绑定的卡")
}
card, err := s.iotCardStore.GetByID(ctx, req.IotCardID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeIotCardNotFound)
}
return nil, err
}
activeBinding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if activeBinding != nil {
return nil, errors.New(errors.CodeIotCardBoundToDevice, "该卡已绑定到其他设备")
}
binding := &model.DeviceSimBinding{
DeviceID: device.ID,
IotCardID: card.ID,
SlotPosition: req.SlotPosition,
BindStatus: 1,
}
if err := s.deviceSimBindingStore.Create(ctx, binding); err != nil {
return nil, err
}
return &dto.BindCardToDeviceResponse{
BindingID: binding.ID,
Message: "绑定成功",
}, nil
}
func (s *Service) UnbindCard(ctx context.Context, deviceID uint, cardID uint) (*dto.UnbindCardFromDeviceResponse, error) {
device, err := s.deviceStore.GetByID(ctx, deviceID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "设备不存在")
}
return nil, err
}
binding, err := s.deviceSimBindingStore.GetByDeviceAndCard(ctx, device.ID, cardID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "该卡未绑定到此设备")
}
return nil, err
}
if err := s.deviceSimBindingStore.Unbind(ctx, binding.ID); err != nil {
return nil, err
}
return &dto.UnbindCardFromDeviceResponse{
Message: "解绑成功",
}, nil
}
func (s *Service) loadCarrierData(ctx context.Context, cards []*model.IotCard) map[uint]string {
carrierIDs := make([]uint, 0)
carrierIDSet := make(map[uint]bool)
for _, card := range cards {
if card.CarrierID > 0 && !carrierIDSet[card.CarrierID] {
carrierIDs = append(carrierIDs, card.CarrierID)
carrierIDSet[card.CarrierID] = true
}
}
carrierMap := make(map[uint]string)
if len(carrierIDs) > 0 {
var carriers []model.Carrier
s.db.WithContext(ctx).Where("id IN ?", carrierIDs).Find(&carriers)
for _, c := range carriers {
carrierMap[c.ID] = c.CarrierName
}
}
return carrierMap
}

View File

@@ -0,0 +1,551 @@
package device
import (
"context"
"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"
"gorm.io/gorm"
)
type Service struct {
db *gorm.DB
deviceStore *postgres.DeviceStore
deviceSimBindingStore *postgres.DeviceSimBindingStore
iotCardStore *postgres.IotCardStore
shopStore *postgres.ShopStore
assetAllocationRecordStore *postgres.AssetAllocationRecordStore
}
func New(
db *gorm.DB,
deviceStore *postgres.DeviceStore,
deviceSimBindingStore *postgres.DeviceSimBindingStore,
iotCardStore *postgres.IotCardStore,
shopStore *postgres.ShopStore,
assetAllocationRecordStore *postgres.AssetAllocationRecordStore,
) *Service {
return &Service{
db: db,
deviceStore: deviceStore,
deviceSimBindingStore: deviceSimBindingStore,
iotCardStore: iotCardStore,
shopStore: shopStore,
assetAllocationRecordStore: assetAllocationRecordStore,
}
}
// List 获取设备列表
func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.ListDeviceResponse, 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.DeviceNo != "" {
filters["device_no"] = req.DeviceNo
}
if req.DeviceName != "" {
filters["device_name"] = req.DeviceName
}
if req.Status != nil {
filters["status"] = *req.Status
}
if req.ShopID != nil {
filters["shop_id"] = req.ShopID
}
if req.BatchNo != "" {
filters["batch_no"] = req.BatchNo
}
if req.DeviceType != "" {
filters["device_type"] = req.DeviceType
}
if req.Manufacturer != "" {
filters["manufacturer"] = req.Manufacturer
}
if req.CreatedAtStart != nil {
filters["created_at_start"] = *req.CreatedAtStart
}
if req.CreatedAtEnd != nil {
filters["created_at_end"] = *req.CreatedAtEnd
}
devices, total, err := s.deviceStore.List(ctx, opts, filters)
if err != nil {
return nil, err
}
shopMap := s.loadShopData(ctx, devices)
bindingCounts, err := s.getBindingCounts(ctx, s.extractDeviceIDs(devices))
if err != nil {
return nil, err
}
list := make([]*dto.DeviceResponse, 0, len(devices))
for _, device := range devices {
item := s.toDeviceResponse(device, shopMap, bindingCounts)
list = append(list, item)
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.ListDeviceResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
// Get 获取设备详情
func (s *Service) Get(ctx context.Context, id uint) (*dto.DeviceResponse, error) {
device, err := s.deviceStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "设备不存在")
}
return nil, err
}
shopMap := s.loadShopData(ctx, []*model.Device{device})
bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID})
if err != nil {
return nil, err
}
return s.toDeviceResponse(device, shopMap, bindingCounts), nil
}
func (s *Service) Delete(ctx context.Context, id uint) error {
device, err := s.deviceStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "设备不存在")
}
return err
}
if err := s.deviceSimBindingStore.UnbindByDeviceID(ctx, device.ID); err != nil {
return err
}
return s.deviceStore.Delete(ctx, id)
}
// AllocateDevices 批量分配设备
func (s *Service) AllocateDevices(ctx context.Context, req *dto.AllocateDevicesRequest, operatorID uint, operatorShopID *uint) (*dto.AllocateDevicesResponse, error) {
// 验证目标店铺是否为直属下级
if err := s.validateDirectSubordinate(ctx, operatorShopID, req.TargetShopID); err != nil {
return nil, err
}
devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
if err != nil {
return nil, err
}
if len(devices) == 0 {
return &dto.AllocateDevicesResponse{
SuccessCount: 0,
FailCount: 0,
FailedItems: []dto.AllocationDeviceFailedItem{},
}, nil
}
var deviceIDs []uint
var failedItems []dto.AllocationDeviceFailedItem
isPlatform := operatorShopID == nil
for _, device := range devices {
// 平台只能分配 shop_id=NULL 的设备
if isPlatform && device.ShopID != nil {
failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
DeviceID: device.ID,
DeviceNo: device.DeviceNo,
Reason: "平台只能分配库存设备",
})
continue
}
// 代理只能分配自己店铺的设备
if !isPlatform && (device.ShopID == nil || *device.ShopID != *operatorShopID) {
failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
DeviceID: device.ID,
DeviceNo: device.DeviceNo,
Reason: "设备不属于当前店铺",
})
continue
}
deviceIDs = append(deviceIDs, device.ID)
}
if len(deviceIDs) == 0 {
return &dto.AllocateDevicesResponse{
SuccessCount: 0,
FailCount: len(failedItems),
FailedItems: failedItems,
}, nil
}
newStatus := constants.DeviceStatusDistributed
targetShopID := req.TargetShopID
err = s.db.Transaction(func(tx *gorm.DB) error {
txDeviceStore := postgres.NewDeviceStore(tx, nil)
txCardStore := postgres.NewIotCardStore(tx, nil)
txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil)
if err := txDeviceStore.BatchUpdateShopIDAndStatus(ctx, deviceIDs, &targetShopID, newStatus); err != nil {
return err
}
boundCardIDs, err := s.deviceSimBindingStore.GetBoundCardIDsByDeviceIDs(ctx, deviceIDs)
if err != nil {
return err
}
if len(boundCardIDs) > 0 {
if err := txCardStore.BatchUpdateShopIDAndStatus(ctx, boundCardIDs, &targetShopID, constants.IotCardStatusDistributed); err != nil {
return err
}
}
allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate)
records := s.buildAllocationRecords(devices, deviceIDs, operatorShopID, targetShopID, operatorID, allocationNo, req.Remark)
return txRecordStore.BatchCreate(ctx, records)
})
if err != nil {
return nil, err
}
return &dto.AllocateDevicesResponse{
SuccessCount: len(deviceIDs),
FailCount: len(failedItems),
FailedItems: failedItems,
}, nil
}
// RecallDevices 批量回收设备
func (s *Service) RecallDevices(ctx context.Context, req *dto.RecallDevicesRequest, operatorID uint, operatorShopID *uint) (*dto.RecallDevicesResponse, error) {
devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
if err != nil {
return nil, err
}
if len(devices) == 0 {
return &dto.RecallDevicesResponse{
SuccessCount: 0,
FailCount: 0,
FailedItems: []dto.AllocationDeviceFailedItem{},
}, nil
}
var deviceIDs []uint
var failedItems []dto.AllocationDeviceFailedItem
isPlatform := operatorShopID == nil
for _, device := range devices {
// 验证设备所属店铺是否为直属下级
if device.ShopID == nil {
failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
DeviceID: device.ID,
DeviceNo: device.DeviceNo,
Reason: "设备已在平台库存中",
})
continue
}
// 验证直属下级关系
if err := s.validateDirectSubordinate(ctx, operatorShopID, *device.ShopID); err != nil {
failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
DeviceID: device.ID,
DeviceNo: device.DeviceNo,
Reason: "只能回收直属下级店铺的设备",
})
continue
}
deviceIDs = append(deviceIDs, device.ID)
}
if len(deviceIDs) == 0 {
return &dto.RecallDevicesResponse{
SuccessCount: 0,
FailCount: len(failedItems),
FailedItems: failedItems,
}, nil
}
var newShopID *uint
var newStatus int
if isPlatform {
newShopID = nil
newStatus = constants.DeviceStatusInStock
} else {
newShopID = operatorShopID
newStatus = constants.DeviceStatusDistributed
}
err = s.db.Transaction(func(tx *gorm.DB) error {
txDeviceStore := postgres.NewDeviceStore(tx, nil)
txCardStore := postgres.NewIotCardStore(tx, nil)
txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil)
if err := txDeviceStore.BatchUpdateShopIDAndStatus(ctx, deviceIDs, newShopID, newStatus); err != nil {
return err
}
boundCardIDs, err := s.deviceSimBindingStore.GetBoundCardIDsByDeviceIDs(ctx, deviceIDs)
if err != nil {
return err
}
if len(boundCardIDs) > 0 {
var cardStatus int
if isPlatform {
cardStatus = constants.IotCardStatusInStock
} else {
cardStatus = constants.IotCardStatusDistributed
}
if err := txCardStore.BatchUpdateShopIDAndStatus(ctx, boundCardIDs, newShopID, cardStatus); err != nil {
return err
}
}
allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall)
records := s.buildRecallRecords(devices, deviceIDs, operatorShopID, newShopID, operatorID, allocationNo, req.Remark)
return txRecordStore.BatchCreate(ctx, records)
})
if err != nil {
return nil, err
}
return &dto.RecallDevicesResponse{
SuccessCount: len(deviceIDs),
FailCount: len(failedItems),
FailedItems: failedItems,
}, nil
}
// 辅助方法
func (s *Service) validateDirectSubordinate(ctx context.Context, operatorShopID *uint, targetShopID uint) error {
if operatorShopID != nil && *operatorShopID == targetShopID {
return errors.ErrCannotAllocateToSelf
}
targetShop, err := s.shopStore.GetByID(ctx, targetShopID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeShopNotFound)
}
return err
}
if operatorShopID == nil {
if targetShop.ParentID != nil {
return errors.ErrNotDirectSubordinate
}
} else {
if targetShop.ParentID == nil || *targetShop.ParentID != *operatorShopID {
return errors.ErrNotDirectSubordinate
}
}
return nil
}
func (s *Service) loadShopData(ctx context.Context, devices []*model.Device) map[uint]string {
shopIDs := make([]uint, 0)
shopIDSet := make(map[uint]bool)
for _, device := range devices {
if device.ShopID != nil && *device.ShopID > 0 && !shopIDSet[*device.ShopID] {
shopIDs = append(shopIDs, *device.ShopID)
shopIDSet[*device.ShopID] = true
}
}
shopMap := make(map[uint]string)
if len(shopIDs) > 0 {
var shops []model.Shop
s.db.WithContext(ctx).Where("id IN ?", shopIDs).Find(&shops)
for _, shop := range shops {
shopMap[shop.ID] = shop.ShopName
}
}
return shopMap
}
func (s *Service) getBindingCounts(ctx context.Context, deviceIDs []uint) (map[uint]int64, error) {
result := make(map[uint]int64)
if len(deviceIDs) == 0 {
return result, nil
}
bindings, err := s.deviceSimBindingStore.ListByDeviceIDs(ctx, deviceIDs)
if err != nil {
return nil, err
}
for _, binding := range bindings {
result[binding.DeviceID]++
}
return result, nil
}
func (s *Service) extractDeviceIDs(devices []*model.Device) []uint {
ids := make([]uint, len(devices))
for i, device := range devices {
ids[i] = device.ID
}
return ids
}
func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string, bindingCounts map[uint]int64) *dto.DeviceResponse {
resp := &dto.DeviceResponse{
ID: device.ID,
DeviceNo: device.DeviceNo,
DeviceName: device.DeviceName,
DeviceModel: device.DeviceModel,
DeviceType: device.DeviceType,
MaxSimSlots: device.MaxSimSlots,
Manufacturer: device.Manufacturer,
BatchNo: device.BatchNo,
ShopID: device.ShopID,
Status: device.Status,
StatusName: s.getDeviceStatusName(device.Status),
BoundCardCount: int(bindingCounts[device.ID]),
ActivatedAt: device.ActivatedAt,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
}
if device.ShopID != nil && *device.ShopID > 0 {
resp.ShopName = shopMap[*device.ShopID]
}
return resp
}
func (s *Service) getDeviceStatusName(status int) string {
switch status {
case constants.DeviceStatusInStock:
return "在库"
case constants.DeviceStatusDistributed:
return "已分销"
case constants.DeviceStatusActivated:
return "已激活"
case constants.DeviceStatusSuspended:
return "已停用"
default:
return "未知"
}
}
func (s *Service) buildAllocationRecords(devices []*model.Device, successDeviceIDs []uint, fromShopID *uint, toShopID uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord {
successIDSet := make(map[uint]bool)
for _, id := range successDeviceIDs {
successIDSet[id] = true
}
var records []*model.AssetAllocationRecord
for _, device := range devices {
if !successIDSet[device.ID] {
continue
}
record := &model.AssetAllocationRecord{
AllocationNo: allocationNo,
AllocationType: constants.AssetAllocationTypeAllocate,
AssetType: constants.AssetTypeDevice,
AssetID: device.ID,
AssetIdentifier: device.DeviceNo,
ToOwnerType: constants.OwnerTypeShop,
ToOwnerID: toShopID,
OperatorID: operatorID,
Remark: remark,
}
if fromShopID == nil {
record.FromOwnerType = constants.OwnerTypePlatform
record.FromOwnerID = nil
} else {
record.FromOwnerType = constants.OwnerTypeShop
record.FromOwnerID = fromShopID
}
records = append(records, record)
}
return records
}
func (s *Service) buildRecallRecords(devices []*model.Device, successDeviceIDs []uint, fromShopID *uint, toShopID *uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord {
successIDSet := make(map[uint]bool)
for _, id := range successDeviceIDs {
successIDSet[id] = true
}
var records []*model.AssetAllocationRecord
for _, device := range devices {
if !successIDSet[device.ID] {
continue
}
record := &model.AssetAllocationRecord{
AllocationNo: allocationNo,
AllocationType: constants.AssetAllocationTypeRecall,
AssetType: constants.AssetTypeDevice,
AssetID: device.ID,
AssetIdentifier: device.DeviceNo,
OperatorID: operatorID,
Remark: remark,
}
if fromShopID == nil {
record.FromOwnerType = constants.OwnerTypePlatform
record.FromOwnerID = nil
} else {
record.FromOwnerType = constants.OwnerTypeShop
record.FromOwnerID = fromShopID
}
if toShopID == nil {
record.ToOwnerType = constants.OwnerTypePlatform
record.ToOwnerID = 0
} else {
record.ToOwnerType = constants.OwnerTypeShop
record.ToOwnerID = *toShopID
}
records = append(records, record)
}
return records
}

View File

@@ -0,0 +1,209 @@
package device_import
import (
"context"
"fmt"
"path/filepath"
"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"
"gorm.io/gorm"
)
type Service struct {
db *gorm.DB
importTaskStore *postgres.DeviceImportTaskStore
queueClient *queue.Client
}
type DeviceImportPayload struct {
TaskID uint `json:"task_id"`
}
func New(db *gorm.DB, importTaskStore *postgres.DeviceImportTaskStore, queueClient *queue.Client) *Service {
return &Service{
db: db,
importTaskStore: importTaskStore,
queueClient: queueClient,
}
}
func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportDeviceRequest) (*dto.ImportDeviceResponse, error) {
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
taskNo := s.importTaskStore.GenerateTaskNo(ctx)
fileName := filepath.Base(req.FileKey)
task := &model.DeviceImportTask{
TaskNo: taskNo,
Status: model.ImportTaskStatusPending,
BatchNo: req.BatchNo,
FileName: fileName,
StorageKey: req.FileKey,
}
task.Creator = userID
task.Updater = userID
if err := s.importTaskStore.Create(ctx, task); err != nil {
return nil, fmt.Errorf("创建导入任务失败: %w", err)
}
payload := DeviceImportPayload{TaskID: task.ID}
err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeDeviceImport, payload)
if err != nil {
s.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "任务入队失败: "+err.Error())
return nil, fmt.Errorf("任务入队失败: %w", err)
}
return &dto.ImportDeviceResponse{
TaskID: task.ID,
TaskNo: taskNo,
Message: "导入任务已创建Worker 将异步处理文件",
}, 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.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))
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.ListDeviceImportTaskResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, 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,
DeviceNo: item.ICCID,
Reason: item.Reason,
})
}
for _, item := range task.FailedItems {
resp.FailedItems = append(resp.FailedItems, &dto.DeviceImportResultItemDTO{
Line: item.Line,
DeviceNo: item.ICCID,
Reason: item.Reason,
})
}
for _, item := range task.WarningItems {
resp.WarningItems = append(resp.WarningItems, &dto.DeviceImportResultItemDTO{
Line: item.Line,
DeviceNo: 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,
Status: task.Status,
StatusText: getStatusText(task.Status),
BatchNo: task.BatchNo,
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,
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 "未知"
}
}

View File

@@ -32,7 +32,7 @@ func New(
}
}
func (s *Service) allocateCardsPreview(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsPreviewReq) (*dto.AllocateCardsPreviewResp, error) {
func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsPreviewReq) (*dto.AllocateCardsPreviewResp, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
@@ -181,7 +181,7 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
}
preview, err := s.allocateCardsPreview(ctx, enterpriseID, &dto.AllocateCardsPreviewReq{ICCIDs: req.ICCIDs})
preview, err := s.AllocateCardsPreview(ctx, enterpriseID, &dto.AllocateCardsPreviewReq{ICCIDs: req.ICCIDs})
if err != nil {
return nil, err
}