kboss/b/docs/user_tenant_refactor.md
2026-07-16 11:17:50 +08:00

328 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 用户按租户隔离改造规划
## 背景
当前用户注册时,手机号和用户名按 `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. 订单、余额、扣费、结算不串账。