新增字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s

This commit is contained in:
2026-05-12 14:10:48 +08:00
parent 8f3a68a673
commit a5388446d3
13 changed files with 278 additions and 182 deletions

View File

@@ -20,6 +20,10 @@
后台卡/设备套餐系列绑定接口支持按显式列表、号段范围、筛选条件三种方式批量选择资源。运营人员可按完整筛选结果批量设置或清除套餐系列绑定,不受列表分页勾选限制。
### 设备状态口径
设备 `status` 仅表示归属状态(在库/已分销),业务激活状态通过 `activation_status` 返回;设备需同时具备生效中主套餐和任意已实名绑定卡才视为已激活。
### 三种客户类型
| 客户类型 | 业务特点 | 典型场景 | 钱包归属 |

View File

@@ -25,7 +25,7 @@ type Device struct {
Manufacturer string `gorm:"column:manufacturer;type:varchar(255);comment:制造商" json:"manufacturer"`
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
ShopID *uint `gorm:"column:shop_id;index;comment:店铺ID(NULL=平台库存,有值=店铺所有)" json:"shop_id,omitempty"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
Status int `gorm:"column:status;type:int;default:1;not null;comment:归属状态 1-在库 2-已分销" json:"status"`
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"`
DeviceUsername string `gorm:"column:device_username;type:varchar(100);comment:设备登录用户名" json:"device_username"`
DevicePasswordEncrypted string `gorm:"column:device_password_encrypted;type:varchar(255);comment:设备登录密码(加密)" json:"device_password_encrypted"`

View File

@@ -8,7 +8,7 @@ type AssetResolveResponse struct {
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:已分销, 3:已激活, 4:已停用)"`
Status int `json:"status" description:"资产归属状态 (1:在库, 2:已分销;卡仍沿用原卡状态枚举)"`
StatusName string `json:"status_name" description:"资产状态名称(中文)"`
BatchNo string `json:"batch_no" description:"批次号"`
ShopID *uint `json:"shop_id,omitempty" description:"所属店铺ID"`
@@ -19,6 +19,8 @@ type AssetResolveResponse struct {
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:"激活状态名称(中文)"`
RealNameStatus int `json:"real_name_status" description:"实名状态0未实名 1已实名"`
RealNameAt *time.Time `json:"real_name_at" description:"最近一次完成实名的时间,未实名时为 null"`
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
@@ -55,17 +57,16 @@ type AssetResolveResponse struct {
SwitchMode string `json:"switch_mode,omitempty" description:"切卡模式0=自动1=手动(设备类型时有效)"`
LastGatewaySyncAt *time.Time `json:"last_gateway_sync_at,omitempty" description:"最后 sync-info 同步时间(设备类型时有效)"`
// 卡专属字段device类型时为零值
CarrierID uint `json:"carrier_id,omitempty" description:"运营商ID"`
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型"`
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
MSISDN string `json:"msisdn,omitempty" description:"手机号"`
IMSI string `json:"imsi,omitempty" description:"IMSI"`
CardCategory string `json:"card_category,omitempty" description:"卡业务类型"`
Supplier string `json:"supplier,omitempty" description:"供应商"`
ActivationStatus int `json:"activation_status,omitempty" description:"激活状态"`
EnablePolling bool `json:"enable_polling" description:"是否参与轮询"`
NetworkStatus int `json:"network_status" description:"网络状态0停机 1开机asset_type=card时有效"`
GatewayExtend string `json:"gateway_extend" description:"Gateway 卡状态扩展字段,原样返回上游 extend用于展示运营商侧实际停机原因"`
CarrierID uint `json:"carrier_id,omitempty" description:"运营商ID"`
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型"`
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
MSISDN string `json:"msisdn,omitempty" description:"手机号"`
IMSI string `json:"imsi,omitempty" description:"IMSI"`
CardCategory string `json:"card_category,omitempty" description:"卡业务类型"`
Supplier string `json:"supplier,omitempty" description:"供应商"`
EnablePolling bool `json:"enable_polling" description:"是否参与轮询"`
NetworkStatus int `json:"network_status" description:"网络状态0停机 1开机asset_type=card时有效"`
GatewayExtend string `json:"gateway_extend" description:"Gateway 卡状态扩展字段,原样返回上游 extend用于展示运营商侧实际停机原因"`
}
// BoundCardInfo 设备绑定的卡信息

View File

@@ -18,19 +18,21 @@ type AssetInfoResponse struct {
BoundPhone string `json:"bound_phone" description:"当前登录用户绑定的手机号"`
// === 基础信息(通用) ===
AssetType string `json:"asset_type" description:"资产类型card:卡, device:设备)"`
AssetID uint `json:"asset_id" description:"资产ID"`
Identifier string `json:"identifier" description:"资产标识符"`
VirtualNo string `json:"virtual_no" description:"虚拟号"`
Status int `json:"status" description:"状态1:在库, 2:已分销, 3:已激活, 4:已停用"`
StatusName string `json:"status_name" description:"状态名称(中文)"`
RealNameStatus int `json:"real_name_status" description:"实名状态0:未实名, 1:已实名)"`
RealNameStatusName string `json:"real_name_status_name" description:"实名状态名称(中文)"`
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
CarrierName string `json:"carrier_name" description:"运营商名称"`
Generation string `json:"generation" description:"世代"`
WalletBalance int64 `json:"wallet_balance" description:"钱包余额(分)"`
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
AssetType string `json:"asset_type" description:"资产类型card:卡, device:设备)"`
AssetID uint `json:"asset_id" description:"资产ID"`
Identifier string `json:"identifier" description:"资产标识符"`
VirtualNo string `json:"virtual_no" description:"虚拟号"`
Status int `json:"status" description:"资产归属状态1:在库, 2:已分销;卡仍沿用原卡状态枚举"`
StatusName string `json:"status_name" description:"状态名称(中文)"`
ActivationStatus int `json:"activation_status" description:"激活状态0:未激活, 1:已激活;设备需有生效中主套餐且任意绑定卡已实名)"`
ActivationStatusName string `json:"activation_status_name" description:"激活状态名称(中文)"`
RealNameStatus int `json:"real_name_status" description:"实名状态0:未实名, 1:已实名"`
RealNameStatusName string `json:"real_name_status_name" description:"实名状态名称(中文)"`
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
CarrierName string `json:"carrier_name" description:"运营商名称"`
Generation string `json:"generation" description:"世代"`
WalletBalance int64 `json:"wallet_balance" description:"钱包余额(分)"`
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
// === 套餐信息(通用) ===
CurrentPackage string `json:"current_package" description:"当前套餐名称(无套餐时为空)"`
@@ -57,16 +59,14 @@ type AssetInfoResponse struct {
DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"设备保护期状态none:无, stop:停机保护, start:开机保护)"`
// === 卡专属字段asset_type=card 时有效) ===
ICCID string `json:"iccid,omitempty" description:"卡ICCID"`
MSISDN string `json:"msisdn,omitempty" description:"手机号"`
CarrierID uint `json:"carrier_id,omitempty" description:"运营商ID"`
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型CMCC/CUCC/CTCC/CBN"`
NetworkStatus int `json:"network_status,omitempty" description:"网络状态0:停机, 1:开机)"`
NetworkStatusName string `json:"network_status_name,omitempty" description:"网络状态名称(中文)"`
GatewayExtend string `json:"gateway_extend" description:"Gateway 卡状态扩展字段,原样返回上游 extend用于展示运营商侧实际停机原因"`
ActivationStatus int `json:"activation_status,omitempty" description:"激活状态0:未激活, 1:已激活"`
ActivationStatusName string `json:"activation_status_name,omitempty" description:"激活状态名称(中文)"`
CardCategory string `json:"card_category,omitempty" description:"卡业务类型normal:普通卡, industry:行业卡)"`
ICCID string `json:"iccid,omitempty" description:"卡ICCID"`
MSISDN string `json:"msisdn,omitempty" description:"手机号"`
CarrierID uint `json:"carrier_id,omitempty" description:"运营商ID"`
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型CMCC/CUCC/CTCC/CBN"`
NetworkStatus int `json:"network_status,omitempty" description:"网络状态0:停机, 1:开机)"`
NetworkStatusName string `json:"network_status_name,omitempty" description:"网络状态名称(中文)"`
GatewayExtend string `json:"gateway_extend" description:"Gateway 卡状态扩展字段,原样返回上游 extend用于展示运营商侧实际停机原因"`
CardCategory string `json:"card_category,omitempty" description:"卡业务类型normal:普通卡, industry:行业卡"`
// === 卡绑定设备信息asset_type=card 且绑定了设备时有效) ===
BoundDeviceID *uint `json:"bound_device_id,omitempty" description:"绑定的设备ID"`

View File

@@ -3,43 +3,46 @@ package dto
import "time"
type ListDeviceRequest 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:"每页数量"`
VirtualNo string `json:"virtual_no" query:"virtual_no" validate:"omitempty,max=100" maxLength:"100" description:"虚拟号(模糊查询)"`
DeviceName string `json:"device_name" query:"device_name" validate:"omitempty,max=255" maxLength:"255" description:"设备名称(模糊查询)"`
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
ShopID *uint `json:"shop_id" query:"shop_id" description:"店铺ID兼容旧参数单选NULL表示平台库存"`
ShopIDs []uint `json:"shop_ids" query:"shop_ids" validate:"omitempty,dive,min=1" description:"店铺ID列表多选"`
SeriesID *uint `json:"series_id" query:"series_id" description:"套餐系列ID"`
BatchNo string `json:"batch_no" query:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"`
DeviceType string `json:"device_type" query:"device_type" validate:"omitempty,max=50" maxLength:"50" description:"设备类型"`
Manufacturer string `json:"manufacturer" query:"manufacturer" validate:"omitempty,max=255" maxLength:"255" description:"制造商(模糊查询)"`
CreatedAtStart *time.Time `json:"created_at_start" query:"created_at_start" description:"创建时间起始"`
CreatedAtEnd *time.Time `json:"created_at_end" query:"created_at_end" description:"创建时间结束"`
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:"每页数量"`
VirtualNo string `json:"virtual_no" query:"virtual_no" validate:"omitempty,max=100" maxLength:"100" description:"虚拟号(模糊查询)"`
DeviceName string `json:"device_name" query:"device_name" validate:"omitempty,max=255" maxLength:"255" description:"设备名称(模糊查询)"`
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=2" minimum:"1" maximum:"2" description:"归属状态 (1:在库, 2:已分销)"`
ActivationStatus *int `json:"activation_status" query:"activation_status" validate:"omitempty,min=0,max=1" minimum:"0" maximum:"1" description:"激活状态 (0:未激活, 1:已激活;需有生效中主套餐且任意绑定卡已实名)"`
ShopID *uint `json:"shop_id" query:"shop_id" description:"店铺ID兼容旧参数单选NULL表示平台库存"`
ShopIDs []uint `json:"shop_ids" query:"shop_ids" validate:"omitempty,dive,min=1" description:"店铺ID列表多选"`
SeriesID *uint `json:"series_id" query:"series_id" description:"套餐系列ID"`
BatchNo string `json:"batch_no" query:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"`
DeviceType string `json:"device_type" query:"device_type" validate:"omitempty,max=50" maxLength:"50" description:"设备类型"`
Manufacturer string `json:"manufacturer" query:"manufacturer" validate:"omitempty,max=255" maxLength:"255" description:"制造商(模糊查询)"`
CreatedAtStart *time.Time `json:"created_at_start" query:"created_at_start" description:"创建时间起始"`
CreatedAtEnd *time.Time `json:"created_at_end" query:"created_at_end" description:"创建时间结束"`
}
type DeviceResponse struct {
ID uint `json:"id" description:"设备ID"`
VirtualNo string `json:"virtual_no" description:"设备虚拟号/别名"`
IMEI string `json:"imei" description:"设备IMEI"`
SN string `json:"sn" description:"设备序列号"`
DeviceName string `json:"device_name" description:"设备名称"`
DeviceModel string `json:"device_model" description:"设备型号"`
DeviceType string `json:"device_type" description:"设备类型"`
MaxSimSlots int `json:"max_sim_slots" description:"最大插槽数"`
Manufacturer string `json:"manufacturer" description:"制造商"`
BatchNo string `json:"batch_no" description:"批次号"`
ShopID *uint `json:"shop_id" description:"店铺ID"`
ShopName string `json:"shop_name" description:"店铺名称"`
Status int `json:"status" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
StatusName string `json:"status_name" description:"状态名称"`
BoundCardCount int `json:"bound_card_count" description:"已绑定卡数量"`
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
SeriesName string `json:"series_name" description:"套餐系列名称"`
ActivatedAt *time.Time `json:"activated_at" description:"激活时间"`
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
ID uint `json:"id" description:"设备ID"`
VirtualNo string `json:"virtual_no" description:"设备虚拟号/别名"`
IMEI string `json:"imei" description:"设备IMEI"`
SN string `json:"sn" description:"设备序列号"`
DeviceName string `json:"device_name" description:"设备名称"`
DeviceModel string `json:"device_model" description:"设备型号"`
DeviceType string `json:"device_type" description:"设备类型"`
MaxSimSlots int `json:"max_sim_slots" description:"最大插槽数"`
Manufacturer string `json:"manufacturer" description:"制造商"`
BatchNo string `json:"batch_no" description:"批次号"`
ShopID *uint `json:"shop_id" description:"店铺ID"`
ShopName string `json:"shop_name" description:"店铺名称"`
Status int `json:"status" description:"归属状态 (1:在库, 2:已分销)"`
StatusName string `json:"status_name" description:"归属状态名称(中文)"`
ActivationStatus int `json:"activation_status" description:"激活状态 (0:未激活, 1:已激活;需有生效中主套餐且任意绑定卡已实名)"`
ActivationStatusName string `json:"activation_status_name" description:"激活状态名称(中文)"`
BoundCardCount int `json:"bound_card_count" description:"已绑定卡数量"`
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
SeriesName string `json:"series_name" description:"套餐系列名称"`
ActivatedAt *time.Time `json:"activated_at" description:"激活时间"`
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
// 设备实时同步字段(由 sync-info 接口写入)
OnlineStatus int `json:"online_status" description:"在线状态0未知 1在线 2离线"`
LastOnlineTime *time.Time `json:"last_online_time" description:"最后在线时间"`
@@ -146,21 +149,22 @@ type RecallDevicesResponse struct {
// BatchSetDeviceSeriesBindngRequest 批量设置设备的套餐系列绑定请求
type BatchSetDeviceSeriesBindngRequest struct {
SelectionType string `json:"selection_type" validate:"omitempty,oneof=list range filter" enum:"list,range,filter" description:"选设备方式 (list:设备ID列表, range:设备虚拟号范围, filter:筛选条件不传时兼容旧版设备ID列表)"`
DeviceIDs []uint `json:"device_ids" validate:"omitempty,max=1000,dive,required" maxItems:"1000" description:"设备ID列表selection_type=list时使用最多1000个"`
VirtualNoStart string `json:"virtual_no_start" validate:"omitempty,max=100" maxLength:"100" description:"起始设备虚拟号selection_type=range时使用"`
VirtualNoEnd string `json:"virtual_no_end" validate:"omitempty,max=100" maxLength:"100" description:"结束设备虚拟号selection_type=range时使用"`
VirtualNo string `json:"virtual_no" validate:"omitempty,max=100" maxLength:"100" description:"设备虚拟号模糊查询selection_type=filter时可选"`
DeviceName string `json:"device_name" validate:"omitempty,max=255" maxLength:"255" description:"设备名称模糊查询selection_type=filter时可选"`
Status *int `json:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)selection_type=filter时可选"`
ShopID *uint `json:"shop_id" description:"店铺IDselection_type=filter时可选,兼容旧参数,单选)"`
ShopIDs []uint `json:"shop_ids" validate:"omitempty,dive,min=1" description:"店铺ID列表selection_type=filter时可选选)"`
FilterSeriesID *uint `json:"filter_series_id" description:"当前绑定套餐系列IDselection_type=filter时可选目标套餐系列仍使用series_id"`
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号selection_type=filter时可选"`
DeviceType string `json:"device_type" validate:"omitempty,max=50" maxLength:"50" description:"设备类型selection_type=filter时可选"`
Manufacturer string `json:"manufacturer" validate:"omitempty,max=255" maxLength:"255" description:"制造商模糊查询selection_type=filter时可选"`
CreatedAtStart *time.Time `json:"created_at_start" description:"创建时间起始selection_type=filter时可选"`
CreatedAtEnd *time.Time `json:"created_at_end" description:"创建时间结束selection_type=filter时可选"`
SelectionType string `json:"selection_type" validate:"omitempty,oneof=list range filter" enum:"list,range,filter" description:"选设备方式 (list:设备ID列表, range:设备虚拟号范围, filter:筛选条件不传时兼容旧版设备ID列表)"`
DeviceIDs []uint `json:"device_ids" validate:"omitempty,max=1000,dive,required" maxItems:"1000" description:"设备ID列表selection_type=list时使用最多1000个"`
VirtualNoStart string `json:"virtual_no_start" validate:"omitempty,max=100" maxLength:"100" description:"起始设备虚拟号selection_type=range时使用"`
VirtualNoEnd string `json:"virtual_no_end" validate:"omitempty,max=100" maxLength:"100" description:"结束设备虚拟号selection_type=range时使用"`
VirtualNo string `json:"virtual_no" validate:"omitempty,max=100" maxLength:"100" description:"设备虚拟号模糊查询selection_type=filter时可选"`
DeviceName string `json:"device_name" validate:"omitempty,max=255" maxLength:"255" description:"设备名称模糊查询selection_type=filter时可选"`
Status *int `json:"status" validate:"omitempty,min=1,max=2" minimum:"1" maximum:"2" description:"归属状态 (1:在库, 2:已分销)selection_type=filter时可选"`
ActivationStatus *int `json:"activation_status" validate:"omitempty,min=0,max=1" minimum:"0" maximum:"1" description:"激活状态 (0:未激活, 1:已激活;selection_type=filter时可选)"`
ShopID *uint `json:"shop_id" description:"店铺IDselection_type=filter时可选兼容旧参数,单选)"`
ShopIDs []uint `json:"shop_ids" validate:"omitempty,dive,min=1" description:"店铺ID列表selection_type=filter时可选,多选"`
FilterSeriesID *uint `json:"filter_series_id" description:"当前绑定套餐系列IDselection_type=filter时可选目标套餐系列仍使用series_id"`
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号selection_type=filter时可选"`
DeviceType string `json:"device_type" validate:"omitempty,max=50" maxLength:"50" description:"设备类型selection_type=filter时可选"`
Manufacturer string `json:"manufacturer" validate:"omitempty,max=255" maxLength:"255" description:"制造商模糊查询selection_type=filter时可选"`
CreatedAtStart *time.Time `json:"created_at_start" description:"创建时间起始selection_type=filter时可选"`
CreatedAtEnd *time.Time `json:"created_at_end" description:"创建时间结束selection_type=filter时可选"`
SeriesID uint `json:"series_id" validate:"required,min=0" required:"true" minimum:"0" description:"目标套餐系列ID0表示清除关联"`
}

View File

@@ -367,13 +367,6 @@ func (h *PackageActivationHandler) updateDeviceSuspendedStatus(ctx context.Conte
}
}
if err := tx.WithContext(ctx).
Model(&model.Device{}).
Where("id = ?", deviceID).
Update("status", constants.DeviceStatusSuspended).Error; err != nil {
return err
}
if len(cardIDs) > 0 {
if err := tx.WithContext(ctx).
Model(&model.IotCard{}).

View File

@@ -145,7 +145,7 @@ func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model.
AssetID: device.ID,
VirtualNo: device.VirtualNo,
Status: device.Status,
StatusName: constants.GetIotCardStatusName(device.Status),
StatusName: constants.GetDeviceStatusName(device.Status),
BatchNo: device.BatchNo,
ShopID: device.ShopID,
SeriesID: device.SeriesID,
@@ -226,6 +226,13 @@ func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model.
// 查当前主套餐
s.fillPackageInfo(ctx, resp, "device", device.ID)
activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备激活状态失败")
}
resp.ActivationStatus = activationStatuses[device.ID]
resp.ActivationStatusName = constants.GetActivationStatusName(resp.ActivationStatus)
// 查 shop 名称
s.fillShopName(ctx, resp)
@@ -241,32 +248,33 @@ func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model.
// buildCardResolveResponse 构建卡类型的资产解析响应
func (s *Service) buildCardResolveResponse(ctx context.Context, card *model.IotCard) (*dto.AssetResolveResponse, error) {
resp := &dto.AssetResolveResponse{
AssetType: "card",
AssetID: card.ID,
VirtualNo: card.VirtualNo,
Status: card.Status,
StatusName: constants.GetIotCardStatusName(card.Status),
BatchNo: card.BatchNo,
ShopID: card.ShopID,
SeriesID: card.SeriesID,
ActivatedAt: card.ActivatedAt,
CreatedAt: card.CreatedAt,
UpdatedAt: card.UpdatedAt,
RealNameStatus: card.RealNameStatus,
RealNameAt: card.FirstRealnameAt,
RealnamePolicy: card.RealnamePolicy,
NetworkStatus: card.NetworkStatus,
GatewayExtend: card.GatewayExtend,
ICCID: card.ICCID,
CarrierID: card.CarrierID,
CarrierType: card.CarrierType,
CarrierName: card.CarrierName,
MSISDN: card.MSISDN,
IMSI: card.IMSI,
CardCategory: card.CardCategory,
Supplier: card.Supplier,
ActivationStatus: card.ActivationStatus,
EnablePolling: card.EnablePolling,
AssetType: "card",
AssetID: card.ID,
VirtualNo: card.VirtualNo,
Status: card.Status,
StatusName: constants.GetIotCardStatusName(card.Status),
BatchNo: card.BatchNo,
ShopID: card.ShopID,
SeriesID: card.SeriesID,
ActivatedAt: card.ActivatedAt,
CreatedAt: card.CreatedAt,
UpdatedAt: card.UpdatedAt,
RealNameStatus: card.RealNameStatus,
RealNameAt: card.FirstRealnameAt,
RealnamePolicy: card.RealnamePolicy,
NetworkStatus: card.NetworkStatus,
GatewayExtend: card.GatewayExtend,
ICCID: card.ICCID,
CarrierID: card.CarrierID,
CarrierType: card.CarrierType,
CarrierName: card.CarrierName,
MSISDN: card.MSISDN,
IMSI: card.IMSI,
CardCategory: card.CardCategory,
Supplier: card.Supplier,
ActivationStatus: card.ActivationStatus,
ActivationStatusName: constants.GetActivationStatusName(card.ActivationStatus),
EnablePolling: card.EnablePolling,
}
// 查绑定设备

View File

@@ -93,6 +93,9 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.Li
if req.Status != nil {
filters["status"] = *req.Status
}
if req.ActivationStatus != nil {
filters["activation_status"] = *req.ActivationStatus
}
shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs)
if hasShopIDs {
filters["shop_ids"] = shopIDs
@@ -129,14 +132,19 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.Li
shopMap := s.loadShopData(ctx, devices)
seriesMap := s.loadSeriesNames(ctx, devices)
bindingCounts, err := s.getBindingCounts(ctx, s.extractDeviceIDs(devices))
deviceIDs := s.extractDeviceIDs(devices)
bindingCounts, err := s.getBindingCounts(ctx, deviceIDs)
if err != nil {
return nil, err
}
activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, deviceIDs)
if err != nil {
return nil, err
}
list := make([]*dto.DeviceResponse, 0, len(devices))
for _, device := range devices {
item := s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts)
item := s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses)
list = append(list, item)
}
@@ -183,8 +191,12 @@ func (s *Service) Get(ctx context.Context, id uint) (*dto.DeviceResponse, error)
if err != nil {
return nil, err
}
activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
if err != nil {
return nil, err
}
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts), nil
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses), nil
}
// GetByDeviceNo 通过设备号获取设备详情
@@ -203,8 +215,12 @@ func (s *Service) GetByDeviceNo(ctx context.Context, deviceNo string) (*dto.Devi
if err != nil {
return nil, err
}
activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
if err != nil {
return nil, err
}
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts), nil
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses), nil
}
// GetByIdentifier 通过任意标识符获取设备详情
@@ -224,8 +240,12 @@ func (s *Service) GetByIdentifier(ctx context.Context, identifier string) (*dto.
if err != nil {
return nil, err
}
activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
if err != nil {
return nil, err
}
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts), nil
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses), nil
}
// GetDeviceByIdentifier 通过任意标识符获取设备模型(内部使用,不转为 DTO
@@ -884,31 +904,34 @@ func (s *Service) extractDeviceIDs(devices []*model.Device) []uint {
return ids
}
func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string, seriesMap map[uint]string, bindingCounts map[uint]int64) *dto.DeviceResponse {
func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string, seriesMap map[uint]string, bindingCounts map[uint]int64, activationStatuses map[uint]int) *dto.DeviceResponse {
activationStatus := activationStatuses[device.ID]
resp := &dto.DeviceResponse{
ID: device.ID,
VirtualNo: device.VirtualNo,
IMEI: device.IMEI,
SN: device.SN,
DeviceName: device.DeviceName,
DeviceModel: device.DeviceModel,
DeviceType: device.DeviceType,
MaxSimSlots: device.MaxSimSlots,
Manufacturer: device.Manufacturer,
BatchNo: device.BatchNo,
ShopID: device.ShopID,
Status: device.Status,
StatusName: s.getDeviceStatusName(device.Status),
BoundCardCount: int(bindingCounts[device.ID]),
SeriesID: device.SeriesID,
ActivatedAt: device.ActivatedAt,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
OnlineStatus: device.OnlineStatus,
LastOnlineTime: device.LastOnlineTime,
SoftwareVersion: device.SoftwareVersion,
SwitchMode: device.SwitchMode,
LastGatewaySyncAt: device.LastGatewaySyncAt,
ID: device.ID,
VirtualNo: device.VirtualNo,
IMEI: device.IMEI,
SN: device.SN,
DeviceName: device.DeviceName,
DeviceModel: device.DeviceModel,
DeviceType: device.DeviceType,
MaxSimSlots: device.MaxSimSlots,
Manufacturer: device.Manufacturer,
BatchNo: device.BatchNo,
ShopID: device.ShopID,
Status: device.Status,
StatusName: constants.GetDeviceStatusName(device.Status),
ActivationStatus: activationStatus,
ActivationStatusName: constants.GetActivationStatusName(activationStatus),
BoundCardCount: int(bindingCounts[device.ID]),
SeriesID: device.SeriesID,
ActivatedAt: device.ActivatedAt,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
OnlineStatus: device.OnlineStatus,
LastOnlineTime: device.LastOnlineTime,
SoftwareVersion: device.SoftwareVersion,
SwitchMode: device.SwitchMode,
LastGatewaySyncAt: device.LastGatewaySyncAt,
}
if device.ShopID != nil && *device.ShopID > 0 {
@@ -922,21 +945,6 @@ func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string
return resp
}
func (s *Service) getDeviceStatusName(status int) string {
switch status {
case constants.DeviceStatusInStock:
return "在库"
case constants.DeviceStatusDistributed:
return "已分销"
case constants.DeviceStatusActivated:
return "已激活"
case constants.DeviceStatusSuspended:
return "已停用"
default:
return "未知"
}
}
func (s *Service) buildAllocationRecords(devices []*model.Device, successDeviceIDs []uint, fromShopID *uint, toShopID uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord {
successIDSet := make(map[uint]bool)
for _, id := range successDeviceIDs {
@@ -1312,6 +1320,7 @@ func hasDeviceSeriesBindingFilters(req *dto.BatchSetDeviceSeriesBindngRequest) b
return req.VirtualNo != "" ||
req.DeviceName != "" ||
req.Status != nil ||
req.ActivationStatus != nil ||
req.ShopID != nil ||
len(req.ShopIDs) > 0 ||
req.FilterSeriesID != nil ||
@@ -1355,6 +1364,9 @@ func buildDeviceSeriesBindingFilters(req *dto.BatchSetDeviceSeriesBindngRequest)
if req.Status != nil {
filters["status"] = *req.Status
}
if req.ActivationStatus != nil {
filters["activation_status"] = *req.ActivationStatus
}
shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs)
if hasShopIDs {
filters["shop_ids"] = shopIDs

View File

@@ -604,23 +604,8 @@ func (s *ActivationService) syncCarrierStatusActivated(ctx context.Context, tx *
}
if usage.UsageType == constants.PackageUsageTypeDevice || usage.DeviceID > 0 || carrierType == constants.AssetTypeDevice {
deviceID := usage.DeviceID
if deviceID == 0 {
deviceID = carrierID
}
if deviceID == 0 {
return
}
if err := tx.WithContext(ctx).
Model(&model.Device{}).
Where("id = ? AND status < ?", deviceID, constants.DeviceStatusActivated).
Update("status", constants.DeviceStatusActivated).Error; err != nil {
s.logger.Warn("套餐激活后更新设备状态失败",
zap.Uint("device_id", deviceID),
zap.Uint("usage_id", usage.ID),
zap.Error(err))
}
// 设备激活状态由“生效中主套餐 + 任意绑定卡已实名”动态计算,
// 不再写入设备归属状态字段。
return
}

View File

@@ -143,6 +143,9 @@ func (s *DeviceStore) applyDeviceFilters(query *gorm.DB, filters map[string]any)
if status, ok := filters["status"].(int); ok && status > 0 {
query = query.Where("status = ?", status)
}
if activationStatus, ok := filters["activation_status"].(int); ok {
query = s.applyActivationStatusFilter(query, activationStatus)
}
if shopIDs, ok := filters["shop_ids"].([]uint); ok {
if len(shopIDs) == 0 {
query = query.Where("1 = 0")
@@ -177,6 +180,37 @@ func (s *DeviceStore) applyDeviceFilters(query *gorm.DB, filters map[string]any)
return query
}
func (s *DeviceStore) applyActivationStatusFilter(query *gorm.DB, activationStatus int) *gorm.DB {
activeCondition := `EXISTS (
SELECT 1
FROM tb_package_usage AS pu
JOIN tb_device_sim_binding AS b
ON b.device_id = pu.device_id
AND b.bind_status = ?
AND b.deleted_at IS NULL
JOIN tb_iot_card AS c
ON c.id = b.iot_card_id
AND c.real_name_status = ?
AND c.deleted_at IS NULL
WHERE pu.device_id = tb_device.id
AND pu.status = ?
AND pu.master_usage_id IS NULL
AND pu.deleted_at IS NULL
)`
args := []any{
constants.BindStatusBound,
constants.RealNameStatusVerified,
constants.PackageUsageStatusActive,
}
if activationStatus == constants.ActivationStatusActive {
return query.Where(activeCondition, args...)
}
if activationStatus == constants.ActivationStatusInactive {
return query.Where("NOT "+activeCondition, args...)
}
return query
}
// GetByFilters 根据筛选条件查询设备
// 用于批量设置套餐系列时选择完整筛选结果,不受列表分页限制。
func (s *DeviceStore) GetByFilters(ctx context.Context, filters map[string]any) ([]*model.Device, error) {
@@ -206,6 +240,38 @@ func (s *DeviceStore) BatchUpdateShopIDAndStatus(ctx context.Context, ids []uint
return s.db.WithContext(ctx).Model(&model.Device{}).Where("id IN ?", ids).Updates(updates).Error
}
// GetActivationStatusMap 批量查询设备激活状态。
// 设备激活规则:存在生效中主套餐,且任意已绑定卡已实名。
func (s *DeviceStore) GetActivationStatusMap(ctx context.Context, ids []uint) (map[uint]int, error) {
result := make(map[uint]int, len(ids))
if len(ids) == 0 {
return result, nil
}
for _, id := range ids {
result[id] = constants.ActivationStatusInactive
}
var activeDeviceIDs []uint
err := s.db.WithContext(ctx).
Table("tb_package_usage AS pu").
Distinct("pu.device_id").
Joins("JOIN tb_device_sim_binding AS b ON b.device_id = pu.device_id AND b.bind_status = ? AND b.deleted_at IS NULL", constants.BindStatusBound).
Joins("JOIN tb_iot_card AS c ON c.id = b.iot_card_id AND c.real_name_status = ? AND c.deleted_at IS NULL", constants.RealNameStatusVerified).
Where("pu.device_id IN ?", ids).
Where("pu.status = ?", constants.PackageUsageStatusActive).
Where("pu.master_usage_id IS NULL").
Where("pu.deleted_at IS NULL").
Pluck("pu.device_id", &activeDeviceIDs).Error
if err != nil {
return nil, err
}
for _, id := range activeDeviceIDs {
result[id] = constants.ActivationStatusActive
}
return result, nil
}
func (s *DeviceStore) ExistsByDeviceNoBatch(ctx context.Context, deviceNos []string) (map[string]bool, error) {
result := make(map[string]bool)
if len(deviceNos) == 0 {

View File

@@ -0,0 +1,2 @@
-- 回滚字段注释;历史 status=3/4 数据无法从归属状态无损还原。
COMMENT ON COLUMN tb_device.status IS '状态 1-在库 2-已分销 3-已激活 4-已停用';

View File

@@ -0,0 +1,10 @@
-- 将设备 status 收敛为归属状态1=在库2=已分销。
-- 设备激活状态不落库,由“生效中主套餐 + 任意绑定卡已实名”动态计算。
UPDATE tb_device
SET status = CASE
WHEN shop_id IS NULL THEN 1
ELSE 2
END
WHERE status NOT IN (1, 2);
COMMENT ON COLUMN tb_device.status IS '归属状态 1-在库 2-已分销';

View File

@@ -26,8 +26,6 @@ const (
const (
DeviceStatusInStock = 1 // 在库
DeviceStatusDistributed = 2 // 已分销
DeviceStatusActivated = 3 // 已激活
DeviceStatusSuspended = 4 // 已停用
)
// 号卡状态
@@ -293,6 +291,19 @@ func GetIotCardStatusName(status int) string {
}
}
// GetDeviceStatusName 获取设备归属状态名称
// 对应 DeviceStatus* 常量1=在库, 2=已分销
func GetDeviceStatusName(status int) string {
switch status {
case DeviceStatusInStock:
return "在库"
case DeviceStatusDistributed:
return "已分销"
default:
return "未知"
}
}
// GetActivationStatusName 获取激活状态名称
// 对应 ActivationStatus* 常量0=未激活, 1=已激活
func GetActivationStatusName(status int) string {