# 需求:onboard-hub-后台卡片列表「卡标题」列每行渲染成破折号：列表接口不返回-title

状态: proposed
归属: 公司基建
来源: 登记缺口(ops/products/#onboard-hub)
日期: 2026-10-20
建议: 做,第三轮实测收口时发现的操作台缺陷：列表页绑了 field=title 但 Handler.List 不带该字段，标题只在加密 payload 里；影响是操作者分不清同一成员多张卡里哪张是当前活跃卡

## 现状

- 盘点缺口来源产品:onboard-hub
- 现象(2026-10-20 第三轮实测收口时真实像素复验发现):后台「入职卡」列表的**卡标题列每行都是 `—`**,
  只剩成员/状态/时间能看。操作者因此分不清同一成员多张卡里哪张是当前活跃卡——重发链
  (`prevCardId`/`replacedBy`)在库里是有的,但界面上没有可读的标题就没法一眼认出「这条是给谁的第几张」。
- 根因(代码定位,`onboard-hub` @ `7603d2d`):前端列表列绑的是 `field:'title'`
  (`frontend/src/views/onboard/card/index.vue:82-84`),而后端回的是明文 `Card` 行——`Card` 结构体
  **根本没有 Title 字段**(`backend/internal/card/model.go:13-35`),标题只存在于 `PayloadEnc` 密文里的
  `Payload.Title`;`Handler.List`(`backend/internal/card/handler.go:457`)不解密,所以字段恒缺,
  表格落到空值占位符。
- 这不是「前端少写一行」的偶发,而是**同一份内容两条产生路径只验了一条**的老毛病(与本轮 N10 的
  Reissue 旧快照同族):列表页从来没被任何判据断言过标题非空,所以缺字段一直绿着。

## 方案

签发那一刻就把标题以明文另存一列(标题本身不含凭据,明文可接受;凭据仍留在密文 payload 里):

1. `backend/internal/card/model.go`:`Card` 加 `Title string` (`json:"title" gorm:"size:128"`),
   与 `CredFp` 同处按「为什么要有」写注释;`Payload` 侧不动。
2. `backend/internal/card/service.go`:`Issue`/`Reissue`/手写卡三条落库路径统一在写行时带上渲染出的
   标题(与首次签发同一个渲染函数产出,禁止从旧卡搬——这是 N10 立的规矩)。
3. migrations 加列;历史行一次性回填:遍历 `PayloadEnc` 能解出标题就写,解不了(密钥轮换/手写卡)留空,
   列表对空值显示「历史卡未记标题」而不是 `—`,避免被读成「渲染坏了」。
4. 负向判据一条:标题里出现凭据值(口令/令牌明文)必须被拒——防止以后有人把敏感值拼进标题绕过加密。
5. 前端不改绑定(`field:'title'` 是对的),只补一条真实像素复验。

## 验收标准

- [ ] 后端判据先红后绿:新建卡与**重发卡**两条路径都断言 `GET /api/cards` 返回行 `title` 非空且等于卡面标题(成对判据,不许只验新建)
- [ ] 负向用例:把口令值拼进标题签发 → 被拒,断言精确到错误码/错误信息
- [ ] 历史卡回填实跑:NAS 现网存量行执行后列表标题可读,解不了的行显示「历史卡未记标题」
- [ ] `deploy/probes/` 增一条列表标题断言并入 `card-read-safety` 项数,部署后线上实跑全绿
- [ ] 真实像素复验(截图 + computed style)证明列里不再是 `—`,截图当场从线上实例生成不复用旧快照
