diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index 8ba30ac..4bd0dbc 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -658,6 +658,16 @@ components: current_package: description: 当前套餐名称(无套餐时为空) type: string + current_package_activated_at: + description: 当前主套餐开始时间 + format: date-time + nullable: true + type: string + current_package_expires_at: + description: 当前主套餐过期时间 + format: date-time + nullable: true + type: string current_package_usage_id: description: 当前主套餐的套餐使用记录ID,无主套餐时为 null minimum: 0 @@ -1179,6 +1189,16 @@ components: current_package: description: 当前套餐名称(无套餐时为空) type: string + current_package_activated_at: + description: 当前主套餐开始时间 + format: date-time + nullable: true + type: string + current_package_expires_at: + description: 当前主套餐过期时间 + format: date-time + nullable: true + type: string current_package_usage_id: description: 当前主套餐的套餐使用记录ID,无主套餐时为 null minimum: 0 @@ -13591,6 +13611,7 @@ paths: - 文件格式:仅支持 .xlsx (Excel 2007+) - 必须包含列(首行为表头): - `virtual_no`: 设备虚拟号(**必填**,全局唯一;**为空的行记为失败,不再静默跳过**) + - `sn`: 设备 SN - `device_name`: 设备名称 - `device_model`: 设备型号 - `device_type`: 设备类型 @@ -13599,6 +13620,7 @@ paths: - `max_sim_slots`: 最大插槽数(默认4,范围1-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` 必须留空,否则该行失败 + - 固定列顺序:`virtual_no`、`sn`、`device_name`、`device_model`、`device_type`、`imei`、`manufacturer`、`max_sim_slots`、`iccid_1`、`iccid_2`、`iccid_3`、`iccid_4` - 列格式:设置为文本格式(避免长数字被转为科学记数法) requestBody: content: diff --git a/docs/excel-import-frontend-guide.md b/docs/excel-import-frontend-guide.md index d5d2b3f..6b7eb00 100644 --- a/docs/excel-import-frontend-guide.md +++ b/docs/excel-import-frontend-guide.md @@ -114,16 +114,17 @@ function validateFile(file) { **文件名**: `device_import_template.xlsx` **格式**: -| virtual_no | device_name | device_model | device_type | imei | manufacturer | max_sim_slots | iccid_1 | iccid_2 | iccid_3 | iccid_4 | -|------------|-------------|--------------|-------------|------|--------------|---------------|---------|---------|---------|---------| -| DEV-001 | GPS追踪器A | GT06N | GPS Tracker | 123456789012345 | Concox | 4 | 89860012345678901234 | 89860012345678901235 | | | -| DEV-002 | GPS追踪器B | GT06N | GPS Tracker | 123456789012346 | Concox | 2 | | 89860012345678901236 | 89860012345678901237 | | +| virtual_no | sn | device_name | device_model | device_type | imei | manufacturer | max_sim_slots | iccid_1 | iccid_2 | iccid_3 | iccid_4 | +|------------|----|-------------|--------------|-------------|------|--------------|---------------|---------|---------|---------|---------| +| DEV-001 | SN-001 | GPS追踪器A | GT06N | GPS Tracker | 123456789012345 | Concox | 4 | 89860012345678901234 | 89860012345678901235 | | | +| DEV-002 | SN-002 | GPS追踪器B | GT06N | GPS Tracker | 123456789012346 | Concox | 3 | | 89860012345678901236 | 89860012345678901237 | | **字段说明**: | 列名 | 是否必填 | 说明 | |------|---------|------| | virtual_no | **是** | 设备虚拟号,全局唯一;**留空不再跳过,将记录为失败行** | +| sn | 否 | 设备 SN,建议填写厂商序列号 | | device_name | 否 | 设备名称 | | device_model | 否 | 设备型号 | | device_type | 否 | 设备类型 | @@ -135,7 +136,7 @@ function validateFile(file) { **要点**: - 所有列都必须设置为**文本格式** - `virtual_no` 为必填项,留空将记录为失败行(而非静默跳过) -- 设备导入按**固定列顺序**读取:`virtual_no`、`device_name`、`device_model`、`device_type`、`imei`、`manufacturer`、`max_sim_slots`、`iccid_1`、`iccid_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` - `iccid_1 ~ iccid_4` 为可选项,填写时对应的 ICCID 必须已存在于系统中 - `iccid_1 ~ iccid_4` 表示**固定槽位**而非“按非空顺序绑定” 例如只填写 `iccid_2` 和 `iccid_3` 时,导入后仍应绑定到槽2和槽3,而不是槽1和槽2 diff --git a/internal/handler/app/client_device.go b/internal/handler/app/client_device.go index d392cdf..dfd55d3 100644 --- a/internal/handler/app/client_device.go +++ b/internal/handler/app/client_device.go @@ -136,12 +136,14 @@ func (h *ClientDeviceHandler) GetDeviceCards(c *fiber.Ctx) error { return response.Success(c, &dto.DeviceCardListResponse{Cards: []dto.DeviceCardItem{}}) } - // 收集卡 ID 并记录插槽位置映射 + // 收集卡 ID,并记录插槽位置与当前在用标识 cardIDs := make([]uint, 0, len(bindings)) slotMap := make(map[uint]int, len(bindings)) + isCurrentMap := make(map[uint]bool, len(bindings)) for _, b := range bindings { cardIDs = append(cardIDs, b.IotCardID) slotMap[b.IotCardID] = b.SlotPosition + isCurrentMap[b.IotCardID] = b.IsCurrent } // 批量查询卡详情 @@ -153,7 +155,7 @@ func (h *ClientDeviceHandler) GetDeviceCards(c *fiber.Ctx) error { return errors.New(errors.CodeInternalError) } - // 组装响应,slot_position == 1 视为当前激活卡 + // 组装响应,当前激活卡以绑定表 is_current 为准 items := make([]dto.DeviceCardItem, 0, len(cards)) for _, card := range cards { slot := slotMap[card.ID] @@ -166,7 +168,8 @@ func (h *ClientDeviceHandler) GetDeviceCards(c *fiber.Ctx) error { NetworkStatus: networkStatusText(card.NetworkStatus), RealNameStatus: card.RealNameStatus, SlotPosition: slot, - IsActive: slot == 1, + IsActive: isCurrentMap[card.ID], + RealnamePolicy: card.RealnamePolicy, }) }