# `sms_auth_event` 表设计 日期:`2026-04-21` ## 1. 文档目的 这份文档用于完成 `M2` 的第六条任务:`设计 sms_auth_event`。 目标是把短信鉴权事件表固定成一张“短信发送与验证码校验的统计源表”,并明确: 1. 哪些短信相关动作要写入事件 2. 哪些动作不应该写进这张表 3. 发送频控、失败次数限制、captcha 触发、风险保护分别依赖什么统计口径 4. 它与 `auth_risk_block`、`auth_audit_log` 的边界 ## 2. 当前基线 当前 Node 后端已经存在 `sms_auth_events` 表,并有稳定的读写链路: 1. `POST /api/auth/phone/send-code` 在短信发送成功后写入一条 `send_code` 事件 2. `POST /api/auth/phone/login` 在验证码校验成功或失败后写入 `verify_code` 事件 3. `POST /api/auth/wechat/bind-phone` 在绑定手机号时复用同一套 `verify_code` 事件 4. `POST /api/auth/phone/change` 在换绑手机号时复用同一套 `verify_code` 事件 5. 发送频控、失败次数限制、captcha 触发与风险保护,全部依赖这张表统计 当前 Node `sms_auth_events` 字段基线: 1. `id` 2. `phone_number` 3. `scene` 4. `action` 5. `success` 6. `ip` 7. `user_agent` 8. `created_at` 当前已落地的 `scene` 基线: 1. `login` 2. `bind_phone` 3. `change_phone` 当前已落地的 `action` 基线: 1. `send_code` 2. `verify_code` ## 3. 表职责边界 ### 3.1 `sms_auth_event` 负责 1. 记录短信验证码发送成功事件 2. 记录短信验证码校验成功或失败事件 3. 作为手机号维度与 IP 维度的短信鉴权统计源 4. 为发送频控、失败次数限制、captcha 触发、风险保护提供统一统计基础 ### 3.2 它不负责 1. 保存验证码明文或验证码 hash 2. 保存阿里云短信 provider 的完整原始响应 3. 记录当前风险保护是否仍生效 4. 记录账号安全动作的长期审计历史 ### 3.3 与其他表的边界 1. `sms_auth_event` 负责“发生过哪些短信发送/校验事件” 2. `auth_risk_block` 负责“经过统计后当前是否处于保护状态” 3. `auth_audit_log` 负责“账号维度发生过哪些安全动作” ## 4. 访问级别 `sms_auth_event` 固定为 `private table`。 原因: 1. 原始手机号、原始 IP、原始 UA 都属于敏感安全数据 2. 这张表主要服务于后端风控与统计,不直接对前端暴露 3. 前端不应该直接查询或订阅短信验证码操作明细 ## 5. 字段设计 | 字段名 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `sms_event_id` | `String` | 是 | 主键,建议继续沿用 `smsev_*` 前缀。 | | `phone_e164` | `String` | 是 | 标准化后的 `E.164` 手机号。 | | `scene` | `String` | 是 | 业务场景,枚举固定为 `login`、`bind_phone`、`change_phone`。 | | `action` | `String` | 是 | 动作类型,枚举固定为 `send_code`、`verify_code`。 | | `success` | `bool` | 是 | 当前动作是否成功。 | | `ip` | `Option` | 否 | 请求来源 IP;缺失时为 `null`。 | | `user_agent` | `Option` | 否 | 请求来源 UA;缺失时为 `null`。 | | `created_at` | `String` | 是 | 事件发生时间,UTC RFC3339。 | 补充约束: 1. 当前阶段不存 `provider_request_id`,因为这张表不是供应商排障日志表。 2. 当前阶段不存 `provider_code`、`provider_message`,避免把供应商响应日志职责压进统计源表。 3. 当前阶段不存 `user_id`,因为验证码发送和校验发生时,场景并不总是已经稳定归属到某个正式账号。 ## 6. 枚举与语义设计 ### 6.1 `scene` 当前阶段固定只支持: 1. `login` 2. `bind_phone` 3. `change_phone` 解释: 1. `login` 对应手机号验证码登录 2. `bind_phone` 对应微信待激活账号绑定手机号 3. `change_phone` 对应正式账号更换手机号 ### 6.2 `action` 当前阶段固定只支持: 1. `send_code` 2. `verify_code` 解释: 1. `send_code` 表示验证码已成功发出 2. `verify_code` 表示一次验证码校验尝试已经结束 ### 6.3 `success` 固定语义: 1. `send_code + success = true` 表示供应商确认发送成功 2. `verify_code + success = true` 表示验证码校验通过 3. `verify_code + success = false` 表示验证码校验失败或已失效 当前阶段特别约束: 1. `send_code + success = false` 暂不入表 2. 发送失败继续由应用日志、供应商日志与 tracing 承担排障,不混入当前统计口径 这样做的原因是: 1. 现有发送频控按 `action = send_code` 的总数统计 2. Node 当前只在发送成功后写事件 3. 若直接把发送失败也写入,会改变当前频控语义 ## 7. 统计口径设计 ### 7.1 发送频控 固定统计来源: 1. 按手机号统计 `action = send_code` 的事件数 2. 按 IP 统计 `action = send_code` 的事件数 当前 Node 兼容窗口: 1. 单手机号:`过去 1 天` 2. 单 IP:`过去 1 小时` ### 7.2 验证失败次数限制 固定统计来源: 1. 按手机号统计 `action = verify_code AND success = false` 2. 按 IP 统计 `action = verify_code AND success = false` 当前 Node 兼容窗口: 1. 单手机号:`过去 1 小时` 2. 单 IP:`过去 1 小时` ### 7.3 captcha 触发 固定统计来源: 1. 按手机号统计 `verify_code` 失败次数 2. 按 IP 统计 `verify_code` 失败次数 说明: 1. `captcha challenge` 自身不写进 `sms_auth_event` 2. `captcha_required` 审计事件继续写进 `auth_audit_log` ### 7.4 风险保护触发 固定统计来源: 1. 按手机号统计 `verify_code` 失败次数 2. 按 IP 统计 `verify_code` 失败次数 说明: 1. 风险保护命中后真正的当前态写进 `auth_risk_block` 2. `sms_auth_event` 只提供统计基础,不直接承载保护状态 ## 8. 写入规则 ### 8.1 `POST /api/auth/phone/send-code` 固定流程: 1. 先检查当前手机号 / 当前 IP 是否存在活跃 `auth_risk_block` 2. 再根据 `sms_auth_event` 统计发送频控 3. 再根据 `sms_auth_event` 统计决定是否需要 captcha 4. 短信 provider 返回发送成功后,写入一条: - `scene = 当前请求场景` - `action = send_code` - `success = true` ### 8.2 `POST /api/auth/phone/login` 固定流程: 1. 先检查当前手机号 / 当前 IP 是否存在活跃 `auth_risk_block` 2. 再根据 `sms_auth_event` 统计失败次数限制 3. 校验成功时写入: - `scene = login` - `action = verify_code` - `success = true` 4. 校验失败时写入: - `scene = login` - `action = verify_code` - `success = false` 5. 校验失败写入完成后,再按统计结果决定是否触发 `auth_risk_block` ### 8.3 `POST /api/auth/wechat/bind-phone` 固定流程: 1. 与手机号登录复用同一套校验前检查 2. 校验成功写入 `bind_phone + verify_code + success = true` 3. 校验失败写入 `bind_phone + verify_code + success = false` 4. 校验失败后再决定是否触发 `auth_risk_block` ### 8.4 `POST /api/auth/phone/change` 固定流程: 1. 与手机号登录复用同一套校验前检查 2. 校验成功写入 `change_phone + verify_code + success = true` 3. 校验失败写入 `change_phone + verify_code + success = false` 4. 校验失败后再决定是否触发 `auth_risk_block` ## 9. 查询索引与统计要求 ### 9.1 必须具备的唯一约束 1. `sms_event_id` 主键唯一 ### 9.2 必须具备的查询索引 1. `(phone_e164, action, created_at DESC)` 作用:支撑按手机号统计发送次数与失败次数 2. `(ip, action, created_at DESC)` 作用:支撑按 IP 统计发送次数与失败次数 3. `(phone_e164, action, success, created_at DESC)` 作用:支撑按手机号统计 `verify_code` 失败窗口 4. `(ip, action, success, created_at DESC)` 作用:支撑按 IP 统计 `verify_code` 失败窗口 说明: 1. 当前阶段不强求单独按 `scene` 建主索引,因为已有统计主要按手机号/IP 与动作窗口展开。 2. `scene` 继续作为事件上下文字段保留,便于后续如果要细分某一场景的频控,再追加索引。 ## 10. 读取规则 当前阶段 `sms_auth_event` 不直接对外暴露 DTO。 它只支撑后端内部这几类聚合查询: 1. `count_since_by_phone(phone_e164, action, success?, since)` 2. `count_since_by_ip(ip, action, success?, since)` 读取约束: 1. `ip = null` 时,按 IP 统计固定返回 `0` 2. 统计窗口由 Axum 应用层提供,不把“过去 1 小时”“过去 1 天”写死进表层 3. 表层只提供原子计数,不在表层拼装“是否需要 captcha / 是否需要封禁”的业务判断 ## 11. 与其他鉴权表的协作规则 ### 11.1 与 `auth_risk_block` 固定规则: 1. `sms_auth_event` 先累积失败事实 2. Axum 根据失败统计决定是否写入或刷新 `auth_risk_block` 3. 不能直接从 `auth_risk_block` 反推历史失败次数 ### 11.2 与 `auth_audit_log` 固定规则: 1. `sms_auth_event` 不承担安全操作审计展示职责 2. `captcha_required`、`risk_block_phone`、`risk_block_ip` 等用户可见安全事件,继续写进 `auth_audit_log` 3. 不允许为了省表而把 `sms_auth_event` 直接拿去充当账号操作记录 ## 12. 与当前 Node `sms_auth_events` 的映射关系 | Node 列 | 新字段 | 迁移规则 | | --- | --- | --- | | `id` | `sms_event_id` | 原样迁移。 | | `phone_number` | `phone_e164` | 重命名迁移,值原样保留。 | | `scene` | `scene` | 原样迁移。 | | `action` | `action` | 原样迁移。 | | `success` | `success` | 原样迁移。 | | `ip` | `ip` | 原样迁移。 | | `user_agent` | `user_agent` | 原样迁移。 | | `created_at` | `created_at` | 原样迁移。 | ## 13. reducer / service 落地约束 ### 13.1 `module-auth` reducer 层 必须至少具备: 1. `append_sms_auth_event` 说明: 1. 这是一张追加型统计源表,不做就地更新。 2. 后续若要做归档或清理,再单独增加 maintenance reducer 或离线清理任务。 ### 13.2 Axum 应用层 固定负责: 1. 标准化手机号为 `E.164` 2. 组织 `scene`、`action`、`success` 3. 在正确的时机写入事件 4. 基于统计结果决定是否触发 captcha 与 `auth_risk_block` ## 14. 不允许的设计漂移 后续实现时禁止出现以下情况: 1. 把验证码明文、验证码 hash 写进 `sms_auth_event` 2. 把阿里云 provider 的完整响应 JSON 直接写进 `sms_auth_event` 3. 把当前是否被保护的状态写进 `sms_auth_event` 4. 发送失败也无条件入表,却继续沿用当前“按 `send_code` 总量限流”的统计口径 5. 为了展示账号安全记录,直接把 `sms_auth_event` 暴露给前端 ## 15. 本任务完成定义 当以下条件满足时,`设计 sms_auth_event` 视为完成: 1. 发送与校验事件的写入范围已经固定。 2. 当前发送频控、失败限制、captcha 触发与风险保护的统计口径已经固定。 3. 已和 `auth_risk_block`、`auth_audit_log` 明确切开职责。 4. 后续可以直接按这份文档编码 reducer、计数查询与 Axum 应用层判断逻辑。 ## 16. 依据文件 1. `server-node/src/repositories/smsAuthEventRepository.ts` 2. `server-node/src/auth/authService.ts` 3. `server-node/src/services/smsVerificationService.ts` 4. `server-node/src/routes/authRoutes.ts` 5. `server-node/src/db/migrations.ts` 6. `packages/shared/src/contracts/auth.ts` 7. `docs/prd/ACCOUNT_SYSTEM_AND_LOGIN_ENTRY_PRD_2026-04-09.md` 8. `docs/prd/MY_TAB_SETTINGS_AND_SECURITY_PRD_2026-04-16.md` 9. `docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md` 10. `docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md`