From dd35774400d4e4f0b980f6f11376cbde37b293c5 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Thu, 16 Jul 2026 11:17:50 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- b/customer/registerUser.dspy | 43 ++++- b/docs/user_tenant_refactor.md | 327 +++++++++++++++++++++++++++++++++ 2 files changed, 361 insertions(+), 9 deletions(-) create mode 100644 b/docs/user_tenant_refactor.md 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/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. 订单、余额、扣费、结算不串账。 + From 04321fe6a29fb139944936451b07ae67338430d0 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Thu, 16 Jul 2026 11:28:57 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- b/user/loginUser.dspy | 38 +++++++++++++++++++++++++++------- b/user/logintype.dspy | 47 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 16 deletions(-) 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} From 52d9bb16f89394df2f13aefa4b38de1e143c8688 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Thu, 16 Jul 2026 11:36:00 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E5=BF=98=E8=AE=B0=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E5=92=8C=E5=8F=91=E9=80=81=E7=9F=AD=E4=BF=A1=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- b/customer/forgotPassword.dspy | 32 ++++++++++++++++++++++++++++---- b/user/mobilecode.dspy | 34 +++++++++++++++++++++++++++++----- b/user/retrievecode.dspy | 29 ++++++++++++++++++++++++++--- 3 files changed, 83 insertions(+), 12 deletions(-) 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/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: From 90225fee1ccee112632fe171243b6ecac06dd8ed Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Thu, 16 Jul 2026 13:32:43 +0800 Subject: [PATCH 4/8] update --- b/reseller/reseller_add_user.dspy | 19 ++++++++++++++++++- b/user/addUser.dspy | 20 +++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) 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()}) From 2d3425c7c1930fc7df667e7cd710bb9171ce2914 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Fri, 17 Jul 2026 09:58:17 +0800 Subject: [PATCH 5/8] update --- b/docs/upload_file.dspy | 50 +++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/b/docs/upload_file.dspy b/b/docs/upload_file.dspy index b39816e..9e6d223 100644 --- a/b/docs/upload_file.dspy +++ b/b/docs/upload_file.dspy @@ -1,26 +1,39 @@ async def upload_file(ns={}): import os - import base64 import datetime + async def read_file_bytes(upload_file): + if isinstance(upload_file, bytes): + return upload_file + if isinstance(upload_file, bytearray): + return bytes(upload_file) + if isinstance(upload_file, dict): + upload_file = upload_file.get('file') or upload_file.get('content') or upload_file.get('body') + if hasattr(upload_file, 'file'): + upload_file = upload_file.file + if hasattr(upload_file, 'read'): + file_content = upload_file.read() + if hasattr(file_content, '__await__'): + file_content = await file_content + return file_content + return None + + upload_file_data = ns.get('file') or ns.get('upload_file') or ns.get('file_bytes') file_name = ns.get('file_name') - file_content = ns.get('file_content') or ns.get('file_base64') - storage_type = ns.get('storage_type', 'local') + if not file_name and hasattr(upload_file_data, 'filename'): + file_name = upload_file_data.filename + if not file_name and isinstance(upload_file_data, dict): + file_name = upload_file_data.get('filename') or upload_file_data.get('file_name') or upload_file_data.get('name') if not file_name: return { 'status': False, 'msg': '请传递file_name' } - if not file_content: + if not upload_file_data: return { 'status': False, - 'msg': '请传递file_content' - } - if storage_type != 'local': - return { - 'status': False, - 'msg': '暂不支持该存储类型' + 'msg': '请上传文件' } safe_file_name = os.path.basename(file_name).replace('\\', '').replace('/', '') @@ -31,21 +44,24 @@ async def upload_file(ns={}): } 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) + file_bytes = await read_file_bytes(upload_file_data) + if not file_bytes: + return { + 'status': False, + 'msg': '文件内容为空' + } except Exception as e: return { 'status': False, - 'msg': '文件内容解析失败, %s' % str(e) + '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) + file_path = '/data/%s/%s' % (date_path, safe_file_name) try: if not os.path.exists(save_dir): @@ -56,9 +72,9 @@ async def upload_file(ns={}): 'status': True, 'msg': 'upload success', 'data': { - 'storage_type': storage_type, + 'storage_type': 'local', 'file_name': safe_file_name, - 'file_path': relative_path, + 'file_path': file_path, 'save_path': save_path, 'file_size': len(file_bytes) } From 78f0c9e0de863342ae3bba1e772ccf68482c4a94 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Fri, 17 Jul 2026 10:54:01 +0800 Subject: [PATCH 6/8] update --- b/docs/upload_file.dspy | 75 ++++++----------------------------------- 1 file changed, 11 insertions(+), 64 deletions(-) diff --git a/b/docs/upload_file.dspy b/b/docs/upload_file.dspy index 9e6d223..955df40 100644 --- a/b/docs/upload_file.dspy +++ b/b/docs/upload_file.dspy @@ -1,82 +1,29 @@ async def upload_file(ns={}): - import os - import datetime - - async def read_file_bytes(upload_file): - if isinstance(upload_file, bytes): - return upload_file - if isinstance(upload_file, bytearray): - return bytes(upload_file) - if isinstance(upload_file, dict): - upload_file = upload_file.get('file') or upload_file.get('content') or upload_file.get('body') - if hasattr(upload_file, 'file'): - upload_file = upload_file.file - if hasattr(upload_file, 'read'): - file_content = upload_file.read() - if hasattr(file_content, '__await__'): - file_content = await file_content - return file_content - return None - - upload_file_data = ns.get('file') or ns.get('upload_file') or ns.get('file_bytes') - file_name = ns.get('file_name') - if not file_name and hasattr(upload_file_data, 'filename'): - file_name = upload_file_data.filename - if not file_name and isinstance(upload_file_data, dict): - file_name = upload_file_data.get('filename') or upload_file_data.get('file_name') or upload_file_data.get('name') + 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 upload_file_data: + domain_name = ns.get('domain_name') + if not domain_name: 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: - file_bytes = await read_file_bytes(upload_file_data) - if not file_bytes: - return { - 'status': False, - 'msg': '文件内容为空' - } - except Exception as e: - return { - 'status': False, - 'msg': '文件内容读取失败, %s' % str(e) - } - - now_date = datetime.datetime.now() - date_path = now_date.strftime('%Y/%m/%d') - 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) - file_path = '/data/%s/%s' % (date_path, 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': 'local', - 'file_name': safe_file_name, - 'file_path': file_path, - 'save_path': save_path, - 'file_size': len(file_bytes) + 'save_url': save_url } } except Exception as e: From 7d88136d03191f5e9d006416d8d990256d2db414 Mon Sep 17 00:00:00 2001 From: ping <1017253325@qq.com> Date: Fri, 17 Jul 2026 14:31:14 +0800 Subject: [PATCH 7/8] update --- b/reseller/get_ipc_logo.dspy | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/b/reseller/get_ipc_logo.dspy b/b/reseller/get_ipc_logo.dspy index 32da07f..ec9e29a 100644 --- a/b/reseller/get_ipc_logo.dspy +++ b/b/reseller/get_ipc_logo.dspy @@ -46,7 +46,7 @@ async def get_ipc_logo(ns={}): if '?domain_name=' in url_link: domain_url = url_link.split('?domain_name=')[1] # 如果是业主机构 - if ('ncmatch' in domain_url or '9527' in domain_url or '8889' in domain_url or '8891' in domain_url or domain_url in ['xterm.kaiyuancloud.cn','www.kaiyuancloud.cn', 'dev.kaiyuancloud.cn', 'dev.opencomputing.cn', 'test.kaiyuancloud.cn', 'localhost', 'www.opencomputing.cn', 'opencomputing.cn']) and '/domain/' not in url_link: + if ('ncmatch' in domain_url or 'zgcopc.opencomputing.cn' in domain_url or '9527' in domain_url or '8889' in domain_url or '8891' in domain_url or domain_url in ['xterm.kaiyuancloud.cn','www.kaiyuancloud.cn', 'dev.kaiyuancloud.cn', 'dev.opencomputing.cn', 'test.kaiyuancloud.cn', 'localhost', 'www.opencomputing.cn', 'opencomputing.cn']) and '/domain/' not in url_link: yezhu_info = (await sor.R('organization', {'org_type': '0'}))[0] domain_res = (await sor.R('params', {'pname': '业主机构域名'}))[0]['pvalue'] # yezhu = { @@ -91,6 +91,37 @@ async def get_ipc_logo(ns={}): ] } } + elif 'zgcopc.opencomputing.cn' in domain_url or '9527' in domain_url: + yezhu['logo'] = "https://zgcopc.opencomputing.cn/idfile?path=logo_zgcopc.png" + yezhu['additional_msg'] = { + "home": { + "logoImg": "https://zgcopc.opencomputing.cn/idfile?path=logo_zgcopc.png", + "bannerTitle": "中关村国际孵化器", + "adress": "北京市海淀区上地信息路2号", + "footerTitle": "中关村国际孵化器有限公司", + 'qrCode': 'https://zgcopc.opencomputing.cn/idfile?path=firstpagehot/ncmatch_inquiry.jpg', + 'footer_info': '京ICP备13007859号-1 北京中关村国际孵化器有限公司', + "copyright": "", + "domain_name": "zgcopc.opencomputing.cn", + "email": "zhangjian@bjzai.org.cn", + "license": "京ICP备13007859号-1", + "logo": "https://zgcopc.opencomputing.cn/idfile?path=logo_zgcopc.png", + "mobile": "13601380912", + "publicsecurity": "11010502054007", + "businesslicense": "京ICP备13007859号-1" + }, + "about": { + "title": "开元数智", + "logo": "https://www.opencomputing.cn/idfile?path=logo_ncmatch.png", + "content": [ + "开元数智(北京)科技有限公司(以下简称“开元数智”)创立于2022年,总部位于北京中关村科技园区,现已形成以上海、南京、深圳、济南为区域枢纽的全国性服务网络。作为新型数字基础设施建设领域的创新企业,公司主营业务涵盖智能算力中心规划建设与运营、算力网络体系构建、企业级AI算力服务及大模型应用整体解决方案。", + "公司自主研发的NCMatch智能算力公共服务平台,作为行业领先的一站式人工智能算力服务解决方案,已构建云网算数智“五位一体”的服务体系。该平台深度整合算力资源调度系统、智能算法模型库及行业应用矩阵,为高等院校、科研院所、政企机构及人工智能开发企业提供专业级算力云服务,全面覆盖基础算力供给、大模型训练调优及产业智能化升级等多维度需求。", + "开元数智作为国家算力网络建设的重要参与单位,已获得工信部、教育部等部委直属机构指导,并与中关村数智人工智能产业联盟、国家超级计算中心、南京未来网络研究院等权威机构建立战略合作关系。公司深度参与全国超算互联网及全国一体化算力网国家级重点工程,在算力基础设施建设领域具备成熟的技术积累和项目实施经验。", + "开元数智(北京)科技有限公司合作的客户有:中国联通、中国电信、中国移动、未来网络、中信网络、国家超级计算济南中心、华为、阿里云、百度智能云、腾讯云、火山、智谱·AI、衔远科技、创新工场、竹间智能、深言科技、澜舟科技、商汤科技、MINMAX、云从科技、第四范式、深度求索、百川智能、Moonshot AI、昆仑万维、深蓝等。", + + ] + } + } elif 'kaiyuancloud' in domain_url or 'opencomputing' in domain_url or 'localhost' in domain_url: yezhu['additional_msg'] = { "home": { From 314173e3813b2b2986c5b7136d0dd5d0121d2dcc Mon Sep 17 00:00:00 2001 From: hrx <18603305412@163.com> Date: Fri, 17 Jul 2026 15:28:41 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- f/web-kboss/src/App.vue | 183 +++++++++++++ f/web-kboss/src/api/newsapi/newsapi.js | 10 + f/web-kboss/src/i18n/lang/en-US.js | 88 ++++++- f/web-kboss/src/i18n/lang/zh-CN.js | 80 +++++- f/web-kboss/src/layout/components/Navbar.vue | 2 +- .../src/layout/components/Sidebar/index.vue | 21 ++ f/web-kboss/src/router/index.js | 16 +- f/web-kboss/src/utils/request.js | 11 +- .../src/views/LoginDialog/LoginDialog.vue | 3 +- f/web-kboss/src/views/beforeLogin/index.vue | 2 +- f/web-kboss/src/views/homePage/bak12_25.vue | 2 +- .../homePage/components/topBox/index.vue | 17 +- .../homePage/computeMarket/createInstance.vue | 8 +- f/web-kboss/src/views/homePage/indexNew.vue | 2 +- .../src/views/homePage/mainPage/OPC/index.vue | 6 +- .../src/views/homePage/mainPage/index.vue | 61 +++-- .../src/views/homePage/ncmatch/index.vue | 2 +- .../views/homePage/ncmatch/mainPage/index.vue | 6 +- .../ncmatch/supplyAndDemandSquare/index.vue | 2 +- .../homePage/news/ArticleEditorDialog.vue | 249 ++++++++++++++---- .../src/views/homePage/news/NewsDetail.vue | 110 +++++--- .../src/views/homePage/news/newsLog.vue | 67 +++-- .../src/views/homePage/news/newsView.vue | 17 +- .../views/homePage/trainPlatform/index.vue | 2 +- .../src/views/homePage/wxDetailPage.vue | 7 +- f/web-kboss/src/views/homePage/wxPage.vue | 9 +- .../src/views/homePageMobile/indexNew.vue | 2 +- .../login/components/ForgotPasswordDialog.vue | 55 ++-- .../views/modelManagement/modelManagement.vue | 2 + .../views/operation/modelInfoConfig/index.vue | 1 + .../src/views/product/allProduct/index.vue | 28 +- .../src/views/tokenManagement/index.vue | 1 + f/web-kboss/src/views/tokenUsage/index.vue | 10 +- 33 files changed, 846 insertions(+), 236 deletions(-) diff --git a/f/web-kboss/src/App.vue b/f/web-kboss/src/App.vue index 1663d7f..6e799fe 100644 --- a/f/web-kboss/src/App.vue +++ b/f/web-kboss/src/App.vue @@ -313,10 +313,193 @@ body { width: 100% !important; } +.news-confirm-box { + position: relative; + width: 460px !important; + padding: 28px 32px 24px; + border: 0; + border-radius: 20px; + box-shadow: 0 20px 56px rgba(15, 23, 42, 0.2); +} + +.news-confirm-box .el-message-box__header { + padding: 0; + text-align: left; +} + +.news-confirm-box .el-message-box__title { + display: flex; + align-items: center; + gap: 14px; + width: 100%; + padding-left: 0; + text-align: left; + color: #1f2937; + font-size: 20px; + font-weight: 800; + line-height: 1.35; +} + +.news-confirm-box .el-message-box__title span { + display: block; + flex: 1; + text-align: left; +} + +.news-confirm-box .el-message-box__status { + display: none !important; +} + +.news-confirm-box .el-message-box__title::before { + width: 44px; + height: 44px; + flex: 0 0 44px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 24px; + font-weight: 800; + line-height: 1; +} + +.news-confirm-box--warning .el-message-box__title::before { + content: '!'; + color: #f59e0b; + background: #fff7ed; +} + +.news-confirm-box--danger .el-message-box__title::before { + content: '×'; + color: #ef4444; + background: #fef2f2; +} + +.news-confirm-box--success .el-message-box__title::before { + content: '✓'; + color: #22c55e; + background: #f0fdf4; +} + +.news-confirm-box .el-message-box__headerbtn { + display: none; +} + +.news-confirm-box .el-message-box__content { + position: static; + padding: 16px 0 0 58px; + text-align: left; +} + +.news-confirm-box .el-message-box__message { + margin-left: 0 !important; + padding-left: 0 !important; + display: block; + width: 100%; + text-align: left; + color: #64748b; + font-size: 16px; + line-height: 1.65; +} + +.news-confirm-box .el-message-box__message p { + margin: 0; + text-align: left; +} + +.news-confirm-box .el-message-box__btns { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 24px 0 0; +} + +.news-confirm-btn { + min-width: 88px; + height: 42px; + margin-left: 0 !important; + padding: 0 22px; + border-radius: 10px; + font-size: 15px; + font-weight: 600; +} + +.news-confirm-btn--cancel { + color: #64748b; + background: #fff; + border-color: #d8e0ec; +} + +.news-confirm-btn--cancel:hover, +.news-confirm-btn--cancel:focus { + color: #334155; + background: #f8fafc; + border-color: #cbd5e1; +} + +.news-confirm-btn--confirm { + color: #fff; + background: #3b82f6; + border-color: #3b82f6; +} + +.news-confirm-btn--confirm:hover, +.news-confirm-btn--confirm:focus { + color: #fff; + background: #2563eb; + border-color: #2563eb; +} + +.news-confirm-btn--confirm.is-danger { + background: #ef4444; + border-color: #ef4444; +} + +.news-confirm-btn--confirm.is-danger:hover, +.news-confirm-btn--confirm.is-danger:focus { + background: #dc2626; + border-color: #dc2626; +} + +.news-confirm-btn--confirm.is-success { + background: #22c55e; + border-color: #22c55e; +} + +.news-confirm-btn--confirm.is-success:hover, +.news-confirm-btn--confirm.is-success:focus { + background: #16a34a; + border-color: #16a34a; +} + @media screen and (max-width: 760px) { .el-message-box { width: 320px !important; } + + .news-confirm-box { + width: calc(100vw - 32px) !important; + padding: 24px 20px 22px; + } + + .news-confirm-box .el-message-box__title { + font-size: 18px; + } + + .news-confirm-box .el-message-box__title::before { + width: 40px; + height: 40px; + flex-basis: 40px; + font-size: 22px; + } + + .news-confirm-box .el-message-box__content { + padding-left: 52px; + } + + .news-confirm-box .el-message-box__message { + font-size: 15px; + } } * { diff --git a/f/web-kboss/src/api/newsapi/newsapi.js b/f/web-kboss/src/api/newsapi/newsapi.js index de2f50e..a5b3cf4 100644 --- a/f/web-kboss/src/api/newsapi/newsapi.js +++ b/f/web-kboss/src/api/newsapi/newsapi.js @@ -66,4 +66,14 @@ export const reqUnpublishNews = (data) => { method: 'get', params: data }) +} + +// 新闻图片上传 +export const reqUploadNewsImage = (data) => { + return request({ + url: '/docs/upload_file.dspy', + method: 'post', + data, + headers: { 'Content-Type': 'multipart/form-data' } + }) } \ No newline at end of file diff --git a/f/web-kboss/src/i18n/lang/en-US.js b/f/web-kboss/src/i18n/lang/en-US.js index 29ff115..2e01858 100644 --- a/f/web-kboss/src/i18n/lang/en-US.js +++ b/f/web-kboss/src/i18n/lang/en-US.js @@ -37,35 +37,99 @@ export default { solutionCards: { collaboration: { title: 'Collaborative Intelligence', - subtitle: 'Multi-Agent Coordination' + subtitle: 'Multi-Agent Collaboration', + detailTitle: 'Collective Intelligence', + detailSubTitle: 'Multi-Agent Collaboration', + detailDesc: 'Multiple AI agents collaborate autonomously based on role specialization, completing the full workflow of information gathering, data analysis, logical reasoning, and conclusion generation. With dynamic task allocation and shared context, complex business decisions shift from human sequential workflows to parallel agent execution, significantly reducing cycle times for high-knowledge-density work.', + metrics: { + first: { value: '10x+', label: 'Knowledge work efficiency improvement' }, + second: { value: 'Real-time', label: 'Multi-source information fusion' }, + third: { value: 'High complexity', label: 'Decision scenario adaptability' } + } }, spatial: { title: 'Spatial Intelligence', - subtitle: 'GIS Smart Analysis' + subtitle: 'GIS Intelligent Analysis', + detailTitle: 'Spatial Awakening', + detailSubTitle: 'GIS Intelligent Analysis', + detailDesc: 'Integrating Geographic Information Systems, satellite remote sensing imagery, and drone aerial photography, AI automatically identifies land feature boundaries, analyzes spatial relationships through overlay analysis, and verifies compliance constraints. Geographic spatial judgments that once required manual layer-by-layer comparison are now upgraded to automated spatial intelligence decisions delivered in minutes.', + metrics: { + first: { value: 'Days to minutes', label: 'Spatial analysis' }, + second: { value: '90%+', label: 'Violation detection accuracy' }, + third: { value: '70%', label: 'Labor cost savings' } + } }, predictive: { title: 'Predictive Insight', - subtitle: 'Intelligent Forecasting Engine' + subtitle: 'Intelligent Prediction Engine', + detailTitle: 'One Step Ahead', + detailSubTitle: 'Intelligent Prediction Engine', + detailDesc: 'Integrating physics-based models with data-driven approaches, the system collects real-time equipment operating parameters to autonomously identify early anomaly characteristics and accurately predict remaining useful life. Warnings are issued 72 hours before actual failure occurs, transforming maintenance from reactive repair to proactive defense and maximizing production continuity.', + metrics: { + first: { value: '95%+', label: 'Fault warning accuracy' }, + second: { value: '60%', label: 'Unplanned downtime reduction' }, + third: { value: '72 hours', label: 'Advance prediction lead time' } + } }, vision: { - title: 'Microscopic Insight', - subtitle: 'Machine Vision Inspection' + title: 'Insight into the Microscopic', + subtitle: 'Machine Vision Inspection', + detailTitle: 'Insight into the Microscopic', + detailSubTitle: 'Machine Vision Inspection', + detailDesc: 'Deep learning-based high-speed full-coverage defect detection for production lines, covering the entire quality inspection process including surface treatment, coating spraying, and welding assembly. This is not sampling inspection; every inch of product undergoes AI pixel-by-pixel comparison, completing defect identification and classification in milliseconds, ensuring zero compromise on product quality.', + metrics: { + first: { value: 'Milliseconds', label: 'Inspection speed' }, + second: { value: '95%', label: 'Missed detection rate reduction' }, + third: { value: '99.5%+', label: 'Identification accuracy' } + } }, qa: { title: 'Instant Answers', - subtitle: 'Intelligent Q&A Knowledge Base' + subtitle: 'Intelligent Q&A Knowledge Base', + detailTitle: 'Instant Answers', + detailSubTitle: 'Intelligent Q&A Knowledge Base', + detailDesc: 'An AI-powered Q&A engine built on enterprise private knowledge bases, supporting natural language queries for business data, technical specifications, and policy documents. Every answer is automatically annotated with authoritative sources, ensuring information is traceable and verifiable. Expert-level knowledge is no longer locked in documents but available on demand, like asking a colleague.', + metrics: { + first: { value: '<3 sec', label: 'Response time' }, + second: { value: '90%', label: 'Expert knowledge coverage' }, + third: { value: '5x', label: 'Decision-making efficiency improvement' } + } }, protection: { - title: 'Continuous Protection', - subtitle: 'Intelligent Device Monitoring' + title: 'Full Protection', + subtitle: 'Intelligent Device Monitoring', + detailTitle: 'Full Protection', + detailSubTitle: 'Intelligent Equipment Monitoring', + detailDesc: 'Building independent monitoring strategies for each device, with 3D visualization presenting real-time operating status. AI automatically identifies anomalies and highlights fault locations, while synchronously outputting detailed diagnostic recommendations and troubleshooting steps. Operations staff are liberated from being glued to screens hunting for issues; the system proactively tells you where the issue is and how to fix it.', + metrics: { + first: { value: '1000+', label: 'Monitoring points supported' }, + second: { value: '<1 sec', label: 'Alert response' }, + third: { value: '40%', label: 'Maintenance cost reduction' } + } }, document: { - title: 'Document Intelligence', - subtitle: 'Smart Document Parsing' + title: 'Read and Comprehend', + subtitle: 'Intelligent Document Parsing', + detailTitle: 'Read and Comprehend', + detailSubTitle: 'Intelligent Document Parsing', + detailDesc: 'Supporting one-click parsing of multiple formats including PDF, Word, and scanned documents. AI automatically extracts key clauses, qualification requirements, scoring criteria, and other core information for structured presentation. This is not simple OCR text recognition; it understands document semantics, judges information importance, and highlights risk points, making the core content of lengthy documents clear at a glance.', + metrics: { + first: { value: '98%+', label: 'Parsing accuracy' }, + second: { value: '85%', label: 'Manual review time reduction' }, + third: { value: 'Zero omission', label: 'Key information' } + } }, edge: { - title: 'Edge Intelligence', - subtitle: 'Edge AI Inference' + title: 'On-Site Intelligence', + subtitle: 'Edge AI Inference', + detailTitle: 'On-Site Intelligence', + detailSubTitle: 'Edge AI Inference', + detailDesc: 'Deploying AI inference capabilities at the edge of production sites, achieving millisecond-level real-time decisions through lightweight algorithms. Data does not need to be transmitted back to the cloud; sensing to judgment is completed locally, ensuring data never leaves the facility and zero-latency response. Redundant architecture guarantees 7x24 continuous operation with uninterrupted production line inspection.', + metrics: { + first: { value: '<50ms', label: 'Inference latency' }, + second: { value: 'Local', label: 'Data processed on site' }, + third: { value: '7x24h', label: 'Continuous operation' } + } } }, cases: { diff --git a/f/web-kboss/src/i18n/lang/zh-CN.js b/f/web-kboss/src/i18n/lang/zh-CN.js index a6ebcf6..aa964ac 100644 --- a/f/web-kboss/src/i18n/lang/zh-CN.js +++ b/f/web-kboss/src/i18n/lang/zh-CN.js @@ -37,35 +37,99 @@ export default { solutionCards: { collaboration: { title: '群智协作', - subtitle: '多智能体协同' + subtitle: '多智能体协同', + detailTitle: '群智协作', + detailSubTitle: '多智能体协同', + detailDesc: '多个AI智能体按角色分工,自主完成信息采集、数据分析、逻辑推理与结论生成的全流程协作。支持动态任务分配与上下文共享,将复杂业务决策从"人串联"变为"智能体并行",大幅压缩高知识密度工作的完成周期。', + metrics: { + first: { value: '10倍+', label: '知识工作效率提升' }, + second: { value: '多源', label: '信息实时融合' }, + third: { value: '高复杂度', label: '决策场景适配' } + } }, spatial: { title: '空间觉醒', - subtitle: 'GIS智能分析' + subtitle: 'GIS智能分析', + detailTitle: '空间觉醒', + detailSubTitle: 'GIS智能分析', + detailDesc: '融合地理信息系统、卫星遥感影像、无人机航拍等多维空间数据,AI自动识别地物边界、叠加分析空间关系、校验合规约束。将传统需要人工逐层比对的地理空间判断,升级为分钟级自动完成的空间智能决策。', + metrics: { + first: { value: '分钟级', label: '空间分析' }, + second: { value: '90%+', label: '违规识别准确率' }, + third: { value: '70%', label: '人力成本节省' } + } }, predictive: { title: '先见一步', - subtitle: '智能预测引擎' + subtitle: '智能预测引擎', + detailTitle: '先见一步', + detailSubTitle: '智能预测引擎', + detailDesc: '融合机理模型与数据驱动模型,通过实时采集设备运行参数,AI自主识别早期异常特征并精准预测剩余使用寿命。在故障真正发生前72小时发出预警,让运维从"事后抢修"转变为"事前防御",最大化保障生产连续性。', + metrics: { + first: { value: '95%+', label: '故障预警准确率' }, + second: { value: '60%', label: '非计划停机减少' }, + third: { value: '72小时', label: '预测提前量' } + } }, vision: { title: '洞察入微', - subtitle: '机器视觉检测' + subtitle: '机器视觉检测', + detailTitle: '洞察入微', + detailSubTitle: '机器视觉检测', + detailDesc: '基于深度学习的高速产线全幅面缺陷检测能力,覆盖表面处理、涂层喷涂、焊接装配等全流程质检环节。不是抽样检查,而是每一寸产品都经过AI逐像素比对,在毫秒级时间内完成缺陷识别与分类,确保出厂品质零妥协。', + metrics: { + first: { value: '毫秒级', label: '检测速度' }, + second: { value: '95%', label: '漏检率降低' }, + third: { value: '99.5%+', label: '识别准确率' } + } }, qa: { title: '一问即达', - subtitle: '智能问答知识库' + subtitle: '智能问答知识库', + detailTitle: '一问即达', + detailSubTitle: '智能问答知识库', + detailDesc: '基于企业私有知识库构建的AI问答引擎,支持自然语言直接查询业务数据、技术规范、制度文件。每条回答自动标注权威来源,信息可追溯、可验证。让专家级知识不再锁在文档里,而是像问同事一样即问即答。', + metrics: { + first: { value: '<3秒', label: '响应速度' }, + second: { value: '90%', label: '专家知识覆盖率' }, + third: { value: '5倍', label: '决策效率提升' } + } }, protection: { title: '全程守护', - subtitle: '设备智能监控' + subtitle: '设备智能监控', + detailTitle: '全程守护', + detailSubTitle: '设备智能监控', + detailDesc: '为每台设备构建独立监控策略,通过三维可视化实时呈现运行状态。AI自动识别异常并高亮定位故障点,同步输出详细诊断建议和排查步骤。将运维人员从"盯屏找问题"解放出来,让系统主动告诉你问题在哪、怎么修。', + metrics: { + first: { value: '1000+', label: '监控点位支持' }, + second: { value: '<1秒', label: '告警响应' }, + third: { value: '40%', label: '运维成本降低' } + } }, document: { title: '触文即懂', - subtitle: '智能文档解析' + subtitle: '智能文档解析', + detailTitle: '触文即懂', + detailSubTitle: '智能文档解析', + detailDesc: '支持PDF、Word、扫描件等多种格式一键解析,AI自动提取关键条款、资质要求、评分细则等核心信息并结构化呈现。不是简单的OCR文字识别,而是理解文档语义、判断信息重要性、标注风险点,让冗长文档的核心内容一目了然。', + metrics: { + first: { value: '98%+', label: '解析准确率' }, + second: { value: '85%', label: '人工核查时间减少' }, + third: { value: '零遗漏', label: '关键信息' } + } }, edge: { title: '端侧即算', - subtitle: '边缘AI推理' + subtitle: '边缘AI推理', + detailTitle: '端侧即算', + detailSubTitle: '边缘AI推理', + detailDesc: '将AI推理能力部署在生产现场边缘侧,通过轻量化算法实现毫秒级实时决策。数据无需回传云端,在本地即可完成从感知到判断的全流程,确保数据不出厂、响应零延迟。冗余架构设计保障7×24小时持续运行,产线检测永不中断。', + metrics: { + first: { value: '<50ms', label: '推理延迟' }, + second: { value: '本地', label: '数据处理不出厂' }, + third: { value: '7×24h', label: '持续运行' } + } } }, cases: { diff --git a/f/web-kboss/src/layout/components/Navbar.vue b/f/web-kboss/src/layout/components/Navbar.vue index 218233d..ededb2a 100644 --- a/f/web-kboss/src/layout/components/Navbar.vue +++ b/f/web-kboss/src/layout/components/Navbar.vue @@ -391,7 +391,7 @@ export default { const userId = sessionStorage.getItem('userId'); if (!userId) { this.$message.warning('请先登录'); - this.$router.push('/login'); + window.dispatchEvent(new Event('kboss-open-login-dialog')); return; } this.messageCenterVisible = true; diff --git a/f/web-kboss/src/layout/components/Sidebar/index.vue b/f/web-kboss/src/layout/components/Sidebar/index.vue index f4a5c7f..b2d45d3 100644 --- a/f/web-kboss/src/layout/components/Sidebar/index.vue +++ b/f/web-kboss/src/layout/components/Sidebar/index.vue @@ -122,6 +122,27 @@ export default { box-sizing: border-box; } } + + ::v-deep .happy-scroll-strip, + ::v-deep .happy-scroll-bar, + ::v-deep .happy-scroll-rail { + display: none !important; + opacity: 0 !important; + width: 0 !important; + } + + ::v-deep .happy-scroll-content, + ::v-deep .happy-scroll-container { + scrollbar-width: none; + -ms-overflow-style: none; + } + + ::v-deep .happy-scroll-content::-webkit-scrollbar, + ::v-deep .happy-scroll-container::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + } } .sidebar-collapse-btn { diff --git a/f/web-kboss/src/router/index.js b/f/web-kboss/src/router/index.js index 58e9c71..81298f0 100644 --- a/f/web-kboss/src/router/index.js +++ b/f/web-kboss/src/router/index.js @@ -610,7 +610,7 @@ export const asyncRoutes = [ path: "/newsLog", component: Layout, meta: { - title: "新闻动态", + title: "企业动态", fullPath: "/newsLog", noCache: true, icon: "el-icon-document", @@ -622,7 +622,7 @@ export const asyncRoutes = [ component: () => import('@/views/homePage/news/newsLog.vue'), name: 'NewsLog', meta: { - title: "新闻动态", + title: "企业动态", fullPath: "/newsLog", noCache: true, roles: ["运营"] @@ -855,6 +855,18 @@ export const asyncRoutes = [ name: "detail", hidden: true, meta: { title: "详情", cache: true }, + }, { + path: "news", + component: () => import("@/views/homePage/news/newsView.vue"), + name: "homePageNewsLegacy", + hidden: true, + meta: { title: "企业动态", cache: true }, + }, { + path: "news/detail/:id", + component: () => import("@/views/homePage/news/NewsDetail.vue"), + name: "homePageNewsDetailLegacy", + hidden: true, + meta: { title: "新闻详情", cache: true }, }, { path: "hospital", component: () => import("@/views/homePage/solve/hospital/index.vue"), diff --git a/f/web-kboss/src/utils/request.js b/f/web-kboss/src/utils/request.js index 6c46ead..a713084 100644 --- a/f/web-kboss/src/utils/request.js +++ b/f/web-kboss/src/utils/request.js @@ -165,9 +165,16 @@ service.interceptors.response.use( isRedirectingToLogin = true saveLoginRedirectPath() showErrorMessage('登录已过期,请重新登录') - router.push({path: '/login'}).finally(() => { + const currentPath = (router.currentRoute && router.currentRoute.path) || '' + const isHomePage = currentPath.includes('/homePage') || currentPath.includes('/ncmatchHome') + if (isHomePage) { + window.dispatchEvent(new Event('kboss-open-login-dialog')) isRedirectingToLogin = false - }) + } else { + router.push({path: '/login'}).finally(() => { + isRedirectingToLogin = false + }) + } } // window.location.href = 'https://www.opencomputing.cn/' } else if (code == 403) { diff --git a/f/web-kboss/src/views/LoginDialog/LoginDialog.vue b/f/web-kboss/src/views/LoginDialog/LoginDialog.vue index 3762027..263a61f 100644 --- a/f/web-kboss/src/views/LoginDialog/LoginDialog.vue +++ b/f/web-kboss/src/views/LoginDialog/LoginDialog.vue @@ -13,8 +13,7 @@
{{ activeTab === 'login' ? '登录后继续使用智能服务' : '注册开启全新体验' }}
+