feat: 实现设备管理和设备导入功能,修复测试问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s
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:
43
internal/model/device_import_task.go
Normal file
43
internal/model/device_import_task.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DeviceImportTask 设备导入任务模型
|
||||
// 记录设备批量导入的任务状态和处理结果
|
||||
// 通过异步任务处理 CSV 文件导入设备并绑定卡
|
||||
type DeviceImportTask struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
TaskNo string `gorm:"column:task_no;type:varchar(50);uniqueIndex:idx_device_import_task_no,where:deleted_at IS NULL;not null;comment:任务编号(唯一)" json:"task_no"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
StorageKey string `gorm:"column:storage_key;type:varchar(500);comment:对象存储文件路径" json:"storage_key"`
|
||||
FileName string `gorm:"column:file_name;type:varchar(255);comment:原始文件名" json:"file_name"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:任务状态 1-待处理 2-处理中 3-已完成 4-失败" json:"status"`
|
||||
TotalCount int `gorm:"column:total_count;type:int;default:0;comment:总记录数" json:"total_count"`
|
||||
SuccessCount int `gorm:"column:success_count;type:int;default:0;comment:成功数" json:"success_count"`
|
||||
SkipCount int `gorm:"column:skip_count;type:int;default:0;comment:跳过数" json:"skip_count"`
|
||||
FailCount int `gorm:"column:fail_count;type:int;default:0;comment:失败数" json:"fail_count"`
|
||||
SkippedItems ImportResultItems `gorm:"column:skipped_items;type:jsonb;comment:跳过记录详情" json:"skipped_items"`
|
||||
FailedItems ImportResultItems `gorm:"column:failed_items;type:jsonb;comment:失败记录详情" json:"failed_items"`
|
||||
WarningCount int `gorm:"column:warning_count;default:0;comment:警告数量(部分成功的设备)" json:"warning_count"`
|
||||
WarningItems ImportResultItems `gorm:"column:warning_items;type:jsonb;comment:警告记录详情" json:"warning_items"`
|
||||
ErrorMessage string `gorm:"column:error_message;type:text;comment:错误信息" json:"error_message"`
|
||||
StartedAt *time.Time `gorm:"column:started_at;comment:开始处理时间" json:"started_at"`
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (DeviceImportTask) TableName() string {
|
||||
return "tb_device_import_task"
|
||||
}
|
||||
|
||||
// DeviceImportResultItem 设备导入结果项
|
||||
type DeviceImportResultItem struct {
|
||||
Line int `json:"line"`
|
||||
DeviceNo string `json:"device_no"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
24
internal/model/device_sim_binding.go
Normal file
24
internal/model/device_sim_binding.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DeviceSimBinding 设备-IoT卡绑定关系模型
|
||||
// 管理设备与 IoT 卡的多对多绑定关系(1 设备绑定 1-4 张 IoT 卡)
|
||||
type DeviceSimBinding struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
DeviceID uint `gorm:"column:device_id;index:idx_device_slot;not null;comment:设备ID" json:"device_id"`
|
||||
IotCardID uint `gorm:"column:iot_card_id;index;not null;comment:IoT卡ID" json:"iot_card_id"`
|
||||
SlotPosition int `gorm:"column:slot_position;type:int;index:idx_device_slot;comment:插槽位置(1, 2, 3, 4)" json:"slot_position"`
|
||||
BindStatus int `gorm:"column:bind_status;type:int;default:1;comment:绑定状态 1-已绑定 2-已解绑" json:"bind_status"`
|
||||
BindTime *time.Time `gorm:"column:bind_time;comment:绑定时间" json:"bind_time"`
|
||||
UnbindTime *time.Time `gorm:"column:unbind_time;comment:解绑时间" json:"unbind_time"`
|
||||
}
|
||||
|
||||
func (DeviceSimBinding) TableName() string {
|
||||
return "tb_device_sim_binding"
|
||||
}
|
||||
120
internal/model/dto/device_dto.go
Normal file
120
internal/model/dto/device_dto.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
type ListDeviceRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
DeviceNo string `json:"device_no" query:"device_no" validate:"omitempty,max=100" maxLength:"100" description:"设备号(模糊查询)"`
|
||||
DeviceName string `json:"device_name" query:"device_name" validate:"omitempty,max=255" maxLength:"255" description:"设备名称(模糊查询)"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" description:"店铺ID (NULL表示平台库存)"`
|
||||
BatchNo string `json:"batch_no" query:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"`
|
||||
DeviceType string `json:"device_type" query:"device_type" validate:"omitempty,max=50" maxLength:"50" description:"设备类型"`
|
||||
Manufacturer string `json:"manufacturer" query:"manufacturer" validate:"omitempty,max=255" maxLength:"255" description:"制造商(模糊查询)"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start" query:"created_at_start" description:"创建时间起始"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end" query:"created_at_end" description:"创建时间结束"`
|
||||
}
|
||||
|
||||
type DeviceResponse struct {
|
||||
ID uint `json:"id" description:"设备ID"`
|
||||
DeviceNo string `json:"device_no" description:"设备号"`
|
||||
DeviceName string `json:"device_name" description:"设备名称"`
|
||||
DeviceModel string `json:"device_model" description:"设备型号"`
|
||||
DeviceType string `json:"device_type" description:"设备类型"`
|
||||
MaxSimSlots int `json:"max_sim_slots" description:"最大插槽数"`
|
||||
Manufacturer string `json:"manufacturer" description:"制造商"`
|
||||
BatchNo string `json:"batch_no" description:"批次号"`
|
||||
ShopID *uint `json:"shop_id,omitempty" description:"店铺ID"`
|
||||
ShopName string `json:"shop_name,omitempty" description:"店铺名称"`
|
||||
Status int `json:"status" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称"`
|
||||
BoundCardCount int `json:"bound_card_count" description:"已绑定卡数量"`
|
||||
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
type ListDeviceResponse struct {
|
||||
List []*DeviceResponse `json:"list" description:"设备列表"`
|
||||
Total int64 `json:"total" description:"总数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
TotalPages int `json:"total_pages" description:"总页数"`
|
||||
}
|
||||
|
||||
type GetDeviceRequest struct {
|
||||
ID uint `path:"id" description:"设备ID" required:"true"`
|
||||
}
|
||||
|
||||
type DeleteDeviceRequest struct {
|
||||
ID uint `path:"id" description:"设备ID" required:"true"`
|
||||
}
|
||||
|
||||
type ListDeviceCardsRequest struct {
|
||||
ID uint `path:"id" description:"设备ID" required:"true"`
|
||||
}
|
||||
|
||||
type DeviceCardBindingResponse struct {
|
||||
ID uint `json:"id" description:"绑定记录ID"`
|
||||
SlotPosition int `json:"slot_position" description:"插槽位置 (1-4)"`
|
||||
IotCardID uint `json:"iot_card_id" description:"IoT卡ID"`
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
MSISDN string `json:"msisdn,omitempty" description:"接入号"`
|
||||
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
|
||||
Status int `json:"status" description:"卡状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
BindTime *time.Time `json:"bind_time,omitempty" description:"绑定时间"`
|
||||
}
|
||||
|
||||
type ListDeviceCardsResponse struct {
|
||||
Bindings []*DeviceCardBindingResponse `json:"bindings" description:"绑定列表"`
|
||||
}
|
||||
|
||||
type BindCardToDeviceRequest struct {
|
||||
ID uint `path:"id" description:"设备ID" required:"true"`
|
||||
IotCardID uint `json:"iot_card_id" validate:"required,min=1" required:"true" minimum:"1" description:"IoT卡ID"`
|
||||
SlotPosition int `json:"slot_position" validate:"required,min=1,max=4" required:"true" minimum:"1" maximum:"4" description:"插槽位置 (1-4)"`
|
||||
}
|
||||
|
||||
type BindCardToDeviceResponse struct {
|
||||
BindingID uint `json:"binding_id" description:"绑定记录ID"`
|
||||
Message string `json:"message" description:"提示信息"`
|
||||
}
|
||||
|
||||
type UnbindCardFromDeviceRequest struct {
|
||||
ID uint `path:"id" description:"设备ID" required:"true"`
|
||||
CardID uint `path:"cardId" description:"IoT卡ID" required:"true"`
|
||||
}
|
||||
|
||||
type UnbindCardFromDeviceResponse struct {
|
||||
Message string `json:"message" description:"提示信息"`
|
||||
}
|
||||
|
||||
type AllocateDevicesRequest struct {
|
||||
TargetShopID uint `json:"target_shop_id" validate:"required,min=1" required:"true" minimum:"1" description:"目标店铺ID"`
|
||||
DeviceIDs []uint `json:"device_ids" validate:"required,min=1,max=100" required:"true" minItems:"1" maxItems:"100" description:"设备ID列表"`
|
||||
Remark string `json:"remark" validate:"omitempty,max=500" maxLength:"500" description:"备注"`
|
||||
}
|
||||
|
||||
type AllocationDeviceFailedItem struct {
|
||||
DeviceID uint `json:"device_id" description:"设备ID"`
|
||||
DeviceNo string `json:"device_no" description:"设备号"`
|
||||
Reason string `json:"reason" description:"失败原因"`
|
||||
}
|
||||
|
||||
type AllocateDevicesResponse struct {
|
||||
SuccessCount int `json:"success_count" description:"成功数量"`
|
||||
FailCount int `json:"fail_count" description:"失败数量"`
|
||||
FailedItems []AllocationDeviceFailedItem `json:"failed_items" description:"失败详情列表"`
|
||||
}
|
||||
|
||||
type RecallDevicesRequest struct {
|
||||
DeviceIDs []uint `json:"device_ids" validate:"required,min=1,max=100" required:"true" minItems:"1" maxItems:"100" description:"设备ID列表"`
|
||||
Remark string `json:"remark" validate:"omitempty,max=500" maxLength:"500" description:"备注"`
|
||||
}
|
||||
|
||||
type RecallDevicesResponse struct {
|
||||
SuccessCount int `json:"success_count" description:"成功数量"`
|
||||
FailCount int `json:"fail_count" description:"失败数量"`
|
||||
FailedItems []AllocationDeviceFailedItem `json:"failed_items" description:"失败详情列表"`
|
||||
}
|
||||
66
internal/model/dto/device_import_dto.go
Normal file
66
internal/model/dto/device_import_dto.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
type ImportDeviceRequest struct {
|
||||
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"`
|
||||
FileKey string `json:"file_key" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"对象存储文件路径(通过 /storage/upload-url 获取)"`
|
||||
}
|
||||
|
||||
type ImportDeviceResponse struct {
|
||||
TaskID uint `json:"task_id" description:"导入任务ID"`
|
||||
TaskNo string `json:"task_no" description:"任务编号"`
|
||||
Message string `json:"message" description:"提示信息"`
|
||||
}
|
||||
|
||||
type ListDeviceImportTaskRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||
BatchNo string `json:"batch_no" query:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号(模糊查询)"`
|
||||
StartTime *time.Time `json:"start_time" query:"start_time" description:"创建时间起始"`
|
||||
EndTime *time.Time `json:"end_time" query:"end_time" description:"创建时间结束"`
|
||||
}
|
||||
|
||||
type DeviceImportTaskResponse struct {
|
||||
ID uint `json:"id" description:"任务ID"`
|
||||
TaskNo string `json:"task_no" description:"任务编号"`
|
||||
Status int `json:"status" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||
StatusText string `json:"status_text" description:"任务状态文本"`
|
||||
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
|
||||
FileName string `json:"file_name,omitempty" description:"文件名"`
|
||||
TotalCount int `json:"total_count" description:"总数"`
|
||||
SuccessCount int `json:"success_count" description:"成功数"`
|
||||
SkipCount int `json:"skip_count" description:"跳过数"`
|
||||
FailCount int `json:"fail_count" description:"失败数"`
|
||||
WarningCount int `json:"warning_count" description:"警告数(部分成功的设备数量)"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty" description:"开始处理时间"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" description:"完成时间"`
|
||||
ErrorMessage string `json:"error_message,omitempty" description:"错误信息"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
}
|
||||
|
||||
type ListDeviceImportTaskResponse struct {
|
||||
List []*DeviceImportTaskResponse `json:"list" description:"任务列表"`
|
||||
Total int64 `json:"total" description:"总数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
TotalPages int `json:"total_pages" description:"总页数"`
|
||||
}
|
||||
|
||||
type DeviceImportResultItemDTO struct {
|
||||
Line int `json:"line" description:"行号"`
|
||||
DeviceNo string `json:"device_no" description:"设备号"`
|
||||
Reason string `json:"reason" description:"原因"`
|
||||
}
|
||||
|
||||
type GetDeviceImportTaskRequest struct {
|
||||
ID uint `path:"id" description:"任务ID" required:"true"`
|
||||
}
|
||||
|
||||
type DeviceImportTaskDetailResponse struct {
|
||||
DeviceImportTaskResponse
|
||||
SkippedItems []*DeviceImportResultItemDTO `json:"skipped_items" description:"跳过记录详情"`
|
||||
FailedItems []*DeviceImportResultItemDTO `json:"failed_items" description:"失败记录详情"`
|
||||
WarningItems []*DeviceImportResultItemDTO `json:"warning_items" description:"警告记录详情(部分成功的设备及其卡绑定失败原因)"`
|
||||
}
|
||||
@@ -62,24 +62,6 @@ func (AgentPackageAllocation) TableName() string {
|
||||
return "tb_agent_package_allocation"
|
||||
}
|
||||
|
||||
// DeviceSimBinding 设备-IoT卡绑定关系模型
|
||||
// 管理设备与 IoT 卡的多对多绑定关系(1 设备绑定 1-4 张 IoT 卡)
|
||||
type DeviceSimBinding struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
DeviceID uint `gorm:"column:device_id;index:idx_device_slot;not null;comment:设备ID" json:"device_id"`
|
||||
IotCardID uint `gorm:"column:iot_card_id;index;not null;comment:IoT卡ID" json:"iot_card_id"`
|
||||
SlotPosition int `gorm:"column:slot_position;type:int;index:idx_device_slot;comment:插槽位置(1, 2, 3, 4)" json:"slot_position"`
|
||||
BindStatus int `gorm:"column:bind_status;type:int;default:1;comment:绑定状态 1-已绑定 2-已解绑" json:"bind_status"`
|
||||
BindTime *time.Time `gorm:"column:bind_time;comment:绑定时间" json:"bind_time"`
|
||||
UnbindTime *time.Time `gorm:"column:unbind_time;comment:解绑时间" json:"unbind_time"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (DeviceSimBinding) TableName() string {
|
||||
return "tb_device_sim_binding"
|
||||
}
|
||||
|
||||
// PackageUsage 套餐使用情况模型
|
||||
// 跟踪单卡套餐和设备级套餐的流量使用
|
||||
type PackageUsage struct {
|
||||
|
||||
Reference in New Issue
Block a user