diff --git a/b/customer/forgotPassword.dspy b/b/customer/forgotPassword.dspy index b12e8b5..d59b248 100644 --- a/b/customer/forgotPassword.dspy +++ b/b/customer/forgotPassword.dspy @@ -1,3 +1,19 @@ +async def get_password_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 forgotPassword(ns): """ 忘记密码:校验短信验证码后重置密码。 @@ -25,6 +41,10 @@ async def forgotPassword(ns): db = DBPools() async with db.sqlorContext('kboss') as sor: try: + tenant_orgid = await get_password_tenant_orgid(sor, ns) + if not tenant_orgid: + return {'status': False, 'msg': '未找到当前域名所属租户'} + code = await sor.R('validatecode', {'id': ns.get('codeid'), 'vcode': ns.get('vcode')}) if code: create_at = code[0]['create_at'] @@ -37,17 +57,21 @@ async def forgotPassword(ns): user = None if ns.get('id'): - users = await sor.R('users', {'id': ns.get('id'), 'del_flg': '0'}) + user_sql = """select * from users where id = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('id'), tenant_orgid) + users = await sor.sqlExe(user_sql, {}) if users: user = users[0] elif ns.get('mobile'): - users = await sor.R('users', {'mobile': ns.get('mobile'), 'del_flg': '0'}) + user_sql = """select * from users where mobile = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('mobile'), tenant_orgid) + users = await sor.sqlExe(user_sql, {}) if users: user = users[0] elif ns.get('username'): - users = await sor.R('users', {'username': ns.get('username'), 'del_flg': '0'}) + user_sql = """select * from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('username'), tenant_orgid) + users = await sor.sqlExe(user_sql, {}) if not users: - users = await sor.R('users', {'mobile': ns.get('username'), 'del_flg': '0'}) + user_sql = """select * from users where mobile = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('username'), tenant_orgid) + users = await sor.sqlExe(user_sql, {}) if users: user = users[0] else: diff --git a/b/customer/registerUser.dspy b/b/customer/registerUser.dspy index 1d754e9..9f78beb 100644 --- a/b/customer/registerUser.dspy +++ b/b/customer/registerUser.dspy @@ -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 = ['管理员', '客户'] diff --git a/b/docs/upload_file.dspy b/b/docs/upload_file.dspy index b39816e..955df40 100644 --- a/b/docs/upload_file.dspy +++ b/b/docs/upload_file.dspy @@ -1,66 +1,29 @@ async def upload_file(ns={}): - import os - import base64 - import datetime - - file_name = ns.get('file_name') - file_content = ns.get('file_content') or ns.get('file_base64') - storage_type = ns.get('storage_type', 'local') + debug('ns: %s' % ns) + file_name = ns.get('file_data') if not file_name: return { 'status': False, - 'msg': '请传递file_name' + 'msg': 'file_data不能为空' } - if not file_content: + domain_name = ns.get('domain_name') + if not domain_name: return { 'status': False, - 'msg': '请传递file_content' - } - if storage_type != 'local': - return { - 'status': False, - 'msg': '暂不支持该存储类型' - } - - safe_file_name = os.path.basename(file_name).replace('\\', '').replace('/', '') - if not safe_file_name: - return { - 'status': False, - 'msg': 'file_name错误' + 'msg': 'domain_name不能为空' } + if 'localhost' in domain_name: + domain_name = 'dev.opencomputing.cn' + + save_url = 'https://%s/idfile?path=%s' % (domain_name, file_name) try: - if ',' in file_content and file_content.split(',', 1)[0].startswith('data:'): - file_content = file_content.split(',', 1)[1] - file_bytes = base64.b64decode(file_content) - except Exception as e: - return { - 'status': False, - 'msg': '文件内容解析失败, %s' % str(e) - } - - now_date = datetime.datetime.now() - date_path = now_date.strftime('%Y/%m/%d') - relative_path = '%s/%s' % (date_path, safe_file_name) - base_path = '/data' - save_dir = os.path.join(base_path, now_date.strftime('%Y'), now_date.strftime('%m'), now_date.strftime('%d')) - save_path = os.path.join(save_dir, safe_file_name) - - try: - if not os.path.exists(save_dir): - os.makedirs(save_dir) - with open(save_path, 'wb') as f: - f.write(file_bytes) return { 'status': True, 'msg': 'upload success', 'data': { - 'storage_type': storage_type, - 'file_name': safe_file_name, - 'file_path': relative_path, - 'save_path': save_path, - 'file_size': len(file_bytes) + 'save_url': save_url } } except Exception as e: diff --git a/b/docs/user_tenant_refactor.md b/b/docs/user_tenant_refactor.md new file mode 100644 index 0000000..4d22fee --- /dev/null +++ b/b/docs/user_tenant_refactor.md @@ -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. 订单、余额、扣费、结算不串账。 + diff --git a/b/reseller/reseller_add_user.dspy b/b/reseller/reseller_add_user.dspy index 3eebcdf..53736dc 100644 --- a/b/reseller/reseller_add_user.dspy +++ b/b/reseller/reseller_add_user.dspy @@ -1,3 +1,15 @@ +async def get_reseller_user_tenant_orgid(sor, orgid): + reseller = await sor.R('reseller', {'orgid': orgid, 'del_flg': '0'}) + if len(reseller) >= 1: + return orgid + org = await sor.R('organization', {'id': orgid, 'del_flg': '0'}) + if len(org) >= 1 and org[0].get('parentid'): + return org[0]['parentid'] + main_org = await sor.R('organization', {'org_type': '0', 'del_flg': '0'}) + if len(main_org) >= 1: + return main_org[0]['id'] + return None + async def reseller_add_user(ns): """ 添加用户 @@ -15,11 +27,16 @@ async def reseller_add_user(ns): try: ns['id'] = uuid() ns['password'] = password_encode(ns['password']) - user = await sor.R('users', {'username': ns['username']}) + tenant_orgid = await get_reseller_user_tenant_orgid(sor, ns.get('orgid')) + if not tenant_orgid: + return {'status': False, 'msg': '未找到用户所属租户'} + user_sql = """select id from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns['username'], tenant_orgid) + user = await sor.sqlExe(user_sql, {}) if len(user) >= 1: return {'status': False, 'msg': '用户名重复'} rolereacs = await sor.R('role', {'role': '管理员', 'del_flg': '0', 'org_type': '1'}) ns['user_reseller'] = '1' + ns['tenant_orgid'] = tenant_orgid await sor.C('users', ns) await sor.C('userrole', {'userid': ns['id'], 'roleid': rolereacs[0]['id'], 'id': uuid()}) return {'status': True, 'msg': '用户添加成功', 'userid': ns['id']} diff --git a/b/user/addUser.dspy b/b/user/addUser.dspy index a561aa4..2c63bfb 100644 --- a/b/user/addUser.dspy +++ b/b/user/addUser.dspy @@ -1,3 +1,15 @@ +async def get_user_tenant_orgid(sor, orgid): + reseller = await sor.R('reseller', {'orgid': orgid, 'del_flg': '0'}) + if len(reseller) >= 1: + return orgid + org = await sor.R('organization', {'id': orgid, 'del_flg': '0'}) + if len(org) >= 1 and org[0].get('parentid'): + return org[0]['parentid'] + main_org = await sor.R('organization', {'org_type': '0', 'del_flg': '0'}) + if len(main_org) >= 1: + return main_org[0]['id'] + return None + async def addUser(ns): """ 添加用户 @@ -17,11 +29,16 @@ async def addUser(ns): else: user_reseller = '0' ns['password'] = password_encode(ns['password']) - user = await sor.R('users', {'username': ns['username']}) + tenant_orgid = await get_user_tenant_orgid(sor, ns.get('orgid')) + if not tenant_orgid: + return {'status': False, 'msg': '未找到用户所属租户'} + user_sql = """select id from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns['username'], tenant_orgid) + user = await sor.sqlExe(user_sql, {}) if len(user) >= 1: return {'status': False, 'msg': '用户名重复'} ns['id'] = uuid() if not ns.get('type'): + ns['tenant_orgid'] = tenant_orgid await sor.C('users', ns) return {'status': True, 'msg': '添加成功', 'userid': ns['id']} rolereacs = await sor.R('role', {'role': '管理员', 'del_flg': '0', 'org_type': '0'}) @@ -30,6 +47,7 @@ async def addUser(ns): return {'status': False, 'msg': '已有管理员'} ns['orgid'] = 'mIWUHBeeDM8mwAFPIQ8pS' ns['user_reseller'] = user_reseller + ns['tenant_orgid'] = ns['orgid'] await sor.C('users', ns) ns['orgid'] = 'mIWUHBeeDM8mwAFPIQ8pS' await sor.C('userrole', {'userid': ns['id'], 'roleid': rolereacs[0]['id'], 'id': uuid()}) diff --git a/b/user/loginUser.dspy b/b/user/loginUser.dspy index 5efdb04..803b154 100644 --- a/b/user/loginUser.dspy +++ b/b/user/loginUser.dspy @@ -57,6 +57,21 @@ async def check_wechat_login_status(code): except Exception as e: raise e +async def get_login_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 loginUser(ns): """ @@ -65,6 +80,9 @@ async def loginUser(ns): db = DBPools() async with db.sqlorContext('kboss') as sor: if ns: + tenant_orgid = await get_login_tenant_orgid(sor, ns) + if not tenant_orgid and ns.get('username') not in ['admin', '开元云(北京)科技有限公司']: + return {'status': False, 'msg': '未找到当前域名所属租户'} # 处理微信授权后的登录 if ns.get('wechatCode'): # 微信扫码登录后的`code` wechat_user_info = await check_wechat_login_status(ns.get('wechatCode')) @@ -91,7 +109,8 @@ async def loginUser(ns): if wechat_user_info: openid_f = wechat_user_info['openid'] # 查询数据库,查看openid是否已绑定账户 - userreacs = await sor.R('users', {'wechat_openid': openid_f, 'del_flg': '0'}) + wechat_sql = """select * from users where wechat_openid = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (openid_f, tenant_orgid) + userreacs = await sor.sqlExe(wechat_sql, {}) if len(userreacs) >= 1: # 如果openid绑定了用户, 查找用户名和密码 使用账号密码登录 ns['username'] = userreacs[0]['username'] @@ -152,10 +171,12 @@ async def loginUser(ns): type = 0 if type1 == 1: # 手机号验证码登录 - userreacs = await sor.R('users', {'mobile': ns.get('mobile')}) + user_sql = """select * from users where mobile = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('mobile'), tenant_orgid) + userreacs = await sor.sqlExe(user_sql, {}) if not userreacs: - userreacs = await sor.R('users', {'username': ns.get('username')}) + user_sql = """select * from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('username'), tenant_orgid) + userreacs = await sor.sqlExe(user_sql, {}) # 如果是微信扫码后绑定已有账号 if ns.get('wechat_openid'): @@ -170,14 +191,17 @@ async def loginUser(ns): else: ns['password'] = password_encode(ns['password']) ns['del_flg'] = '0' - userreacs = await sor.R('users', {'username': ns.get('username'), 'password': ns.get('password')}) + if ns.get('username') in ['admin', '开元云(北京)科技有限公司']: + userreacs = await sor.R('users', {'username': ns.get('username'), 'password': ns.get('password'), 'del_flg': '0'}) + else: + user_sql = """select * from users where username = '%s' and password = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('username'), ns.get('password'), tenant_orgid) + userreacs = await sor.sqlExe(user_sql, {}) if len(userreacs) >= 1: type += 1 await remember_user(userreacs[0]['id'], username=userreacs[0]['username'], userorgid=userreacs[0]['orgid']) else: - ns['mobile'] = ns['username'] - ns.pop('username') - userreacs = await sor.R('users', ns) + user_sql = """select * from users where mobile = '%s' and password = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('username'), ns.get('password'), tenant_orgid) + userreacs = await sor.sqlExe(user_sql, {}) if len(userreacs) >= 1: type += 1 await remember_user(userreacs[0]['id'], username=userreacs[0]['username'], userorgid=userreacs[0]['orgid']) diff --git a/b/user/logintype.dspy b/b/user/logintype.dspy index 8151223..a208675 100644 --- a/b/user/logintype.dspy +++ b/b/user/logintype.dspy @@ -93,6 +93,22 @@ async def check_login_allowed(user_name): print("exception:", user_name, e) return {'status': False, 'msg': '登录操作失败, %s' % str(e)} +async def get_login_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 logintype(ns): """ 1、判断用户是否为主级(如果在reseller没要找到数据,证明就是主级) @@ -140,11 +156,16 @@ async def logintype(ns): async with db.sqlorContext('kboss') as sor: domain_name = ns.get('domain_name') + tenant_orgid = await get_login_tenant_orgid(sor, ns) + if not tenant_orgid and ns.get('username') != 'admin': + return {'status': False, 'msg': '未找到当前域名所属租户'} + login_name = ns.get('username') or ns.get('mobile') or '' + login_fail_key = '%s:%s' % (tenant_orgid, login_name) # if domain_name in ['www.opencomputing.cn', 'dev.opencomputing.cn', 'localhost:9527', 'www.ncmatch.cn'] and ns.get('username') not in ['开元云(北京)科技有限公司', 'admin', 'kyy_root', 'kyy_kaiyuan', 'kyacloud', 'kyy_运营', 'kyy_销售', 'kyy_财务', '测试用户', 'kycloud']: if domain_name in ['www.opencomputing.cn', 'dev.opencomputing.cn', 'localhost:9527', 'www.ncmatch.cn'] and not ns.get('username') and not ns.get('codeid'): # 登录失败次数限制 - login_allowed = await check_login_allowed(ns.get('username')) + login_allowed = await check_login_allowed(login_fail_key) if not login_allowed.get('status'): return {'status': False, 'msg': login_allowed.get('msg')} @@ -153,7 +174,8 @@ async def logintype(ns): 'status': False, 'msg': '请输入手机号' } - real_mobile_li = await sor.R('users', {'username': ns['username']}) + real_mobile_sql = """select * from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('username'), tenant_orgid) + real_mobile_li = await sor.sqlExe(real_mobile_sql, {}) if not real_mobile_li: return { 'status': False, @@ -179,13 +201,14 @@ async def logintype(ns): } code = await sor.R('validatecode', {'id': ns.get('codeid'), 'vcode': ns.get('vcode')}) if len(code) < 1: - await handle_login_failed(ns.get('username')) + await handle_login_failed(login_fail_key) return {'status': False, 'msg': '验证码不正确'} password = password_encode(ns['password']) - users = await sor.R('users', {'username': ns.get('username'), 'password': password}) + users_sql = """select * from users where username = '%s' and password = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('username'), password, tenant_orgid) + users = await sor.sqlExe(users_sql, {}) if len(users) < 1: - await handle_login_failed(ns.get('username')) + await handle_login_failed(login_fail_key) return {"status": False,'msg':'用户名或密码错误'} return {'status': True} @@ -199,14 +222,20 @@ async def logintype(ns): return {'status': False, 'msg': '验证码不正确'} if type == 1: # 手机号登录 - users = await sor.R('users', {'mobile': ns.get('mobile')}) + users_sql = """select * from users where mobile = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('mobile'), tenant_orgid) + users = await sor.sqlExe(users_sql, {}) if not users: - users = await sor.R('users', {'username': ns.get('username')}) + users_sql = """select * from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('username'), tenant_orgid) + users = await sor.sqlExe(users_sql, {}) else: password = password_encode(ns['password']) - users = await sor.R('users', {'username': ns.get('username'), 'password': password}) + if ns.get('username') == 'admin': + users = await sor.R('users', {'username': ns.get('username'), 'password': password}) + else: + users_sql = """select * from users where username = '%s' and password = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns.get('username'), password, tenant_orgid) + users = await sor.sqlExe(users_sql, {}) if len(users) < 1: - await handle_login_failed(ns.get('username')) + await handle_login_failed(login_fail_key) return {"status": False,'msg':'用户名或密码错误'} elif ns.get('username') == "admin": return {'status': True} diff --git a/b/user/mobilecode.dspy b/b/user/mobilecode.dspy index 3bb2a31..5b79bc5 100644 --- a/b/user/mobilecode.dspy +++ b/b/user/mobilecode.dspy @@ -65,10 +65,30 @@ async def handle_login_failed(mobile: str) -> bool: return True +async def get_mobilecode_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 mobilecode(ns): """发送短信验证码,支持注册和登录筛选""" db = DBPools() async with db.sqlorContext('kboss') as sor: + tenant_orgid = await get_mobilecode_tenant_orgid(sor, ns) + if not tenant_orgid: + return {'status': False, 'msg': '未找到当前域名所属租户'} + # 获取操作类型:register 或 login action_type = ns.get('action_type') # register 或 login @@ -81,11 +101,13 @@ async def mobilecode(ns): return {'status': False, 'msg': '手机号不能为空'} # 检查短信发送限制 - can_send = await handle_login_failed(mobile) + sms_limit_key = '%s:%s' % (tenant_orgid[:8], mobile) + can_send = await handle_login_failed(sms_limit_key) if not can_send: return {'status': False, 'msg': '发送过于频繁,请10分钟后再试'} - userreacs = await sor.R('users', {'mobile': mobile, 'del_flg': '0'}) + user_sql = """select * from users where mobile = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (mobile, tenant_orgid) + userreacs = await sor.sqlExe(user_sql, {}) # 注册逻辑:检查手机号是否已存在 if action_type == 'register': @@ -128,12 +150,14 @@ async def mobilecode(ns): # 原有逻辑:如果没有指定action_type,保持原有逻辑 else: type = 0 - ns['del_flg'] = '0' - userreacs = await sor.R('users', ns) + username = ns.get('username') + user_sql = """select * from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (username, tenant_orgid) + userreacs = await sor.sqlExe(user_sql, {}) if len(userreacs) >= 1: type += 1 else: - userreacs = await sor.R('users', {'mobile': ns['username'], 'del_flg': '0'}) + user_sql = """select * from users where mobile = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (username, tenant_orgid) + userreacs = await sor.sqlExe(user_sql, {}) if len(userreacs) >= 1: type += 1 if type >= 1: diff --git a/b/user/retrievecode.dspy b/b/user/retrievecode.dspy index 5ef7985..2453fef 100644 --- a/b/user/retrievecode.dspy +++ b/b/user/retrievecode.dspy @@ -1,14 +1,37 @@ +async def get_retrieve_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 retrievecode(ns): """找回密码发送短信验证码""" db = DBPools() async with db.sqlorContext('kboss') as sor: + tenant_orgid = await get_retrieve_tenant_orgid(sor, ns) + if not tenant_orgid: + return {'status': False, 'msg': '未找到当前域名所属租户'} type = 0 - ns['del_flg'] = '0' - userreacs = await sor.R('users', ns) + username = ns.get('username') or ns.get('mobile') + if not username: + return {'status': False, 'msg': '用户标识不能为空'} + user_sql = """select * from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (username, tenant_orgid) + userreacs = await sor.sqlExe(user_sql, {}) if len(userreacs) >= 1: type += 1 else: - userreacs = await sor.R('users', {'mobile': ns['username'], 'del_flg': '0'}) + user_sql = """select * from users where mobile = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (username, tenant_orgid) + userreacs = await sor.sqlExe(user_sql, {}) if len(userreacs) >= 1: type += 1 if type >= 1: