feat: 实现单卡资产分配与回收功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m45s

- 新增单卡分配/回收 API(支持 ICCID 列表、号段范围、筛选条件三种选卡方式)
- 新增资产分配记录查询 API(支持多条件筛选和分页)
- 新增 AssetAllocationRecord 模型、Store、Service、Handler 完整实现
- 扩展 IotCardStore 新增批量更新、号段查询、筛选查询等方法
- 修复 GORM Callback 处理 slice 类型(BatchCreate)的问题
- 新增完整的单元测试和集成测试
- 同步 OpenSpec 规范并归档 change
This commit is contained in:
2026-01-24 15:46:15 +08:00
parent a924e63e68
commit 194078674a
33 changed files with 2785 additions and 92 deletions

View File

@@ -10,7 +10,7 @@ import (
type AssetAllocationRecord struct {
gorm.Model
BaseModel `gorm:"embedded"`
AllocationNo string `gorm:"column:allocation_no;type:varchar(50);uniqueIndex:uk_asset_allocation_no,where:deleted_at IS NULL;not null;comment:分配单号(唯一" json:"allocation_no"`
AllocationNo string `gorm:"column:allocation_no;type:varchar(50);index;not null;comment:分配单号(同批次相同" json:"allocation_no"`
AllocationType string `gorm:"column:allocation_type;type:varchar(20);index;not null;comment:分配类型 allocate=分配 recall=回收" json:"allocation_type"`
AssetType string `gorm:"column:asset_type;type:varchar(20);index;not null;comment:资产类型 iot_card=物联网卡 device=设备" json:"asset_type"`
AssetID uint `gorm:"column:asset_id;index;not null;comment:资产ID" json:"asset_id"`

View File

@@ -0,0 +1,62 @@
package dto
import "time"
// ListAssetAllocationRecordRequest 分配记录列表请求
type ListAssetAllocationRecordRequest 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:"每页数量"`
AllocationType string `json:"allocation_type" query:"allocation_type" validate:"omitempty,oneof=allocate recall" enum:"allocate,recall" description:"分配类型 (allocate:分配, recall:回收)"`
AssetType string `json:"asset_type" query:"asset_type" validate:"omitempty,oneof=iot_card device" enum:"iot_card,device" description:"资产类型 (iot_card:物联网卡, device:设备)"`
AssetIdentifier string `json:"asset_identifier" query:"asset_identifier" validate:"omitempty,max=50" maxLength:"50" description:"资产标识符ICCID或设备号模糊查询"`
AllocationNo string `json:"allocation_no" query:"allocation_no" validate:"omitempty,max=50" maxLength:"50" description:"分配单号(精确匹配)"`
FromShopID *uint `json:"from_shop_id" query:"from_shop_id" description:"来源店铺ID"`
ToShopID *uint `json:"to_shop_id" query:"to_shop_id" description:"目标店铺ID"`
OperatorID *uint `json:"operator_id" query:"operator_id" description:"操作人ID"`
CreatedAtStart *time.Time `json:"created_at_start" query:"created_at_start" description:"创建时间起始"`
CreatedAtEnd *time.Time `json:"created_at_end" query:"created_at_end" description:"创建时间结束"`
}
// AssetAllocationRecordResponse 分配记录响应
type AssetAllocationRecordResponse struct {
ID uint `json:"id" description:"记录ID"`
AllocationNo string `json:"allocation_no" description:"分配单号"`
AllocationType string `json:"allocation_type" description:"分配类型 (allocate:分配, recall:回收)"`
AllocationName string `json:"allocation_name" description:"分配类型名称"`
AssetType string `json:"asset_type" description:"资产类型 (iot_card:物联网卡, device:设备)"`
AssetTypeName string `json:"asset_type_name" description:"资产类型名称"`
AssetID uint `json:"asset_id" description:"资产ID"`
AssetIdentifier string `json:"asset_identifier" description:"资产标识符ICCID或设备号"`
FromOwnerType string `json:"from_owner_type" description:"来源所有者类型"`
FromOwnerID *uint `json:"from_owner_id,omitempty" description:"来源所有者ID"`
FromOwnerName string `json:"from_owner_name" description:"来源所有者名称"`
ToOwnerType string `json:"to_owner_type" description:"目标所有者类型"`
ToOwnerID uint `json:"to_owner_id" description:"目标所有者ID"`
ToOwnerName string `json:"to_owner_name" description:"目标所有者名称"`
OperatorID uint `json:"operator_id" description:"操作人ID"`
OperatorName string `json:"operator_name" description:"操作人名称"`
Remark string `json:"remark,omitempty" description:"备注"`
RelatedDeviceID *uint `json:"related_device_id,omitempty" description:"关联设备ID"`
RelatedCardCount int `json:"related_card_count,omitempty" description:"关联卡数量"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
}
// ListAssetAllocationRecordResponse 分配记录列表响应
type ListAssetAllocationRecordResponse struct {
List []*AssetAllocationRecordResponse `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:"总页数"`
}
// GetAssetAllocationRecordRequest 获取分配记录详情请求
type GetAssetAllocationRecordRequest struct {
ID uint `json:"-" params:"id" path:"id" validate:"required,min=1" required:"true" minimum:"1" description:"记录ID"`
}
// AssetAllocationRecordDetailResponse 分配记录详情响应
type AssetAllocationRecordDetailResponse struct {
AssetAllocationRecordResponse
RelatedCardIDs []uint `json:"related_card_ids,omitempty" description:"关联卡ID列表"`
}

View File

@@ -0,0 +1,108 @@
package dto
// ========== 选卡方式常量 ==========
const (
// SelectionTypeList ICCID列表选择
SelectionTypeList = "list"
// SelectionTypeRange 号段范围选择
SelectionTypeRange = "range"
// SelectionTypeFilter 筛选条件选择
SelectionTypeFilter = "filter"
)
// ========== 分配请求/响应 ==========
// AllocateStandaloneCardsRequest 分配单卡请求
type AllocateStandaloneCardsRequest struct {
// ToShopID 目标店铺ID必填必须是直属下级
ToShopID uint `json:"to_shop_id" validate:"required,min=1" required:"true" minimum:"1" description:"目标店铺ID"`
// SelectionType 选卡方式(必填)
// list: ICCID列表选择
// range: 号段范围选择
// filter: 筛选条件选择
SelectionType string `json:"selection_type" validate:"required,oneof=list range filter" required:"true" enum:"list,range,filter" description:"选卡方式 (list:ICCID列表, range:号段范围, filter:筛选条件)"`
// ===== selection_type=list 时使用 =====
// ICCIDs ICCID列表最多1000个
ICCIDs []string `json:"iccids" validate:"required_if=SelectionType list,omitempty,max=1000,dive,required,max=20" description:"ICCID列表selection_type=list时必填最多1000个"`
// ===== selection_type=range 时使用 =====
// ICCIDStart 起始ICCID
ICCIDStart string `json:"iccid_start" validate:"required_if=SelectionType range,omitempty,max=20" maxLength:"20" description:"起始ICCIDselection_type=range时必填"`
// ICCIDEnd 结束ICCID
ICCIDEnd string `json:"iccid_end" validate:"required_if=SelectionType range,omitempty,max=20" maxLength:"20" description:"结束ICCIDselection_type=range时必填"`
// ===== selection_type=filter 时使用 =====
// CarrierID 运营商ID
CarrierID *uint `json:"carrier_id" description:"运营商IDselection_type=filter时可选"`
// BatchNo 批次号
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号selection_type=filter时可选"`
// Status 卡状态
Status *int `json:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"卡状态 (1:在库, 2:已分销)selection_type=filter时可选"`
// Remark 备注
Remark string `json:"remark" validate:"omitempty,max=500" maxLength:"500" description:"备注"`
}
// AllocateStandaloneCardsResponse 分配单卡响应
type AllocateStandaloneCardsResponse struct {
// TotalCount 待分配总数
TotalCount int `json:"total_count" description:"待分配总数"`
// SuccessCount 成功数
SuccessCount int `json:"success_count" description:"成功数"`
// FailCount 失败数
FailCount int `json:"fail_count" description:"失败数"`
// AllocationNo 分配单号
AllocationNo string `json:"allocation_no" description:"分配单号"`
// FailedItems 失败项列表
FailedItems []AllocationFailedItem `json:"failed_items" description:"失败项列表"`
}
// AllocationFailedItem 分配失败项
type AllocationFailedItem struct {
// ICCID 卡ICCID
ICCID string `json:"iccid" description:"ICCID"`
// Reason 失败原因
Reason string `json:"reason" description:"失败原因"`
}
// ========== 回收请求/响应 ==========
// RecallStandaloneCardsRequest 回收单卡请求
type RecallStandaloneCardsRequest struct {
// FromShopID 来源店铺ID必填被回收方必须是直属下级
FromShopID uint `json:"from_shop_id" validate:"required,min=1" required:"true" minimum:"1" description:"来源店铺ID被回收方"`
// SelectionType 选卡方式(必填)
SelectionType string `json:"selection_type" validate:"required,oneof=list range filter" required:"true" enum:"list,range,filter" description:"选卡方式 (list:ICCID列表, range:号段范围, filter:筛选条件)"`
// ===== selection_type=list 时使用 =====
ICCIDs []string `json:"iccids" validate:"required_if=SelectionType list,omitempty,max=1000,dive,required,max=20" description:"ICCID列表selection_type=list时必填最多1000个"`
// ===== selection_type=range 时使用 =====
ICCIDStart string `json:"iccid_start" validate:"required_if=SelectionType range,omitempty,max=20" maxLength:"20" description:"起始ICCIDselection_type=range时必填"`
ICCIDEnd string `json:"iccid_end" validate:"required_if=SelectionType range,omitempty,max=20" maxLength:"20" description:"结束ICCIDselection_type=range时必填"`
// ===== selection_type=filter 时使用 =====
CarrierID *uint `json:"carrier_id" description:"运营商IDselection_type=filter时可选"`
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号selection_type=filter时可选"`
// Remark 备注
Remark string `json:"remark" validate:"omitempty,max=500" maxLength:"500" description:"备注"`
}
// RecallStandaloneCardsResponse 回收单卡响应
type RecallStandaloneCardsResponse struct {
// TotalCount 待回收总数
TotalCount int `json:"total_count" description:"待回收总数"`
// SuccessCount 成功数
SuccessCount int `json:"success_count" description:"成功数"`
// FailCount 失败数
FailCount int `json:"fail_count" description:"失败数"`
// AllocationNo 回收单号
AllocationNo string `json:"allocation_no" description:"回收单号"`
// FailedItems 失败项列表
FailedItems []AllocationFailedItem `json:"failed_items" description:"失败项列表"`
}