注册用户改造

This commit is contained in:
ping 2026-07-16 11:17:50 +08:00
parent 64073f7e9c
commit dd35774400
2 changed files with 361 additions and 9 deletions

View File

@ -108,6 +108,22 @@ async def sync_cn_ai_user(userid=None, orgid=None, username=None, name=None, ema
'msg': f"sync_cn_ai_user{userid}同步用户失败: {e}"
}
async def get_register_tenant_orgid(sor, ns):
domain_name = ns.get('domain_name')
if (not domain_name) and ns.get('url_link'):
domain_name = ns.get('url_link').split("//")[1].split("/")[0]
if domain_name:
domain_name = domain_name.strip().replace('https://', '').replace('http://', '').replace('/', '')
if 'localhost' in domain_name:
domain_name = 'dev.opencomputing.cn'
reseller = await sor.R('reseller', {'domain_name': domain_name, 'del_flg': '0'})
if len(reseller) >= 1:
return reseller[0]['orgid']
org = await sor.R('organization', {'org_type': '0', 'del_flg': '0'})
if len(org) >= 1:
return org[0]['id']
return None
async def registerUser(ns):
"""
用户注册
@ -116,25 +132,30 @@ async def registerUser(ns):
db = DBPools()
async with db.sqlorContext('kboss') as sor:
if ns:
tenant_orgid = await get_register_tenant_orgid(sor, ns)
if not tenant_orgid:
return {'status': False, 'msg': '未找到当前域名所属租户'}
# 用户名没有 则用手机号作为用户名
if not ns.get('username'):
ns['username'] = ns['mobile']
if ns.get('username'):
userns = {'username': ns['username']}
isuser = await sor.R('users', userns)
user_sql = """select id from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns['username'], tenant_orgid)
isuser = await sor.sqlExe(user_sql, {})
if len(isuser) >= 1:
return {'status': False, 'msg': '用户名已注册'}
if ns.get('email'):
useremai = {'email': ns['email']}
isuser = await sor.R('users', useremai)
email_sql = """select id from users where email = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns['email'], tenant_orgid)
isuser = await sor.sqlExe(email_sql, {})
if len(isuser) >= 1:
return {'status': False, 'msg': '该邮箱已注册'}
usermobile = {'mobile':ns['mobile']}
isuser = await sor.R('users', usermobile)
mobile_sql = """select id from users where mobile = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns['mobile'], tenant_orgid)
isuser = await sor.sqlExe(mobile_sql, {})
if len(isuser) >= 1:
return {'status': False, 'msg': '该手机号已注册'}
# 用户名没有 则用手机号作为用户名
if not ns.get('username'):
ns['username'] = ns['mobile']
# if ns.get('password'):
# # 至少8位包含大小写字母、特殊字符、数字
@ -188,7 +209,9 @@ async def registerUser(ns):
userid = ns['id']
ns['orgid'] = orgid
ns['password'] = password_encode(ns['password'])
ns['tenant_orgid'] = tenant_orgid
await sor.C('users', ns)
ns.pop('tenant_orgid', None)
listrole = ['管理员', '客户']
for i in listrole:
role = await sor.R('role', {'role': i, 'org_type': ns['org_type']})
@ -274,7 +297,9 @@ async def registerUser(ns):
ns['orgid'] = org_id
# 新增用户信息
ns['tenant_orgid'] = tenant_orgid
await sor.C('users', ns)
ns.pop('tenant_orgid', None)
# 新增用户角色信息
listrole = ['管理员', '客户']

View File

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