## ADDED Requirements

### Requirement: 轻量实时状态查询

系统 SHALL 提供基于持久化数据的轻量状态查询接口，供前端在已知资产 ID 后进行快速轮询。

**API 端点**: `GET /api/admin/assets/:asset_type/:id/realtime-status`

**约束**:
- `:asset_type` 取值为 `device` 或 `card`
- 此接口**不调用网关**，仅读取 DB/Redis 中持久化的最新数据
- 不包含套餐流量计算（与 resolve 的区别）
- "实时性"依赖轮询系统定期刷新（实名状态约 5 分钟，流量约 10 分钟）

**card 类型响应字段**:
- `network_status`: 网络状态（0-停机 1-开机）
- `real_name_status`: 实名状态（0-未实名 1-已实名）
- `current_month_usage_mb`: 本月已用流量（持久化缓存值）
- `last_sync_at`: 最后与 Gateway 同步时间

**device 类型响应字段**:
- `device_protect_status`: 保护期状态（`"none"` / `"stop"` / `"start"`）
- `cards`: 所有绑定卡的状态列表（同 DeviceCardInfo 结构）

#### Scenario: 查询单卡实时状态

- **WHEN** 管理员调用 `GET /api/admin/assets/card/123/realtime-status`
- **THEN** 系统返回该卡的 network_status、real_name_status、current_month_usage_mb、last_sync_at

#### Scenario: 查询设备实时状态

- **WHEN** 管理员调用 `GET /api/admin/assets/device/456/realtime-status`
- **THEN** 系统返回设备的保护期状态及所有绑定卡的当前状态列表

#### Scenario: asset_type 参数非法

- **WHEN** 管理员调用 `GET /api/admin/assets/unknown-type/123/realtime-status`
- **THEN** 系统返回 HTTP 400 参数错误

---

### Requirement: 手动刷新接口

系统 SHALL 提供手动触发网关同步的接口，用于客服主动刷新资产最新状态。

**API 端点**: `POST /api/admin/assets/:asset_type/:id/refresh`

**行为规则**:
- card 类型：直接调用 `RefreshCardDataFromGateway(iccid)` 同步网络状态、实名状态、本月流量、最后同步时间
- device 类型：对该设备所有绑定卡遍历调用 `RefreshCardDataFromGateway`

**设备类型频率限制**:
- 使用 Redis Key `RedisDeviceRefreshCooldownKey(deviceID)` 限频
- 同一设备 30 秒冷却期内不允许重复触发
- 冷却期内调用返回 HTTP 429

**响应**:
- 刷新完成后返回刷新后的最新状态（与 realtime-status 响应结构相同）

#### Scenario: 刷新单卡状态

- **WHEN** 客服调用 `POST /api/admin/assets/card/123/refresh`
- **THEN** 系统调用 RefreshCardDataFromGateway，更新 DB 中的卡状态字段，返回刷新后的最新状态

#### Scenario: 刷新设备状态（首次）

- **WHEN** 管理员调用 `POST /api/admin/assets/device/456/refresh`，该设备有 3 张绑定卡
- **THEN** 系统依次刷新 3 张卡，设置 30 秒冷却期，返回最新状态

#### Scenario: 设备刷新冷却期内重复触发

- **WHEN** 管理员在 30 秒冷却期内第二次调用 `POST /api/admin/assets/device/456/refresh`
- **THEN** 系统返回 HTTP 429，提示"刷新过于频繁，请稍后再试"

---

### Requirement: 套餐历史列表查询

系统 SHALL 提供资产的全量套餐记录查询接口，包含历史和当前生效套餐。

**API 端点**: `GET /api/admin/assets/:asset_type/:id/packages`

**排序**: 按 `created_at` 倒序（最新套餐在前）

**分页**: 不分页，全量返回

**范围**: 包含所有状态（含 status=4 已失效的历史套餐）

**按 asset_type 区分查询**:
- card：查询 `PackageUsage.iot_card_id = :id`
- device：查询 `PackageUsage.device_id = :id`

**每条记录响应字段**:
- `package_usage_id`: 套餐使用记录 ID
- `package_name`: 套餐名称
- `package_type`: 套餐类型（formal/addon）
- `master_usage_id`: 主套餐 ID（加油包时有值，主套餐时为 null）
- `real_data_mb`: 真总流量（MB）
- `virtual_data_mb`: 虚总流量/停机阈值（MB）
- `package_used_mb`: 展示已使用流量（经虚流量换算）
- `package_remain_mb`: 展示剩余流量
- `activated_at`: 生效时间
- `expires_at`: 过期时间
- `status`: 套餐状态（0-待生效 1-生效中 2-已用完 3-已过期 4-已失效）

#### Scenario: 查询卡的套餐历史

- **WHEN** 管理员调用 `GET /api/admin/assets/card/123/packages`，该卡有 3 条套餐记录（含 1 条已失效）
- **THEN** 系统返回全部 3 条记录，按创建时间倒序排列

#### Scenario: 查询设备的套餐历史

- **WHEN** 管理员调用 `GET /api/admin/assets/device/456/packages`
- **THEN** 系统返回该设备 device_id 下的所有套餐记录

#### Scenario: 资产无套餐记录

- **WHEN** 管理员查询一张从未购买过套餐的卡
- **THEN** 系统返回空数组，不报错

---

### Requirement: 当前主套餐详情查询

系统 SHALL 提供查询资产当前生效主套餐的接口，用于展示套餐详细信息。

**API 端点**: `GET /api/admin/assets/:asset_type/:id/current-package`

**查询条件**: `status = 1（生效中）AND master_usage_id IS NULL`

**多套餐同时生效时**：只返回主套餐（master_usage_id IS NULL），不返回加油包

**响应字段**:
- 完整套餐信息（同套餐历史列表中的单条记录字段）
- 当无生效主套餐时，返回 HTTP 404

#### Scenario: 返回当前主套餐

- **WHEN** 管理员调用 `GET /api/admin/assets/card/123/current-package`，该卡有 1 个生效主套餐和 1 个加油包
- **THEN** 系统只返回主套餐信息，不包含加油包

#### Scenario: 无当前生效主套餐

- **WHEN** 管理员查询没有生效中主套餐的资产
- **THEN** 系统返回 HTTP 404

---

### Requirement: RefreshCardDataFromGateway 完整同步

系统 SHALL 提供从 Gateway 完整同步卡数据的方法，替代原 `SyncCardStatusFromGateway`（仅为示例实现）。

**方法签名**: `RefreshCardDataFromGateway(ctx context.Context, iccid string) error`

**同步字段**:
- `network_status`: 网络状态（从网关卡状态映射）
- `real_name_status`: 实名状态（从网关实名接口获取）
- `current_month_usage_mb`: 本月已用流量（从网关流量接口获取）
- `last_sync_time`: 更新为当前时间

**错误处理**: 网关调用失败时记录 Error 日志并返回错误，不更新 DB

#### Scenario: 完整同步卡数据

- **WHEN** 调用 `RefreshCardDataFromGateway(ctx, "89860123456789012345")`
- **THEN** 系统调用网关接口，将 network_status、real_name_status、current_month_usage_mb、last_sync_time 写回 DB
