# 联调验收与排障

## 先确认环境

1. 平台是否部署了包含本公开文档的版本；公开 `/docs` 不需要管理员登录。
2. 发现文档 issuer 是否与产品配置完全一致；不要混用 `sso.too1.net` 与 `platform.too1.net`。
3. Client 类型是否正确，回调是否精确登记，产品和 Client 是否启用。
4. 短信/支付 API 是否使用独立服务 Token，并具备相应 Scope、模板授权与产品商户绑定。
5. 服务器时间、HTTPS、DNS 和出口 IP/CIDR 白名单是否正确；不要关闭 TLS 校验来“修复”网络错误。

## 错误分类

OIDC Token 错误为 `{"error":"invalid_client","error_description":"..."}`。平台业务 API 错误为 `{"error":{"code":"...","message":"...","request_id":"..."}}`。不能用同一 JSON 字段类型直接解析两者。

| 场景 / 错误 | 重点检查 | 恢复方式 |
| --- | --- | --- |
| OIDC `invalid_client` | Client ID、类型、Basic 密钥、启用状态 | 修正配置，Native 不发 Secret |
| OIDC `invalid_request` | state/nonce/PKCE、回调、scope | 重新生成登录事务，不删安全参数 |
| OIDC `invalid_grant` | 授权码过期/重放、verifier、不匹配的回调、刷新重放 | 重新登录，禁止循环重放旧 Token |
| `login_required` | 无中央会话却要求静默登录 | 由用户发起交互式登录 |
| `invalid_token` | 服务令牌过期、用户令牌误用、Client 禁用 | 检查类型，再按策略重新获取服务 Token |
| `insufficient_scope` | Scope 或来源 IP 限制 | 管理员最小权限授权，不扩大到所有 Scope |
| `template_not_granted` | 模板授权/状态 | 后台登记并授权该产品 |
| `quota_exceeded` / `rate_limited` | 产品/手机号/模板额度与频率 | 等待窗口；不循环换 Key 绕限流 |
| `idempotency_conflict` | 同一个 Key 的原始请求字节不同 | 找回原请求，新的业务操作才用新 Key |
| `idempotency_in_progress` | 同 Key 请求仍在处理 | 等待并查询已知资源，不能当失败重新下单 |
| `return_url_not_allowed` | 返回 URL 与白名单不完全一致 | 登记实际地址，不传任意前端参数 |
| `duplicate_external_order` | 同产品订单已创建 | 使用保存的 payment_id 查询；无 ID 则运维核查 |
| `duplicate_external_refund` | 同业务退款号已申请 | 查询原退款，不能再次申请同一退款 |
| `channel_result_unknown` | 渠道未确认操作结果 | 保持处理中并查询，不把 202 当成功 |
| `storage_error` / `idempotency_storage_error` | 操作可能已到渠道但本地结果保存失败 | 保留原 Key/业务单号，查询或人工核查，禁止盲目重发 |
| `sms_disabled` / `payments_disabled` | 渠道、产品或事故开关 | 联系管理员；不要自动开启 |

平台的查询接口按产品隔离；其他产品的资源或不存在资源可能均返回 404，不要用这种差异枚举记录。

## 登录两次就限流、退出后立即又登录

先看失败请求的域名、路径和响应格式，区分产品网关与平台认证接口。产品返回的 `{"success":false,"error":{"code":"RATE_LIMITED"}}` 不是平台业务 API 的响应结构；不能仅凭“请求过于频繁”就断定平台密码失败计数达到阈值。

产品接入方应检查以下链路：

1. 登录入口、OIDC 回调、会话探测、退出是否共用同一个 IP 限流桶；计数可能是 HTTP 请求次数而非输错密码次数。
2. 浏览器 `OPTIONS` 是否被错误计入认证桶，429 是否带 `Retry-After` 和合法 Origin 的 CORS 响应头。
3. 反向代理是否传递真实 IP，以及后端是否仅信任实际代理的精确 CIDR。若所有用户被识别为同一个代理 IP，会共同消耗限额。不要开启全网可信代理。
4. `/me` 的 429/5xx 是否被当成未登录后自动跳转，以及登录失败页、退出结果页是否又自动启动 SSO。退出后保留中央会话是正常范围选择；自动登录循环不是。
5. 产品自己的 Session 是否确实在服务端撤销，不能仅删除前端缓存或旧 localStorage 项。

平台 `/logout` 的 GET 现在只显示确认页，POST 要求 CSRF。OIDC 产品请使用发现文档中的登出端点及登记的回跳地址。数据库故障时平台保留可重试 Cookie、返回错误及请求编号，不显示成功；重试成功后原 Cookie 必须无法再访问账号中心。不要把错误页或网络超时当成已经退出，也不要把回跳网页显示成功当成所有产品的业务会话都已撤销。

## 记录哪些排障信息

保留时间、平台请求编号 `X-Request-ID` / `error.request_id`、接口路由模板、HTTP 状态、安全错误码、产品内部操作编号、耗时。管理员凭请求编号在“系统日志”检索。

不要贴完整 Authorization、密码、短信验证码、Cookie、Refresh Token、一次性凭据或带收银台令牌的完整 URL。接口错误发生时，记录“创建支付失败”及安全编号即可，不输出全部 HTTP request/response。

公开文档不显示环境内部状态、管理员会话或实际配置。系统日志、渠道验证状态、审计和凭据仍只在受保护后台查看。

## 最小自动化验收矩阵

| 模块 | 必测成功 | 必测失败 |
| --- | --- | --- |
| Web OIDC | 首次认证、既有会话、原业务用户映射 | 错误 state/nonce/PKCE、错误 issuer/audience、过期签名、码重放 |
| Native | 回调接收、取消、超时、刷新保存 | Scheme 不符、跨事务、端口冲突、并发刷新 |
| Service | Token 获取、Scope 子集、到期更新 | 用户 Token 冒用、Native 密钥、禁用/IP 限制 |
| SMS | 授权模板、受理、查询 | 非 +86、额外参数、额度、未知结果、幂等冲突 |
| 支付 | 创建、查询、回调、权益 | 前端改金额、越权、重复下单、伪造返回成功 |
| 退款 | 部分/全额、审批后处理 | 超额、重复、并发、未知结果 |
| Webhook | 验签、可靠入库、幂等权益 | 伪造/超窗、重复/乱序、数据库失败和重启 |
| 迁移 | 正确映射、续跑、首登协议 | 空/重复手机号、外部 ID 冲突、无映射误建新号 |

## 平台提供的验收命令

```bash
./ssoctl doctor
./ssoctl audit verify
./ssoctl oidc smoke web --issuer https://platform.too1.net --client-id smoke-web --secret-file smoke-web.secret
./ssoctl oidc smoke native --issuer https://platform.too1.net --client-id smoke-native
./ssoctl platform payment simulate --channel wechat
./ssoctl platform payment simulate --channel alipay
```

doctor/audit 由持有平台运维配置的人运行。OIDC smoke 用独立测试 Client；模拟支付不等于真实 HTTP/数据库或商户验收。真实短信发送、真实支付/退款不能在 CI 中默认执行。

## 正式切换门禁

先升级 staging 并完成真实产品回调。至少完成一条真实联麓短信、公众号未关注/已关注扫码，微信和支付宝各 0.01 元支付、查询、回调、退款；未完成的渠道保持关闭。完成迁移对账、恢复演练、目标服务器容量测试和独立随机密钥检查，再逐步放量。

单元测试通过、文档公开或看到“登录成功”页面，都不能替代产品侧映射和本地会话验收。真实交易产生后回滚不能停掉回调/查询 Worker，更不能直接恢复不认识新交易数据的旧二进制。
