diff --git a/docs/im/IM-注册、邀请码与认证数据模型改造需求文档.md b/docs/im/IM-注册、邀请码与认证数据模型改造需求文档.md new file mode 100644 index 0000000..cb0e7ca --- /dev/null +++ b/docs/im/IM-注册、邀请码与认证数据模型改造需求文档.md @@ -0,0 +1,539 @@ +# IM 注册、邀请码与认证数据模型改造需求文档 + +> 版本:V1.0 评审稿 +> 编写日期:2026-08-18 +> 适用范围:IM 客户端、IM 服务端、HotGo 管理后台、MySQL +> 关联文档:IM-前后端对接文档.md 1.4、IM-系统设计文档.md + +## 1. 文档目的 + +本文统一定义以下改造: + +1. 注册账号支持“用户名、手机号、用户名加手机号”三种输入组合。 +2. 手机号注册支持短信验证,后端固定补中国国家号 +86。 +3. HotGo 后台增加注册短信发送开关。 +4. 邀请码作为无短信验证时的注册放行凭证。 +5. 邀请码支持单个生成、批量生成、自定义有效期、撤销和使用审计。 +6. im_users 增加非空 username,phone 改为可空。 +7. im_auth 删除数据库字段 scheme 和 uname,只按 userid 保存 Basic 密码凭据。 + +## 2. 核心业务定义 + +| 名称 | 定义 | +| --- | --- | +| 用户名输入 | 注册页面独立的用户名输入框 | +| 手机号输入 | 注册页面独立的手机号输入框,只接受 11 位 ASCII 数字 | +| 放行优先级 | 手机号非空且短信开关开启时以验证码为注册放行依据;短信开关关闭或未填写手机号时以邀请码为注册放行依据 | +| 已验证手机号 | 已成功消费 register 场景 verification_token,并建立已完成 tel credential 的手机号 | +| 用户名注册 | 保存用户填写的 username;未填写手机号时依靠有效邀请码放行,phone 为 NULL | +| 手机号注册 | 手机号字段非空的注册;保存用户填写的 username 和规范化 phone,未填写 username 时由服务端随机生成 username;短信验证状态由 tel credential 区分 | +| 注册短信开关 | 控制服务端是否真正调用短信服务商的全局后台配置 | +| 邀请码 | 租户内生成的 5 位大小写英文字母明文码,区分大小写、不可重复,有效期内可供多人使用、可撤销 | + +## 3. 用户账号数据规则 + +### 3.1 im_users 字段 + +目标字段: + +| 字段 | 约束 | 说明 | +| --- | --- | --- | +| username | 非空、租户内唯一 | 主登录账号;用户填写的普通用户名必须匹配 `^[a-z0-9]{1,10}$`。未填写 username 但填写手机号时,由服务端随机生成 3 位小写字母 + 6 位数字的系统 username | +| phone | 可空、租户内唯一 | 用户填写手机号时的注册号码,统一保存 E.164;是否完成短信验证由 tel credential 判断 | + +落库规则: + +| 注册方式 | username | phone | +| --- | --- | --- | +| 用户名加手机号,验证码注册 | alice | +8613800138000 | +| 仅手机号,验证码注册 | abc123456 | +8613800138000 | +| 普通用户名加邀请码注册 | alice | NULL | +| 用户名加手机号,短信关闭邀请码注册 | alice | +8613800138000 | +| 仅手机号,短信关闭邀请码注册 | abc123456 | +8613800138000 | + +普通用户名注册规则: + +1. 必须匹配 `^[a-z0-9]{1,10}$`,长度为 1~10 位。 +2. 只允许 ASCII 小写英文字母 `a-z` 和数字 `0-9`,允许纯字母、字母数字组合或纯数字。 +3. 大写字母、中文、空格、点、下划线、冒号及其他符号均不允许;服务端校验原始输入,不通过静默转小写来放行不合规注册。 +4. 前端按相同规则限制输入并提示,服务端必须再次校验,不能只依赖客户端。 +5. 该规则只约束用户填写的普通用户名。服务端随机生成的系统 username 固定为 9 位 `^[a-z]{3}[0-9]{6}$`,属于系统值例外;存量迁移生成的内部保留账号也不适用此正则,不能作为普通用户名注册。 + +### 3.2 字段保存和手机号规范化 + +1. 页面分别展示用户名输入框和手机号输入框。 +2. 用户名字段非空时必须按原值校验并保存到 `im_users.username`,不能因为同时填写手机号而忽略用户名。 +3. 手机号字段非空时必须校验并保存到 `im_users.phone`;手机号字段为空时 `phone` 保持 NULL。 +4. 手机号字段使用 ^[0-9]{11}$ 校验;手机号非空但格式错误时直接拒绝,不能降级为用户名方式。 +5. 发送短信和落库前,后端把 11 位数字规范化为 +86xxxxxxxxxxx。 +6. 用户名和手机号同时填写时,`username` 保存用户填写的用户名,`phone` 保存规范化手机号。 +7. 仅手机号注册且 username 为空时,服务端随机生成 `^[a-z]{3}[0-9]{6}$` 格式的 username,检查租户内唯一,冲突时重新生成;`phone` 保存规范化手机号。 +8. 不提供国家和区号选择,本期固定中国 +86。 +9. 登录页面仍使用统一的“用户名/手机号”输入框;先精确查询 username,未命中时将 11 位输入补 +86 后查询 phone。 +10. 注册、绑定手机号时必须检查 username 和 phone 两个命名空间,防止同一输入命中两个用户。 +11. 普通用户名注册必须先校验 `^[a-z0-9]{1,10}$`;用户名字段非空且格式错误时必须拒绝,不能因手机号合法而跳过用户名校验。 +12. 注册创建公开资料时,账户昵称暂时默认等于最终保存的 username;客户端未传 `desc.public.fn` 或传入值为空时,服务端使用最终 username 填充昵称。 + +## 4. 注册放行规则 + +### 4.1 决策表 + +| 用户名字段 | 手机号字段 | 短信开关 | 有效验证票据 | 邀请码 | 服务端结果 | +| --- | --- | --- | --- | --- | --- | +| 已填 | 空 | 任意 | 不适用 | 有效 | 放行,保存 username,phone=NULL | +| 已填 | 空 | 任意 | 不适用 | 未填或无效 | 拒绝 | +| 已填 | 11 位 | 开 | 有 | 未填 | 放行,保存 username 和 phone | +| 已填 | 11 位 | 开 | 有 | 有效 | 放行,保存 username 和 phone,并记录邀请码使用关系 | +| 空 | 11 位 | 开 | 有 | 未填或有效 | 放行,随机生成 username,phone 写手机号 | +| 任意 | 11 位 | 开 | 无 | 任意 | 拒绝,邀请码不能绕过已开启的短信验证 | +| 已填 | 11 位 | 关 | 无 | 有效 | 放行,保存 username 和 phone | +| 空 | 11 位 | 关 | 无 | 有效 | 放行,随机生成 username,phone 写手机号 | +| 任意 | 11 位 | 关 | 无 | 未填或无效 | 拒绝 | +| 已填 | 格式错误 | 任意 | 任意 | 任意 | 拒绝,不能降级为用户名方式 | +| 空 | 空 | 任意 | 任意 | 任意 | 拒绝,必须填写用户名或手机号 | + +补充规则: + +1. 有效 verification_token 的优先级高于当前短信开关。开关关闭不能让已验证票据失效。 +2. 短信开关开启时,邀请码不能绕过手机号验证。 +3. 用户主动填写邀请码后,服务端必须校验;不能在邀请码无效时静默忽略。 +4. 手机号字段非空且短信开关开启时,以验证码为准,邀请码可不填;手机号字段为空或短信开关关闭时,必须填写有效邀请码。 +5. 短信关闭时邀请码替代短信验证作为注册授权,但不建立已完成的 tel credential。 +6. 用户创建、租户用户配额校验、邀请码使用记录、手机号票据消费应原子完成。 + +### 4.2 客户端页面行为 + +1. 页面分别显示“请输入用户名”和“请输入手机号”。 +2. 用户名和手机号是独立字段;两个都填写时都参与注册,用户名保存为主登录账号,手机号保存为绑定号码。 +3. 手机号为空时不发送短信,使用用户名加邀请码方式注册。 +4. 手机号为 11 位数字时允许点击“获取验证码”。 +5. 用户名方式不发送短信,验证码字段不作为提交项。 +6. 邀请码字段始终显示,文案为“请输入邀请码(非必填)”。 +7. 短信开关关闭时,发码接口仍返回正常倒计时,前端进入与开关开启时相同的倒计时状态。 +8. 前端不自行决定最终是否可以免验证码注册,最终放行完全由服务端判断。 +9. 用户名方式注册前提示:未绑定手机号时无法自助找回密码,只能联系企业管理员重置。 + +## 5. 注册短信发送开关 + +### 5.1 HotGo 配置 + +HotGo 后台增加全局配置: + +~~~text +group = im +key = register_sms_enabled +type = bool +value = 1 / 0 +~~~ + +实现约定: + +1. 管理界面的开关选项复用 HotGo 已有 sys_switch 字典。 +2. 当前开关值保存到 hg_sys_config;字典只负责提供开/关选项,不作为配置值真源。 +3. 只有平台管理员可以修改。 +4. 修改操作写 HotGo 后台审计日志。 +5. IM 服务端在可控缓存时间内读取最新值。 +6. 本期为全局开关;未来需要按企业控制时再迁移到 im_tenant_config。 + +### 5.2 开关开启 + +1. 服务端生成强随机验证码。 +2. 创建 register 场景挑战并执行限频。 +3. 调用短信服务商发送验证码。 +4. 短信服务商失败时返回稳定错误 SMS_DELIVERY_FAILED。 +5. 禁止继续使用固定测试码 123456。 + +### 5.3 开关关闭 + +1. 发码接口仍创建限频挑战。 +2. 返回 challenge_id、expires_in_seconds 和 resend_after_seconds。 +3. 不调用短信服务商。 +4. 不生成客户端可猜测或可验证的固定验证码。 +5. 前端仍正常显示倒计时,但用户不会收到短信。 +6. 记录内部 delivery_suppressed 审计事件,不向客户端暴露开关状态。 + +## 6. 邀请码管理 + +### 6.1 基本规则 + +1. 邀请码按租户隔离,不能跨企业使用。 +2. 邀请码固定为 5 位 ASCII 大小写英文字母,格式为 `^[A-Za-z]{5}$`,不包含数字和符号。 +3. 邀请码区分大小写,`AbCdE` 与 `abcde` 是两个不同的邀请码;客户端和服务端不得自动转换大小写。 +4. 邀请码由服务端使用密码学安全随机数从 `A-Z`、`a-z` 字符集中生成,不能使用递增数字、用户 ID 或批次 ID。 +5. 同一租户内邀请码不能重复,数据库唯一索引是最终约束;单个或批量生成发生冲突时必须重新生成,不得返回重复码。 +6. 邀请码以明文落库,并使用完整明文进行大小写敏感的精确查询和注册校验。 +7. 每个邀请码在有效期内可以成功注册多个账号,不设置使用次数上限。 +8. 管理员创建时自行设置 expires_at,必须晚于当前时间。 +9. 状态包括 active、expired、revoked,不存在 used 状态。 +10. expired、revoked 状态不可恢复为 active。 +11. 邀请码校验必须同时检查租户、完整明文、状态和到期时间,不因已有其他用户使用记录而失效。 + +### 6.2 单个生成 + +请求示例: + +~~~json +{ + "expires_at": "2026-09-01T00:00:00+08:00", + "remark": "新员工入职" +} +~~~ + +服务端返回一个 5 位邀请码。具备邀请码查看权限的租户管理员可在后续列表和详情中查看完整明文。 + +### 6.3 批量生成 + +请求示例: + +~~~json +{ + "quantity": 100, + "expires_at": "2026-09-01T00:00:00+08:00", + "remark": "2026年8月新员工入职" +} +~~~ + +规则: + +1. 一次批量生成共用 batch_id 和 expires_at。 +2. 每个邀请码均为独立生成的 5 位大小写英文字母,租户内唯一且区分大小写。 +3. 批量请求必须支持幂等键,重试不能生成重复批次。 +4. 单批数量由后台配置限制,建议默认上限 1000。 +5. 生成成功后支持明文导出;后续再次查询或导出必须校验邀请码查看/导出权限并写审计。 + +### 6.4 查询、撤销和审计 + +管理后台支持: + +1. 按完整邀请码进行大小写敏感的精确查询。 +2. 按状态、批次、创建人、创建时间、到期时间查询。 +3. 列表和详情返回邀请码明文,接口必须校验当前租户的邀请码查看权限。 +4. 单个撤销仍处于 active 的邀请码;撤销不删除历史使用记录。 +5. 按 batch_id 批量撤销仍处于 active 的邀请码。 +6. 查看邀请码的累计使用次数、使用用户、使用时间和请求 ID。 +7. 所有生成、查询、查看、导出、撤销操作写后台审计日志。 + +### 6.5 HotGo 管理接口 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| POST | /imInvite/create | 单个生成 | +| POST | /imInvite/batchCreate | 批量生成 | +| POST | /imInvite/list | 分页或明文精确查询 | +| POST | /imInvite/revoke | 单个撤销 | +| POST | /imInvite/batchRevoke | 批量撤销 | +| GET | /imInvite/batchView | 批次详情 | +| POST | /imInvite/export | 按过滤条件导出 CSV | + +HotGo 根据可信登录上下文取得 `im_tenant.id`,通过 `im_tenant.db_dsn/db_name` 选择租户业务库并直接维护邀请码管理数据。浏览器不能提交可信 DSN 或决定实际租户库。 + +IM 不再提供 `/admin/v1/im/*` HMAC 管理接口。注册校验和 `im_registration_invite_uses` 写入仍由 IM 在用户注册事务中完成,HotGo 不参与注册事务。 + +管理员重置密码时,HotGo 在同一个租户库事务中更新 `im_auth.secret`、递增 `im_user_security.auth_version`,并写入 `im_business_events_outbox` 的 `security.password_reset` 事件。IM 当前轮询此事件并清退在线会话;后续迁移 Kafka 时保持事件类型和 payload 不变,只替换投递与消费通道。 + +## 7. 客户端注册协议 + +### 7.1 发码 + +~~~http +POST /v1/im/auth/verification-codes +~~~ + +~~~json +{ + "scene": "register", + "phone": "13800138000" +} +~~~ + +后端补 +86 后发送并保存手机号哈希。开关关闭时返回字段和倒计时保持一致。 + +### 7.2 提交注册 + +在现有 WebSocket acc 消息中增加 username、phone 和 invite_code: + +~~~json +{ + "acc": { + "id": "acc-1", + "user": "new", + "scheme": "basic", + "username": "alice", + "phone": "", + "secret": "base64(account:password)", + "invite_code": "AbCdE", + "login": true, + "cred": [] + } +} +~~~ + +用户名方式时,phone 为空,secret 使用 username:password,cred 为空且 invite_code 必须有效。 + +手机号字段非空时,短信开关开启则必须通过验证码放行,cred 传递一次性 verification_token;短信开关关闭则走邀请码放行,cred 可为空且 invite_code 必须有效。用户名字段非空时保存用户名并使用 username:password 生成 secret;用户名字段为空时由服务端随机生成 3 位小写字母 + 6 位数字的 username,客户端提交 secret 时仍使用 phone:password。短信关闭路径不建立已完成的 tel credential。 + +服务端必须校验 secret 中的登录名与客户端可确定的注册账号一致:username 非空时必须匹配 username;username 为空且 phone 非空时必须匹配规范化手机号。服务端生成的随机 username 在账号创建后返回给客户端,并作为后续登录主账号。username 和 phone 独立传递是注册路由信息,不能只根据 secret 是否为 11 位数字推断,因为普通用户名允许使用纯数字。 + +账户昵称暂时与最终 username 保持一致:服务端创建用户资料时以最终 username 写入 `desc.public.fn` 或资料表昵称;仅手机号注册时昵称等于随机生成的 username,不使用手机号作为昵称。 + +客户端不得提交 phone_account、sms_enabled、skip_verification 等服务端决策字段。 + +## 8. im_auth 改造 + +### 8.1 改造边界 + +当前只有 Basic 密码凭据写入 im_auth: + +1. token 是无状态签名,不写 im_auth。 +2. code 使用临时缓存,不写 im_auth。 +3. anonymous 不保存凭据。 +4. rest 调用外部认证服务。 + +因此数据库中的 scheme=basic 是冗余信息。登录账号真源迁移到 im_users 后,uname 和 basic: 前缀同样冗余。 + +协议层仍保留 acc.scheme 和 login.scheme,用于选择 basic 或 token 认证处理器。本次只删除数据库字段。 + +这里删除的 uname 仅指 `im_auth.uname` 数据库列。Basic 协议仍从 `secret` 中解析登录账号和密码,登录账号改为查询 `im_users.username` 或 `im_users.phone`;除非后续同时改造客户端和协议消息结构,否则 `account:password` 格式及用户名禁止冒号的技术约束继续保留。 + +### 8.2 目标表结构 + +~~~sql +CREATE TABLE im_auth ( + id INT NOT NULL AUTO_INCREMENT, + userid BIGINT NOT NULL, + authlvl INT NOT NULL, + secret VARCHAR(255) NOT NULL, + expires DATETIME, + PRIMARY KEY (id), + UNIQUE INDEX auth_userid (userid), + CONSTRAINT fk_im_auth_user + FOREIGN KEY (userid) REFERENCES im_users(id) +); +~~~ + +变更内容: + +1. 删除 scheme。 +2. 删除 uname。 +3. 删除 auth_uname 和 auth_userid_scheme。 +4. 增加 UNIQUE(userid)。 +5. 保留 authlvl,以兼容普通用户和 ROOT 账号。 +6. 保留 secret 和 expires。 + +### 8.3 登录和密码处理 + +1. Basic 登录先规范化账号输入。 +2. 先从 im_users.username 查询 userid。 +3. 未命中且输入为 11 位数字时,补 +86 后从 im_users.phone 查询 userid。 +4. 按 userid 从 im_auth 读取 bcrypt 密码摘要。 +5. 修改密码、重置密码、删除认证和 ROOT 提升都按 userid 操作。 +6. Store 和 Adapter 接口不再传递 scheme、unique 或 basic: 前缀。 + +### 8.4 密码找回 + +1. im_users.phone 非空的账号可以使用短信找回密码。 +2. 找回流程向 im_users.phone 发送 reset_password 场景验证码,成功消费验证码后才允许设置新密码。 +3. 如果该手机号注册时未建立完成状态的 tel credential,成功完成密码找回验证后可以补建该凭证。 +4. im_users.phone 为 NULL 的用户名账号不提供短信或其他自助找回方式。 +5. 无手机号账号忘记密码后,只能联系当前租户管理员执行后台密码重置。 +6. 管理员重置密码必须递增 auth_version、撤销旧 Token 和在线 Session,并写安全审计。 +7. 管理员只能重置当前租户用户,不能通过用户名跨租户查找或操作。 + +### 8.5 密码与登录 Token + +1. 客户端按现有 Basic 协议提交 `base64(account + ":" + password)`。Base64 只是协议编码,不是密码加密;账号注册和登录必须使用 HTTPS/WSS。 +2. 服务端解析出密码后使用 bcrypt 单向哈希,当前成本参数为 `bcrypt.DefaultCost=10`;`im_auth.secret` 只保存 bcrypt 哈希,不保存可逆密文或明文。 +3. 登录 Token 继续沿用现有 Tinode HMAC 签名 Token,默认有效期为 14 天,服务端不保存 Token 明文。 +4. 客户端保存登录响应中的 `params.token` 和 `params.expires`,Token 必须放入 iOS Keychain 或 Android Keystore,不得写入普通数据库、日志或埋点。 +5. 每次 Basic 登录或有效 Token 登录成功后,服务端都返回新的 14 天 Token;客户端必须用新 Token 覆盖旧 Token。 +6. 当前不增加 Refresh Token 和独立刷新接口。Token 未过期时可通过 `login.scheme=token` 登录并取得新 Token;Token 已过期或校验失败时必须重新进行账号密码登录。 +7. Token 内绑定 `tenant_id`、`uid`、`auth_version` 和过期时间。密码重置、管理员重置或账号安全状态变化后递增 `auth_version`,使旧 Token 失效。 + +## 9. 数据库设计 + +### 9.1 邀请码批次 + +~~~sql +CREATE TABLE im_registration_invite_batches ( + id BIGINT NOT NULL, + tenant_id BIGINT NOT NULL, + quantity INT UNSIGNED NOT NULL, + idempotency_key VARCHAR(128) NOT NULL, + expires_at DATETIME(3) NOT NULL, + created_by BIGINT NOT NULL, + remark VARCHAR(255) NOT NULL DEFAULT '', + created_at DATETIME(3) NOT NULL, + updated_at DATETIME(3) NOT NULL, + PRIMARY KEY (id), + UNIQUE INDEX uk_im_registration_invite_batch_tenant_idempotency (tenant_id, idempotency_key) +); +~~~ + +### 9.2 邀请码 + +~~~sql +CREATE TABLE im_registration_invite_codes ( + id BIGINT NOT NULL, + tenant_id BIGINT NOT NULL, + batch_id BIGINT, + code CHAR(5) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + status VARCHAR(16) NOT NULL, + expires_at DATETIME(3) NOT NULL, + usage_count INT UNSIGNED NOT NULL DEFAULT 0, + last_used_at DATETIME(3), + created_by BIGINT NOT NULL, + remark VARCHAR(255) NOT NULL DEFAULT '', + created_at DATETIME(3) NOT NULL, + updated_at DATETIME(3) NOT NULL, + PRIMARY KEY (id), + UNIQUE INDEX uk_im_registration_invite_code_tenant (tenant_id, code), + INDEX idx_im_registration_invite_tenant_status_expires (tenant_id, status, expires_at), + INDEX idx_im_registration_invite_tenant_batch (tenant_id, batch_id) +); +~~~ + +### 9.3 邀请码使用记录 + +~~~sql +CREATE TABLE im_registration_invite_uses ( + id BIGINT NOT NULL, + invite_code_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + request_id VARCHAR(64) NOT NULL, + used_at DATETIME(3) NOT NULL, + PRIMARY KEY (id), + INDEX idx_im_registration_invite_use_code (invite_code_id), + UNIQUE INDEX uk_im_registration_invite_use_user (user_id) +); +~~~ + +`im_registration_invite_batches` 和 `im_registration_invite_codes` 位于 HotGo 控制库,使用 `tenant_id` 隔离租户,便于平台统一查询、导出和管理。`usage_count`、`last_used_at` 是租户使用明细的查询投影,不是使用事实源。 + +`im_registration_invite_uses` 位于对应租户业务库,是邀请码使用事实源。注册成功时,IM 在同一租户库事务中写入使用明细和 `registration.invite_used` outbox 事件;当前由 IM 数据库轮询器按该邀请码的租户明细重新聚合并覆盖控制库统计,重试不会重复计数。后续切换 Kafka 时保持事件类型和 payload 不变。三张表的 BIGINT 主键均使用服务端雪花 ID。 + +邀请码以明文保存在 HotGo 控制库中。`ascii_bin` 保证查询和唯一索引区分大小写;所有校验必须同时使用 `tenant_id` 和完整 5 位邀请码精确匹配,禁止 `LOWER(code)`、`UPPER(code)` 或模糊查询。 + +## 10. 主要错误码 + +| 业务码 | 含义 | +| --- | --- | +| ACCOUNT_REQUIRED | 账号输入为空 | +| ACCOUNT_POLICY_VIOLATION | 账号违反最低技术约束 | +| ACCOUNT_ALREADY_EXISTS | 用户名或手机号命名空间冲突 | +| PHONE_VERIFICATION_REQUIRED | 手机号注册缺少有效验证票据 | +| INVITE_CODE_REQUIRED | 用户名注册缺少邀请码 | +| INVITE_CODE_INVALID | 邀请码不存在 | +| INVITE_CODE_EXPIRED | 邀请码已过期 | +| INVITE_CODE_REVOKED | 邀请码已撤销 | +| INVITE_CODE_RATE_LIMITED | 邀请码校验过于频繁 | +| SMS_DELIVERY_FAILED | 短信服务商发送失败 | +| USERNAME_INVALID_FORMAT | 用户名不是 1~10 位小写英文字母或数字 | + +邀请码无效、过期、已撤销可对客户端统一显示“邀请码无效”,详细原因仅写安全审计,降低枚举风险。 + +## 11. 存量迁移 + +采用“扩容、回填、切换、收缩”方式: + +1. 为 im_users 增加可空 username。 +2. 去除 im_auth.uname 中的 basic: 前缀后回填 im_users.username。 +3. 将现有 phone 统一规范化为 E.164。 +4. 对无 Basic 认证记录的内部、已删除或异常用户生成 reserved_,并输出迁移报告供人工复核。 +5. 检查重复 username、重复 phone 和跨字段冲突。 +6. 新版本短期双读旧 im_auth 和新 im_users 账号字段。 +7. 切换注册、登录、修改密码、密码重置和 ROOT 工具链。 +8. 将 username 改为非空并增加唯一索引。 +9. 观察期结束后删除 im_auth.scheme、im_auth.uname 和旧索引。 + +回滚时在观察期内继续保留旧列和旧读路径,不允许通过重新拼接 basic: 猜测恢复数据。 + +## 12. 安全与已知风险 + +1. 5 位邀请码属于短授权码,必须使用密码学安全随机数生成、设置有效期、可撤销并可审计;注册校验必须严格限频以降低枚举风险。邀请码允许多人使用意味着泄露后的影响面更大,应缩短有效期并及时撤销异常邀请码。 +2. 邀请码校验按租户、IP、设备和账号输入限频。 +3. 邀请码虽按业务要求明文落库和授权查询,但不得写入普通日志、埋点或错误响应;密码、短信验证码和 verification_token 同样禁止记录。 +4. 邀请码使用记录、用户创建、票据消费和租户配额扣减应在同一事务完成;邀请码本身不核销、不切换为 used。 +5. 短信开关关闭时不能保留固定测试验证码。 +6. 批量生成、批量导出和批量撤销必须限制数量并单独授权。 + +### 12.1 邀请码手机号注册风险 + +短信关闭时,有效邀请码可以在未完成短信验证的情况下把手机号写入 phone;如果用户同时填写 username,username 保存用户填写值,否则由服务端随机生成 3 位小写字母 + 6 位数字的 username。 + +为避免把“已填写手机号”和“已验证手机号”混为一谈: + +1. im_users.phone 保存手机号方式注册时的号码。 +2. 只有成功消费 verification_token 时才建立完成状态的 tel credential。 +3. 短信关闭加邀请码注册不建立完成状态的 tel credential。 +4. phone 非空账号可以通过 reset_password 短信挑战证明当前持有手机号后自助重置密码。 +5. phone=NULL 的账号不能自助找回密码,只能由当前租户管理员重置。 +6. 其他要求“已验证手机号”的敏感操作必须检查 tel credential,不能只检查 phone 非空。 +7. 后续完成短信验证后,可为同一用户补建 tel credential。 +8. 邀请码不证明手机号所有权,允许该路径意味着平台接受手机号被提前占用的业务风险。 + +## 13. 验收用例 + +### 13.1 注册 + +1. 只填写手机号,短信开启且验证成功,不填邀请码可以注册,username 为服务端随机生成的 3 位小写字母 + 6 位数字,phone 为规范化手机号。 +2. 只填写手机号,短信开启但没有验证票据,即使邀请码有效也不能注册。 +3. 只填写手机号,短信关闭且邀请码有效,无验证码可以注册,username 为服务端随机生成的 3 位小写字母 + 6 位数字,phone 为规范化手机号。 +4. 只填写手机号,短信关闭且不填邀请码不能注册。 +5. 只填写用户名并提供有效邀请码可以注册,phone=NULL。 +6. 只填写用户名但不填邀请码不能注册。 +7. 用户名和手机号同时填写且短信开启时,验证码验证成功后可以注册,username 保存用户填写的用户名,phone 保存规范化手机号。 +8. 用户名和手机号同时填写且短信关闭时,邀请码有效即可注册,username 保存用户填写的用户名,phone 保存规范化手机号。 +9. 手机号非空但格式错误时拒绝,不能回退到用户名方式。 +10. 用户名和手机号同时为空时拒绝注册。 +11. 过期、已撤销邀请码均不能注册。 +12. 同一有效邀请码并发注册时可以有多个不同用户成功,每个用户分别写入一条使用记录,邀请码保持 active。 +13. 用户名账号 phone=NULL 时,短信找回接口不能重置密码。 +14. 管理员可以重置 phone=NULL 用户的密码,重置后旧 Token 和 Session 全部失效。 +15. 普通用户名 `alice01`、`a`、`1234567890` 可以进入注册校验。 +16. 普通用户名 `Alice`、`alice_1`、`alice.1`、`中文`、空字符串或超过 10 位时返回 `USERNAME_INVALID_FORMAT`。 +17. 用户名格式不合法但手机号合法且非空时仍拒绝,不能忽略用户名输入。 +18. 仅手机号注册时,随机 username 必须在租户内唯一;发生唯一索引冲突时服务端重新生成并重试。 +19. 注册成功后的账户昵称等于最终 username:填写用户名时昵称等于该用户名;仅手机号注册时昵称等于随机 username。 + +### 13.2 短信开关 + +1. 开关关闭后发码接口不调用短信服务商。 +2. 开关关闭后发码接口仍返回正常倒计时。 +3. 开关从关闭改为开启后,新请求恢复调用短信服务商。 +4. 开关修改在规定缓存时间内对 IM 服务生效。 + +### 13.3 邀请码管理 + +1. 单个生成可设置有效期。 +2. 单个和批量生成的邀请码均匹配 `^[A-Za-z]{5}$`,不包含数字或符号。 +3. 批量生成数量正确,同一租户所有邀请码互不重复;并发生成冲突时由唯一索引拦截并重试。 +4. `AbCdE` 和 `abcde` 按两个不同邀请码精确查询和校验,不得自动转换大小写。 +5. 有权限的管理员可以按完整邀请码查询,并在列表、详情和导出中取得明文。 +6. 批量请求重试不会生成第二批数据。 +7. 单个和批量撤销立即生效,历史使用用户不受影响。 +8. 无权限用户不能查看、查询或导出邀请码明文。 +9. 管理员可以查看批次使用统计和单码使用记录。 + +### 13.4 认证回归 + +1. 存量 Basic 用户迁移后可使用原账号和密码登录。 +2. 登录、注册、修改密码、密码重置和 ROOT 提升不再依赖 im_auth.scheme、im_auth.uname 或 basic: 前缀。 +3. 同一 username 或 phone 在不同租户仍可独立注册。 +4. im_auth 每个 userid 最多保留一条记录。 + +## 14. 评审结论 + +本方案的核心决策为: + +1. 注册页的用户名和手机号是两个独立输入框,用户名非空时必须保存,手机号非空时必须保存。 +2. username 是所有用户非空的主登录账号。 +3. 用户名和手机号同时填写时,username 保存用户名,phone 保存规范化手机号;仅手机号注册时 username 由服务端随机生成 3 位小写字母 + 6 位数字。 +4. 手机号非空且短信开关开启时以验证码为注册放行依据;短信开关关闭时以邀请码为注册放行依据。 +5. 短信关闭时,手机号输入搭配有效邀请码可注册,但不建立已完成的 tel credential。 +6. 邀请码为 5 位大小写英文字母,区分大小写,以明文落库并在租户内唯一;有效期内可供多人使用,支持单个、批量、自定义有效期、明文查询/导出、撤销和使用审计。 +7. im_auth 只按 userid 保存 Basic 密码凭据,数据库删除 scheme、uname 和 basic: 前缀。 +8. 协议层仍保留 basic/token scheme,不与数据库结构混淆。 +9. phone=NULL 的用户名账号不能自助找回密码,只能由租户管理员重置。 +10. HotGo 与 IM 当前以共享租户业务库集成,不再使用内部 HMAC 管理 HTTP;跨进程安全事件先经数据库 outbox,后续迁移 Kafka。 + +评审通过后,实施工作拆分为数据库迁移、IM 注册与认证、HotGo 后台、客户端对接和全量回归五个任务。 diff --git a/docs/im/归档.zip b/docs/im/归档.zip new file mode 100644 index 0000000..459e0ea Binary files /dev/null and b/docs/im/归档.zip differ