DashGo零基础入门 纯Python的管理系统搭建

DashGo学习笔记

一、简介

DashGo是一款完全基于Python的开发管理后台的开源项目。

二、使用方法

1.创建应用

应用,即一个新的网页页面,开发者可以设计该网页页面的所有组件及其样式。所有的应用都需要定义到项目文件夹src/dash_view/application中,通过新建一个.py文件实现app的创建。

DashGo中根据应用脚本在application中的层级来划分其属于几级标题:

  • myapp.py直接位于application文件夹中,则该应用为一级应用
python 复制代码
  # src/dash_view/application/myapp.py
  from common.utilities.util_menu_access import MenuAccess
  import feffery_antd_components as fac
  
  # 一级菜单的标题、图标和显示顺序
  title = '我的单页应用'
  icon = 'antd-rocket'
  order = 5
  
  # 权限定义
  access_metas = (
      '我的单页应用-页面',
      '我的单页应用-功能1',
  )
  
  # 渲染页面内容
  def render_content(menu_access: MenuAccess, **kwargs):
      return fac.AntdCenter(
          fac.AntdSpace([
              fac.AntdTitle('这是一个一级菜单应用!', level=2),
              fac.AntdText('点击菜单直接进入,无需展开子菜单'),
          ], direction='vertical'),
          style={'marginTop': '100px'}
      )
  • 若想要该应用位于某个二级菜单下,例如在管理(一级菜单)下有两个二级应用用户管理接口管理。则需要在application文件夹下添加一个文件夹(命名规则暂时不确定),并创建3个py脚本,分别是__init__.pyuserManager.pyinterfaceManager.py,其中

    • __init_.py定义一级菜单
python 复制代码
    # 一级菜单的标题、图标和显示顺序
    title = '管理'
    icon = 'antd-audit'
    # order = 9999即将该一级菜单的顺序为9999,一般情况下就是最后一个
    order = 9999
  • userManager.pyinterfaceManager中则定义二级菜单(即该应用)的页面,例如:
python 复制代码
# 二级菜单的标题、图标和显示顺序
title = '我的应用1'
icon = None
order = 2
logger = Log.get_logger(__name__)

# 定义该应用中的权限组件
def access_metas():
    return (
        '应用1-基础权限',
        '应用1-权限1',
        '应用1-权限2',
    )


def render_content(menu_access: MenuAccess, **kwargs):
    return fac.AntdFlex(
        [
            *(
                [
                    Card(
                        fac.AntdStatistic(
                            title='展示',
                            value=fuc.FefferyCountUp(end=100, duration=3),
                        ),
                        title='应用1-权限1',
                    )
                ]
                # 如果登录用户拥有该权限 则显示
                if menu_access.has_access('应用1-权限1')
                # 否则该组件不显示
                else []
            ),
            *(
                [
                    Card(
                        fac.AntdStatistic(
                            title='展示',
                            value=fuc.FefferyCountUp(end=200, duration=3),
                        ),
                        title='应用1-权限2',
                    )
                ]
                # 同上 进行权限验证,拥有权限则显示,否则不显示
                if menu_access.has_access('应用1-权限2')
                else []
            ),
        ],
        wrap='wrap',
    )

2.应用注册

创建应用后还需要注册应用。通过在src/config/access_factory.py中将需要注册的应用的添加到apps变量中,例如:

python 复制代码
apps = [subapp2, subapp1]

注册应用后,是不会将该应用的访问权限授予任何人的,包括系统管理员自己,如果想要:

1.将该应用权限设置为某种角色默认拥有的

可以在access_factory.py中的角色默认权限中配置:

python 复制代码
# 基础默认权限,主页和个人中心,每人都有,无需分配
default_access_meta = (
    '个人信息-页面',
    '工作台-页面',
    '监控页-页面',
)

# 团队管理员默认权限
group_access_meta = ('团队授权-页面',)

# 系统管理员默认权限
admin_access_meta = (
    '用户管理-页面',
    '角色管理-页面',
    '团队管理-页面',
    '公告管理-页面',
    '任务管理-页面',
    '任务日志-页面',
    '通知接口-页面',
    '监听接口-页面',
)

# 内置可以分配的权限
assignable_access_meta = (
    '任务管理-页面',
    '任务日志-页面',
)

2.将该应用权限设置为分配后才可以拥有的

在这种情况下,不需要做任何操作,只需要保证应用创建和注册无误,此后在系统管理员的权限管理部分可以分配这部分权限。

但是我们发现似乎在上面提到的默认分配权限中有一个变量assignable_access_meta中的权限也是可以分配的。这个变量的意义是,在管理员的权限分配树中会默认过滤掉所有自动分配的权限

python 复制代码
if access_meta in (default_access_meta, admin_access_meta, group_access_meta):
 continue  # 不显示在权限树中

因此如果希望将某些自动分配的权限也可以通过手动分配给其他用户,就需要将其保存在该变量中

3.应用界面绘制

官方文档:feffery-antd-components在线文档

AntdSpace布局

作用 :用于解决基础的多个组件水平方向或竖直方向上单一的等间距 排列需求
示例代码

python 复制代码
fac.AntdSpace(
    [
        html.Div(
            style={
                'backgroundColor': 'rgba(64, 173, 255, 1)',
                'height': '50px',
                'width': '50px',
            }
        )
        for i in range(3)
    ],
    # 此处使用`*3`与上面的`for i in range(3`等价
    # 设置组件内部子组件的间隔大小size (默认值:'small',可选项有:'small', 'middle', 'large' | number )
    size = 'small',
    # 设置组件内部对齐方式(可选项有'start'、'end'、'center'、'baseline')
    align='start',
    # 设置组件内部排列方向(默认值:'horizontal',可选项:'vertical'、'horizontal')
    direction = 'vertical',
    # 子元素之间是否添加默认分割线(默认值:False)
    # 也可通过 customSplit 属性使用自定义分割线,参考https://fac.feffery.tech/AntdSpace#demo-container-align
    addSplitLine = True,
    # 设置默认换行(仅direction='horizontal'时有效,默认值:False)
    warp = 'False'    
)
AntdFlex布局

对于Flex布局,主轴是 Flex 容器中元素的排列方向交叉轴是与主轴垂直的方向。

python 复制代码
fac.AntdFlex(
    [
        html.Div(
            style={
                'width': '25%',
                'height': 48,
                'background': '#1677ff'
                if i % 2 == 0
                else '#1677ffbf',
            }
        )
        for i in range(4)
    ],
    # 组件内部元素是否以垂直方向为主轴(若为True,则内部元素垂直排布)
    vertical=True,
    # 组件内部元素在主轴方向上的对齐方式(默认为`normal`)
    justify = "space-around" ,
    # 组件内部元素在交叉轴方向上的对齐方式(默认为`normal`)
    align = "center",
    # 组件内部元素之间的间距 (string | number | 可选项有:'small', 'middle', 'large')
    gap = 10,
)
4.应用组件事件

DashGo 采用了视图层和逻辑层分离的设计模式:

层级 目录 文件命名 作用
视图层 src/dash_view/application/ xxx.py 定义 UI 组件、布局
逻辑层 src/dash_callback/application/ xxx_c.py 定义回调函数、事件处理

而对于逻辑层回调函数的编写,回调函数有三种机制:

  • Input:监听指定ID组件的属性值,若属性值变化则触发回调函数
  • OutPut:更新指定ID组件的属性值,不触发回调函数
  • Status:读取指定ID组件的属性值,不触发回调函数
python 复制代码
"""
示例回调函数
该函数的作用是监听一个ID为:counter-button的按钮组件,按钮组件中有一个由DashGo自动维护的nClicks属性,记录着该按钮被点击的次数。当按钮被点击后,nClicks的值会自动加1,此时回调函数将被调用,回调函数返回了一个字符串给Output,而Output绑定的是一个ID为counter-text的文本组件,文本组件的文本值存储于children中,因此当点击后页面中该组件的文本值会发生变化。
"""
@app.callback(
    Output('counter-text', 'children'),  # 更新文本内容
    Input('counter-button', 'nClicks'),  # 监听按钮点击
    prevent_initial_call=True,
)
def update_counter(n_clicks):
    """更新计数器"""
    return f'点击次数: {n_clicks}'
5.角色创建管理

在DashGo中默认为三种角色身份:普通用户、团队管理员、系统管理员。这三种角色拥有自己的默认权限,但是在实际业务中可能不知这三种角色,如果我们需要创建新的角色,可以按照下面的步骤进行:

  1. 定义新角色的自动分配权限

    src/config/access_factory.py中创建新的自动分配权限

    python 复制代码
    # 【新增】编辑者默认权限
    editor_access_meta = (
        '公告管理-页面',
        '任务管理-页面',
    )

    需要注意的是:

    • 权限名称必须在某个应用的 access_metas() 中定义
    • 如果权限不存在,系统启动时会报错
  2. 更新权限检查逻辑

    修改 src/config/access_factory.pycheck_access_meta() 方法

    python 复制代码
    outliers = (
        set(cls.default_access_meta) 
        | set(cls.group_access_meta) 
        | set(cls.admin_access_meta)
        | set(cls.editor_access_meta)      # 【新增】
    ) - set(cls.get_dict_access_meta2menu_item().keys())
  3. 实现权限分配逻辑

    编辑 src/common/utilities/util_menu_access.pyget_extra_access_meta() 方法,使用户拥有某个角色时,自动赋予该角色的默认权限。

    python 复制代码
    @staticmethod
    def get_extra_access_meta(user_roles) -> List[str]:
        from config.access_factory import AccessFactory
    
        extra_access_metas = []
        
        # admin角色添加默认权限
        if 'admin' in user_roles:
            extra_access_metas.extend(AccessFactory.admin_access_meta)
        
        # 【新增】editor角色添加默认权限
        if 'editor' in user_roles:
            extra_access_metas.extend(AccessFactory.editor_access_meta)
        return extra_access_metas
  4. 更新权限树显示逻辑

    python 复制代码
    if (
        access_meta in (
            *AccessFactory.default_access_meta, 
            *AccessFactory.admin_access_meta, 
            *AccessFactory.group_access_meta,
            *AccessFactory.editor_access_meta #【新增】
        )
        and access_meta not in AccessFactory.assignable_access_meta
    ):
        continue  # 不显示在权限树中

    该操作使得自动分配的这些属性不会出现在权限树中,则相关权限只能被指定角色用户所有,如果希望这些权限也能分配给其他角色的用户,需要将相关权限添加至assignable_access_meta中。

  5. 在前端创建角色

    • 登录系统(使用 admin 账号)

    • 进入 权限管理角色管理

    • 点击 新建角色

    • 填写信息:

      • 角色名称editor(必须与代码中定义的一致)

      • 角色状态:启用

      • 角色描述:编辑者角色

      • 菜单权限:勾选需要的额外权限(默认权限会自动拥有)

    1. 点击 确定
6.数据库配置
切换数据库

DashGo默认使用的是sqlite数据库,一般我们使用MySQL,为此我们需要修改相关配置:

src/config/dashgo.ini修改数据库配置

ini 复制代码
[SqlDbConf]
RDB_TYPE = mysql          # 改为 mysql
# sqlite
SQLITE_DB_PATH = ../app.db
# mysql
HOST = 127.0.0.1          # MySQL 服务器地址
PORT = 3306               # MySQL 端口
USER = root               # MySQL 用户名
PASSWORD = 你的MySQL密码   # 改为你的实际密码
DATABASE = 你的数据库名     # 数据库名称
POOL_SIZE = 5             # 连接池大小

使用系统演示初始化脚本

bash 复制代码
# 安装依赖
pip install pymysql

# 登录数据库
mysql -u root -p

# 创建数据库
-- 创建数据库(使用 UTF-8 编码)
CREATE DATABASE dashgo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 查看数据库是否创建成功
SHOW DATABASES;

-- 退出 MySQL
EXIT;

# 切换到 src 目录
cd src

# 创建数据库表
python -c "from database.sql_db.conn import create_rds_table; create_rds_table()"

# 初始化管理员账号
python -c "from database.sql_db.conn import init_rds_data; init_rds_data()"

# 启动应用
python app.py
新增数据库表

DashGo采用ORM模型,数据库表由 Python 代码定义,而不是直接在数据库中创建,新增数据库表的步骤如下:

1.定义数据库模型(Entity)

src/database/sql_db/entity/ 目录下创建新文件,如table_product.py.

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


class BaseModel(Model):
    """基础模型类,所有表都继承这个类"""
    class Meta:
        database = db()


class Product(BaseModel):
    """产品表"""
    product_id = CharField(primary_key=True, max_length=32, help_text='产品ID')
    product_name = CharField(max_length=128, help_text='产品名称')
    product_price = DecimalField(max_digits=10, decimal_places=2, help_text='产品价格')
    product_stock = IntegerField(help_text='库存数量')
    product_status = BooleanField(help_text='产品状态(0:下架,1:上架)')
    create_datetime = DateTimeField(help_text='创建时间')
    create_by = CharField(max_length=32, help_text='创建人')
    update_datetime = DateTimeField(help_text='更新时间')
    update_by = CharField(max_length=32, help_text='更新人')
    product_remark = CharField(max_length=255, help_text='产品描述')

    class Meta:
        table_name = 'product'  # 数据库中的表名
        indexes = (
            (('product_name',), False),  # 为产品名称创建索引
        )

2.在 create_rds_table() 中注册表

编辑 src/database/sql_db/conn.py,在 create_rds_table() 函数中添加你的表。

python 复制代码
def create_rds_table():
    db_instance = db()
    from .entity.table_user import SysUser, SysRoleAccessMeta, SysUserRole, SysGroupUser, SysRole, SysGroupRole, SysGroup
    from .entity.table_announcement import SysAnnouncement
    from .entity.table_oauth2 import OAuth2Client, OAuth2AuthorizationCode, OAuth2Token
    from .entity.table_apscheduler import SysApschedulerResults, SysApschedulerExtractValue, SysApschedulerRunning
    from .entity.table_notify_api import SysNotifyApi
    from .entity.table_listen_api import SysListenApi
    from .entity.table_listen_task import ApschedulerJobsActiveListen
    # ← 在这里导入你的表
    from .entity.table_product import Product

    db_instance.create_tables(
        [
            SysUser,
            SysRoleAccessMeta,
            SysUserRole,
            SysGroupUser,
            SysRole,
            SysGroupRole,
            SysGroup,
            SysAnnouncement,
            OAuth2Client,
            OAuth2AuthorizationCode,
            OAuth2Token,
            SysApschedulerResults,
            SysApschedulerExtractValue,
            SysApschedulerRunning,
            SysNotifyApi,
            SysListenApi,
            ApschedulerJobsActiveListen,
            Product,  # ← 在这里添加你的表
        ],
        safe=True,  # safe=True 表示如果表已存在则不创建
    )

3.创建数据访问层(DAO)

src/database/sql_db/dao/ 目录下创建 DAO 文件,用于操作数据库。

python 复制代码
from database.sql_db.conn import db
from typing import List, Optional
from dataclasses import dataclass
from datetime import datetime
from peewee import DoesNotExist
from ..entity.table_product import Product


@dataclass
class ProductInfo:
    """产品信息数据类"""
    product_id: str
    product_name: str
    product_price: float
    product_stock: int
    product_status: bool
    create_datetime: datetime
    create_by: str
    update_datetime: datetime
    update_by: str
    product_remark: str


def get_all_products(exclude_disabled=True) -> List[ProductInfo]:
    """获取所有产品"""
    query = Product.select()
    if exclude_disabled:
        query = query.where(Product.product_status == True)
    
    return [
        ProductInfo(
            product_id=p.product_id,
            product_name=p.product_name,
            product_price=float(p.product_price),
            product_stock=p.product_stock,
            product_status=p.product_status,
            create_datetime=p.create_datetime,
            create_by=p.create_by,
            update_datetime=p.update_datetime,
            update_by=p.update_by,
            product_remark=p.product_remark,
        )
        for p in query
    ]


def get_product_by_id(product_id: str) -> Optional[ProductInfo]:
    """根据ID获取产品"""
    try:
        p = Product.get(Product.product_id == product_id)
        return ProductInfo(
            product_id=p.product_id,
            product_name=p.product_name,
            product_price=float(p.product_price),
            product_stock=p.product_stock,
            product_status=p.product_status,
            create_datetime=p.create_datetime,
            create_by=p.create_by,
            update_datetime=p.update_datetime,
            update_by=p.update_by,
            product_remark=p.product_remark,
        )
    except DoesNotExist:
        return None


def create_product(product_id: str, product_name: str, product_price: float, 
                   product_stock: int, create_by: str, product_remark: str = '') -> bool:
    """创建产品"""
    try:
        Product.create(
            product_id=product_id,
            product_name=product_name,
            product_price=product_price,
            product_stock=product_stock,
            product_status=True,
            create_datetime=datetime.now(),
            create_by=create_by,
            update_datetime=datetime.now(),
            update_by=create_by,
            product_remark=product_remark,
        )
        return True
    except Exception as e:
        print(f'创建产品失败: {e}')
        return False


def update_product(product_id: str, product_name: str = None, product_price: float = None,
                   product_stock: int = None, update_by: str = None, product_remark: str = None) -> bool:
    """更新产品"""
    try:
        product = Product.get(Product.product_id == product_id)
        if product_name is not None:
            product.product_name = product_name
        if product_price is not None:
            product.product_price = product_price
        if product_stock is not None:
            product.product_stock = product_stock
        if product_remark is not None:
            product.product_remark = product_remark
        if update_by is not None:
            product.update_by = update_by
        product.update_datetime = datetime.now()
        product.save()
        return True
    except DoesNotExist:
        return False


def delete_product(product_id: str) -> bool:
    """删除产品(软删除,设置状态为禁用)"""
    try:
        product = Product.get(Product.product_id == product_id)
        product.product_status = False
        product.update_datetime = datetime.now()
        product.save()
        return True
    except DoesNotExist:
        return False

4.执行数据库迁移

python 复制代码
cd src
python -c "from database.sql_db.conn import create_rds_table; create_rds_table()"

三、示例项目

Gitcode地址:DashGo学习演示项目

相关推荐
Andy3 小时前
Python基础语法4
开发语言·python
但要及时清醒3 小时前
ArrayList和LinkedList
java·开发语言
孚亭3 小时前
Swift添加字体到项目中
开发语言·ios·swift
hweiyu003 小时前
Go、DevOps运维开发实战(视频教程)
开发语言·golang·运维开发
mm-q29152227293 小时前
Python+Requests零基础系统掌握接口自动化测试
开发语言·python
星星火柴9364 小时前
笔记 | C++面向对象高级开发
开发语言·c++·笔记·学习
码界奇点4 小时前
Rust 性能优化全流程从 flamegraph 定位瓶颈到 unsafe 与 SIMD 加速响应快
开发语言·性能优化·rust·simulated annealing
电院工程师4 小时前
SIMON64/128算法Verilog流水线实现(附Python实现)
python·嵌入式硬件·算法·密码学