---
id: PRD-001
title: 实名认证
status: approved
owner: @yarnb
created_at: 2026-04-23
approved_at: 2026-04-23
linked_issue: "#009"
linked_objective: O4-production-ready
linked_milestone: M7
linked_features: []
---

# PRD-001 — 实名认证

## 1. 问题 (Why)

**现在做**的原因（按紧迫性）：

1. **合规红线**：中国相亲 / 社交类 App 2019 年网信办《互联网个人信息保护》《未成年人保护》强制实名。**不做无法上应用商店**（App Store / 国内安卓市场均审核卡这条）。
2. **反诈骗 / 反骚扰**：xiangqin 当前只手机号注册，任何人买虚商号都能注册发信，诈骗 + 骚扰成本极低。相亲场景（涉及感情 + 资金）尤其敏感。
3. **举报闭环**：#025 admin 封号机制已上线，但被封号的人换虚商号重注册成本 0 —— 实名是封号的真实效力基础。

**不做的代价**：
- 上架被拒 → 用户获取渠道只剩手动分发 → 天花板低
- 一次恶性事件（诈骗 / 骚扰上热搜）→ 被举报 → 下架 / 约谈 / 公司信誉损失
- 现有用户信任度打折（"这 app 谁都能注册，不敢真交流"）

## 2. 目标用户 & 场景

- **新注册用户**：完成手机号 verify 后立刻进入实名流程
- **存量用户**（已注册未实名）：下次发信前必须实名，读信不拦
- **scenarios 引用**：待新增 `scenarios/day-in-life-realname-onboarding.md`（目前 scenarios/ 里没有，本 PRD 落地时补）

## 3. 成功指标

| 指标 | 目标 | 反向保护 |
|---|---|---|
| 新注册用户实名完成率 | ≥ 85% | 注册 → 实名放弃率 ≤ 15% |
| 实名 API 成功率（一次通过） | ≥ 95% | 误拦率 ≤ 1%（真人被挡） |
| 合规覆盖率 | 100% | 未实名用户发信 = 0 |
| 用户体验成本 | 平均实名耗时 ≤ 60s | 活体重试次数 ≤ 1.2 次 |

## 4. 方案范围 (What)

### In scope（v1）

- **身份证 OCR**：用户上传正反面照片，三方 API 返姓名 + 证件号 + 有效期
- **二要素核验**：姓名 + 证件号 → 公安库核验（三方 API）
- **存储加密**：
  - 身份证图片 OSS KMS 加密
  - 证件号 HMAC-SHA256 hash 入库（同 phone 规则）
  - 姓名脱敏存（明文，展示 `张**`）
- **失败重试**：同一号 24h 内最多 3 次（避免暴力撞库）
- **业务闸门**：`users.verified_at` 字段；`mail.send` 前置校验，未实名抛 `not_verified`

### Out of scope（v2+）

- **活体检测 + 人脸比对**（3 要素核验）→ v2 接阿里云人脸核身。v1 只做 2 要素已足够合规最低线
- **港澳台 + 外国人证件** → 用户量极少，v3 再加
- **未成年判断**：v1 靠 OCR 返回的年龄字段，业务层直接拒 < 18 注册（**低成本**但算 in scope 里顺手做）
- **审核工单 / 人工申诉**：v2 再建，v1 失败就失败，引导联系客服邮箱
- **信息变更流程**（换身份证 / 改名）：v3，当前一次绑定终身

## 5. 用户流程

```
┌─ 新注册 ────────────────────────────────────────────┐
│ 手机号 verify → 进入实名页                          │
│   ↓                                                  │
│ 填姓名 + 证件号（CLI: xq auth verify-id <name> <id>） │
│   ↓                                                  │
│ 可选上传照片（OCR 辅助填写，用户可跳过手填）        │
│   ↓                                                  │
│ 调三方核验 API                                       │
│   ↓                                                  │
│ 通过 → users.verified_at=now, 跳转填资料             │
│ 失败 → 重试（24h 内 3 次上限）                       │
│ API 超时 → 显示"稍后重试"，不扣重试次数              │
└──────────────────────────────────────────────────────┘

存量用户首次发信拦截：
mail.send → check verified_at IS NULL → 抛 not_verified → 引导走上面流程
```

## 6. 方案选型

| 维度 | 阿里云实人认证 | 腾讯云 FaceID | 公安一所（eID） |
|---|---|---|---|
| 2 要素核验单价 | ¥0.3-0.5 | ¥0.3-0.5 | 合规性最高但接入复杂 |
| OCR | 已集成 | 已集成 | 无 |
| 沙箱 | ✅ 有官方测试身份证号 | ✅ 有 | ❌ |
| 现有 vault AK | ✅ aliyun-main 可扩权限 | ❌ 要新开账号 | N/A |
| 生态覆盖 | 已用 OSS / SMS / ECS | 无 | 无 |

**选阿里云**。理由：现有 vault 凭证可扩权限，不引入新厂商；SDK 成熟；跟 SMS / OSS 同账号计费统一。

决策会在上线前固化到 `decisions/YYYY-MM-DD-realname-vendor-aliyun.md`（本 PRD approved 时补）。

## 7. 拆解到 FEAT（待落地）

- [ ] FEAT-030: `xq auth verify-id` CLI（用户手输姓名+证件号）
- [ ] FEAT-031: `/auth/verify-id` API endpoint + 调阿里云实人认证 2 要素
- [ ] FEAT-032: 身份证 OCR API（可选，用户上传图片辅助填写）
- [ ] FEAT-033: `users.verified_at` + `users.cert_id_hash` + `users.legal_name` DB 迁移
- [ ] FEAT-034: `mail.send` 前置 `assert_user_verified` 闸门
- [ ] FEAT-035: 重试限流（24h 内 3 次）+ 暴力防护
- [ ] FEAT-036: `xiangqin-verify-id` 沙箱 credential vault 声明 + E2E 测试（复用 #026 白名单测试身份证号模式）

## 8. 风险 & 回滚

| 风险 | 概率 | 影响 | 缓解 |
|---|---|---|---|
| 阿里云实人 API 宕机 / 限流 | 低 | 高（新用户无法注册完成） | 失败写入 `verification_pending` 队列，24h 内自动重试；用户看到"稍后再试" |
| 用户拿虚假身份证过 OCR 但 2 要素核验挂 | 中 | 低 | 2 要素本身是对公安库，绝大多数能查到；真挂了 = 合规走到边界 |
| 误拦真实用户 | 中 | 中 | v1 引导客服邮箱 + 人工兜底（邮件提工单，team-lead 手动白名单） |
| 存量用户抗拒补实名导致流失 | 高 | 中 | 只在"发信"时拦，"读信 + 查询"不拦，流失只损失不活跃的 |
| 实名数据泄漏 | 低 | **极高** | OSS KMS 加密 + 证件号仅 hash + 姓名脱敏 + 访问日志 + 最小权限 RAM |

**回滚**：`XIANGQIN_SKIP_VERIFY=1` env 开关，一键跳过实名闸门。仅紧急用 + 明确标合规风险窗口。

## 9. 里程碑

| T+X | 交付 |
|---|---|
| T+0 | PRD approved，ADR 选型 decisions/ 落地，FEAT-030~036 创建 |
| T+3 | 沙箱 E2E 打通（阿里云测试身份证号）  |
| T+5 | CLI + API + DB 迁移全实现 |
| T+7 | 10% 白名单用户灰度（XIANGQIN_REQUIRE_VERIFY_FOR_PHONES=<白名单>） |
| T+10 | 指标达标，全量（新注册必须实名；存量发信拦截） |

## 10. 讨论 / 反馈

### 2026-04-23 @claude — 初稿

PRD 草稿初版，覆盖 P0 合规最低线（2 要素）+ 未成年保护（OCR 年龄）。活体 / 人脸比对留 v2，避免一次做太大挡住上线。

### 2026-04-23 @yarn — approved (按 claude 推荐)

4 个待定点全选推荐：
1. ✅ v1 只 2 要素（姓名+证件号），活体留 v2
2. ✅ 存量"发信拦读信不拦"（最小破坏）
3. ✅ 沙箱对齐 #026 白名单模式
4. ✅ 指标 85% / ≤15% 暂定，上线后调整

### 下一步执行

不直接拆 FEAT 写代码。**先做方案可行性 POC**：
1. 调研阿里云实名认证 API（2 要素 / 沙箱测试号 / 返回格式）
2. 写一个独立脚本（`scripts/poc-aliyun-realname.py`）真调沙箱 API，验证能拿到"通过"返回
3. POC 通过 → 才拆 FEAT-030~036 进 xiangqin 主代码

这样避免拆了 FEAT 做一半发现 API 规格不匹配返工。

### 2026-04-23 @claude — POC 阻塞

**现状**：`aliyun cloudauth Id2MetaVerify` API 存在（Cloudauth CLI 子命令），参数清晰（`UserName` + `IdentifyNum` + `ParamType=normal`）。直接调用返回 `Code: 410 未开通服务`。

**发现**：阿里云实人认证**分两层**：
- 顶层 Cloudauth 产品（金融级实人认证 / 信息核验 / 企业身份识别 三大类）
- **信息核验** 是独立子服务，Id2MetaVerify 归这一类
- 必须**额外开通信息核验 + 买流量包**才能调 API（4 步流程：开通 → 买流量包 → SDK → 调用）
- 即使按量付费也要先买一个最小流量包激活

**阻塞老板**（system rule 卡，我代不了）：
1. ✅ 2026-04-23 晚已开通顶层 Cloudauth
2. ⏳ 开通"信息核验"子服务 + 买流量包（购买页 https://common-buy.aliyun.com/?commodityCode=cloudauth_info_check）

开通完成后我继续：
- 在 vault 建 RAM 子账号 `aliyun-cloudauth` 最小权限 AK（复用 aliyun-main 也行，第一步 POC 不做 scope 隔离）
- `vault.json` 加 credential `aliyun_cloudauth` 声明（POC 阶段可跳过，直接 env）
- 用 `aliyun cloudauth Id2MetaVerify` CLI 一行真调 API 看返回
- 拿到"核验通过"返回 → POC ✅ → 拆 FEAT-030~036 进 xiangqin 主代码
