feat(手机号资产关联): AUG26-009 手机号—资产关联、十项上限与后台解绑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
- 新增成对迁移 000223(tb_phone_asset_association,含有效关系部分唯一索引与 down 守卫)与 000224(解绑导入任务表),不回填历史 - H5:need_bind_phone 三支判定(开关关闭完全短路);已有主号幂等建联;十项上限按手机号 advisory 串行化(含换绑到全新号的并发场景);换绑原子迁移与冲突整单回滚;不写遗留列 - 后台:关联列表、单项/批量解绑、CSV 导入解绑(B1–B16),超管/平台 gate + 资产数据范围复核,三态统一文案 - 读侧:卡/设备列表与详情按页一次 IN 聚合;两类导出补「关联手机号」列并保留历史表头反解兼容 - 脱敏:关联审计走独立动作/资源只写脱敏手机号;访问日志手机号类字段脱敏 - 同步主 Spec openspec/specs/phone-asset-association 并归档 AUG26-009,补齐 requirement-evidence 与入口矩阵,context-health 通过
This commit is contained in:
@@ -37,12 +37,13 @@ type AssetResolveResponse struct {
|
||||
EnableVirtualData bool `json:"enable_virtual_data" description:"当前主套餐是否启用虚流量(按套餐使用记录快照返回)"`
|
||||
DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"设备保护期状态:none/stop/start(仅asset_type=device时有效)"`
|
||||
// 绑定关系字段
|
||||
ICCID string `json:"iccid,omitempty" description:"卡ICCID(asset_type=card时有效)"`
|
||||
BoundDeviceID *uint `json:"bound_device_id,omitempty" description:"绑定的设备ID(asset_type=card时有效)"`
|
||||
BoundDeviceNo string `json:"bound_device_no,omitempty" description:"绑定的设备虚拟号(asset_type=card时有效)"`
|
||||
BoundDeviceName string `json:"bound_device_name,omitempty" description:"绑定的设备名称(asset_type=card时有效)"`
|
||||
BoundCardCount int `json:"bound_card_count,omitempty" description:"绑定的卡数量(asset_type=device时有效)"`
|
||||
Cards []BoundCardInfo `json:"cards,omitempty" description:"绑定的卡列表(asset_type=device时有效)"`
|
||||
AssociatedPhones []string `json:"associated_phones" description:"当前关联手机号列表(按资产数据范围返回完整手机号,无关联时为空数组)"`
|
||||
ICCID string `json:"iccid,omitempty" description:"卡ICCID(asset_type=card时有效)"`
|
||||
BoundDeviceID *uint `json:"bound_device_id,omitempty" description:"绑定的设备ID(asset_type=card时有效)"`
|
||||
BoundDeviceNo string `json:"bound_device_no,omitempty" description:"绑定的设备虚拟号(asset_type=card时有效)"`
|
||||
BoundDeviceName string `json:"bound_device_name,omitempty" description:"绑定的设备名称(asset_type=card时有效)"`
|
||||
BoundCardCount int `json:"bound_card_count,omitempty" description:"绑定的卡数量(asset_type=device时有效)"`
|
||||
Cards []BoundCardInfo `json:"cards,omitempty" description:"绑定的卡列表(asset_type=device时有效)"`
|
||||
// 设备专属字段(card类型时为零值)
|
||||
DeviceName string `json:"device_name,omitempty" description:"设备名称"`
|
||||
IMEI string `json:"imei,omitempty" description:"设备IMEI"`
|
||||
|
||||
@@ -58,11 +58,12 @@ type DeviceResponse struct {
|
||||
LastGatewaySyncAt *time.Time `json:"last_gateway_sync_at" description:"最后 sync-info 同步时间"`
|
||||
// asset_status=3 表示该资产已换货且尚未执行"旧资产转新",不再流通
|
||||
// generation>1 表示该资产历史上曾执行过换货后转新,目前仍在使用
|
||||
AssetStatus int `json:"asset_status" description:"业务状态 (1:在库, 2:已销售, 3:已换货, 4:已停用)"`
|
||||
AssetStatusName string `json:"asset_status_name" description:"业务状态名称(中文)"`
|
||||
Generation int `json:"generation" description:"资产世代编号(初始值1,每次换货转新后+1)"`
|
||||
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" description:"当前有效授权的企业ID,未授权时为 null"`
|
||||
AuthorizedEnterpriseName string `json:"authorized_enterprise_name" description:"当前有效授权的企业名称,未授权时为空字符串"`
|
||||
AssetStatus int `json:"asset_status" description:"业务状态 (1:在库, 2:已销售, 3:已换货, 4:已停用)"`
|
||||
AssetStatusName string `json:"asset_status_name" description:"业务状态名称(中文)"`
|
||||
Generation int `json:"generation" description:"资产世代编号(初始值1,每次换货转新后+1)"`
|
||||
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" description:"当前有效授权的企业ID,未授权时为 null"`
|
||||
AuthorizedEnterpriseName string `json:"authorized_enterprise_name" description:"当前有效授权的企业名称,未授权时为空字符串"`
|
||||
AssociatedPhones []string `json:"associated_phones" description:"当前关联手机号列表(按资产数据范围返回完整手机号,无关联时为空数组)"`
|
||||
}
|
||||
|
||||
type ListDeviceResponse struct {
|
||||
|
||||
@@ -74,6 +74,7 @@ type StandaloneIotCardResponse struct {
|
||||
Generation int `json:"generation" description:"资产世代编号(初始值1,每次换货转新后+1)"`
|
||||
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" description:"当前有效授权的企业ID,未授权时为 null"`
|
||||
AuthorizedEnterpriseName string `json:"authorized_enterprise_name" description:"当前有效授权的企业名称,未授权时为空字符串"`
|
||||
AssociatedPhones []string `json:"associated_phones" description:"当前关联手机号列表(按资产数据范围返回完整手机号,无关联时为空数组)"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
152
internal/model/dto/phone_asset_association_dto.go
Normal file
152
internal/model/dto/phone_asset_association_dto.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// ListPhoneAssetAssociationRequest 后台查询手机号—资产关联列表请求。
|
||||
// 仅超级管理员与平台用户可调用,范围外资产不进入结果集,越权与不存在不形成可枚举差异。
|
||||
type ListPhoneAssetAssociationRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码,默认 1"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量,默认 20,最大 100"`
|
||||
AssetIdentifier string `json:"asset_identifier" query:"asset_identifier" validate:"omitempty,max=100" maxLength:"100" description:"资产标识(ICCID、虚拟号、IMEI、SN 或接入号,精确匹配)"`
|
||||
Phone string `json:"phone" query:"phone" validate:"omitempty,max=20" maxLength:"20" description:"手机号(完整值精确匹配)"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,min=0,max=1" minimum:"0" maximum:"1" description:"关联状态 (0:已失效, 1:有效)"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start" query:"created_at_start" description:"关联创建时间起(含)"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end" query:"created_at_end" description:"关联创建时间止(含)"`
|
||||
}
|
||||
|
||||
// PhoneAssetAssociationResponse 手机号—资产关联列表项。
|
||||
// 手机号为读取范围内的完整值(读侧不脱敏);审计与日志仍只写脱敏值。
|
||||
type PhoneAssetAssociationResponse struct {
|
||||
ID uint `json:"id" description:"关联关系ID"`
|
||||
Phone string `json:"phone" description:"关联手机号(完整值)"`
|
||||
AssetType string `json:"asset_type" description:"资产类型 (iot_card:物联网卡, device:设备)"`
|
||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||
AssetIdentifier string `json:"asset_identifier" description:"资产当前标识(卡为 ICCID,设备为虚拟号)"`
|
||||
Status int `json:"status" description:"关联状态 (0:已失效, 1:有效)"`
|
||||
StatusName string `json:"status_name" description:"关联状态中文名称"`
|
||||
Source string `json:"source" description:"建立来源,固定为 h5_sms_verification"`
|
||||
EstablishedAt string `json:"established_at" description:"建立时间(短信验证通过时间)"`
|
||||
InvalidatedAt string `json:"invalidated_at" description:"失效时间;有效关系为空"`
|
||||
InvalidationMethod string `json:"invalidation_method" description:"失效方式 (backend_single:后台单项, backend_batch:后台批量, csv_import:CSV导入);有效关系为空"`
|
||||
InvalidationMethodName string `json:"invalidation_method_name" description:"失效方式中文名称;有效关系为空"`
|
||||
InvalidationReason string `json:"invalidation_reason" description:"失效原因;有效关系为空"`
|
||||
}
|
||||
|
||||
// PhoneAssetAssociationPageResult 手机号—资产关联列表分页响应。
|
||||
type PhoneAssetAssociationPageResult struct {
|
||||
Items []*PhoneAssetAssociationResponse `json:"items" description:"关联列表"`
|
||||
Total int64 `json:"total" description:"总记录数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
Size int `json:"size" description:"每页数量"`
|
||||
}
|
||||
|
||||
// UnbindPhoneAssetAssociationRequest 单项解除手机号—资产关联请求。
|
||||
// 路径主键即指定一条关系;解除必须二次确认并填写原因,缺一即拒绝。
|
||||
type UnbindPhoneAssetAssociationRequest struct {
|
||||
ID uint `json:"-" path:"id" description:"关联关系ID" required:"true" minimum:"1"`
|
||||
Reason string `json:"reason" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"解除原因(1~500 字符)"`
|
||||
Confirmed bool `json:"confirmed" validate:"required,eq=true" required:"true" description:"二次确认,必须为 true"`
|
||||
}
|
||||
|
||||
// UnbindPhoneAssetAssociationResponse 单项解除结果。
|
||||
type UnbindPhoneAssetAssociationResponse struct {
|
||||
ID uint `json:"id" description:"关联关系ID"`
|
||||
AssetType string `json:"asset_type" description:"资产类型 (iot_card:物联网卡, device:设备)"`
|
||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||
UnboundCount int `json:"unbound_count" description:"本次解除的有效关系数,成功时为 1"`
|
||||
InvalidatedAt string `json:"invalidated_at" description:"失效时间"`
|
||||
}
|
||||
|
||||
// BatchUnbindAssetItem 批量解除中的单项资产。
|
||||
type BatchUnbindAssetItem struct {
|
||||
AssetType string `json:"asset_type" validate:"required,oneof=iot_card device" required:"true" description:"资产类型 (iot_card:物联网卡, device:设备)"`
|
||||
AssetID uint `json:"asset_id" validate:"required,min=1" required:"true" minimum:"1" description:"资产ID"`
|
||||
}
|
||||
|
||||
// BatchUnbindPhoneAssetAssociationRequest 按资产批量解除手机号关联请求。
|
||||
// 每项资产解除其全部当前有效关系,逐资产独立执行,部分成功不回滚成功项。
|
||||
type BatchUnbindPhoneAssetAssociationRequest struct {
|
||||
Assets []BatchUnbindAssetItem `json:"assets" validate:"required,min=1,max=200,dive" required:"true" description:"待解除的资产集合,按 (资产类型, 资产ID) 去重后执行"`
|
||||
Reason string `json:"reason" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"解除原因(1~500 字符)"`
|
||||
Confirmed bool `json:"confirmed" validate:"required,eq=true" required:"true" description:"二次确认,必须为 true"`
|
||||
}
|
||||
|
||||
// BatchUnbindAssetResult 批量解除的逐项结果。
|
||||
type BatchUnbindAssetResult struct {
|
||||
AssetType string `json:"asset_type" description:"资产类型 (iot_card:物联网卡, device:设备)"`
|
||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||
Success bool `json:"success" description:"该项是否解除成功"`
|
||||
UnboundCount int `json:"unbound_count" description:"该项解除的有效关系数"`
|
||||
Reason string `json:"reason,omitempty" description:"该项失败原因,成功时为空"`
|
||||
}
|
||||
|
||||
// CreatePhoneAssetUnbindImportRequest 创建手机号—资产关联 CSV 解绑导入任务请求。
|
||||
type CreatePhoneAssetUnbindImportRequest struct {
|
||||
FileKey string `json:"file_key" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"CSV 对象存储 Key,必须以 phone-unbind-imports/ 开头且扩展名为 .csv(通过 POST /api/admin/storage/upload-url 获取)"`
|
||||
Reason string `json:"reason" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"任务级解绑原因(1~500 字符),写入每次解除的失效原因"`
|
||||
Confirmed bool `json:"confirmed" validate:"required,eq=true" required:"true" description:"二次确认,必须为 true"`
|
||||
}
|
||||
|
||||
// ListPhoneAssetUnbindImportRequest 查询解绑导入任务列表请求。
|
||||
type ListPhoneAssetUnbindImportRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码,默认 1"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量,默认 20,最大 100"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||
}
|
||||
|
||||
// GetPhoneAssetUnbindImportRequest 解绑导入任务详情路径参数。
|
||||
type GetPhoneAssetUnbindImportRequest struct {
|
||||
ID uint `path:"id" required:"true" description:"导入任务ID"`
|
||||
}
|
||||
|
||||
// PhoneAssetUnbindImportTaskResponse 解绑导入任务响应。
|
||||
type PhoneAssetUnbindImportTaskResponse struct {
|
||||
ID uint `json:"id" description:"导入任务ID"`
|
||||
TaskNo string `json:"task_no" description:"导入任务编号"`
|
||||
FileName string `json:"file_name" description:"上传的源 CSV 文件名"`
|
||||
UnbindReason string `json:"unbind_reason" description:"任务级解绑原因"`
|
||||
Status int `json:"status" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||
StatusName string `json:"status_name" description:"任务状态中文名称"`
|
||||
TotalCount int `json:"total_count" description:"数据行总数;任务级失败时为 0"`
|
||||
SuccessCount int `json:"success_count" description:"成功行数"`
|
||||
FailCount int `json:"fail_count" description:"失败行数"`
|
||||
ErrorMessage string `json:"error_message" description:"任务级失败原因;与行级失败原因分开记录"`
|
||||
CreatorName string `json:"creator_name" description:"任务创建人名称快照"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
StartedAt string `json:"started_at" description:"开始处理时间"`
|
||||
CompletedAt string `json:"completed_at" description:"完成时间"`
|
||||
}
|
||||
|
||||
// PhoneAssetUnbindImportTaskPageResult 解绑导入任务分页响应。
|
||||
type PhoneAssetUnbindImportTaskPageResult struct {
|
||||
Items []*PhoneAssetUnbindImportTaskResponse `json:"items" description:"导入任务列表"`
|
||||
Total int64 `json:"total" description:"总记录数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
Size int `json:"size" description:"每页数量"`
|
||||
}
|
||||
|
||||
// PhoneAssetUnbindImportItemResponse 解绑导入逐行结果。
|
||||
// AssociatedPhones 是解绑当时的完整手机号快照:关系已失效,只有快照能事后展示被解绑的手机号。
|
||||
type PhoneAssetUnbindImportItemResponse struct {
|
||||
Line int `json:"line" description:"行号,自数据首行起计(表头不计入)"`
|
||||
AssetType string `json:"asset_type" description:"资产类型 (iot_card:物联网卡, device:设备);未定位到资产时为空"`
|
||||
AssetIdentifier string `json:"asset_identifier" description:"导入文件中的资产标识原文"`
|
||||
AssetID uint `json:"asset_id" description:"资产ID;未定位到资产时为 0"`
|
||||
UnboundCount int `json:"unbound_count" description:"该行解除的有效关系数"`
|
||||
AssociatedPhones []string `json:"associated_phones" description:"解绑当时的完整关联手机号快照;失败行为空数组"`
|
||||
Status int `json:"status" description:"行状态 (3:成功, 4:失败)"`
|
||||
StatusName string `json:"status_name" description:"行状态中文名称"`
|
||||
Reason string `json:"reason" description:"失败原因;成功行为空"`
|
||||
}
|
||||
|
||||
// PhoneAssetUnbindImportTaskDetailResponse 解绑导入任务详情响应。
|
||||
type PhoneAssetUnbindImportTaskDetailResponse struct {
|
||||
PhoneAssetUnbindImportTaskResponse
|
||||
Items []PhoneAssetUnbindImportItemResponse `json:"items" description:"逐行结果明细;任务级失败时为空数组"`
|
||||
}
|
||||
|
||||
type BatchUnbindPhoneAssetAssociationResponse struct {
|
||||
SuccessCount int `json:"success_count" description:"解除成功的资产项数"`
|
||||
FailCount int `json:"fail_count" description:"解除失败的资产项数"`
|
||||
Items []BatchUnbindAssetResult `json:"items" description:"逐项结果,顺序与请求去重后的资产集合一致"`
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package dto
|
||||
type GetUploadURLRequest struct {
|
||||
FileName string `json:"file_name" validate:"required,min=1,max=255" required:"true" minLength:"1" maxLength:"255" description:"文件名(如:cards.csv)"`
|
||||
ContentType string `json:"content_type" validate:"omitempty,max=100" maxLength:"100" description:"文件 MIME 类型(如:text/csv),留空则自动推断"`
|
||||
Purpose string `json:"purpose" validate:"required,oneof=iot_import export attachment batch_purchase device_batch_allocation shop_import" required:"true" enum:"iot_import,export,attachment,batch_purchase,device_batch_allocation,shop_import" description:"文件用途 (iot_import:ICCID导入, export:数据导出, attachment:附件, batch_purchase:资产套餐批量订购CSV, device_batch_allocation:设备批量分配或回收CSV, shop_import:店铺负责人导入CSV)"`
|
||||
Purpose string `json:"purpose" validate:"required,oneof=iot_import export attachment batch_purchase device_batch_allocation shop_import phone_unbind_import" required:"true" enum:"iot_import,export,attachment,batch_purchase,device_batch_allocation,shop_import,phone_unbind_import" description:"文件用途 (iot_import:ICCID导入, export:数据导出, attachment:附件, batch_purchase:资产套餐批量订购CSV, device_batch_allocation:设备批量分配或回收CSV, shop_import:店铺负责人导入CSV, phone_unbind_import:手机号资产解绑导入CSV)"`
|
||||
}
|
||||
|
||||
type GetUploadURLResponse struct {
|
||||
|
||||
Reference in New Issue
Block a user