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

7.6 KiB
Raw Permalink Blame History

用户按租户隔离改造规划

背景

当前用户注册时,手机号和用户名按 users 全表做唯一性判断。两个不同域名下,如果使用同一个手机号注册,会触发“该手机号已注册”。登录时也存在类似问题:手机号、用户名查询没有先限定当前域名所属租户,若数据库中出现跨域名同手机号用户,可能查到错误用户。

本次改造目标是支持“不同域名/站点下允许同手机号注册和登录”,同时不影响余额、计费、扣费、结算链路。

核心原则

  1. 前端不传 tenant_orgid
  2. 后端根据当前访问域名解析 tenant_orgid
  3. 注册、登录、找回密码、验证码等入口按 tenant_orgid + 手机号/用户名 查询用户。
  4. 不改 users.idusers.orgidorganization.idcustomer.customerid
  5. 不改账务核心表:accountledgercustomerbz_orderorder_goods 等。
  6. 当前数据库暂时无法新增 INDEX/UNIQUE唯一性由代码逻辑保证。

tenant_orgid 来源

tenant_orgid 表示当前站点所属租户机构 ID由后端通过域名解析。

解析规则:

  1. domain_name 获取域名;如果只有 url_link,从 url_link 中解析域名。
  2. 查询 reseller
select orgid
from reseller
where domain_name = '当前域名'
  and del_flg = '0'
limit 1;
  1. 如果查到,说明当前是分销商站点:
tenant_orgid:reseller.orgid
  1. 如果查不到,说明当前是主站,查询主站机构:
select id
from organization
where org_type = '0'
  and del_flg = '0'
limit 1;

主站场景:

tenant_orgid:主站机构id

数据库改造

只改 users 表,新增字段,不新增索引。

ALTER TABLE users
ADD COLUMN tenant_orgid varchar(32) DEFAULT NULL COMMENT '注册/登录所属租户机构id';

历史数据回填

客户用户通常满足:users.orgid 是客户机构,organization.parentid 是该客户所属分销商或主站机构。

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

UPDATE users u
INNER JOIN reseller r ON u.orgid = r.orgid
SET u.tenant_orgid = r.orgid
WHERE u.tenant_orgid IS NULL;

主站/内部用户兜底回填为主站机构。

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;

回填后检查:

select id, username, mobile, orgid, tenant_orgid
from users
where tenant_orgid is null;

注册接口改造

文件:

b/customer/registerUser.dspy

改造点:

  1. 注册开始先解析 tenant_orgid
  2. 手机号重复校验从全局校验:
mobile:手机号

改成租户内校验:

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 时写入:
tenant_orgid:当前tenant_orgid
  1. 如果未传 username,仍可用手机号作为用户名,但唯一范围变为当前租户内。

登录接口改造

文件:

b/user/loginUser.dspy
b/user/logintype.dspy

改造点:

  1. 登录开始先解析 tenant_orgid
  2. 手机号验证码登录按当前租户查询:
select *
from users
where mobile = '手机号'
  and tenant_orgid = '当前tenant_orgid'
  and del_flg = '0'
limit 1;
  1. 用户名密码登录按当前租户查询:
select *
from users
where username = '用户名'
  and password = '加密后密码'
  and tenant_orgid = '当前tenant_orgid'
  and del_flg = '0'
limit 1;
  1. 如果使用手机号作为 username,也必须加 tenant_orgid
  2. 原有域名归属判断可以保留,但查用户时必须先限定租户,避免查到其他域名同手机号用户。

登录失败锁定改造

文件:

b/user/logintype.dspy

当前 login_fail_loguser_name 记录失败次数。跨域名同手机号后A 域名失败可能锁定 B 域名同手机号。

暂不改表时,建议使用拼接 key

login_fail_key = tenant_orgid + ':' + username_or_mobile

然后传给:

handle_login_failed(login_fail_key)
check_login_allowed(login_fail_key)

找回密码和验证码改造

建议同步改造:

b/customer/forgotPassword.dspy
b/user/retrievecode.dspy
b/user/mobilecode.dspy

原因:这些接口也会通过手机号或用户名全局查 users。改造后应先解析 tenant_orgid,再按当前租户查用户。

查询条件示例:

select *
from users
where mobile = '手机号'
  and tenant_orgid = '当前tenant_orgid'
  and del_flg = '0'
limit 1;

其他代码排查

全局排查这些模式:

sor.R('users', {'mobile': ...})
sor.R('users', {'username': ...})

处理原则:

  1. 注册、登录、找回密码、验证码:必须加 tenant_orgid
  2. 前台用户入口:优先加 tenant_orgid
  3. 平台后台管理查询:如果需要跨租户管理,可以不加;如果是当前站点后台,则应加。
  4. 账务、订单、结算:通常按 useridorgidcustomerid 查询,不需要因手机号隔离改造。

对余额、计费、扣费、结算的影响

按本规划实施,理论上不影响账务链路。

原因是账务核心链路依赖以下字段:

users.id
users.orgid
organization.id
customer.customerid
account.orgid
bz_order.customerid
bz_order.userid

本次只新增 users.tenant_orgid,并改变登录/注册时如何定位用户,不改变已有账务关联 ID。

并发风险

由于当前不能新增数据库唯一索引,租户内手机号唯一性只能由代码保证。

风险:

两个请求同时注册同一 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. 全局排查剩余按 mobileusernameusers 的代码。
  11. 测试主站注册登录。
  12. 测试分销商 A 注册登录。
  13. 测试分销商 B 使用同手机号注册登录。
  14. 测试找回密码、验证码登录、用户名密码登录。
  15. 测试余额、下单、扣费、结算查询链路是否仍按原 orgid/customerid 工作。

验收标准

  1. 同一域名下,同手机号不能重复注册。
  2. 不同域名下,同手机号可以分别注册。
  3. 不同域名下,同手机号登录时只登录当前域名对应用户。
  4. 登录失败锁定只影响当前域名对应用户。
  5. 找回密码只影响当前域名对应用户。
  6. 原有用户能正常登录。
  7. 订单、余额、扣费、结算不串账。