DashGO框架开发应用的笔记-1

一、概述

去年11月份用DashGo框架开发应用,在开发过程中陆续完成的一些笔记工作,将陆续分享。DashGo 是一个基于 Plotly Dash 框架和 Fac 开源组件库开发的低代码 WEB 框架,以下详细阐述其核心原理:

1. 整体架构原理

DashGo 采用模块化架构,将不同功能拆分成独立的模块,包括静态资源管理、共享功能库、配置管理、回调处理、视图展示、数据库操作和国际化支持等。这种架构使得代码结构清晰,易于维护和扩展。

  • 静态资源管理 :通过 assets 目录管理图片、js 文件等静态资源,为前端页面提供必要的资源支持。

  • 共享功能库common 目录下的 utilities 子目录包含各种 Python 工具类,实现了权限校验、日志记录、配置读取等通用功能,提高了代码的复用性。

  • 配置管理config 目录负责管理项目的各种配置,通过 dashgo_conf.py 和 dashgo.ini 文件,将配置信息与代码分离,方便不同环境下的配置调整。

  • 回调处理dash_callback 目录包含 Dash 回调库,处理用户交互事件和数据更新,实现页面的动态交互效果。

  • 视图展示dash_view 目录负责页面的视图展示,将不同的应用视图和框架视图分离,方便开发和维护。

  • 数据库操作database 目录下的 sql_db 子目录负责关系型数据库的配置和操作,通过 ORM 抽象实现数据库表的增删改查操作。

  • 国际化支持translations 目录提供国际化支持,通过 JSON 文件存储不同语言的翻译内容,实现多语言切换。

2. 权限管理原理

DashGo 实现了组件和函数级别的细粒度权限管理,通过用户、角色和团队的权限体系来控制用户对系统资源的访问。

  • 权限对象获取 :通过 common.utilities.util_menu_access 模块中的 get_menu_access 函数,根据 cookie 中的 JWT 用户名字段查询数据库,获取用户的权限对象。
复制代码
from common.utilities.util_menu_access import get_menu_access
menu_access = get_menu_access()
menu_access.has_access('xxx')
  • 权限校验 :在 render_content 函数中,通过 menu_access 对象进行权限校验,确保用户具有访问特定页面的权限。
复制代码
if url_menu_item not in menu_access.menu_items:
    return page_403.render_content()

3. 多页面管理原理

DashGo 支持多页面管理和多级菜单嵌套,通过动态生成菜单项和路由跳转实现多页面的切换和展示。

  • 菜单生成 :根据代码视图结构自动生成菜单项,无需单独维护菜单。在 render_aside_content 函数中,通过 MenuAccess 对象获取菜单信息,并使用 fac.AntdMenu 组件生成菜单。
复制代码
def render_aside_content(menu_access: MenuAccess):
    return fuc.FefferyDiv(
        [
            # logo 和 app名
            ...
            # 目录
            fac.AntdRow(
                fac.AntdMenu(
                    id='main-menu',
                    menuItems=menu_access.menu,
                    mode='inline',
                    theme='dark',
                    onlyExpandCurrentSubMenu=True,
                    expandIcon={
                        'expand': fac.AntdIcon(icon='antd-right'),
                        'collapse': fac.AntdIcon(icon='antd-caret-down'),
                    },
                )
            ),
        ],
        ...
    )
  • 路由跳转 :通过 dcc.Location 组件监听 URL 变化,根据 URL 中的菜单项和查询参数,动态加载相应的页面内容。
复制代码
def render_content(menu_access: MenuAccess, href: str):
    _, url_menu_item, url_query, _, param = parse_url(href=href)
    try:
        module_page = importlib.import_module(f'dash_view.application.{url_menu_item}')
    except Exception:
        return page_404.render_content()
    if url_menu_item not in menu_access.menu_items:
        return page_403.render_content()
    if is_independent(url_query):
        return module_page.render_content(menu_access, **param)
    ...

4. 国际化原理

DashGo 提供了 i18n 国际化组件,支持多语言切换。通过在 translations 目录下创建 JSON 文件,存储不同语言的翻译内容,并在代码中使用 translator.t 函数进行翻译。

  • 创建翻译文件 :在 translations\topic_locales 中新建 JSON 文件,内容格式参考已存在的文件,推荐一级目录的名字来新建文件,下级的应用共用一个国际化 JSON 文件,JSON 中的 topic 字段为主题字段,也推荐和一级目录的名字保持一致。

  • 添加翻译函数 :在 i18n.py 中添加翻译函数,将主题字段作为参数传递给 translator.t 函数。

复制代码
from functools import partial
from i18n import translator

t__xxxx = partial(translator.t, locale_topic='xxxx')
  • 使用翻译函数 :在需要翻译的视图或者回调函数中,使用 t__xxxx 函数替换字符串,即可完成国际化。
复制代码
from i18n import t__xxxx
t__xxxx('Chrome内核版本号太低,请升级浏览器')

5. 任务模块原理

DashGo 的任务模块支持周期任务、定时任务和监听任务,通过 ApScheduler 库实现任务的调度和执行。

  • 任务配置:在 dashgo.ini 文件中配置任务相关参数,如任务数据过期时间、调度器的主机和端口等。

ini

复制代码
[ApSchedulerConf]
DATA_EXPIRE_DAY = 90
HOST = 127.0.0.1
PORT = 8091
  • 任务调度 :在代码中使用 ApScheduler 库创建任务调度器,并根据配置信息添加任务。
复制代码
from apscheduler.schedulers.background import BackgroundScheduler
​
scheduler = BackgroundScheduler()
scheduler.add_job(func=task_function, trigger='interval', minutes=10)  # 周期任务
scheduler.add_job(func=task_function, trigger='cron', hour=2)  # 定时任务
scheduler.start()
  • 监听任务 :监听任务暂时只支持邮件 POP3 协议触发,通过 dao_listen 模块获取监听 API 信息,并在回调函数中处理监听任务。
复制代码
def get_tabs_items():
    items = []
    listen_apis = dao_listen.get_listen_api_by_name(api_name=None)
    for listen_api in listen_apis:
        api_type = listen_api.api_type
        if api_type not in dao_listen.support_api_types:
            raise Exception(f'不支持{api_type}类型的消息监听')
        ...

6. 登录认证原理

DashGo 支持密码和 OTP 登录,同时支持 OAuth2 provider。通过 JWT 实现用户的身份认证和授权。

  • 密码登录:用户输入用户名和密码,系统验证用户名和密码的正确性,验证通过后生成 JWT 并返回给客户端。

  • OTP 登录:用户输入动态码,系统验证动态码的正确性,验证通过后生成 JWT 并返回给客户端。

  • OAuth2 认证:通过 OAuth2 provider 实现第三方登录,用户授权后,系统获取用户信息并生成 JWT 。

  • JWT 验证 :在每个受保护的端点中,使用 require_oauth 装饰器验证 JWT 的有效性,确保用户具有访问权限。

python 复制代码
from flask import request
from flask_oauthlib.provider import require_oauth
​
@server.route('/api/userinfo')
@require_oauth('userinfo')
def userinfo():
    token = current_token()
    user_name = jwt_decode(token.token)['user_name']
    if user_name != token.user_name:
        abort(HttpStatusConstant.ERROR)
    ...

二、前后端工作机制

1. 前端页面渲染与交互基础

DashGo 使用 Plotly Dash 框架,结合 Feffery 系列组件库(如 feffery_antd_componentsfeffery_utils_components)进行前端页面的构建。前端页面主要由多个视图组件构成,通过 Dash 的回调机制实现交互。

1.1 前端页面布局

在 src/app.py 中定义了全局的应用布局,包含了全局功能组件、消息提示容器、通知信息容器、URL 监听和控制组件等。

python 复制代码
app.layout = lambda: fuc.FefferyTopProgress(
    [
        fuc.FefferySetFavicon(favicon='/assets/logo.ico'),
        fuc.FefferyLocation(id='global-url-location'),
        dcc.Location(id='global-dcc-url', refresh=False),
        fac.Fragment(id='global-message-container'),
        fac.Fragment(id='global-notification-container'),
        fuc.FefferyExecuteJs(id='global-execute-js-output'),
        fuc.FefferyReload(id='global-reload'),
        dcc.Store(id='global-url-init-load'),
        html.Div(id='root-container'),
    ],
    listenPropsMode='include',
    includeProps=['root-container.children'],
    minimum=0.33,
    color='#1677ff',
)
1.2 前端路由处理

前端路由通过 dcc.Locationfuc.FefferyLocation 组件监听 URL 变化,触发回调函数进行页面切换和渲染。例如,在 src/app.py 中定义了根路由回调函数 root_router

python 复制代码
@app.callback(
    [
        Output('root-container', 'children'),
        Output('global-dcc-url', 'pathname'),
        Output('global-dcc-url', 'search'),
    ],
    Input('global-url-init-load', 'data'),
    prevent_initial_call=True,
    on_error=handle_root_router_error,
)
def root_router(href):
    parsed_url = URL(href)
    rt_access = util_authorization.auth_validate(verify_exp=True)
    if isinstance(rt_access, util_jwt.AccessFailType):
        return (
            login.render_content(),
            '/login',
            URL.build(query={'to': to_path_qs}).__str__() if to_path_qs else dash.no_update,
        )
    else:
        menu_access = MenuAccess(rt_access['user_name'])
        return (
            main.render_content(
                menu_access=menu_access,
                href=href,
            ),
            dash.no_update,
            dash.no_update,
        )
2. 前端请求触发机制

前端请求主要通过用户交互(如点击按钮、切换标签页等)触发,这些交互事件会触发 Dash 的回调函数,进而向后端发送请求。

2.1 按钮点击事件

例如,在 src/dash_callback/pages/main_c.py 中,定义了折叠侧边栏按钮的回调函数:

python 复制代码
app.clientside_callback(
    """(nClicks, collapsed) => {
        if (collapsed){
            return [!collapsed, 'antd-menu-fold',{'display':'block'}];
        }else{
            return [!collapsed, 'antd-menu-unfold',{'display':'None'}];
        }
    }""",
    [
        Output('menu-collapse-sider', 'collapsed', allow_duplicate=True),
        Output('btn-menu-collapse-sider-menu-icon', 'icon'),
        Output('logo-text', 'style'),
    ],
    Input('btn-menu-collapse-sider-menu', 'nClicks'),
    State('menu-collapse-sider', 'collapsed'),
    prevent_initial_call=True,
)
2.2 URL 变化事件

URL 变化会触发地址栏相关的回调函数,如 src/dash_callback/pages/main_c.py 中的地址栏更新回调:

python 复制代码
app.clientside_callback(
    """
        (href,activeKey_tab,has_open_tab_keys,opened_tab_pathname_infos,collapsed) => {
            if (has_open_tab_keys === undefined){
                has_open_tab_keys = [];
            }
            const urlObj = new URL(href);
            pathname = urlObj.pathname;
            if (has_open_tab_keys.includes(pathname)){
                if (collapsed){
                    return [window.dash_clientside.no_update, window.dash_clientside.no_update, opened_tab_pathname_infos[pathname][1], opened_tab_pathname_infos[pathname][2],pathname,opened_tab_pathname_infos[pathname][3]];
                }else{
                    return [window.dash_clientside.no_update, [opened_tab_pathname_infos[pathname][0]], opened_tab_pathname_infos[pathname][1], opened_tab_pathname_infos[pathname][2],pathname,opened_tab_pathname_infos[pathname][3]];
                }
            }else{
                return [href, window.dash_clientside.no_update, window.dash_clientside.no_update, window.dash_clientside.no_update, window.dash_clientside.no_update, window.dash_clientside.no_update];
            }
        }
    """,
    [
        Output('main-url-relay', 'data', allow_duplicate=True),
        Output('main-menu', 'openKeys', allow_duplicate=True),
        Output('main-menu', 'currentKey', allow_duplicate=True),
        Output('main-header-breadcrumb', 'items', allow_duplicate=True),
        Output('tabs-container', 'activeKey', allow_duplicate=True),
        Output('main-dcc-url', 'search', allow_duplicate=True),
    ],
    Input('main-url-location', 'href'),
    [
        State('tabs-container', 'activeKey'),
        State('tabs-container', 'itemKeys'),
        State('main-opened-tab-pathname-infos', 'data'),
        State('menu-collapse-sider', 'collapsed'),
    ],
    prevent_initial_call=True,
)

3. 后端请求处理机制

DashGo 的后端基于 Flask 框架,通过 Flask 的路由装饰器定义不同的接口,处理前端发送的请求。

3.1 接口定义

在 src/server.py 中定义了多个接口,如头像获取接口、任务日志 SSE 接口、OAuth2 授权和令牌发放接口等。

python 复制代码
# 头像获取接口
@server.route('/avatar/<user_name>')
def download_file(user_name):
    file_name = f'{user_name}.jpg'
    if '..' in user_name:
        logger.warning(f'有人尝试通过头像文件接口攻击,URL:{request.url},IP:{request.remote_addr}')
        abort(HttpStatusConstant.FORBIDDEN)
    else:
        return send_from_directory(PathProj.AVATAR_DIR_PATH, file_name)
​
# 任务日志 SSE 接口
@server.route('/task_log_sse', methods=['POST'])
def task_log_sse():
    menu_access = get_menu_access()
    if not menu_access.has_access('任务日志-页面'):
        response = jsonify({'error': 'Task Log SSE Permission Rejected.'})
        response.status_code = HttpStatusConstant.FORBIDDEN
        return response
​
    job_id = unquote(request.headers.get('job-id'))
    start_datetime = request.headers.get('start-datetime')
    start_datetime = datetime.strptime(start_datetime, '%Y-%m-%dT%H:%M:%S.%f')
​
    def _stream():
        total_log = None
        order = 0
        while True:
            time.sleep(1)
            total_log = get_done_log(job_id=job_id, start_datetime=start_datetime)
            if total_log is not None:
                break
            else:
                order_log = get_running_log(job_id=job_id, start_datetime=start_datetime, order=order)
                if order_log is not None:
                    yield 'data: <执行中>{}\n\n'.format(json.dumps({'context': order_log}))
                    order += 1
                else:
                    yield 'data: <无更新>{}\n\n'.format(json.dumps({'context': ''}))
        yield 'data: <响应结束>{}\n\n'.format(json.dumps({'context': total_log}))
​
    return Response(_stream(), mimetype='text/event-stream')
3.2 权限验证

在处理请求时,会进行权限验证,确保用户具有访问相应资源的权限。例如,在任务日志 SSE 接口中,会检查用户是否具有访问任务日志页面的权限:

python 复制代码
menu_access = get_menu_access()
if not menu_access.has_access('任务日志-页面'):
    response = jsonify({'error': 'Task Log SSE Permission Rejected.'})
    response.status_code = HttpStatusConstant.FORBIDDEN
    return response

4. 数据交互与响应

前端通过 AJAX 请求向后端发送数据,后端处理请求后返回相应的数据或页面内容。

4.1 前端发送请求

前端在触发回调函数时,会将相关的数据作为参数传递给后端接口。例如,在 src/dash_callback/application/setting_/notify_api_c.py 中,保存消息通知 API 配置时,会将表单数据发送给后端。

4.2 后端响应请求

后端接口处理请求后,返回相应的数据或页面内容。例如,在头像获取接口中,返回用户的头像文件;在任务日志 SSE 接口中,通过服务器发送事件(SSE)的方式实时返回任务日志信息。

三、权限设置

权限元定义与管理

  • 权限元配置 :在 Plotly - DashGo/src/config/access_factory.py 文件中,定义了应用的各种权限元。例如,default_access_meta 定义了基础默认权限,包括主页和个人中心;group_access_meta 定义了团队管理员默认权限;admin_access_meta 定义了系统管理员默认权限;assignable_access_meta 定义了内置可以分配的权限。
python 复制代码
# 基础默认权限,主页和个人中心,每人都有,无需分配
default_access_meta = (
    '个人信息-页面',
    '工作台-页面',
    '监控页-页面',
)
​
# 团队管理员默认权限
group_access_meta = ('团队授权-页面',)
​
# 系统管理员默认权限
admin_access_meta = (
    '用户管理-页面',
    '角色管理-页面',
    '团队管理-页面',
    '公告管理-页面',
    '任务管理-页面',
    '任务日志-页面',
    '通知接口-页面',
    '监听接口-页面',
)
​
# 内置可以分配的权限
assignable_access_meta = (
    '任务管理-页面',
    '任务日志-页面',
)
复制代码
  • 权限元映射AccessFactory 类中的 get_dict_access_meta2menu_item 方法将权限元与模块路径进行映射,方便后续的权限校验和菜单生成。
python 复制代码
@classmethod
@cache_dict_access_meta2menu_item.memoize(ttl=10, typed=True)
def get_dict_access_meta2menu_item(cls):
    dict_access_meta2module_path = {
        access_meta: view.__name__ for view in cls.views for access_meta in (view.access_metas() if callable(view.access_metas) else view.access_metas)
    }
    return {access_meta: module_path.replace('dash_view.application.', '') for access_meta, module_path in dict_access_meta2module_path.items()}

2. 用户认证

  • 认证方式 :在 Plotly - DashGo/src/common/utilities/util_authorization.py 文件中,定义了两种认证方式:BEARER(JWT 验证)和 BASIC(基本认证)。
python 复制代码
class AuthType(Enum):
    BEARER = 'Bearer'
    BASIC = 'Basic'
​
def auth_validate(verify_exp=True) -> tuple[AuthType, Union[Dict, AccessFailType]]:
    # 因为不是每个组件都能加headers,所以还是也校验cookies中的token
    auth_header = token_ if (token_ := request.headers.get('Authorization')) else request.cookies.get('Authorization')
    if not auth_header:
        return AccessFailType.NO_ACCESS
    auth_info = auth_header.split(' ', 1)
    if len(auth_info) != 2 or not auth_info[0].strip() or not auth_info[1].strip():
        abort(HttpStatusConstant.BAD_REQUEST)
    auth_type, auth_token = auth_info
    if auth_type == AuthType.BEARER.value:
        # jwt验证
        return jwt_decode_rt_type(auth_token, verify_exp=verify_exp)
    elif auth_type == AuthType.BASIC.value:
        # Basic认证
        return validate_basic(auth_token)
    abort(jsonify({'error': f'Unsupport Type {auth_type}'}), HttpStatusConstant.UNSUPPORTED_TYPE)
复制代码
  • 权限验证 :在 root_router 回调函数中,调用 util_authorization.auth_validate 函数进行权限验证,如果验证失败,则重定向到登录页面。
python 复制代码
rt_access = util_authorization.auth_validate(verify_exp=True)
if isinstance(rt_access, util_jwt.AccessFailType):
    return (
        login.render_content(),
        '/login',
        URL.build(query={'to': to_path_qs}).__str__() if to_path_qs else dash.no_update,
    )

3. 用户权限查询

  • 数据库查询 :在 Plotly - DashGo/src/database/sql_db/dao/dao_user.py 文件中,定义了 get_user_access_meta 函数,用于根据用户名查询用户的权限元。
python 复制代码
def get_user_access_meta(user_name: str, exclude_disabled=True) -> Set[str]:
    database = db()  # 假设你有一个函数 db() 返回当前的数据库连接

    if isinstance(database, MySQLDatabase):
        access_meta_agg = fn.JSON_ARRAYAGG(SysRoleAccessMeta.access_meta).alias('access_metas')
    elif isinstance(database, SqliteDatabase):
        access_meta_agg = fn.GROUP_CONCAT(SysRoleAccessMeta.access_meta, '○').alias('access_metas')
    else:
        raise NotImplementedError('Unsupported database type')

    query = (
        SysUser.select(access_meta_agg)
        .join(SysUserRole, on=(SysUser.user_name == SysUserRole.user_name))
        .join(SysRole, on=(SysUserRole.role_name == SysRole.role_name))
        .join(SysRoleAccessMeta, on=(SysRole.role_name == SysRoleAccessMeta.role_name))
        .where(SysUser.user_name == user_name)
    )

    if exclude_disabled:
        query = query.where((SysUser.user_status == Status.ENABLE) & (SysRole.role_status == Status.ENABLE))

    result = query.dicts().get()
    if isinstance(database, MySQLDatabase):
        access_metas = json.loads(result['access_metas']) if result['access_metas'] else []
    elif isinstance(database, SqliteDatabase):
        access_metas = result['access_metas'].split('○') if result['access_metas'] else []
    else:
        raise NotImplementedError('Unsupported database type')

    return set(access_metas)

4. 权限校验

  • 权限对象获取 :通过 get_menu_access 函数,根据 cookie 中的 JWT 的用户名字段,查询数据库,获取权限对象。
python 复制代码
from common.utilities.util_menu_access import get_menu_access
menu_access = get_menu_access()
  • 权限校验方法 :在 MenuAccess 类中,定义了 has_access 方法(代码中未直接给出,但从用法推测),用于校验用户是否拥有某个权限。在 render_content 函数中,可以使用该方法进行权限校验。
python 复制代码
if menu_access.has_access('应用1-权限1'):
    # 显示相应的组件
    pass

5. 菜单生成与权限关联

  • 菜单生成 :在 MenuAccess 类中,定义了 gen_menu 方法,根据用户的权限生成菜单。该方法会根据用户的权限元获取对应的菜单项,并根据 order 属性对菜单进行排序。
python 复制代码
@classmethod
def gen_menu(cls, menu_items: Set[str]):
    # 根据菜单项构建菜单层级
    def add_to_nested_dict(nested_dict, keys):
        # ...
    nested_menu = {}
    for per_menu_item in menu_items:
        menu_hierarchy = per_menu_item.split('.')
        add_to_nested_dict(nested_menu, menu_hierarchy)
    # 根据 order 属性排序嵌套字典
    def sort_nested_dict(nested_dict, parent_key=''):
        # ...
    sorted_menu = sort_nested_dict(nested_menu)
    # 生成菜单结构
    def generate_menu_structure(nested_dict, parent_path=''):
        # ...
    menu = generate_menu_structure(sorted_menu)
    return menu
  • 权限关联:菜单中的每个菜单项都与一个或多个权限元关联,只有当用户拥有相应的权限元时,才能访问该菜单项对应的页面。

6.菜单添加步骤

序号 步骤 文件名 内容
1 创建一级菜单文件夹及配置文件 Plotly-DashGo/src/dash_view/application/新菜单名/_init.py
2 创建二级菜单文件(如果需要) Plotly-DashGo/src/dash_view/application/新菜单名/子菜单名.py
3 注册权限 Plotly-DashGo/src/config/access_factory.py

三、数据库操作步骤

1. 数据库连接配置

要依据配置文件中的数据库类型(如 MySQL 或者 SQLite)来配置数据库连接,代码示例如下:

python 复制代码
from config.dashgo_conf import SqlDbConf
from playhouse.pool import PooledMySQLDatabase
from peewee import SqliteDatabase
from playhouse.shortcuts import ReconnectMixin

if SqlDbConf.RDB_TYPE == 'mysql':
    class ReconnectPooledMySQLDatabase(ReconnectMixin, PooledMySQLDatabase):
        _instance = None

        @classmethod
        def get_db_instance(cls):
            if not cls._instance:
                cls._instance = cls(
                    database=SqlDbConf.DATABASE,
                    max_connections=SqlDbConf.POOL_SIZE,
                    user=SqlDbConf.USER,
                    password=SqlDbConf.PASSWORD,
                    host=SqlDbConf.HOST,
                    port=SqlDbConf.PORT,
                    stale_timeout=300,
                )
            return cls._instance
elif SqlDbConf.RDB_TYPE == 'sqlite':
    sqlite_db = SqliteDatabase(SqlDbConf.SQLITE_DB_PATH, timeout=20)
else:
    raise NotImplementedError('Unsupported database type')

def db():
    if SqlDbConf.RDB_TYPE == 'mysql':
        return ReconnectPooledMySQLDatabase.get_db_instance()
    elif SqlDbConf.RDB_TYPE == 'sqlite':
        return sqlite_db
    else:
        raise NotImplementedError('Unsupported database type')

上述代码会依据配置文件中的数据库类型创建对应的数据库连接实例。

2. 定义数据库表实体

借助peewee库定义与数据库表对应的 Python 类,代码示例如下:

python 复制代码
from peewee import CharField, Model, IntegerField, DateTimeField, ForeignKeyField, BooleanField
from ..conn import db

class BaseModel(Model):
    class Meta:
        database = db()

class SysUser(BaseModel):
    user_name = CharField(primary_key=True, max_length=32, help_text='用户名')
    user_full_name = CharField(max_length=32, help_text='全名')
    user_status = BooleanField(help_text='用户状态(0:停用,1:启用)')
    password_sha256 = CharField(max_length=64, help_text='密码SHA256值')
    user_sex = CharField(max_length=64, help_text='性别')
    user_email = CharField(max_length=128, help_text='电子邮箱')
    phone_number = CharField(max_length=16, help_text='电话号码')
    update_by = CharField(max_length=32, help_text='被谁更新')
    update_datetime = DateTimeField(help_text='更新时间')
    create_by = CharField(max_length=32, help_text='被谁创建')
    create_datetime = DateTimeField(help_text='创建时间')
    user_remark = CharField(max_length=255, help_text='用户描述')
    otp_secret = CharField(max_length=16, help_text='OTP密钥')

    class Meta:
        table_name = 'sys_user'

此代码定义了SysUser类,它对应数据库中的sys_user表。

3. 编写数据访问对象(DAO)

python 复制代码
from database.sql_db.conn import db
from ..entity.table_user import SysUser
from peewee import DoesNotExist

def exists_user_name(user_name: str) -> bool:
    """是否存在这个用户名"""
    try:
        SysUser.get(SysUser.user_name == user_name)
        return True
    except DoesNotExist:
        return False

上述代码实现了检查用户名是否存在的功能。

4. 在业务逻辑中调用 DAO 方法

在业务逻辑代码里调用 DAO 方法进行数据库操作,代码示例如下:

python 复制代码
from database.sql_db.dao import dao_user

if dao_user.exists_user_name('test_user'):
    print('用户存在')
else:
    print('用户不存在')

此代码调用了exists_user_name方法来检查用户名是否存在。

5. 事务处理

在进行数据库操作时,要使用事务来保证数据的一致性,代码示例如下:

python 复制代码
from database.sql_db.conn import db
from ..entity.table_user import SysUser

def update_user(user_name, new_full_name):
    database = db()
    with database.atomic() as txn:
        try:
            user = SysUser.get(SysUser.user_name == user_name)
            user.user_full_name = new_full_name
            user.save()
        except DoesNotExist:
            txn.rollback()
            return False
        except Exception as e:
            txn.rollback()
            return False
        else:
            txn.commit()
            return True

上述代码使用事务来更新用户的全名。

总结

在 DashGo 框架中开发数据库操作的步骤如下:

  1. 配置数据库连接。

  2. 定义数据库表实体。

  3. 编写数据访问对象(DAO)。

  4. 在业务逻辑中调用 DAO 方法。

  5. 使用事务处理保证数据一致性。

src/server.pysrc/server.pysrc/dash_callback/pages/main_c.pysrc/app.pysrc/dash_callback/pages/main_c.pysrc/dash_callback/pages/main_c.py在 DashGo 中,前端页面与后端的交互主要基于Plotly Dash 的回调机制Flask 的路由系统,两者结合实现了前后端数据传递和逻辑交互。以下是具体机制和实现方式:

一、核心交互机制:Dash 回调(Callbacks)

Dash 的核心是声明式回调 ,通过定义输入(Input)、输出(Output)和状态(State),将前端组件的事件(如点击按钮、输入文本)与后端 Python 逻辑绑定,实现 "前端操作触发后端处理" 的交互。

1. 服务器端回调(Python 回调)

前端组件的变化会触发后端 Python 函数执行,处理逻辑后返回结果更新前端组件。 示例代码 (来自src/dash_callback/pages/main_c.py):

python 复制代码
@app.callback(
    [
        Output('tabs-container', 'items', allow_duplicate=True),
        Output('tabs-container', 'activeKey', allow_duplicate=True),
        # 其他输出...
    ],
    Input('main-url-relay', 'data'),  # 输入:URL中继存储的变化
    [
        State('tabs-container', 'itemKeys'),  # 状态:当前已打开的Tab键
        # 其他状态...
    ],
    prevent_initial_call=True,
)
def main_router(href, has_open_tab_keys: List, is_collapsed_menu: bool, trigger):
    # 后端逻辑:解析URL、权限校验、生成Tab内容
    url_pathname, url_menu_item, url_query, url_fragment, param = parse_url(href=href)
    # ... 处理逻辑 ...
    return [p_items, key_url_path, ...]  # 返回结果更新前端组件
  • 触发流程 :当main-url-relay(存储组件)的数据变化时,调用main_router函数,处理后更新tabs-container的内容和状态。

  • 适用场景:需要后端处理业务逻辑(如权限校验、数据库查询)的交互。

2. 客户端回调(Clientside Callbacks)

直接在浏览器中用 JavaScript 执行逻辑,无需与后端通信,适合纯前端交互(如 UI 状态切换),性能更高。 示例代码 (来自src/dash_callback/pages/main_c.py):

python 复制代码
app.clientside_callback(
    """(nClicks, collapsed) => {
        if (collapsed){
            return [!collapsed, 'antd-menu-fold',{'display':'block'}];
        }else{
            return [!collapsed, 'antd-menu-unfold',{'display':'None'}];
        }
    }""",
    [
        Output('menu-collapse-sider', 'collapsed'),
        Output('btn-menu-collapse-sider-menu-icon', 'icon'),
        Output('logo-text', 'style'),
    ],
    Input('btn-menu-collapse-sider-menu', 'nClicks'),  # 输入:折叠按钮点击次数
    State('menu-collapse-sider', 'collapsed'),  # 状态:当前折叠状态
    prevent_initial_call=True,
)
复制代码
  • 触发流程:点击侧边栏折叠按钮后,前端直接执行 JS 逻辑,更新侧边栏状态和图标,无需后端参与。

  • 适用场景:纯 UI 交互(如折叠菜单、切换主题)。

二、HTTP 接口调用:Flask 路由

Dash 基于 Flask 构建,因此可以直接使用 Flask 的@server.route定义 HTTP 接口,前端通过 AJAX 或 Dash 的dcc.Store等组件调用这些接口,获取 / 提交数据。

1. 后端接口定义(Flask 路由)
python 复制代码
@server.route('/oauth/token', methods=['POST'])
def issue_token():
    # 后端逻辑:验证OAuth2客户端凭证,生成令牌
    client_id = request.form.get('client_id')
    client_secret = request.form.get('client_secret')
    # ... 验证逻辑 ...
    return jsonify({'access_token': token_, 'token_type': 'bearer'})
  • 这是一个标准的 Flask 接口,处理POST请求,返回 JSON 数据(如 OAuth2 令牌)。
2. 前端调用方式
  • 通过 Dash 回调间接调用 :在服务器端回调中,直接调用后端函数(如数据库操作),无需显式 HTTP 请求。例如在main_router中调用get_menu_access()获取用户权限。

  • 通过 AJAX 直接调用

    python 复制代码
    // 前端JS代码(可嵌入Clientside Callback)
    fetch('/oauth/token', {
        method: 'POST',
        body: new FormData({client_id: 'xxx', client_secret: 'xxx'})
    }).then(response => response.json())
      .then(data => console.log('Token:', data.access_token));

三、数据传递媒介

  1. 组件属性 :Dash 组件的属性(如valuechildren)是前后端数据传递的主要载体。例如Input组件的value变化作为回调输入,后端处理后通过Output更新Divchildren

  2. 存储组件(dcc.Store :用于存储临时数据(如用户状态、全局配置),在多个回调间共享。例如src/app.py中的global-url-init-load存储初始 URL,触发根路由回调。

    python 复制代码
    dcc.Store(id='global-url-init-load', data=None)  # 存储初始URL
    复制代码
  3. HTTP 请求参数 :通过 URL 的pathnamesearch(查询参数)传递数据,例如src/dash_callback/pages/main_c.py中的parse_url函数解析 URL 参数,实现页面路由。

四、身份验证与安全

前后端交互中,通过JWT 令牌验证用户身份:

  • 登录成功后,后端生成 JWT 令牌并存储在前端(如 Cookie),前端请求时通过Authorization头传递令牌(见src/server.py_db_connect回调)。

  • 后端通过auth_validate函数验证令牌有效性,确保接口调用安全(如src/server.pyauthorize接口)。

总结

DashGo 的前后端交互机制是 "Dash 回调为核心,Flask 路由为补充"

  • 简单交互(UI 状态)用客户端回调(JS),高效无延迟;

  • 复杂逻辑(权限、数据库)用服务器端回调(Python),安全可控;

  • 跨系统交互(如第三方 API)用Flask 路由,通过 HTTP 接口通信。

这种机制既保留了 Python 全栈开发的便捷性,又兼顾了前端交互的灵活性。在 DashGo 中,前端页面与后端的交互主要基于Plotly Dash 的回调机制Flask 的路由系统,两者结合实现了前后端数据传递和逻辑交互。以下是具体机制和实现方式:

相关推荐
航Hang*2 小时前
Photoshop 图形与图像处理技术——第9章:实践训练1——绘制禁烟标志和奥运五环
图像处理·笔记·学习·ui·photoshop
saoys2 小时前
Opencv 学习笔记:图像金字塔实现上采样(pyrUp)与降采样(pyrDown)
笔记·opencv·学习
崔高杰3 小时前
【论文阅读笔记】Agent Memory相关文献追踪——异构存储和经验记忆相关
论文阅读·笔记
心枢AI研习社3 小时前
python学习笔记8--破茧与连接:Python HTTP 全球协作实战复盘
笔记·python·学习
EEG小佬3 小时前
IEEE期刊如何找到模板
笔记
不解风水3 小时前
【自动控制原理】学习笔记
笔记·学习·自动控制原理
暴风游侠3 小时前
如何进行科学的分类
笔记·算法·分类
航Hang*4 小时前
第3章:复习篇——第3节:数据查询与统计---题库
数据库·笔记·sql·学习·mysql·期末·复习
张祥6422889044 小时前
线性代数本质笔记七
笔记·线性代数