# PRD：UR#53 卡和设备实名状态筛选

Status: ready-for-agent

---

## Problem Statement

卡列表和设备列表目前不能按实名状态筛选。卡已有实名字段，但列表未暴露过滤参数；设备没有稳定的设备级实名状态，页面或查询若临时遍历绑定卡会产生口径不一致和 N+1 风险。

设备实名状态还会随卡实名变化、绑卡、解绑和换卡改变，必须有统一刷新机制，避免快照残留。

## Solution

卡列表按卡自身 `real_name_status` 筛选。设备新增实名状态快照：只要存在一张有效绑定且未软删除的已实名卡，设备即为已实名，否则为未实名。

两个列表都支持可选 `real_name_status=0|1`，返回数值和中文名称，并与其他筛选按 AND 组合。设备列表直接查询快照；实名状态变化和绑定关系变化通过统一、可重算的投影刷新旧/新设备。

## User Stories

1. 作为运营人员，我希望分别查看全部、已实名和未实名的卡。
2. 作为运营人员，我希望分别查看全部、已实名和未实名的设备。
3. 作为运营人员，我希望设备状态反映全部有效绑定卡，而不是只看某个插槽或前端临时结果。
4. 作为运营人员，我希望实名筛选与归属、运营商、套餐系列等现有条件同时生效。
5. 作为维护人员，我希望列表不逐设备查询绑定卡，并且绑定变化后不会残留旧状态。
6. 作为代理账号，我希望筛选不能绕过现有店铺数据范围。

## Implementation Decisions

### 状态语义

- 实名状态继续使用项目常量：`0=未实名`、`1=已实名`，属于生命周期状态，类型为 int。
- 卡的列表状态直接读取卡自身 `real_name_status`。
- 设备存在至少一条有效绑定，且关联卡未软删除、`real_name_status=1` 时，设备快照为 1；否则为 0。
- “任意绑定卡已实名”包含所有有效插槽，不仅是 Gateway 当前使用卡。
- 已解绑、已软删除的绑定和已软删除卡不参与计算。
- `realname_link_type=none` 只表示业务无需实名，不会把卡或设备状态伪造为 1；筛选展示实际实名快照。

### API 契约

- `GET /api/admin/iot-cards` 增加可选 `real_name_status=0|1`。
- `GET /api/admin/devices` 增加可选 `real_name_status=0|1`。
- 参数未传时不增加实名条件；传 0 必须正确过滤未实名，不能因零值被当成未提供。
- 新条件与所有已有条件按 AND 组合，并共同作用于 `total` 和分页数据。
- 卡列表继续返回现有 `real_name_status`、`real_name_status_name`。
- 设备列表新增同名数值和中文字段，description 必须与实名状态常量一致。
- 非法值、解析失败或列表 DTO 其他已声明校验失败，统一返回 HTTP 400、`code=1001`、`msg=参数验证失败`，详细原因只写日志。
- 两个列表 Handler 均执行完整请求 DTO 校验；分页默认值、最大 100、权限范围、现有排序和统一响应结构不变。
- 超级管理员和平台账号保持原范围；代理账号继续只看当前权限范围；本需求不得通过实名筛选扩大可见数据。

### 设备快照与迁移

- 设备增加非空 int 快照字段 `real_name_status`，默认 0，并使用中文数据库注释。
- 迁移时按现有有效绑定关系一次性计算全部历史设备，不能简单依赖默认 0。
- 迁移向下删除新增快照及其专用索引，不修改卡实名数据或绑定历史。
- 设备列表使用快照直接过滤，不在分页查询中为每台设备执行 EXISTS、子查询循环或逐设备加载卡。
- 为设备实名筛选建立适合未软删除列表的非唯一索引；卡表是否补充同类索引以实际表结构和 EXPLAIN 为准，不得创建唯一约束。
- 快照刷新实现为可重入的批量投影：输入去重后的设备 ID 集合，按数据库真实绑定关系重新计算，而不是依赖“加一/减一”计数。

### 快照刷新时机与公共 DDD

- 卡实名状态变化必须刷新其当前有效绑定设备。上游实名状态只允许由数据同步公共 `ApplyCardObservation` 写入；快照刷新由同一事务的领域事件/可靠 Outbox 驱动，不在旧轮询 Handler 另写一套。
- 管理员人工实名纠偏也必须进入公共卡状态用例并产生相同状态变化事件，不能单独维护设备快照。
- 绑卡成功后刷新目标设备；解绑成功后刷新原设备。
- 卡从旧设备换到新设备或换货迁移绑定时，同时刷新旧设备和新设备；不得只刷新最终设备。
- 同一次操作涉及多个设备时批量去重刷新，禁止逐条 N+1。
- 重复事件和任务重试必须安全；投影按当前数据库事实重算，最终结果一致。
- 若状态事务成功但快照更新暂时失败，使用可靠任务重试并记录错误；不得永久吞掉失败。列表允许短暂最终一致，但必须有可观测的恢复路径。

### 前端

- 卡列表和设备列表筛选区增加“实名状态”：全部、已实名、未实名。
- 全部时不传参数；已实名传 1；未实名传 0。
- 表格显示后端 `real_name_status_name`，前端不遍历绑定卡计算设备状态。
- 分页和重新加载继续携带当前筛选；实名条件与其他条件一并提交。
- 沿用页面现有加载、空态和失败反馈。

### 文档与发布

- 更新两个列表的 OpenAPI 请求和设备响应定义，并重新生成文档。
- 新增 UR#53 中文实施总结并更新 README 索引。
- 推荐在公共数据同步 DDD 的状态事件可用后接入设备投影；数据库字段、历史初始化、API 和投影刷新需在同一维护窗口发布。

## Testing Decisions

- 领域/投影测试覆盖：无绑定、仅未实名卡、任一实名卡、多张混合卡、软删除卡、软删除绑定和全部解绑。
- 迁移测试验证历史设备正确初始化，向下迁移只移除新增结构，重新向上后结果可重建。
- HTTP 集成测试使用真实 PostgreSQL、Redis、JWT 和进程内 Fiber App，覆盖卡/设备的全部、0、1、非法值、AND 条件、分页及代理隔离。
- 验证 `real_name_status=0` 不因 Go 零值丢失。
- 验证卡状态变化事件、人工纠偏、绑定、解绑、旧设备到新设备换卡分别刷新正确设备。
- 验证重复投影任务结果不变，暂时失败后可重试恢复。
- 对列表查询做 SQL 计数或日志断言，设备数量增加时不得产生逐设备实名查询。
- 使用代表性数据执行 EXPLAIN/耗时核查，满足项目数据库查询与 API 性能目标。
- 前端人工验收筛选值、状态名称、组合条件、分页保留和空态。

## Out of Scope

- 不改变实名状态枚举，不增加“部分实名”等第三种状态。
- 不把“运营商无需实名”展示为“已实名”。
- 不由前端计算设备实名状态。
- 不在列表实时调用 Gateway 或等待同步。
- 不为该读取需求迁移整个卡或设备模块。
- 不修改运营商实名回调规则和解除实名策略；它们属于公共数据同步需求。

## Further Notes

- 设备实名状态是可重建查询投影，不是新的业务真相；绑定卡状态和有效绑定关系仍是权威来源。
- 当前代码已有卡响应实名字段，但两个列表缺少过滤，设备模型也无快照；实现时不要误以为现有设备详情的临时派生值可以直接作为列表字段。
