refactor: 出库时间更名为首次购买套餐时间,换货新资产不再按换货完成时间记录
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 38s

- 字段 outbound_at → first_purchase_at,记录表 tb_asset_outbound_record → tb_asset_first_purchase_record,接口 /outbound-records → /first-purchase-records,筛选参数与导出列同步更名
- 换货后的新资产不因换货写入,在当前世代真正支付套餐订单时才记录;记录来源只保留 order / backfill,去掉换货单字段
- 迁移 000266 重命名列、表、索引与约束;同步 Spec、设计、任务与前端接口变更说明

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
邹进
2026-10-27 16:47:02 +09:00
parent 909f4d0ec2
commit 62ec94adec
46 changed files with 570 additions and 560 deletions

View File

@@ -35,7 +35,7 @@ type Device struct {
FirstRechargeTriggeredBySeriesJSON string `gorm:"column:first_recharge_triggered_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的首充触发状态" json:"-"`
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"`
OutboundAt *time.Time `gorm:"column:outbound_at;comment:出库时间(当前世代首次购买套餐的支付时间)" json:"outbound_at,omitempty"`
FirstPurchaseAt *time.Time `gorm:"column:first_purchase_at;comment:首次购买套餐时间(当前世代第一笔套餐订单的支付时间)" json:"first_purchase_at,omitempty"`
// EnablePolling 是否参与轮询,false 时设备下所有卡不加入轮询队列
EnablePolling bool `gorm:"column:enable_polling;not null;default:true" json:"enable_polling"`
// 以下字段由 Gateway sync-info 接口同步,有离线存储意义

View File

@@ -5,21 +5,21 @@ import "time"
// AssetResolveResponse 统一资产解析响应
type AssetResolveResponse struct {
PackageExpiryEstimate
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:"资产归属状态 (1:在库, 2:已分销;卡仍沿用原卡状态枚举)"`
StatusName string `json:"status_name" description:"资产状态名称(中文)"`
BatchNo string `json:"batch_no" description:"批次号"`
OutboundAt *time.Time `json:"outbound_at,omitempty" description:"出库时间(当前世代首次购买套餐的支付时间)"`
ShopID *uint `json:"shop_id,omitempty" description:"所属店铺ID"`
ShopName string `json:"shop_name,omitempty" description:"所属店铺名称"`
SeriesID *uint `json:"series_id,omitempty" description:"套餐系列ID"`
SeriesName string `json:"series_name,omitempty" description:"套餐系列名称"`
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间(未来准备移除,请勿依赖)"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
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:"资产归属状态 (1:在库, 2:已分销;卡仍沿用原卡状态枚举)"`
StatusName string `json:"status_name" description:"资产状态名称(中文)"`
BatchNo string `json:"batch_no" description:"批次号"`
FirstPurchaseAt *time.Time `json:"first_purchase_at,omitempty" description:"首次购买套餐时间(当前世代第一笔套餐订单的支付时间)"`
ShopID *uint `json:"shop_id,omitempty" description:"所属店铺ID"`
ShopName string `json:"shop_name,omitempty" description:"所属店铺名称"`
SeriesID *uint `json:"series_id,omitempty" description:"套餐系列ID"`
SeriesName string `json:"series_name,omitempty" description:"套餐系列名称"`
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间(未来准备移除,请勿依赖)"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
// 状态聚合字段
ActivationStatus int `json:"activation_status" description:"激活状态:0未激活 1已激活;设备需有生效中主套餐且任意绑定卡已实名"`
ActivationStatusName string `json:"activation_status_name" description:"激活状态名称(中文)"`

View File

@@ -24,8 +24,8 @@ type ListDeviceRequest struct {
Keyword string `json:"keyword" query:"keyword" validate:"omitempty,max=100" maxLength:"100" description:"关键字搜索,匹配虚拟号或IMEI(模糊查询,与virtual_no/imei独立)"`
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" query:"authorized_enterprise_id" description:"按有效授权企业ID过滤(只返回当前授权给该企业的设备)"`
IsAuthorizedToEnterprise *bool `json:"is_authorized_to_enterprise" query:"is_authorized_to_enterprise" description:"企业授权状态过滤 (true:已授权给某企业, false:未授权任何企业)"`
OutboundStart string `json:"outbound_start" query:"outbound_start" description:"出库时间起(RFC3339,含)"`
OutboundEnd string `json:"outbound_end" query:"outbound_end" description:"出库时间止(RFC3339,含)"`
FirstPurchaseStart string `json:"first_purchase_start" query:"first_purchase_start" description:"首次购买套餐时间起(RFC3339,含)"`
FirstPurchaseEnd string `json:"first_purchase_end" query:"first_purchase_end" description:"首次购买套餐时间止(RFC3339,含)"`
}
type DeviceResponse struct {
@@ -68,7 +68,7 @@ type DeviceResponse struct {
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" description:"当前有效授权的企业ID,未授权时为 null"`
AuthorizedEnterpriseName string `json:"authorized_enterprise_name" description:"当前有效授权的企业名称,未授权时为空字符串"`
AssociatedPhones []string `json:"associated_phones" description:"当前关联手机号列表(按资产数据范围返回完整手机号,无关联时为空数组)"`
OutboundAt *time.Time `json:"outbound_at,omitempty" description:"出库时间(当前世代首次购买套餐的支付时间)"`
FirstPurchaseAt *time.Time `json:"first_purchase_at,omitempty" description:"首次购买套餐时间(当前世代第一笔套餐订单的支付时间)"`
}
type ListDeviceResponse struct {

View File

@@ -28,8 +28,8 @@ type ListStandaloneIotCardRequest struct {
RealNameStatus *int `json:"real_name_status" query:"real_name_status" validate:"omitempty,min=0,max=1" minimum:"0" maximum:"1" description:"实名状态 (0:未实名, 1:已实名)"`
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" query:"authorized_enterprise_id" description:"按有效授权企业ID过滤(只返回当前授权给该企业的卡)"`
IsAuthorizedToEnterprise *bool `json:"is_authorized_to_enterprise" query:"is_authorized_to_enterprise" description:"企业授权状态过滤 (true:已授权给某企业, false:未授权任何企业)"`
OutboundStart string `json:"outbound_start" query:"outbound_start" description:"出库时间起(RFC3339,含)"`
OutboundEnd string `json:"outbound_end" query:"outbound_end" description:"出库时间止(RFC3339,含)"`
FirstPurchaseStart string `json:"first_purchase_start" query:"first_purchase_start" description:"首次购买套餐时间起(RFC3339,含)"`
FirstPurchaseEnd string `json:"first_purchase_end" query:"first_purchase_end" description:"首次购买套餐时间止(RFC3339,含)"`
}
type StandaloneIotCardResponse struct {
@@ -60,7 +60,7 @@ type StandaloneIotCardResponse struct {
GatewayCardIMEI string `json:"gateway_card_imei" description:"插拔卡业务 IMEI(由 Gateway 卡状态接口同步,非设备自身 IMEI,无业务含义,仅供查看)"`
DataUsageMB int64 `json:"data_usage_mb" description:"累计流量使用(MB)"`
CurrentMonthUsageMB float64 `json:"current_month_usage_mb" description:"系统自然月累计流量(MB,不等于运营商周期口径)"`
OutboundAt *time.Time `json:"outbound_at,omitempty" description:"出库时间(当前世代首次购买套餐的支付时间;绑定在设备上的卡不记录)"`
FirstPurchaseAt *time.Time `json:"first_purchase_at,omitempty" description:"首次购买套餐时间(当前世代第一笔套餐订单的支付时间;绑定在设备上的卡不记录)"`
LastGatewayReadingMB float64 `json:"last_gateway_reading_mb" description:"运营商当前周期累计流量读数(MB,展示运营商口径时使用)"`
CurrentMonthStartDate *time.Time `json:"current_month_start_date" description:"本月开始日期"`
LastMonthTotalMB float64 `json:"last_month_total_mb" description:"上月流量总量(MB)"`

View File

@@ -420,24 +420,23 @@ type SilencePolicyMatchRequest struct {
PackageID uint `json:"package_id" query:"package_id" validate:"required,min=1" required:"true" description:"套餐 ID"`
}
// AssetOutboundRecordRequest 出库记录查询。
type AssetOutboundRecordRequest struct {
// AssetFirstPurchaseRecordRequest 首次购买套餐记录查询。
type AssetFirstPurchaseRecordRequest struct {
AssetType string `json:"asset_type" params:"asset_type" path:"asset_type" validate:"required,oneof=card device iot_card" required:"true" description:"资产类型 card/iot_card/device"`
ID uint `json:"id" params:"id" path:"id" validate:"required,min=1" required:"true" description:"资产 ID"`
}
// AssetOutboundRecordResponse 出库记录。
type AssetOutboundRecordResponse struct {
// AssetFirstPurchaseRecordResponse 首次购买套餐记录。
type AssetFirstPurchaseRecordResponse struct {
ID uint `json:"id" description:"记录 ID"`
AssetType string `json:"asset_type" description:"资产类型"`
AssetID uint `json:"asset_id" description:"资产 ID"`
AssetIdentifier string `json:"asset_identifier" description:"资产标识快照"`
Generation int `json:"generation" description:"世代"`
OutboundAt time.Time `json:"outbound_at" description:"出库时间"`
Source string `json:"source" description:"来源:order / exchange / backfill"`
FirstPurchaseAt time.Time `json:"first_purchase_at" description:"首次购买套餐时间"`
Source string `json:"source" description:"来源:order 首次购买套餐 / backfill 历史回填"`
SourceName string `json:"source_name" description:"来源名称"`
OrderNo string `json:"order_no,omitempty" description:"订单号"`
ExchangeNo string `json:"exchange_no,omitempty" description:"换货单号"`
EndedAt *time.Time `json:"ended_at,omitempty" description:"该世代结束时间"`
EndReason string `json:"end_reason,omitempty" description:"结束原因:exchange_return 换货回库"`
CreatedAt time.Time `json:"created_at" description:"记录时间"`

View File

@@ -51,7 +51,7 @@ type IotCard struct {
StopReason string `gorm:"column:stop_reason;type:varchar(50);comment:停机原因(traffic_exhausted=流量耗尽,manual=手动停机,arrears=欠费)" json:"stop_reason,omitempty"`
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"`
OutboundAt *time.Time `gorm:"column:outbound_at;comment:出库时间(当前世代首次购买套餐的支付时间)" json:"outbound_at,omitempty"`
FirstPurchaseAt *time.Time `gorm:"column:first_purchase_at;comment:首次购买套餐时间(当前世代第一笔套餐订单的支付时间)" json:"first_purchase_at,omitempty"`
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"`
DeviceVirtualNo string `gorm:"column:device_virtual_no;type:varchar(100);not null;default:'';comment:绑定设备的虚拟号快照(未绑定时为空字符串)" json:"device_virtual_no"`

View File

@@ -218,19 +218,17 @@ func (PackageSilenceExtensionOrder) TableName() string {
return "tb_package_silence_extension_order"
}
// AssetOutboundRecord 资产出库记录:每个世代一条,只新增与填写结束信息。
type AssetOutboundRecord struct {
// AssetFirstPurchaseRecord 资产首次购买套餐记录:每个世代一条,只新增与填写结束信息。
type AssetFirstPurchaseRecord struct {
ID uint `gorm:"column:id;primaryKey" json:"id"`
AssetType string `gorm:"column:asset_type;type:varchar(20);not null" json:"asset_type"`
AssetID uint `gorm:"column:asset_id;not null" json:"asset_id"`
AssetIdentifierSnapshot string `gorm:"column:asset_identifier_snapshot;type:varchar(100);not null;default:''" json:"asset_identifier_snapshot"`
Generation int `gorm:"column:generation;not null" json:"generation"`
OutboundAt time.Time `gorm:"column:outbound_at;not null" json:"outbound_at"`
FirstPurchaseAt time.Time `gorm:"column:first_purchase_at;not null" json:"first_purchase_at"`
Source string `gorm:"column:source;type:varchar(20);not null" json:"source"`
OrderID *uint `gorm:"column:order_id" json:"order_id,omitempty"`
OrderNo string `gorm:"column:order_no;type:varchar(40);not null;default:''" json:"order_no"`
ExchangeOrderID *uint `gorm:"column:exchange_order_id" json:"exchange_order_id,omitempty"`
ExchangeNo string `gorm:"column:exchange_no;type:varchar(64);not null;default:''" json:"exchange_no"`
ShopID *uint `gorm:"column:shop_id" json:"shop_id,omitempty"`
EndedAt *time.Time `gorm:"column:ended_at" json:"ended_at,omitempty"`
EndReason string `gorm:"column:end_reason;type:varchar(40);not null;default:''" json:"end_reason"`
@@ -238,6 +236,6 @@ type AssetOutboundRecord struct {
}
// TableName 指定表名
func (AssetOutboundRecord) TableName() string {
return "tb_asset_outbound_record"
func (AssetFirstPurchaseRecord) TableName() string {
return "tb_asset_first_purchase_record"
}