# 用户按租户隔离改造规划 ## 背景 当前用户注册时,手机号和用户名按 `users` 全表做唯一性判断。两个不同域名下,如果使用同一个手机号注册,会触发“该手机号已注册”。登录时也存在类似问题:手机号、用户名查询没有先限定当前域名所属租户,若数据库中出现跨域名同手机号用户,可能查到错误用户。 本次改造目标是支持“不同域名/站点下允许同手机号注册和登录”,同时不影响余额、计费、扣费、结算链路。 ## 核心原则 1. 前端不传 `tenant_orgid`。 2. 后端根据当前访问域名解析 `tenant_orgid`。 3. 注册、登录、找回密码、验证码等入口按 `tenant_orgid + 手机号/用户名` 查询用户。 4. 不改 `users.id`、`users.orgid`、`organization.id`、`customer.customerid`。 5. 不改账务核心表:`account`、`ledger`、`customer`、`bz_order`、`order_goods` 等。 6. 当前数据库暂时无法新增 INDEX/UNIQUE,唯一性由代码逻辑保证。 ## tenant_orgid 来源 `tenant_orgid` 表示当前站点所属租户机构 ID,由后端通过域名解析。 解析规则: 1. 从 `domain_name` 获取域名;如果只有 `url_link`,从 `url_link` 中解析域名。 2. 查询 `reseller`: ```sql select orgid from reseller where domain_name = '当前域名' and del_flg = '0' limit 1; ``` 1. 如果查到,说明当前是分销商站点: ```text tenant_orgid:reseller.orgid ``` 1. 如果查不到,说明当前是主站,查询主站机构: ```sql select id from organization where org_type = '0' and del_flg = '0' limit 1; ``` 主站场景: ```text tenant_orgid:主站机构id ``` ## 数据库改造 只改 `users` 表,新增字段,不新增索引。 ```sql ALTER TABLE users ADD COLUMN tenant_orgid varchar(32) DEFAULT NULL COMMENT '注册/登录所属租户机构id'; ``` ### 历史数据回填 客户用户通常满足:`users.orgid` 是客户机构,`organization.parentid` 是该客户所属分销商或主站机构。 ```sql UPDATE users u LEFT JOIN organization o ON u.orgid = o.id SET u.tenant_orgid = o.parentid WHERE u.tenant_orgid IS NULL AND o.parentid IS NOT NULL; # 注意管理员角色用户 ``` 分销商自身用户满足:`users.orgid = reseller.orgid`。 ```sql UPDATE users u INNER JOIN reseller r ON u.orgid = r.orgid SET u.tenant_orgid = r.orgid WHERE u.tenant_orgid IS NULL; ``` 主站/内部用户兜底回填为主站机构。 ```sql UPDATE users u SET u.tenant_orgid = ( SELECT id FROM organization WHERE org_type = '0' AND del_flg = '0' LIMIT 1 ) WHERE u.tenant_orgid IS NULL; ``` 回填后检查: ```sql select id, username, mobile, orgid, tenant_orgid from users where tenant_orgid is null; ``` ## 注册接口改造 文件: ```text b/customer/registerUser.dspy ``` 改造点: 1. 注册开始先解析 `tenant_orgid`。 2. 手机号重复校验从全局校验: ```text mobile:手机号 ``` 改成租户内校验: ```sql select id from users where mobile = '手机号' and tenant_orgid = '当前tenant_orgid' and del_flg = '0' limit 1; ``` 1. 用户名重复校验也加 `tenant_orgid`。 2. 邮箱重复校验建议也加 `tenant_orgid`。 3. 新增 `users` 时写入: ```text tenant_orgid:当前tenant_orgid ``` 1. 如果未传 `username`,仍可用手机号作为用户名,但唯一范围变为当前租户内。 ## 登录接口改造 文件: ```text b/user/loginUser.dspy b/user/logintype.dspy ``` 改造点: 1. 登录开始先解析 `tenant_orgid`。 2. 手机号验证码登录按当前租户查询: ```sql select * from users where mobile = '手机号' and tenant_orgid = '当前tenant_orgid' and del_flg = '0' limit 1; ``` 1. 用户名密码登录按当前租户查询: ```sql select * from users where username = '用户名' and password = '加密后密码' and tenant_orgid = '当前tenant_orgid' and del_flg = '0' limit 1; ``` 1. 如果使用手机号作为 `username`,也必须加 `tenant_orgid`。 2. 原有域名归属判断可以保留,但查用户时必须先限定租户,避免查到其他域名同手机号用户。 ## 登录失败锁定改造 文件: ```text b/user/logintype.dspy ``` 当前 `login_fail_log` 按 `user_name` 记录失败次数。跨域名同手机号后,A 域名失败可能锁定 B 域名同手机号。 暂不改表时,建议使用拼接 key: ```text login_fail_key = tenant_orgid + ':' + username_or_mobile ``` 然后传给: ```text handle_login_failed(login_fail_key) check_login_allowed(login_fail_key) ``` ## 找回密码和验证码改造 建议同步改造: ```text b/customer/forgotPassword.dspy b/user/retrievecode.dspy b/user/mobilecode.dspy ``` 原因:这些接口也会通过手机号或用户名全局查 `users`。改造后应先解析 `tenant_orgid`,再按当前租户查用户。 查询条件示例: ```sql select * from users where mobile = '手机号' and tenant_orgid = '当前tenant_orgid' and del_flg = '0' limit 1; ``` ## 其他代码排查 全局排查这些模式: ```text sor.R('users', {'mobile': ...}) sor.R('users', {'username': ...}) ``` 处理原则: 1. 注册、登录、找回密码、验证码:必须加 `tenant_orgid`。 2. 前台用户入口:优先加 `tenant_orgid`。 3. 平台后台管理查询:如果需要跨租户管理,可以不加;如果是当前站点后台,则应加。 4. 账务、订单、结算:通常按 `userid`、`orgid`、`customerid` 查询,不需要因手机号隔离改造。 ## 对余额、计费、扣费、结算的影响 按本规划实施,理论上不影响账务链路。 原因是账务核心链路依赖以下字段: ```text users.id users.orgid organization.id customer.customerid account.orgid bz_order.customerid bz_order.userid ``` 本次只新增 `users.tenant_orgid`,并改变登录/注册时如何定位用户,不改变已有账务关联 ID。 ## 并发风险 由于当前不能新增数据库唯一索引,租户内手机号唯一性只能由代码保证。 风险: ```text 两个请求同时注册同一 tenant_orgid + mobile,可能同时通过注册前校验。 ``` 降低风险方案: 1. 注册前查询一次。 2. 插入前尽量保持注册逻辑短。 3. 插入后再查询一次同 `tenant_orgid + mobile + del_flg = 0` 的用户数量。 4. 如果发现重复,返回异常并人工处理。 后续如果数据库允许新增唯一约束,再补数据库唯一约束。 ## 建议实施顺序 1. 给 `users` 增加 `tenant_orgid` 字段。 2. 回填历史用户 `tenant_orgid`。 3. 抽出或复制一段 `tenant_orgid` 解析逻辑到注册/登录相关接口。 4. 改造 `b/customer/registerUser.dspy`。 5. 改造 `b/user/logintype.dspy`。 6. 改造 `b/user/loginUser.dspy`。 7. 改造 `b/customer/forgotPassword.dspy`。 8. 改造 `b/user/retrievecode.dspy`。 9. 改造 `b/user/mobilecode.dspy`。 10. 全局排查剩余按 `mobile`、`username` 查 `users` 的代码。 11. 测试主站注册登录。 12. 测试分销商 A 注册登录。 13. 测试分销商 B 使用同手机号注册登录。 14. 测试找回密码、验证码登录、用户名密码登录。 15. 测试余额、下单、扣费、结算查询链路是否仍按原 `orgid/customerid` 工作。 ## 验收标准 1. 同一域名下,同手机号不能重复注册。 2. 不同域名下,同手机号可以分别注册。 3. 不同域名下,同手机号登录时只登录当前域名对应用户。 4. 登录失败锁定只影响当前域名对应用户。 5. 找回密码只影响当前域名对应用户。 6. 原有用户能正常登录。 7. 订单、余额、扣费、结算不串账。