## ADDED Requirements ### Requirement: Phone-Asset Association List Page The admin frontend SHALL provide a phone-asset association list page accessible only to super admin and platform accounts, listing phone–asset relationships created via H5 SMS verification. #### Scenario: Query association list - **GIVEN** 用户打开「手机号资产关联」列表页 - **WHEN** 用户提交分页、资产标识、手机号、状态或创建时间范围筛选条件 - **THEN** 系统 MUST call `GET /api/admin/phone-asset-associations` - **AND** 系统 MUST support query parameters `page`, `page_size`, `asset_identifier`, `phone`, `status`, `created_at_start`, and `created_at_end` - **AND** 系统 MUST parse `data.page`, `data.size`, `data.total`, and `data.items` from the response - **AND** 资产标识 MUST 支持 ICCID、虚拟号、IMEI、SN 或接入号精确匹配;手机号 MUST 使用完整值精确匹配 #### Scenario: Render association table columns - **GIVEN** 关联列表接口返回 `items` - **WHEN** 表格渲染每一行关联 - **THEN** 系统 MUST 展示资产类型/ID/标识(`asset_type`、`asset_id`、`asset_identifier`)、完整手机号(`phone`)、建立时间(`established_at`)、建立来源(`source`,中文映射固定为 H5 短信验证)与状态 - **AND** 系统 MUST 用 `status_name` 展示状态中文名称 - **AND** 对已失效关系(`status=0`)MUST 展示失效时间、失效方式(`invalidation_method_name`)与失效原因(`invalidation_reason`,可为空) #### Scenario: Vehicle the 403 semantics - **GIVEN** 非超管/平台账号访问关联列表或任何解绑接口 - **WHEN** 后端返回 403 - **THEN** 前端 MUST 直接展示后端文案「无权限操作该资源或资源不存在」 - **AND** 前端 MUST NOT 区分越权、资产不存在与已无有效关系(后端明确不形成可枚举差异) ### Requirement: Single Association Unbind The admin frontend SHALL support unbinding a single phone–asset association with a mandatory reason and confirmation. #### Scenario: Unbind single association with reason and confirmation - **GIVEN** 用户在关联列表选择一条有效关联执行解绑 - **WHEN** 用户填写解除原因(1–500 字符)并勾选二次确认后提交 - **THEN** 系统 MUST call `DELETE /api/admin/phone-asset-associations/{id}` with the association id in the path - **AND** 请求 MUST 携带 `reason` 与 `confirmed=true`,二者缺一即不发请求(前端表单校验兜底,后端兜底拒绝) - **AND** 系统 MUST 展示响应中的解除结果,包括失效时间与本次解除的有效关系数(`unbound_count`) #### Scenario: Refuse without reason or confirmation - **GIVEN** 用户未填写原因或未勾选二次确认 - **WHEN** 用户点击确认解绑 - **THEN** 前端 MUST 阻止提交并提示原因必填/需二次确认 - **AND** 没有任何关系被解除的承诺 MUST 与后端说明一致,前端不调用接口 ### Requirement: Batch Asset Unbind The admin frontend SHALL support unbinding all current valid associations of multiple assets in one request with per-item results. #### Scenario: Batch unbind selected assets - **GIVEN** 用户勾选多个资产(列表或他处选择的资产集合)执行批量解绑 - **WHEN** 用户填写解除原因并二次确认后提交 - **THEN** 系统 MUST call `POST /api/admin/phone-asset-associations/batch-unbind` - **AND** 请求体 MUST 包含 `assets`(每项 `asset_type`+`asset_id`)、`confirmed=true` 与 `reason` - **AND** 请求前前端 MUST 按 `(asset_type, asset_id)` 去重并限制数量(后端上限 200,前端同步校验) - **AND** 系统 MUST 解析 `success_count`、`fail_count` 与逐项结果 `items`(每项含 `asset_id`、`asset_type`、`success`、失败 `reason` 与 `unbound_count`) - **AND** 部分成功 MUST 不回滚成功项,前端 MUST 按逐项结果展示成功/失败明细 #### Scenario: Show failure copy for failed items - **GIVEN** 批量解绑存在失败项(越权、资产不存在或已无有效关系) - **WHEN** 逐项结果返回失败 - **THEN** 前端 MUST 直接展示后端返回的失败文案 - **AND** 失败项 MUST 可被定位到对应资产标识与类型 ### Requirement: CSV Unbind Import Task The admin frontend SHALL support creating, listing, and viewing phone-asset unbind import tasks driven by an uploaded CSV. #### Scenario: Provide CSV template download - **GIVEN** 用户进入「CSV 解绑导入」入口 - **WHEN** 用户需要模板 - **THEN** 系统 MUST 提供可下载的 CSV 模板,表头固定为 `资产标识,备注`,备注列为可选 - **AND** 模板 MUST 以 UTF-8 编码(可带 BOM) #### Scenario: Upload CSV and create unbind import task - **GIVEN** 用户选择 CSV 文件(UTF-8 可带 BOM,非 UTF-8 按 GBK 解码;无行数上限) - **WHEN** 用户填写任务级解绑原因并二次确认后创建任务 - **THEN** 系统 MUST 先调用 `POST /api/admin/storage/upload-url`(`purpose=phone_unbind_import`,仅接受 `.csv`) - **AND** 系统 MUST 使用预签名 URL 完成 PUT 直传,取回 `file_key` - **AND** 系统 MUST call `POST /api/admin/phone-asset-associations/unbind-imports`,请求体包含 `file_key`、任务级 `reason` 与 `confirmed=true` - **AND** 系统 MUST 展示创建返回的任务编号、状态(1 待处理 / 2 处理中 / 3 已完成 / 4 失败)与中文状态名 #### Scenario: List unbind import tasks - **GIVEN** 用户打开「解绑导入任务」列表 - **WHEN** 用户按分页/状态等条件查询 - **THEN** 系统 MUST call `GET /api/admin/phone-asset-associations/unbind-imports` - **AND** 列表 MUST 展示任务编号、文件名、状态、成功/失败行数、总数、创建人、创建/开始/完成时间与任务级解绑原因 #### Scenario: View unbind import task detail with row results - **GIVEN** 用户在任务列表点击某个任务 - **WHEN** 任务详情加载 - **THEN** 系统 MUST call `GET /api/admin/phone-asset-associations/unbind-imports/{id}` - **AND** 详情 MUST 展示任务级信息(含 `unbind_reason`、`error_message` 与任务统计) - **AND** 逐行结果 MUST 展示行号(`line`,自数据首行起计)、`asset_identifier` 原文、定位到的资产类型/ID(未定位时为空/0)、行状态(3 成功 / 4 失败)与失败原因 - **AND** 成功行 MUST 展示解绑当时完整关联手机号快照(`associated_phones`)与该行解除的有效关系数(`unbound_count`) - **AND** 任务级失败时 `items` 为空数组,前端 MUST 展示任务的 `error_message` ### Requirement: Associated Phones Display on Existing Lists The admin frontend SHALL display the current associated phone numbers of an asset on existing device, standalone IoT card, and asset resolve surfaces. #### Scenario: Show associated phones on device list - **GIVEN** `GET /api/admin/devices` 响应包含 `associated_phones` - **WHEN** 设备列表渲染 - **THEN** 系统 MUST 展示「关联手机号」列,值为完整手机号数组(无关联显示为空/「-」) #### Scenario: Show associated phones on standalone IoT card list - **GIVEN** `GET /api/admin/iot-cards/standalone` 响应包含 `associated_phones` - **WHEN** 单卡列表渲染 - **THEN** 系统 MUST 展示「关联手机号」列,多号以「、」分隔 #### Scenario: Show associated phones on asset resolve detail - **GIVEN** `GET /api/admin/assets/resolve/{identifier}` 响应包含 `associated_phones` - **WHEN** 资产详情渲染 - **THEN** 系统 MUST 展示「关联手机号」字段,无关联显示为空/「-」 ### Requirement: Storage and Export Contract Extension The admin frontend SHALL extend the upload-purpose enum and align with the export column change for card and device export files. #### Scenario: Upload URL for phone unbind imports - **GIVEN** 需要上传解绑导入 CSV - **WHEN** 调用 `POST /api/admin/storage/upload-url` - **THEN** 请求 MUST 使用 `purpose=phone_unbind_import` - **AND** `FilePurpose` 类型 MUST 增加 `phone_unbind_import` - **AND** 上传目录前缀为 `phone-unbind-imports/YYYY/MM/DD/uuid.csv`,前端不感知前缀,仅约束扩展名为 `.csv` #### Scenario: Export files gain associated phone column - **GIVEN** 卡导出/设备导出任务完成并生成文件 - **WHEN** 用户下载导出文件 - **THEN** 文件表头尾部 MUST 包含「关联手机号」列(多号以「、」连接) - **AND** 前端导出任务列表/详情页 MUST NOT 为此改动调用任何新接口 - **AND** 历史任务重导出仍按旧表头输出,前端不需要兼容性处理 ### Requirement: Role-Based Entry Visibility and No Creation Entry The admin frontend SHALL gate all new phone-asset association pages and actions to super admin/platform accounts, and MUST NOT offer any create or supplement entry. #### Scenario: Hide entries for non-platform roles - **GIVEN** 当前用户为代理/企业/个人账号 - **WHEN** 渲染菜单与页面按钮 - **THEN** 导航菜单、解绑按钮与 CSV 导入入口 MUST 不渲染 - **AND** 若有越权直达路由,后端 403 文案 MUST 原样展示 #### Scenario: No create/supplement entry - **GIVEN** 用户打开「手机号资产关联」相关页面 - **WHEN** 页面可用操作被枚举 - **THEN** 页面 MUST NOT 提供创建或补录关联的入口 - **AND** 关联只能由 H5 短信验证产生,前端默认不展示任何新增表单 ### Requirement: Unify Backend-Returned Error Copy The admin frontend SHALL render backend-provided fixed error copy verbatim without composing its own messages. #### Scenario: Display fixed failure copy - **GIVEN** 解绑相关接口返回失败 - **WHEN** 失败文案为约定固定文案之一 - **THEN** 前端 MUST 直接在弹窗/错误提示中展示「无权限操作该资源或资源不存在」 - **AND** 对后端可能返回的「该手机号最多关联10项有效资产」「新手机号已存在与待迁移资产相同的有效关联,换绑已回滚」MUST 仅作透传展示,不在前端拼接或改写