feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
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:
24
internal/model/asset_identifier.go
Normal file
24
internal/model/asset_identifier.go
Normal 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"
|
||||
)
|
||||
@@ -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 更新资产轮询状态响应
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user