From de21b9fd38f6059fa2b8ed2295d9d503627768a7 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 28 May 2026 13:49:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=8B=E6=9C=BA=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81=E7=99=BB=E5=BD=95=E5=AF=B9=E6=8E=A5=20+=20=E6=B3=A8?= =?UTF-8?q?=E5=86=8Ctab=20+=20user=5Flogined=E4=BA=8B=E4=BB=B6=E6=B4=BE?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 code_login.dspy: 接收前端表单(cell_no/codeid/check_code) 映射到sms_engine验证,返回UI widget含自动登录binds - 修复 login.ui 手机验证码tab: gen_code按钮改用script调用 gen_sms_code.dspy并回写key到隐藏字段,submit指向code_login.dspy - login.ui 新增注册tab: 用户名/手机号/密码/确认密码表单 - register.dspy: 注册成功后自动remember_user并返回含binds的 Message widget(加载userinfo、销毁登录窗、派发user_logined) - up_login.dspy: 补充user_logined事件派发bind - load_path.py: code_login.dspy加入any权限,gen_sms_code.dspy 从logined移至any(验证码发送在登录前) --- scripts/load_path.py | 3 +- wwwroot/user/code_login.dspy | 257 +++++++++++++++++++++++++++++++++++ wwwroot/user/login.ui | 78 ++++++++--- wwwroot/user/register.dspy | 65 ++++++++- wwwroot/user/up_login.dspy | 1 - 5 files changed, 377 insertions(+), 27 deletions(-) create mode 100644 wwwroot/user/code_login.dspy diff --git a/scripts/load_path.py b/scripts/load_path.py index 663117a..bf89bd6 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -41,8 +41,10 @@ MOD = "rbac" # any — 无需登录(菜单、登录页等) PATHS_ANY = [ f"/rbac/admin_menu.ui", + f"/rbac/gen_sms_code.dspy", f"/rbac/phone_login.dspy", f"/rbac/qr_scan.ui", + f"/rbac/user/code_login.dspy", f"/rbac/user/login.ui", f"/rbac/user/logout.dspy", f"/rbac/user/register.dspy", @@ -64,7 +66,6 @@ PATHS_LOGINED = [ f"/rbac/add_reseller.dspy", f"/rbac/add_superuser.dspy", f"/rbac/find_unauth_files.dspy", - f"/rbac/gen_sms_code.dspy", f"/rbac/get_all_roles.dspy", f"/rbac/get_normal_roles.dspy", f"/rbac/get_provider.dspy", diff --git a/wwwroot/user/code_login.dspy b/wwwroot/user/code_login.dspy new file mode 100644 index 0000000..4da92e7 --- /dev/null +++ b/wwwroot/user/code_login.dspy @@ -0,0 +1,257 @@ +# 手机验证码登录 - 接收前端表单参数(cell_no, codeid, check_code) +# 调用sms_engine验证后完成登录或自动注册 +debug(f'code_login.dspy: {params_kw=}') + +cellphone = params_kw.cell_no +key = params_kw.codeid +sms_code = params_kw.check_code + +if not cellphone: + return { + "widgettype": "Error", + "options": { + "timeout": 3, + "title": "错误", + "message": "需输入手机号" + } + } +if not sms_code: + return { + "widgettype": "Error", + "options": { + "timeout": 3, + "title": "错误", + "message": "需输入验证码" + } + } +if not key: + return { + "widgettype": "Error", + "options": { + "timeout": 3, + "title": "错误", + "message": "需要短信验证key" + } + } + +# 验证短信码 +ok = await sms_engine.check_sms_code(key, sms_code) +if not ok: + return { + "widgettype": "Error", + "options": { + "timeout": 3, + "title": "验证失败", + "message": "手机短信验证码出错" + } + } + +# 验证通过,查找或注册用户 +ns = { + "username": cellphone, + "password": "^&%UHI", + "cfm_password": "^&%UHI", + "mobile": cellphone, + "user_status": "0" +} +udata = DictObject(**ns) + +try: + async with get_sor_context(request._run_ns, 'rbac') as sor: + recs = await sor.R('users', {'mobile': cellphone}) + if recs: + if len(recs) == 1: + r = recs[0] + now_str = timestampstr() + await sor.sqlExe(""" + UPDATE users + SET last_login = ${now}$, login_fail_count = 0, + last_login_fail = NULL + WHERE id = ${id}$ + """, {'id': r.id, 'now': now_str}) + await remember_user(r.id, username=r.username, userorgid=r.orgid) + return { + "widgettype": "Message", + "options": { + "timeout": 3, + "auto_open": True, + "title": "登录成功", + "message": f"{r.username} 欢迎回来" + }, + "binds": [ + { + "wid": "self", + "event": "dismissed", + "actiontype": "urlwidget", + "target": "window.user_container", + "options": { + "url": entire_url('/rbac/user/userinfo.ui') + } + }, + { + "wid": "self", + "event": "dismissed", + "actiontype": "script", + "target": "body.login_window", + "script": "this.destroy()" + }, + { + "wid": "self", + "event": "dismissed", + "actiontype": "script", + "target": "self", + "script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')" + } + ] + } + # 多个用户绑定同一手机号 + if params_kw.selected_id: + for r in recs: + if r.id == params_kw.selected_id: + now_str = timestampstr() + await sor.sqlExe(""" + UPDATE users + SET last_login = ${now}$, login_fail_count = 0, + last_login_fail = NULL + WHERE id = ${id}$ + """, {'id': r.id, 'now': now_str}) + await remember_user(r.id, username=r.username, userorgid=r.orgid) + return { + "widgettype": "Message", + "options": { + "timeout": 3, + "auto_open": True, + "title": "登录成功", + "message": f"{r.username} 欢迎回来" + }, + "binds": [ + { + "wid": "self", + "event": "dismissed", + "actiontype": "urlwidget", + "target": "window.user_container", + "options": { + "url": entire_url('/rbac/user/userinfo.ui') + } + }, + { + "wid": "self", + "event": "dismissed", + "actiontype": "script", + "target": "body.login_window", + "script": "this.destroy()" + }, + { + "wid": "self", + "event": "dismissed", + "actiontype": "script", + "target": "self", + "script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')" + } + ] + } + else: + # 返回用户选择列表 + buttons = [] + for r in recs: + buttons.append({ + "widgettype": "Button", + "options": { + "label": f"{r.username} ({r.id})", + "width": "100%", + "margin": "4px 0" + }, + "binds": [{ + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "self", + "options": { + "url": entire_url('/rbac/user/code_login.dspy'), + "params": { + "cell_no": cellphone, + "codeid": key, + "check_code": sms_code, + "selected_id": r.id + } + } + }] + }) + return { + "widgettype": "VBox", + "options": {"padding": "12px"}, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "该手机号关联多个账号,请选择:", + "fontSize": "14px", + "margin": "0 0 8px 0" + } + } + ] + buttons + } + + # 新用户自动注册 + d = await register_user(sor, udata) + if d['status'] == 'error': + return { + "widgettype": "Error", + "options": { + "timeout": 5, + "title": "注册失败", + "message": d['data']['message'] + } + } + try: + ownerid = await get_owner_orgid(sor, orgid) + await openCustomerAccounts(sor, ownerid, orgid) + except Exception as e: + exception(f'{e}') + + r = d['data']['user'] + await remember_user(r.id, username=r.username, userorgid=r.orgid) + return { + "widgettype": "Message", + "options": { + "timeout": 3, + "auto_open": True, + "title": "登录成功", + "message": f"{r.username} 欢迎" + }, + "binds": [ + { + "wid": "self", + "event": "dismissed", + "actiontype": "urlwidget", + "target": "window.user_container", + "options": { + "url": entire_url('/rbac/user/userinfo.ui') + } + }, + { + "wid": "self", + "event": "dismissed", + "actiontype": "script", + "target": "body.login_window", + "script": "this.destroy()" + }, + { + "wid": "self", + "event": "dismissed", + "actiontype": "script", + "target": "self", + "script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')" + } + ] + } +except Exception as e: + exception(f'code_login error: {e}') + return { + "widgettype": "Error", + "options": { + "timeout": 5, + "title": "系统错误", + "message": f"{e}" + } + } diff --git a/wwwroot/user/login.ui b/wwwroot/user/login.ui index 7283bac..3a5c1f2 100644 --- a/wwwroot/user/login.ui +++ b/wwwroot/user/login.ui @@ -6,7 +6,7 @@ "auto_open": true, "anthor": "cc", "cwidth": 22, - "cheight": 19 + "cheight": 22 }, "subwidgets": [ { @@ -57,14 +57,6 @@ "content": { "widgettype": "Form", "options": { - "toolbar": { - "tools": [ - { - "name": "gen_code", - "label": "发送验证码" - } - ] - }, "description": "限中国国内手机", "fields": [ { @@ -75,25 +67,30 @@ { "name": "codeid", "uitype": "hide", - "value": "{{uuid()}}" + "value": "" }, { "name": "check_code", + "label": "验证码", "uitype": "str" } - ] + ], + "toolbar": { + "tools": [ + { + "name": "gen_code", + "label": "发送验证码" + } + ] + } }, "binds": [ { "wid": "self", "event": "gen_code", - "actiontype": "urlwidget", - "datawidget": "self", - "datamethod": "getValue", + "actiontype": "script", "target": "self", - "options": { - "url": "{{entire_url('../gen_sms_code.dspy')}}" - } + "script": "var form=this;var vals=form.getValue();if(!vals.cell_no){alert('请输入手机号');return;}fetch(form.app.baseUrl+'/rbac/gen_sms_code.dspy?cellphone='+encodeURIComponent(vals.cell_no)).then(function(r){return r.json()}).then(function(d){if(d.status==='ok'){form.setValue({codeid:d.data.key});alert('验证码已发送')}else{alert(d.data.message)}}).catch(function(e){alert('发送失败:'+e)})" }, { "wid": "self", @@ -101,14 +98,59 @@ "actiontype": "urlwidget", "target": "self", "options": { + "method": "POST", "url": "{{entire_url('code_login.dspy')}}" } } ] } + }, + { + "name": "register", + "label": "注册", + "content": { + "widgettype": "Form", + "options": { + "cols": 1, + "fields": [ + { + "name": "username", + "label": "用户名", + "uitype": "str" + }, + { + "name": "mobile", + "label": "手机号", + "uitype": "str" + }, + { + "name": "password", + "label": "密码", + "uitype": "password" + }, + { + "name": "cfm_password", + "label": "确认密码", + "uitype": "password" + } + ] + }, + "binds": [ + { + "wid": "self", + "event": "submit", + "actiontype": "urlwidget", + "target": "self", + "options": { + "method": "POST", + "url": "{{entire_url('register.dspy')}}" + } + } + ] + } } ] } } ] -} \ No newline at end of file +} diff --git a/wwwroot/user/register.dspy b/wwwroot/user/register.dspy index eee35a7..d0be462 100644 --- a/wwwroot/user/register.dspy +++ b/wwwroot/user/register.dspy @@ -1,18 +1,69 @@ -debug(f'{params_kw=}') +debug(f'register.dspy: {params_kw=}') db = DBPools() dbname = get_module_dbname('rbac') async with db.sqlorContext(dbname) as sor: data = await register_user(sor, params_kw) data = DictObject(**data) + if data.status == 'error': + debug(f"register error: {data.data.message}") + return { + "widgettype": "Error", + "options": { + "timeout": 5, + "title": "注册失败", + "message": data.data.message + } + } + user = data.data.user + orgid = user.orgid try: - if data['status'] == 'error': - debug(f"{data.data.message}") - return UiError(title='Error', message=data.data.message) - orgid = data.data.user.orgid await openCustomerAccounts(sor, '0', orgid) debug(f'{orgid} accounts opened') except Exception as e: exception(f'{e},{orgid=}') - return UiMessage(title="Success", message=f"register success {orgid}") -return UiError(title='Error', message="register failed") + # 注册成功后自动登录 + await remember_user(user.id, username=user.username, userorgid=user.orgid) + return { + "widgettype": "Message", + "options": { + "timeout": 3, + "auto_open": True, + "title": "注册成功", + "message": f"{user.username} 注册成功,已自动登录" + }, + "binds": [ + { + "wid": "self", + "event": "dismissed", + "actiontype": "urlwidget", + "target": "window.user_container", + "options": { + "url": entire_url('/rbac/user/userinfo.ui') + } + }, + { + "wid": "self", + "event": "dismissed", + "actiontype": "script", + "target": "body.login_window", + "script": "if(this.destroy) this.destroy()" + }, + { + "wid": "self", + "event": "dismissed", + "actiontype": "script", + "target": "self", + "script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')" + } + ] + } + +return { + "widgettype": "Error", + "options": { + "timeout": 5, + "title": "注册失败", + "message": "系统错误,请稍后重试" + } +} diff --git a/wwwroot/user/up_login.dspy b/wwwroot/user/up_login.dspy index d456bad..1944143 100644 --- a/wwwroot/user/up_login.dspy +++ b/wwwroot/user/up_login.dspy @@ -1,4 +1,3 @@ - debug(f'{params_kw=}') ns = { "username":params_kw.username,