From 2c986dca131484f4e097c5071bb6781721c85218 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 3 Jul 2026 14:50:52 +0800 Subject: [PATCH] fix: CRUD endpoint return format - remove json.dumps, use widgettype format --- wwwroot/users/add_users.dspy | 54 ++++++ wwwroot/users/delete_users.dspy | 47 +++++ wwwroot/users/get_users.dspy | 170 ++++++++++++++++++ wwwroot/users/index.ui | 302 ++++++++++++++++++++++++++++++++ wwwroot/users/update_users.dspy | 73 ++++++++ 5 files changed, 646 insertions(+) create mode 100644 wwwroot/users/add_users.dspy create mode 100644 wwwroot/users/delete_users.dspy create mode 100644 wwwroot/users/get_users.dspy create mode 100644 wwwroot/users/index.ui create mode 100644 wwwroot/users/update_users.dspy diff --git a/wwwroot/users/add_users.dspy b/wwwroot/users/add_users.dspy new file mode 100644 index 0000000..bdf0f08 --- /dev/null +++ b/wwwroot/users/add_users.dspy @@ -0,0 +1,54 @@ + +ns = params_kw.copy() +for k,v in ns.items(): + if v == 'NaN' or v == 'null': + ns[k] = None +id = params_kw.id +if not id or len(id) > 32: + id = uuid() +ns['id'] = id + +if params_kw.get('password'): + ns['password'] = password_encode(params_kw.get('password')) + + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['orgid'] = userorgid + +db = DBPools() +dbname = get_module_dbname('rbac') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('users', ns.copy()) + return { + "widgettype":"Message", + "options":{ + "cwidth":16, + "cheight":9, + "title":"Add Success", + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Add Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/users/delete_users.dspy b/wwwroot/users/delete_users.dspy new file mode 100644 index 0000000..2a4b3a6 --- /dev/null +++ b/wwwroot/users/delete_users.dspy @@ -0,0 +1,47 @@ + +ns = { + 'id':params_kw['id'], +} + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['orgid'] = userorgid + +db = DBPools() +dbname = get_module_dbname('rbac') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('users', ns) + debug('delete success'); + return { + "widgettype":"Message", + "options":{ + "title":"Delete Success", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok" + } + } + +debug('Delete failed'); +return { + "widgettype":"Error", + "options":{ + "title":"Delete Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/users/get_users.dspy b/wwwroot/users/get_users.dspy new file mode 100644 index 0000000..402650b --- /dev/null +++ b/wwwroot/users/get_users.dspy @@ -0,0 +1,170 @@ + +ns = params_kw.copy() + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['orgid'] = userorgid +ns['userorgid'] = userorgid + +debug(f'get_users.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + + ns['sort'] = 'username' + + + +sql = '''select a.*, b.user_status_text, c.sync_from_text, d.orgid_text +from (select * from users where 1=1 [[filterstr]]) a left join (select k as user_status, + v as user_status_text from appcodes_kv where parentid='user_status') b on a.user_status = b.user_status left join (select id as sync_from, + name as sync_from_text from downapp where 1 = 1) c on a.sync_from = c.sync_from left join (select id as orgid, + orgname as orgid_text from organization where 1 = 1) d on a.orgid = d.orgid''' + +filterjson = params_kw.get('data_filter') +if filterjson and isinstance(filterjson, str): + try: + filterjson = json.loads(filterjson) + except (json.JSONDecodeError, TypeError): + filterjson = None +fields_str=r'''[ + { + "name": "id", + "title": "用户id", + "type": "str", + "length": 32 + }, + { + "name": "username", + "title": "用户名", + "type": "str", + "length": 255 + }, + { + "name": "password", + "title": "密码", + "type": "str", + "length": 255 + }, + { + "name": "email", + "title": "邮件地址", + "type": "str", + "length": 255 + }, + { + "name": "orgid", + "title": "所属机构", + "type": "str", + "length": 32 + }, + { + "name": "nick_name", + "title": "显示名", + "type": "str", + "length": 255 + }, + { + "name": "address", + "title": "地址", + "type": "str", + "length": 255 + }, + { + "name": "mobile", + "title": "手机", + "type": "str", + "length": 255 + }, + { + "name": "user_status", + "title": "用户状态", + "type": "str", + "length": 1, + "default": "0" + }, + { + "name": "created_at", + "title": "注册日期", + "type": "date" + }, + { + "name": "last_login", + "title": "最后登录", + "type": "timestamp", + "nullable": true + }, + { + "name": "login_fail_count", + "title": "连续失败次数", + "type": "short", + "default": "0" + }, + { + "name": "last_login_fail", + "title": "最后登录失败时间", + "type": "timestamp", + "nullable": true + }, + { + "name": "sync_from", + "title": "同步应用id", + "type": "str", + "length": 32, + "nullable": true + } +]''' +ori_fields = json.loads(fields_str) +if not filterjson: + fields = [ f['name'] for f in ori_fields ] + filterjson = default_filterjson(fields, ns) + +# 确保 logined 过滤条件始终生效 +if filterjson: + if not isinstance(filterjson, dict) or 'AND' not in filterjson: + filterjson = {'AND': [filterjson] if filterjson else []} + + filterjson['AND'].append({'field': 'orgid', 'op': '=', 'var': '__logined_orgid__'}) + ns['__logined_orgid__'] = userorgid + + + +filterdic = ns.copy() +filterdic['filterstr'] = '' +filterdic['userorgid'] = '${userorgid}$' +filterdic['userid'] = '${userid}$' +if filterjson: + dbf = DBFilter(filterjson) + conds = dbf.gen(ns) + if conds: + ns.update(dbf.consts) + conds = f' and {conds}' + filterdic['filterstr'] = conds +ac = ArgsConvert('[[', ']]') +vars = ac.findAllVariables(sql) +NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' } +filterdic.update(NameSpace) +sql = ac.convert(sql, filterdic) + +debug(f'{sql=}') +db = DBPools() +dbname = get_module_dbname('rbac') +async with db.sqlorContext(dbname) as sor: + r = await sor.sqlPaging(sql, ns) + return r +return { + "total":0, + "rows":[] +} \ No newline at end of file diff --git a/wwwroot/users/index.ui b/wwwroot/users/index.ui new file mode 100644 index 0000000..7e337af --- /dev/null +++ b/wwwroot/users/index.ui @@ -0,0 +1,302 @@ + +{ + "widgettype":"VBox", + "options":{"cheight":40,"width":"100%"}, + "subwidgets":[{ + "id":"users_tbl", + "widgettype":"Tabular", + "options":{ + "width":"100%", + "height":"100%", + + + "title":"用户", + + + + + "toolbar":{ + "tools": [ + { + "selected_row": true, + "name": "userrole", + "icon": "{{entire_url('/imgs/userrole.svg')}}", + "label": "用户角色" + } + ] +}, + + "css":"card", + + + "editable":{ + + "new_data_url": "{{entire_url('/rbac/api/add_user.dspy')}}", + + + "delete_data_url":"{{entire_url('delete_users.dspy')}}", + + + "update_data_url": "{{entire_url('/rbac/api/update_user.dspy')}}" + + }, + + + "data_url":"{{entire_url('./get_users.dspy')}}", + + "data_method":"GET", + "data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options":{ + + + + "browserfields": { + "exclouded": [ + "id", + "password", + "orgid", + "nick_name" + ], + "cwidth": {}, + "alters": null +}, + + + "editexclouded":[ + "id", + "nick_name", + "orgid", + "last_login_fail", + "last_login", + "sync_from", + "login_fail_count", + "created_at" +], + + "fields":[ + { + "name": "id", + "title": "用户id", + "type": "str", + "length": 32, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "用户id" + }, + { + "name": "username", + "title": "用户名", + "type": "str", + "length": 255, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "用户名" + }, + { + "name": "password", + "title": "密码", + "type": "str", + "length": 255, + "cwidth": 18, + "uitype": "password", + "datatype": "str", + "label": "密码" + }, + { + "name": "email", + "title": "邮件地址", + "type": "str", + "length": 255, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "邮件地址" + }, + { + "name": "orgid", + "title": "所属机构", + "type": "str", + "length": 32, + "label": "所属机构", + "uitype": "code", + "valueField": "orgid", + "textField": "orgid_text", + "params": { + "dbname": "{{get_module_dbname('rbac')}}", + "table": "organization", + "tblvalue": "id", + "tbltext": "orgname", + "valueField": "orgid", + "textField": "orgid_text" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "nick_name", + "title": "显示名", + "type": "str", + "length": 255, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "显示名" + }, + { + "name": "address", + "title": "地址", + "type": "str", + "length": 255, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "地址" + }, + { + "name": "mobile", + "title": "手机", + "type": "str", + "length": 255, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "手机" + }, + { + "name": "user_status", + "title": "用户状态", + "type": "str", + "length": 1, + "default": "0", + "label": "用户状态", + "uitype": "code", + "valueField": "user_status", + "textField": "user_status_text", + "params": { + "dbname": "{{get_module_dbname('rbac')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "user_status", + "textField": "user_status_text", + "cond": "parentid='user_status'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "created_at", + "title": "注册日期", + "type": "date", + "length": 0, + "uitype": "date", + "datatype": "date", + "label": "注册日期" + }, + { + "name": "last_login", + "title": "最后登录", + "type": "timestamp", + "nullable": true, + "length": 0, + "uitype": "str", + "datatype": "timestamp", + "label": "最后登录" + }, + { + "name": "login_fail_count", + "title": "连续失败次数", + "type": "short", + "default": "0", + "length": 0, + "uitype": "int", + "datatype": "short", + "label": "连续失败次数" + }, + { + "name": "last_login_fail", + "title": "最后登录失败时间", + "type": "timestamp", + "nullable": true, + "length": 0, + "uitype": "str", + "datatype": "timestamp", + "label": "最后登录失败时间" + }, + { + "name": "sync_from", + "title": "同步应用id", + "type": "str", + "length": 32, + "nullable": true, + "label": "同步应用id", + "uitype": "code", + "valueField": "sync_from", + "textField": "sync_from_text", + "params": { + "dbname": "{{get_module_dbname('rbac')}}", + "table": "downapp", + "tblvalue": "id", + "tbltext": "name", + "valueField": "sync_from", + "textField": "sync_from_text" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + } +] + }, + + + + + + + + "page_rows":160, + "cache_limit":5 + } + + ,"binds":[ + { + "wid": "self", + "event": "enable_user", + "actiontype": "script", + "target": "self", + "script": "var tid=bricks.getWidgetById('users_tbl');var d=tid.select_row.user_data;fetch('/rbac/users/enable_user.dspy?id='+d.id,{method:'POST'}).then(function(r){return r.json()}).then(function(r){if(r.widgettype==='Message'){tid.render({})}else{bricks.show_error(r.options)}})" + }, + { + "wid": "self", + "event": "disable_user", + "actiontype": "script", + "target": "self", + "script": "var tid=bricks.getWidgetById('users_tbl');var d=tid.select_row.user_data;fetch('/rbac/users/disable_user.dspy?id='+d.id,{method:'POST'}).then(function(r){return r.json()}).then(function(r){if(r.widgettype==='Message'){tid.render({})}else{bricks.show_error(r.options)}})" + }, + { + "wid": "self", + "event": "userrole", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": { + "title": "用户角色", + "icon": "{{entire_url('/appbase/get_icon.dspy')}}?id=userrole", + "resizable": true, + "height": "70%", + "width": "70%" + }, + "params_mapping": { + "mapping": { + "id": "userid", + "referer_widget": "referer_widget" + }, + "need_other": false + }, + "options": { + "method": "POST", + "params": {}, + "url": "{{entire_url('../userrole')}}" + } + } +] + +}] +} \ No newline at end of file diff --git a/wwwroot/users/update_users.dspy b/wwwroot/users/update_users.dspy new file mode 100644 index 0000000..33b6936 --- /dev/null +++ b/wwwroot/users/update_users.dspy @@ -0,0 +1,73 @@ + +ns = params_kw.copy() +for k,v in ns.items(): + if v == 'NaN' or v == 'null': + ns[k] = None + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['orgid'] = userorgid + + +if params_kw.get('password'): + ns['password'] = password_encode(params_kw.get('password')) + + +db = DBPools() +dbname = get_module_dbname('rbac') +async with db.sqlorContext(dbname) as sor: + + ns1 = { + + "orgid": userorgid, + + + "id": params_kw.id + } + recs = await sor.R('users', ns1) + if len(recs) < 1: + return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"Record no exist or with wrong ownership" + } + } + + r = await sor.U('users', ns) + debug('update success'); + return { + "widgettype":"Message", + "options":{ + "title":"Update Success", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file