feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s

主要变更:
- 新增 AssetIdentifier 模型及 Store,统一管理资产标识符(ICCID/IMEI/SN 等)
- 新增迁移:asset_identifier 表、order 表新增 asset_identifier 字段、iot_card.virtual_no NOT NULL 约束
- 资产 Handler/Service/Route 全面重构,支持标识符路由查询与解析
- 新增资产历史订单查询接口,支持跨设备/卡/钱包维度的订单聚合
- 设备与物联卡导入任务强制校验虚拟号,缺失时直接拒绝
- Excel 工具函数优化,前端导入指引文档同步更新
- 归档三个 OpenSpec 提案:asset-identifier-standardization、asset-historical-orders、import-mandatory-virtual-no
- 更新 OpenAPI 文档及相关 DTO

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-07 17:39:36 +08:00
parent 7e489a19fb
commit 80c6f6c756
69 changed files with 3039 additions and 851 deletions

View File

@@ -0,0 +1,24 @@
package model
import "time"
// AssetIdentifier 全局资产标识符注册表模型
// 在数据库层保证 IoT 卡的 ICCID/VirtualNo 与设备的 VirtualNo 跨两张表全局唯一
// 不使用 gorm.Model无需软删除仅包含核心字段
type AssetIdentifier struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Identifier string `gorm:"column:identifier;type:varchar(100);uniqueIndex:uq_asset_identifier;not null;comment:标识符值ICCID 或 VirtualNo全局唯一" json:"identifier"`
AssetType string `gorm:"column:asset_type;type:varchar(20);not null;comment:资产类型iot_card 或 device" json:"asset_type"`
AssetID uint `gorm:"column:asset_id;index:idx_asset_identifier_asset;not null;comment:对应资产的主键 ID" json:"asset_id"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:now();comment:写入时间" json:"created_at"`
}
// TableName 指定表名
func (AssetIdentifier) TableName() string {
return "tb_asset_identifier"
}
const (
AssetTypeIotCard = "iot_card"
AssetTypeDevice = "device"
)

View File

@@ -6,6 +6,7 @@ import "time"
type AssetResolveResponse struct {
AssetType string `json:"asset_type" description:"资产类型card 或 device"`
AssetID uint `json:"asset_id" description:"资产数据库ID"`
Identifier string `json:"identifier,omitempty" description:"解析时使用的标识符"`
VirtualNo string `json:"virtual_no" description:"虚拟号"`
Status int `json:"status" description:"资产状态"`
BatchNo string `json:"batch_no" description:"批次号"`
@@ -118,7 +119,12 @@ type AssetResolveRequest struct {
Identifier string `path:"identifier" description:"资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN" required:"true"`
}
// AssetTypeIDRequest 资产类型+ID请求路径参数
// AssetIdentifierRequest 资产标识符路径参数请求
type AssetIdentifierRequest struct {
Identifier string `path:"identifier" description:"资产标识符ICCID 或 VirtualNo" required:"true"`
}
// AssetTypeIDRequest 资产类型+ID请求路径参数保留用于钱包等向后兼容场景
type AssetTypeIDRequest struct {
AssetType string `path:"asset_type" description:"资产类型card 或 device" required:"true"`
ID uint `path:"id" description:"资产ID" required:"true"`
@@ -197,12 +203,69 @@ type DeviceGatewayInfo struct {
IMSI *string `json:"imsi,omitempty" description:"IMSI用户标识码"`
}
// AssetOrdersRequest 资产历史订单查询请求
type AssetOrdersRequest struct {
Identifier string `path:"identifier" description:"资产标识符ICCID 或 VirtualNo" required:"true"`
Page int `query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码默认1"`
PageSize int `query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页条数默认20最大100"`
IncludePrevious bool `query:"include_previous" description:"是否包含前代订单默认false"`
}
// AssetOrderItemDetail 订单中的套餐明细
type AssetOrderItemDetail struct {
PackageName string `json:"package_name" description:"套餐名称快照"`
Quantity int `json:"quantity" description:"数量"`
UnitPrice int64 `json:"unit_price" description:"单价(分)"`
Amount int64 `json:"amount" description:"小计金额(分)"`
}
// AssetOrderItem 资产历史订单条目
type AssetOrderItem struct {
OrderNo string `json:"order_no" description:"订单号"`
OrderType string `json:"order_type" description:"订单类型single_card/device"`
PaymentStatus int `json:"payment_status" description:"支付状态1待支付 2已支付 3已取消 4已退款"`
PaymentStatusText string `json:"payment_status_text" description:"支付状态文本"`
TotalAmount int64 `json:"total_amount" description:"订单金额(分)"`
PaymentMethod string `json:"payment_method" description:"支付方式wallet/wechat/alipay/offline"`
PaidAt *time.Time `json:"paid_at,omitempty" description:"支付时间"`
Generation int `json:"generation" description:"订单所属资产世代编号"`
Items []*AssetOrderItemDetail `json:"items" description:"套餐明细列表"`
CreatedAt time.Time `json:"created_at" description:"订单创建时间"`
}
// GenerationOrders 本代订单块(支持分页)
type GenerationOrders struct {
Generation int `json:"generation" description:"世代编号"`
Identifier string `json:"identifier" description:"资产标识符"`
AssetType string `json:"asset_type" description:"资产类型card 或 device"`
Total int64 `json:"total" description:"总订单数"`
Page int `json:"page" description:"当前页码"`
PageSize int `json:"page_size" description:"每页条数"`
Items []*AssetOrderItem `json:"items" description:"订单列表"`
}
// PreviousGenerationOrders 前代订单块最多20条不分页
type PreviousGenerationOrders struct {
Generation int `json:"generation" description:"前代世代编号"`
Identifier string `json:"identifier" description:"前代资产标识符"`
AssetType string `json:"asset_type" description:"前代资产类型card 或 device"`
ExchangeNo string `json:"exchange_no" description:"换货单号"`
ExchangedAt time.Time `json:"exchanged_at" description:"换货完成时间"`
Total int64 `json:"total" description:"前代总订单数"`
Items []*AssetOrderItem `json:"items" description:"前代订单列表最多20条"`
}
// AssetOrdersResponse 资产历史订单完整响应
type AssetOrdersResponse struct {
CurrentGeneration *GenerationOrders `json:"current_generation" description:"本代订单(含分页)"`
PreviousGenerations []*PreviousGenerationOrders `json:"previous_generations,omitempty" description:"前代订单列表include_previous=true时返回"`
Truncated bool `json:"truncated" description:"是否已截断换货链超过10代追溯上限时为true"`
}
// UpdateAssetPollingStatusRequest 更新资产轮询状态请求
type UpdateAssetPollingStatusRequest struct {
AssetType string `path:"asset_type" description:"资产类型 (iot_card:IoT卡, device:设备)" required:"true"`
ID uint `path:"id" description:"资产ID" required:"true"`
// EnablePolling 是否启用轮询
EnablePolling bool `json:"enable_polling" description:"是否启用轮询 (true:启用, false:禁用)"`
Identifier string `path:"identifier" description:"资产标识符ICCID 或 VirtualNo" required:"true"`
EnablePolling bool `json:"enable_polling" description:"是否启用轮询 (true:启用, false:禁用)"`
}
// UpdateAssetPollingStatusResponse 更新资产轮询状态响应

View File

@@ -19,8 +19,7 @@ type AssetWalletResponse struct {
// AssetWalletTransactionListRequest 资产钱包流水列表请求(路径参数 + 查询参数)
type AssetWalletTransactionListRequest struct {
AssetType string `path:"asset_type" description:"资产类型card 或 device" required:"true"`
ID uint `path:"id" description:"资产ID" required:"true"`
Identifier string `path:"identifier" description:"资产标识符ICCID 或 VirtualNo" required:"true"`
Page int `json:"page" query:"page" description:"页码默认1"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" description:"每页数量默认20最大100"`
TransactionType *string `json:"transaction_type" query:"transaction_type" validate:"omitempty,oneof=recharge deduct refund" description:"交易类型过滤recharge/deduct/refund"`

View File

@@ -65,11 +65,11 @@ type GetDeviceByIdentifierRequest struct {
}
type DeleteDeviceRequest struct {
ID uint `path:"id" description:"设备ID" required:"true"`
VirtualNo string `path:"virtual_no" description:"设备虚拟号" required:"true"`
}
type ListDeviceCardsRequest struct {
ID uint `path:"id" description:"设备ID" required:"true"`
VirtualNo string `path:"virtual_no" description:"设备虚拟号" required:"true"`
}
type DeviceCardBindingResponse struct {
@@ -89,9 +89,10 @@ type ListDeviceCardsResponse struct {
}
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)"`
VirtualNo string `path:"virtual_no" description:"设备虚拟号" required:"true"`
ID uint `json:"-"`
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 {
@@ -100,8 +101,8 @@ type BindCardToDeviceResponse struct {
}
type UnbindCardFromDeviceRequest struct {
ID uint `path:"id" description:"设备ID" required:"true"`
CardID uint `path:"cardId" description:"IoT卡ID" required:"true"`
VirtualNo string `path:"virtual_no" description:"设备虚拟号" required:"true"`
ICCID string `path:"iccid" description:"IoT卡ICCID" required:"true"`
}
type UnbindCardFromDeviceResponse struct {

View File

@@ -12,9 +12,7 @@ type CreateOrderRequest struct {
// CreateAdminOrderRequest 后台订单创建请求(仅允许 wallet/offline
type CreateAdminOrderRequest struct {
OrderType string `json:"order_type" validate:"required,oneof=single_card device" required:"true" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
IotCardID *uint `json:"iot_card_id" validate:"required_if=OrderType single_card" description:"IoT卡ID(单卡购买时必填)"`
DeviceID *uint `json:"device_id" validate:"required_if=OrderType device" description:"设备ID(设备购买时必填)"`
Identifier string `json:"identifier" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"资产标识符ICCID 或 VirtualNo"`
PackageIDs []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet offline" required:"true" description:"支付方式 (wallet:钱包支付, offline:线下支付)"`
}
@@ -29,6 +27,7 @@ type OrderListRequest struct {
StartTime *time.Time `json:"start_time" query:"start_time" description:"创建时间起始"`
EndTime *time.Time `json:"end_time" query:"end_time" description:"创建时间结束"`
IsExpired *bool `json:"is_expired" query:"is_expired" description:"是否已过期 (true:已过期, false:未过期)"`
Identifier string `json:"identifier" query:"identifier" validate:"omitempty,max=100" maxLength:"100" description:"资产标识符ICCID 或 VirtualNo精确查询"`
}
type PayOrderRequest struct {
@@ -79,6 +78,10 @@ type OrderResponse struct {
// 订单超时信息
ExpiresAt *time.Time `json:"expires_at,omitempty" description:"订单过期时间"`
IsExpired bool `json:"is_expired" description:"是否已过期"`
// 资产标识符快照
AssetIdentifier string `json:"asset_identifier,omitempty" description:"下单时资产的标识符快照ICCID 或 VirtualNo"`
AssetType string `json:"asset_type,omitempty" description:"资产类型 (card:单卡, device:设备)"`
}
type OrderListResponse struct {

View File

@@ -52,7 +52,7 @@ type IotCard struct {
AssetStatus int `gorm:"column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用" json:"asset_status"`
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
IsStandalone bool `gorm:"column:is_standalone;type:boolean;default:true;not null;comment:是否为独立卡(未绑定设备) 由触发器自动维护" json:"is_standalone"`
VirtualNo string `gorm:"column:virtual_no;type:varchar(50);uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> '';comment:虚拟号(可空,全局唯一)" json:"virtual_no,omitempty"`
VirtualNo string `gorm:"column:virtual_no;type:varchar(50);not null;uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL;comment:虚拟号(必填,全局唯一)" json:"virtual_no"`
LastGatewayReadingMB float64 `gorm:"column:last_gateway_reading_mb;type:float;not null;default:0;comment:上次轮询时网关返回的流量读数(MB)" json:"last_gateway_reading_mb"`
}

View File

@@ -22,8 +22,9 @@ type Order struct {
BuyerID uint `gorm:"column:buyer_id;index:idx_order_buyer;not null;comment:买家ID(个人客户ID或店铺ID)" json:"buyer_id"`
// 关联资源
IotCardID *uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡购买时有值)" json:"iot_card_id,omitempty"`
DeviceID *uint `gorm:"column:device_id;index;comment:设备ID(设备购买时有值)" json:"device_id,omitempty"`
IotCardID *uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡购买时有值)" json:"iot_card_id,omitempty"`
DeviceID *uint `gorm:"column:device_id;index;comment:设备ID(设备购买时有值)" json:"device_id,omitempty"`
AssetIdentifier string `gorm:"column:asset_identifier;type:varchar(100);comment:下单时资产的标识符快照ICCID 或 VirtualNo" json:"asset_identifier,omitempty"`
// 金额信息
TotalAmount int64 `gorm:"column:total_amount;type:bigint;not null;comment:订单总金额(分)" json:"total_amount"`