c端当前套餐开始时间过期时间,以及excel导入字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
This commit is contained in:
@@ -152,48 +152,50 @@ func (h *ClientAssetHandler) GetAssetInfo(c *fiber.Ctx) error {
|
|||||||
phone, _ := middleware.GetCustomerPhone(c)
|
phone, _ := middleware.GetCustomerPhone(c)
|
||||||
|
|
||||||
resp := &dto.AssetInfoResponse{
|
resp := &dto.AssetInfoResponse{
|
||||||
BoundPhone: phone,
|
BoundPhone: phone,
|
||||||
AssetType: resolved.Asset.AssetType,
|
AssetType: resolved.Asset.AssetType,
|
||||||
AssetID: resolved.Asset.AssetID,
|
AssetID: resolved.Asset.AssetID,
|
||||||
Identifier: resolved.Identifier,
|
Identifier: resolved.Identifier,
|
||||||
VirtualNo: resolved.Asset.VirtualNo,
|
VirtualNo: resolved.Asset.VirtualNo,
|
||||||
Status: resolved.Asset.Status,
|
Status: resolved.Asset.Status,
|
||||||
StatusName: resolved.Asset.StatusName,
|
StatusName: resolved.Asset.StatusName,
|
||||||
RealNameStatus: resolved.Asset.RealNameStatus,
|
RealNameStatus: resolved.Asset.RealNameStatus,
|
||||||
RealNameStatusName: constants.GetRealNameStatusName(resolved.Asset.RealNameStatus),
|
RealNameStatusName: constants.GetRealNameStatusName(resolved.Asset.RealNameStatus),
|
||||||
CarrierName: resolved.Asset.CarrierName,
|
CarrierName: resolved.Asset.CarrierName,
|
||||||
Generation: strconv.Itoa(resolved.Generation),
|
Generation: strconv.Itoa(resolved.Generation),
|
||||||
WalletBalance: resolved.WalletBalance,
|
WalletBalance: resolved.WalletBalance,
|
||||||
ActivatedAt: resolved.Asset.ActivatedAt,
|
ActivatedAt: resolved.Asset.ActivatedAt,
|
||||||
CurrentPackage: resolved.Asset.CurrentPackage,
|
CurrentPackage: resolved.Asset.CurrentPackage,
|
||||||
CurrentPackageUsageID: resolved.Asset.CurrentPackageUsageID,
|
CurrentPackageUsageID: resolved.Asset.CurrentPackageUsageID,
|
||||||
RealTotalMB: resolved.Asset.RealTotalMB,
|
CurrentPackageActivatedAt: resolved.Asset.CurrentPackageActivatedAt,
|
||||||
RealUsedMB: resolved.Asset.RealUsedMB,
|
CurrentPackageExpiresAt: resolved.Asset.CurrentPackageExpiresAt,
|
||||||
VirtualTotalMB: resolved.Asset.VirtualTotalMB,
|
RealTotalMB: resolved.Asset.RealTotalMB,
|
||||||
VirtualUsedMB: resolved.Asset.VirtualUsedMB,
|
RealUsedMB: resolved.Asset.RealUsedMB,
|
||||||
ReductionPct: resolved.Asset.ReductionPct,
|
VirtualTotalMB: resolved.Asset.VirtualTotalMB,
|
||||||
DeviceName: resolved.Asset.DeviceName,
|
VirtualUsedMB: resolved.Asset.VirtualUsedMB,
|
||||||
IMEI: resolved.Asset.IMEI,
|
ReductionPct: resolved.Asset.ReductionPct,
|
||||||
SN: resolved.Asset.SN,
|
DeviceName: resolved.Asset.DeviceName,
|
||||||
DeviceModel: resolved.Asset.DeviceModel,
|
IMEI: resolved.Asset.IMEI,
|
||||||
DeviceType: resolved.Asset.DeviceType,
|
SN: resolved.Asset.SN,
|
||||||
Manufacturer: resolved.Asset.Manufacturer,
|
DeviceModel: resolved.Asset.DeviceModel,
|
||||||
MaxSimSlots: resolved.Asset.MaxSimSlots,
|
DeviceType: resolved.Asset.DeviceType,
|
||||||
BoundCardCount: resolved.Asset.BoundCardCount,
|
Manufacturer: resolved.Asset.Manufacturer,
|
||||||
Cards: resolved.Asset.Cards,
|
MaxSimSlots: resolved.Asset.MaxSimSlots,
|
||||||
DeviceProtectStatus: resolved.Asset.DeviceProtectStatus,
|
BoundCardCount: resolved.Asset.BoundCardCount,
|
||||||
ICCID: resolved.Asset.ICCID,
|
Cards: resolved.Asset.Cards,
|
||||||
MSISDN: resolved.Asset.MSISDN,
|
DeviceProtectStatus: resolved.Asset.DeviceProtectStatus,
|
||||||
CarrierID: resolved.Asset.CarrierID,
|
ICCID: resolved.Asset.ICCID,
|
||||||
CarrierType: resolved.Asset.CarrierType,
|
MSISDN: resolved.Asset.MSISDN,
|
||||||
NetworkStatus: resolved.Asset.NetworkStatus,
|
CarrierID: resolved.Asset.CarrierID,
|
||||||
NetworkStatusName: constants.GetNetworkStatusName(resolved.Asset.NetworkStatus),
|
CarrierType: resolved.Asset.CarrierType,
|
||||||
ActivationStatus: resolved.Asset.ActivationStatus,
|
NetworkStatus: resolved.Asset.NetworkStatus,
|
||||||
ActivationStatusName: constants.GetActivationStatusName(resolved.Asset.ActivationStatus),
|
NetworkStatusName: constants.GetNetworkStatusName(resolved.Asset.NetworkStatus),
|
||||||
CardCategory: resolved.Asset.CardCategory,
|
ActivationStatus: resolved.Asset.ActivationStatus,
|
||||||
BoundDeviceID: resolved.Asset.BoundDeviceID,
|
ActivationStatusName: constants.GetActivationStatusName(resolved.Asset.ActivationStatus),
|
||||||
BoundDeviceNo: resolved.Asset.BoundDeviceNo,
|
CardCategory: resolved.Asset.CardCategory,
|
||||||
BoundDeviceName: resolved.Asset.BoundDeviceName,
|
BoundDeviceID: resolved.Asset.BoundDeviceID,
|
||||||
|
BoundDeviceNo: resolved.Asset.BoundDeviceNo,
|
||||||
|
BoundDeviceName: resolved.Asset.BoundDeviceName,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用服务层获取设备实时 Gateway 数据(per D-11)
|
// 调用服务层获取设备实时 Gateway 数据(per D-11)
|
||||||
|
|||||||
@@ -19,17 +19,19 @@ type AssetResolveResponse struct {
|
|||||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||||
// 状态聚合字段
|
// 状态聚合字段
|
||||||
RealNameStatus int `json:"real_name_status" description:"实名状态:0未实名 1已实名"`
|
RealNameStatus int `json:"real_name_status" description:"实名状态:0未实名 1已实名"`
|
||||||
RealNameAt *time.Time `json:"real_name_at" description:"最近一次完成实名的时间,未实名时为 null"`
|
RealNameAt *time.Time `json:"real_name_at" description:"最近一次完成实名的时间,未实名时为 null"`
|
||||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||||
CurrentPackage string `json:"current_package" description:"当前套餐名称(无套餐时为空)"`
|
CurrentPackage string `json:"current_package" description:"当前套餐名称(无套餐时为空)"`
|
||||||
CurrentPackageUsageID *uint `json:"current_package_usage_id" description:"当前主套餐的套餐使用记录ID,无主套餐时为 null"`
|
CurrentPackageUsageID *uint `json:"current_package_usage_id" description:"当前主套餐的套餐使用记录ID,无主套餐时为 null"`
|
||||||
RealTotalMB int64 `json:"real_total_mb" description:"当前主套餐真实总量(MB)"`
|
CurrentPackageActivatedAt *time.Time `json:"current_package_activated_at,omitempty" description:"当前主套餐开始时间"`
|
||||||
RealUsedMB int64 `json:"real_used_mb" description:"当前主套餐真实已用量(MB)"`
|
CurrentPackageExpiresAt *time.Time `json:"current_package_expires_at,omitempty" description:"当前主套餐过期时间"`
|
||||||
VirtualTotalMB int64 `json:"virtual_total_mb" description:"当前主套餐业务停机阈值(MB)"`
|
RealTotalMB int64 `json:"real_total_mb" description:"当前主套餐真实总量(MB)"`
|
||||||
VirtualUsedMB float64 `json:"virtual_used_mb" description:"当前主套餐展示已用量(MB)"`
|
RealUsedMB int64 `json:"real_used_mb" description:"当前主套餐真实已用量(MB)"`
|
||||||
ReductionPct float64 `json:"reduction_pct" description:"展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1"`
|
VirtualTotalMB int64 `json:"virtual_total_mb" description:"当前主套餐业务停机阈值(MB)"`
|
||||||
DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"设备保护期状态:none/stop/start(仅asset_type=device时有效)"`
|
VirtualUsedMB float64 `json:"virtual_used_mb" description:"当前主套餐展示已用量(MB)"`
|
||||||
|
ReductionPct float64 `json:"reduction_pct" description:"展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1"`
|
||||||
|
DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"设备保护期状态:none/stop/start(仅asset_type=device时有效)"`
|
||||||
// 绑定关系字段
|
// 绑定关系字段
|
||||||
ICCID string `json:"iccid,omitempty" description:"卡ICCID(asset_type=card时有效)"`
|
ICCID string `json:"iccid,omitempty" description:"卡ICCID(asset_type=card时有效)"`
|
||||||
BoundDeviceID *uint `json:"bound_device_id,omitempty" description:"绑定的设备ID(asset_type=card时有效)"`
|
BoundDeviceID *uint `json:"bound_device_id,omitempty" description:"绑定的设备ID(asset_type=card时有效)"`
|
||||||
|
|||||||
@@ -33,13 +33,15 @@ type AssetInfoResponse struct {
|
|||||||
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
|
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
|
||||||
|
|
||||||
// === 套餐信息(通用) ===
|
// === 套餐信息(通用) ===
|
||||||
CurrentPackage string `json:"current_package" description:"当前套餐名称(无套餐时为空)"`
|
CurrentPackage string `json:"current_package" description:"当前套餐名称(无套餐时为空)"`
|
||||||
CurrentPackageUsageID *uint `json:"current_package_usage_id" description:"当前主套餐的套餐使用记录ID,无主套餐时为 null"`
|
CurrentPackageUsageID *uint `json:"current_package_usage_id" description:"当前主套餐的套餐使用记录ID,无主套餐时为 null"`
|
||||||
RealTotalMB int64 `json:"real_total_mb" description:"当前主套餐真实总量(MB)"`
|
CurrentPackageActivatedAt *time.Time `json:"current_package_activated_at,omitempty" description:"当前主套餐开始时间"`
|
||||||
RealUsedMB int64 `json:"real_used_mb" description:"当前主套餐真实已用量(MB)"`
|
CurrentPackageExpiresAt *time.Time `json:"current_package_expires_at,omitempty" description:"当前主套餐过期时间"`
|
||||||
VirtualTotalMB int64 `json:"virtual_total_mb" description:"当前主套餐业务停机阈值(MB)"`
|
RealTotalMB int64 `json:"real_total_mb" description:"当前主套餐真实总量(MB)"`
|
||||||
VirtualUsedMB float64 `json:"virtual_used_mb" description:"当前主套餐展示已用量(MB)"`
|
RealUsedMB int64 `json:"real_used_mb" description:"当前主套餐真实已用量(MB)"`
|
||||||
ReductionPct float64 `json:"reduction_pct" description:"展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1"`
|
VirtualTotalMB int64 `json:"virtual_total_mb" description:"当前主套餐业务停机阈值(MB)"`
|
||||||
|
VirtualUsedMB float64 `json:"virtual_used_mb" description:"当前主套餐展示已用量(MB)"`
|
||||||
|
ReductionPct float64 `json:"reduction_pct" description:"展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1"`
|
||||||
|
|
||||||
// === 设备专属字段(asset_type=device 时有效) ===
|
// === 设备专属字段(asset_type=device 时有效) ===
|
||||||
DeviceName string `json:"device_name,omitempty" description:"设备名称"`
|
DeviceName string `json:"device_name,omitempty" description:"设备名称"`
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp
|
|||||||
- 文件格式:仅支持 .xlsx (Excel 2007+)
|
- 文件格式:仅支持 .xlsx (Excel 2007+)
|
||||||
- 必须包含列(首行为表头):
|
- 必须包含列(首行为表头):
|
||||||
- ` + "`virtual_no`" + `: 设备虚拟号(**必填**,全局唯一;**为空的行记为失败,不再静默跳过**)
|
- ` + "`virtual_no`" + `: 设备虚拟号(**必填**,全局唯一;**为空的行记为失败,不再静默跳过**)
|
||||||
|
- ` + "`sn`" + `: 设备 SN
|
||||||
- ` + "`device_name`" + `: 设备名称
|
- ` + "`device_name`" + `: 设备名称
|
||||||
- ` + "`device_model`" + `: 设备型号
|
- ` + "`device_model`" + `: 设备型号
|
||||||
- ` + "`device_type`" + `: 设备类型
|
- ` + "`device_type`" + `: 设备类型
|
||||||
@@ -97,6 +98,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp
|
|||||||
- ` + "`max_sim_slots`" + `: 最大插槽数(默认4,范围1-4)
|
- ` + "`max_sim_slots`" + `: 最大插槽数(默认4,范围1-4)
|
||||||
- ` + "`iccid_1`" + ` ~ ` + "`iccid_4`" + `: 固定槽位的卡 ICCID(` + "`iccid_1`" + `=槽1,` + "`iccid_2`" + `=槽2,` + "`iccid_3`" + `=槽3,` + "`iccid_4`" + `=槽4;卡必须已存在且未绑定)
|
- ` + "`iccid_1`" + ` ~ ` + "`iccid_4`" + `: 固定槽位的卡 ICCID(` + "`iccid_1`" + `=槽1,` + "`iccid_2`" + `=槽2,` + "`iccid_3`" + `=槽3,` + "`iccid_4`" + `=槽4;卡必须已存在且未绑定)
|
||||||
- 槽位规则:中间留空不会触发前移补位;如果 ` + "`max_sim_slots=2`" + `,则 ` + "`iccid_3`" + `、` + "`iccid_4`" + ` 必须留空,否则该行失败
|
- 槽位规则:中间留空不会触发前移补位;如果 ` + "`max_sim_slots=2`" + `,则 ` + "`iccid_3`" + `、` + "`iccid_4`" + ` 必须留空,否则该行失败
|
||||||
|
- 固定列顺序:` + "`virtual_no`" + `、` + "`sn`" + `、` + "`device_name`" + `、` + "`device_model`" + `、` + "`device_type`" + `、` + "`imei`" + `、` + "`manufacturer`" + `、` + "`max_sim_slots`" + `、` + "`iccid_1`" + `、` + "`iccid_2`" + `、` + "`iccid_3`" + `、` + "`iccid_4`" + `
|
||||||
- 列格式:设置为文本格式(避免长数字被转为科学记数法)`,
|
- 列格式:设置为文本格式(避免长数字被转为科学记数法)`,
|
||||||
Tags: []string{"设备管理"},
|
Tags: []string{"设备管理"},
|
||||||
Input: new(dto.ImportDeviceRequest),
|
Input: new(dto.ImportDeviceRequest),
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ func (s *Service) fillPackageInfo(ctx context.Context, resp *dto.AssetResolveRes
|
|||||||
|
|
||||||
resp.CurrentPackageUsageID = &usage.ID
|
resp.CurrentPackageUsageID = &usage.ID
|
||||||
resp.CurrentPackage = usage.PackageName
|
resp.CurrentPackage = usage.PackageName
|
||||||
|
resp.CurrentPackageActivatedAt = usage.ActivatedAt
|
||||||
|
resp.CurrentPackageExpiresAt = usage.ExpiresAt
|
||||||
if resp.CurrentPackage == "" {
|
if resp.CurrentPackage == "" {
|
||||||
pkg, pkgErr := s.packageStore.GetByID(ctx, usage.PackageID)
|
pkg, pkgErr := s.packageStore.GetByID(ctx, usage.PackageID)
|
||||||
if pkgErr == nil && pkg != nil {
|
if pkgErr == nil && pkg != nil {
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ func (h *DeviceImportHandler) processBatch(ctx context.Context, task *model.Devi
|
|||||||
|
|
||||||
device := &model.Device{
|
device := &model.Device{
|
||||||
VirtualNo: row.VirtualNo,
|
VirtualNo: row.VirtualNo,
|
||||||
|
SN: row.SN,
|
||||||
IMEI: row.IMEI,
|
IMEI: row.IMEI,
|
||||||
DeviceName: row.DeviceName,
|
DeviceName: row.DeviceName,
|
||||||
DeviceModel: row.DeviceModel,
|
DeviceModel: row.DeviceModel,
|
||||||
|
|||||||
@@ -4,12 +4,18 @@
|
|||||||
|
|
||||||
### Requirement: B1 资产基本信息查询接口
|
### Requirement: B1 资产基本信息查询接口
|
||||||
|
|
||||||
系统 SHALL 提供 `GET /api/c/v1/asset/info?identifier=xxx`,并且 MUST 要求个人客户认证(C 端 Token)。接口 MUST 复用 `asset.Service.Resolve()` 解析标识符,并在调用时使用 `gorm.SkipDataPermission(ctx)` 以绕过 shop_id 数据权限过滤。请求参数 MUST 包含 `identifier`(ICCID、虚拟号、设备号之一)。响应体 SHALL 返回 `asset_type`、`asset_id`、`identifier`、`virtual_no`、`status`、`real_name_status`、`carrier`、`generation`、`wallet_balance`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`ASSET_NOT_FOUND/资产不存在`。
|
系统 SHALL 提供 `GET /api/c/v1/asset/info?identifier=xxx`,并且 MUST 要求个人客户认证(C 端 Token)。接口 MUST 复用 `asset.Service.Resolve()` 解析标识符,并在调用时使用 `gorm.SkipDataPermission(ctx)` 以绕过 shop_id 数据权限过滤。请求参数 MUST 包含 `identifier`(ICCID、虚拟号、设备号之一)。响应体 SHALL 返回 `asset_type`、`asset_id`、`identifier`、`virtual_no`、`status`、`real_name_status`、`carrier`、`generation`、`wallet_balance`。当存在当前主套餐时,响应体 MUST 额外返回 `current_package`、`current_package_usage_id`、`current_package_activated_at`、`current_package_expires_at` 以及当前套餐流量字段。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`ASSET_NOT_FOUND/资产不存在`。
|
||||||
|
|
||||||
#### Scenario: 个人客户查询已绑定资产
|
#### Scenario: 个人客户查询已绑定资产
|
||||||
- **WHEN** 客户携带有效 Token 调用 `GET /api/c/v1/asset/info?identifier=8986xxxx` 且资产已绑定到本人
|
- **WHEN** 客户携带有效 Token 调用 `GET /api/c/v1/asset/info?identifier=8986xxxx` 且资产已绑定到本人
|
||||||
- **THEN** 系统返回 200,包含资产基础信息与当前 generation
|
- **THEN** 系统返回 200,包含资产基础信息与当前 generation
|
||||||
|
|
||||||
|
#### Scenario: 当前主套餐返回开始时间和过期时间
|
||||||
|
- **GIVEN** 该资产存在一条生效中的主套餐使用记录
|
||||||
|
- **WHEN** 客户调用 `GET /api/c/v1/asset/info`
|
||||||
|
- **THEN** 响应体中的 `current_package_activated_at` 等于该主套餐的 `activated_at`
|
||||||
|
- **AND** 响应体中的 `current_package_expires_at` 等于该主套餐的 `expires_at`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Requirement: B2 可购买套餐列表接口
|
### Requirement: B2 可购买套餐列表接口
|
||||||
|
|||||||
@@ -20,16 +20,17 @@ TBD - created by archiving change add-device-management. Update Purpose after ar
|
|||||||
- **列位置(固定,不可变)**:
|
- **列位置(固定,不可变)**:
|
||||||
```
|
```
|
||||||
第1列(索引0): 虚拟号(必填,全局唯一)
|
第1列(索引0): 虚拟号(必填,全局唯一)
|
||||||
第2列(索引1): 设备名称(可选)
|
第2列(索引1): SN(可选)
|
||||||
第3列(索引2): 设备型号(可选)
|
第3列(索引2): 设备名称(可选)
|
||||||
第4列(索引3): 设备类型(可选)
|
第4列(索引3): 设备型号(可选)
|
||||||
第5列(索引4): IMEI(可选)
|
第5列(索引4): 设备类型(可选)
|
||||||
第6列(索引5): 制造商(可选)
|
第6列(索引5): IMEI(可选)
|
||||||
第7列(索引6): 最大SIM槽数(可选,默认4,范围1-4)
|
第7列(索引6): 制造商(可选)
|
||||||
第8列(索引7): 卡1 ICCID(可选,对应槽位1)
|
第8列(索引7): 最大SIM槽数(可选,默认4,范围1-4)
|
||||||
第9列(索引8): 卡2 ICCID(可选,对应槽位2)
|
第9列(索引8): 卡1 ICCID(可选,对应槽位1)
|
||||||
第10列(索引9): 卡3 ICCID(可选,对应槽位3)
|
第10列(索引9): 卡2 ICCID(可选,对应槽位2)
|
||||||
第11列(索引10): 卡4 ICCID(可选,对应槽位4)
|
第11列(索引10): 卡3 ICCID(可选,对应槽位3)
|
||||||
|
第12列(索引11): 卡4 ICCID(可选,对应槽位4)
|
||||||
```
|
```
|
||||||
- **列格式**: 所有列应设置为文本格式(避免数字被转为科学记数法)
|
- **列格式**: 所有列应设置为文本格式(避免数字被转为科学记数法)
|
||||||
|
|
||||||
@@ -67,10 +68,16 @@ TBD - created by archiving change add-device-management. Update Purpose after ar
|
|||||||
|
|
||||||
#### Scenario: 中文表头正常导入
|
#### Scenario: 中文表头正常导入
|
||||||
|
|
||||||
- **GIVEN** Excel 文件第1行表头为 `虚拟号 | 设备名称 | 设备型号 | 设备类型 | IMEI | 制造商 | 最大SIM槽数 | 卡1 | 卡2 | 卡3 | 卡4`
|
- **GIVEN** Excel 文件第1行表头为 `虚拟号 | SN | 设备名称 | 设备型号 | 设备类型 | IMEI | 制造商 | 最大SIM槽数 | 卡1 | 卡2 | 卡3 | 卡4`
|
||||||
- **WHEN** 系统解析该 Excel 文件
|
- **WHEN** 系统解析该 Excel 文件
|
||||||
- **THEN** 系统跳过第1行,从第2行开始按列位置解析数据
|
- **THEN** 系统跳过第1行,从第2行开始按列位置解析数据
|
||||||
|
|
||||||
|
#### Scenario: SN 列按固定顺序导入
|
||||||
|
|
||||||
|
- **GIVEN** 某行第2列(索引1)填写 `SN-001`
|
||||||
|
- **WHEN** 系统解析并导入该行
|
||||||
|
- **THEN** 创建设备记录时 `sn = "SN-001"`
|
||||||
|
|
||||||
#### Scenario: 仅填写卡2和卡3时保留原始槽位
|
#### Scenario: 仅填写卡2和卡3时保留原始槽位
|
||||||
|
|
||||||
- **GIVEN** 某行 `最大SIM槽数 = 3`
|
- **GIVEN** 某行 `最大SIM槽数 = 3`
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type DeviceParseResult struct {
|
|||||||
type DeviceRow struct {
|
type DeviceRow struct {
|
||||||
Line int
|
Line int
|
||||||
VirtualNo string
|
VirtualNo string
|
||||||
|
SN string
|
||||||
IMEI string // 设备IMEI,对应 Excel IMEI 列,用于 Gateway API 调用
|
IMEI string // 设备IMEI,对应 Excel IMEI 列,用于 Gateway API 调用
|
||||||
DeviceName string
|
DeviceName string
|
||||||
DeviceModel string
|
DeviceModel string
|
||||||
@@ -132,8 +133,8 @@ func ParseDeviceExcel(filePath string) (*DeviceParseResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 按固定列位置读取,第1行为表头跳过:
|
// 按固定列位置读取,第1行为表头跳过:
|
||||||
// col0=虚拟号 col1=设备名称 col2=设备型号 col3=设备类型 col4=IMEI
|
// col0=虚拟号 col1=SN col2=设备名称 col3=设备型号 col4=设备类型
|
||||||
// col5=制造商 col6=最大SIM槽数 col7~10=卡1~4 ICCID
|
// col5=IMEI col6=制造商 col7=最大SIM槽数 col8~11=卡1~4 ICCID
|
||||||
for i := 1; i < len(rows); i++ {
|
for i := 1; i < len(rows); i++ {
|
||||||
record := rows[i]
|
record := rows[i]
|
||||||
lineNum := i + 1
|
lineNum := i + 1
|
||||||
@@ -148,26 +149,27 @@ func ParseDeviceExcel(filePath string) (*DeviceParseResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
row.VirtualNo = getCol(0)
|
row.VirtualNo = getCol(0)
|
||||||
row.DeviceName = getCol(1)
|
row.SN = getCol(1)
|
||||||
row.DeviceModel = getCol(2)
|
row.DeviceName = getCol(2)
|
||||||
row.DeviceType = getCol(3)
|
row.DeviceModel = getCol(3)
|
||||||
row.IMEI = getCol(4)
|
row.DeviceType = getCol(4)
|
||||||
row.Manufacturer = getCol(5)
|
row.IMEI = getCol(5)
|
||||||
maxSimSlotsStr := getCol(6)
|
row.Manufacturer = getCol(6)
|
||||||
|
maxSimSlotsStr := getCol(7)
|
||||||
|
|
||||||
row.SlotICCIDs = make([]DeviceSlotICCID, 0, 4)
|
row.SlotICCIDs = make([]DeviceSlotICCID, 0, 4)
|
||||||
for j := 7; j <= 10; j++ {
|
for j := 8; j <= 11; j++ {
|
||||||
iccid := getCol(j)
|
iccid := getCol(j)
|
||||||
if iccid != "" {
|
if iccid != "" {
|
||||||
row.SlotICCIDs = append(row.SlotICCIDs, DeviceSlotICCID{
|
row.SlotICCIDs = append(row.SlotICCIDs, DeviceSlotICCID{
|
||||||
SlotPosition: j - 6,
|
SlotPosition: j - 7,
|
||||||
ICCID: iccid,
|
ICCID: iccid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 整行目标列皆为空,视为空行跳过,不计入 total
|
// 整行目标列皆为空,视为空行跳过,不计入 total
|
||||||
if row.VirtualNo == "" && row.DeviceName == "" && row.DeviceModel == "" &&
|
if row.VirtualNo == "" && row.SN == "" && row.DeviceName == "" && row.DeviceModel == "" &&
|
||||||
row.DeviceType == "" && row.IMEI == "" && row.Manufacturer == "" &&
|
row.DeviceType == "" && row.IMEI == "" && row.Manufacturer == "" &&
|
||||||
maxSimSlotsStr == "" && len(row.SlotICCIDs) == 0 {
|
maxSimSlotsStr == "" && len(row.SlotICCIDs) == 0 {
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user