FAB 后端开发框架(Flask-AppBuilder)
本文是 CubeStudio 后端二次开发的纲领文档。它源自通用 FAB 框架介绍,部分示例为通用 FAB 概念演示;凡与本仓库实际代码有出入处,下文已就近用
文件:行号标注或改正。
简介
几分钟快速搭建前后端管理控制台,集成 OA 登录、RBAC 权限控制、定时调度、缓存、公司平台 SDK、前后端接口自动封装、用户行为记录、数据库升级管理、docker 镜像、docker-compose 调试、k8s 部署。
框架由来
现在对每位开发者全栈能力的要求越来越强烈。团队里经常会产出一些工具优化工作效率,工具共享逐步形成管理端控制台,就开始需要有前后端能力介入。此时就要求开发者具有管理控制台的开发能力,而这些控制台大部分又是 CURD 的基本操作。本项目采用 FAB(Flask-AppBuilder)框架,部署及生成前后端代码,能够快速部署自己的前后端应用。
目前基于 FAB 的开源项目有很多:airflow、superset 都是典型案例。本项目在开源 FAB 基础上改造为前后端分离模式。
环境准备
- 在本地安装 docker、docker-compose 环境。
- 你需要有一个 MySQL 数据库(开源/通用场景下创建名为
myapp的 db;本商业版 docker-compose 中默认库名为kubeflow)。
docker run -p 3306:3306 --restart always --name mysql \
-e MYSQL_ROOT_PASSWORD=admin \
-v $PWD/docker-add-file/mysqld.cnf:/etc/mysql/conf.d/mysqld.cnf \
-d mysql:8.0.32
# 进入数据库创建一个 db(通用示例)
CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
核实并修正:本商业版仓库的数据库连接由环境变量决定,
install/docker/config.py:476读取MYSQL_SERVICE(其次POSTGRESQL_SERVICE/KINGBASE_SERVICE/DM_SERVICE)。install/docker/docker-compose.yml中 MySQL 服务的MYSQL_DATABASE默认是kubeflow,而非通用示例里的myapp。同时支持 PostgreSQL / 人大金仓 / 达梦(按 URI 自动切换migrations/versions-*)。
启动
cd install/docker/
# docker-compose.yml 文件中可以修改 redis / mysql 的配置
docker-compose up # 即可启动
# 然后 http://127.0.0.1:80/ 就可以访问
说明:本商业版
install/docker/docker-compose.yml默认启用的服务为redis、mysql、frontend(nginx 前端容器)、myapp(后端容器);beat、worker、watch三个服务在文件中以注释形式提供,按需放开。后端容器默认command为sleep占位,需 exec 进容器执行/entrypoint.sh或python myapp/run.py(见install/docker/docker-compose.yml)。
用户和权限
SSO 登录
多用户认证授权的相关逻辑在 security.py 中,开源版本默认为账号密码登录。若想添加 OA 认证,可以在 MyCustomRemoteUserView 中添加对应的函数(已添加 API 访问版的 OA 认证)。用户打开页面时会自动发起 OA 认证,并自动注册用户,创建和用户同名的角色。如果你需要修改 OA 认证,可以修改 login 函数(myapp/project.py:64)。用户登录后在右上角个人页面都会有一个秘钥,这个秘钥用来在 API 访问时进行认证。
平台向其他三方平台提供 API 接入能力时,只需要其他平台在 HTTP 请求 header 中提供这个字段,就能正常识别是哪个用户,后面的授权工作就和 Web 访问相同了。
权限管理
登录后还需要进行授权,FAB 使用 RBAC 进行权限管理:用户可以绑定不同的角色,角色可以绑定不同的权限,这样用户就具有了指定的权限。
用户层
系统自带了一个 admin 用户,每个用户登录时会自动注册,同时 admin 也可以手动创建用户。框架还添加了后门链接,管理员可借此模拟其他用户登录、查看用户登录后状态。
角色层
系统自带 Admin、Gamma 角色(myapp/security.py:250 描述:Admin 拥有管理员权限,Gamma 为普通用户角色)。每个用户登录时会自动创建与用户名 username 同名的角色,并将用户绑定到该同名角色和 Gamma 角色上,所以普通用户进来会提示各种没有权限。具有 Admin 角色的用户拥有一切权限(为其他用户加角色、为角色加权限等)。系统自带只有 admin 用户具有 Admin 角色,默认账号密码为 admin/admin。
核实:新用户默认注册角色由
myapp/config.py:94的AUTH_USER_REGISTRATION_ROLE = "Gamma"决定;Admin/Gamma的绑定见myapp/security.py:871-872。
你可以手动添加各种形式的角色:项目组、组织架构、一类用户等。
权限层
权限层包含两部分:视图菜单 view/menu 和权限类型 Base Permissions。即在哪些对象上具有哪些权限(比如在 table1 上具有 add 权限,在 menu1 上具有 click 权限)。
在代码中一般 view/menu 对应 class,Base Permissions 对应 function。所以视图菜单和权限类型两者之间并不是可以随意绑定的。系统自带的视图权限可以在 Web 界面上查看,主要是某些菜单的点击权限、某些 model(table) 的增删改查权限、某些函数的执行权限。
如果想为自己的函数或 model 注册视图权限,只需要在函数上添加 has_access 修饰符:
from flask_appbuilder.security.decorators import has_access
@has_access
def list(self):
pass
这样用户就需要具有该函数的访问权限才能正常进入,否则会报权限异常。
建议基于 myapp/views/baseApi.py 中的 MyappModelRestApi 来编写接口,所有接口都默认做了权限注册。
核实并修正:原文写"建议基于 /view/base.py 中 MyappModelView 编写 api 接口",但当前仓库不存在
MyappModelView类。base.py提供的是BaseMyappView(myapp/views/base.py:272),前后端分离的接口基类是MyappModelRestApi(myapp/views/baseApi.py:243),各业务视图均继承自它(例如myapp/views/view_metadata_metric.py:191)。
添加页面视图和接口
系统自带了 model(数据库表记录)的增删改查接口和页面,所以用户需要做的就只有定义和注册 model 表结构。
本仓库为前后端分离模式:实际开发以"定义 model + 继承
MyappModelRestApi+appbuilder.add_api(...)"为主(见下方第 3 节),菜单入口在home.py的/menu中下发。下方第 1、2 节为通用 FAB 概念演示。
1、仅添加菜单链接
在 home.py 的 menu 接口中添加(详见 新增视图页面与数据库升级.md)。
2、同时添加 CURD 前端界面和后端接口(通用 FAB 概念)
需要先定义 model(表结构),再定义接口视图类,自动生成前后端代码,最后添加链接到菜单:
# 定义 model
class Model2(Model):
__tablename__ = 'model2'
id = Column(Integer, primary_key=True)
attr1 = Column(String(50), unique=True, nullable=False)
def __repr__(self):
return self.attr1
# 定义数据表视图
class Model2_ModelView(MyappModelView):
datamodel = SQLAInterface(Model2)
appbuilder.add_view(
baseview=Model2_ModelView,
name="submenu2",
icon='fa-address-book-o',
category='menu1',
category_icon='fa-envelope'
)
核实并修正:上例为服务端渲染的传统 FAB 用法(
MyappModelView+appbuilder.add_view)。当前 CubeStudio 商业版未使用appbuilder.add_view(...),也没有MyappModelView类,应改用第 3 节的前后端分离写法。此处保留以说明 FAB 原始能力。
因为涉及前端显示,可配置参数较多,下面列举一些(均可不配置,使用默认)。这些参数在前后端分离的 MyappModelRestApi 中同样适用:
base_permissions # model 管理的操作类型
add_fieldsets # 创建页面分组展示
edit_fieldsets # 编辑页面分组展示
label_columns # 列别名
list_columns # list 页面显示的列
show_fieldsets # 单条记录详情分组
base_filters # list 页面默认筛选条件
base_order # list 页面排序方法
validators_columns # add/update 表单自动校验
# 关联字段自定义查询过滤器:add_query_rel_fields / edit_query_rel_fields / search_query_rel_fields
# add_query_rel_fields = {'attr3': [['attr1', FilterStartsWith, 'a']]}
# 自定义页面模板:show_template / edit_template / add_template
def pre_add(self, obj): ... # 添加前
def post_add(self, obj): ... # 添加后
def pre_update(self, obj): ... # 更新前
def post_update(self, obj): ... # 更新后
def pre_delete(self, obj): ... # 删除前
def post_delete(self, obj): ... # 删除后
# 批量操作函数
@action("muldelete", "Delete", "Delete all Really?", "fa-rocket", single=False)
def muldelete(self, items): ...
3、添加 CURD 纯后端接口(本仓库主流写法)
先定义 model,再定义视图类,注册 API 接口:
# 定义数据表视图
class Model3_ModelView_Api(MyappModelRestApi):
datamodel = SQLAInterface(Model2)
route_base = '/model2/api'
appbuilder.add_api(Model3_ModelView_Api)
核实:这正是仓库实际写法,例如
Metadata_metric_ModelView_Api(... , MyappModelRestApi)、route_base = '/metadata_metric_modelview/api',并以appbuilder.add_api(...)注册(myapp/views/view_metadata_metric.py:191)。注意:route_base要与home.py菜单中 API 类型 url 对应。
4、添加自定义后端接口
自己定义一批接口,然后将类注册到系统中:
class Myapp(BaseMyappView):
route_base = '/myapp'
default_view = 'welcome'
@expose('/welcome')
def welcome(self):
if not g.user or not g.user.get_id():
return redirect(appbuilder.get_url_for_login)
msg = 'Hello ' + g.user.username + " !"
return self.render_template('hello.html', msg=msg)
# add_view_no_menu 添加视图,但不在 FAB 自带菜单显示
appbuilder.add_view_no_menu(Myapp)
核实:
add_view_no_menu在仓库中确有使用,如view_chat_dingtalk.py:192、view_chat_wework.py:164、view_chat_wechat.py:486、view_labelstudio.py:696等;菜单入口由/myapp/menu下发。(注:view_blood.py用的是appbuilder.add_api(Blood),不属于此类。)
5、Flask 的传统方法
因为 FAB 基于 Flask,所以你也可以用 Flask 的任何方法,例如注册路由:
@app.route("/health")
def health():
return "OK"
外部组件调用
定时调度能力
管理控制台难免有很多离线定时任务或异步任务。框架集成了 celery,可以实现定时任务、异步任务的发起和执行。在 config.py 的 CeleryConfig 类中配置定时框架参数和要执行的任务参数。系统默认使用 redis 作为任务队列和结果存储数据库。
任务编写
任务编写在 tasks 目录下,框架配置了示例任务 tasks/schedules.py,并添加了任务结果的微信推送:
# 配置 celery 任务
@celery_app.task(name="task.task_name1", bind=True)
def task_name1(task):
pass
核实:
myapp/tasks/schedules.py中实际任务即此形态,如@celery_app.task(name="task.delete_workflow", bind=True)/def delete_workflow(task)(schedules.py:133-134)。
配置任务调度参数
在 config.py 的 CeleryConfig 类中,task_annotations 用来配置单个任务执行时的限制条件:
# 任务的限制,key 是 celery_task 的 name,值是限制配置
task_annotations = {
'task.task_name1': {
'rate_limit': '1/s', # 任务的调度速率限制
'time_limit': 1200, # 运行时长限制,超时直接退出
'soft_time_limit': 1200, # 运行时长限制,会抛异常,可 catch
'ignore_result': True,
},
}
beat_schedule 用来配置定时任务的调度周期:
# 定时任务的配置项,key 为 celery_task 的 name,值是调度配置
beat_schedule = {
'task_task1': {
'task': 'task.task_name1', # 控制任务
'schedule': 10.0, # 调度周期(秒)
# 'schedule': crontab(minute='1', hour='*'), # 也可用 crontab
}
}
核实并修正(重要):原文使用旧式大写常量名
CELERY_ANNOTATIONS/CELERYBEAT_SCHEDULE。本仓库myapp/config.py的CeleryConfig采用 Celery 5 小写配置名:task_annotations(config.py:602)、beat_schedule(config.py:691),以及broker_url、result_backend、imports = ('myapp.tasks',)。请按小写名编写。
任务的调度
框架定时任务基于 celery,可直接用 celery 命令启动 beat 进程产生定时任务:
celery --app=myapp.tasks.celery_app:celery_app beat --loglevel=info
在 docker-compose 中包含(注释形态)beat 服务,用来产生定时任务并推送到 redis。
任务的执行
定时任务发送到 redis 后,需要 worker 接收执行。同样用 celery 命令启动多个 worker 并发执行:
celery --app=myapp.tasks.celery_app:celery_app worker --loglevel=info --pool=prefork -Ofair -c 2 -n worker@%h
在 docker-compose 中包含(注释形态)worker 服务,创建 worker 容器接收并执行任务。
缓存
框架包含缓存配置,config.py 中 CACHE_CONFIG 为缓存配置(config.py:519),系统默认使用 redis 作为缓存数据库(CACHE_REDIS_URL 使用 redis 的 1 号库)。代码中使用缓存:
from myapp import cache # 引入(myapp/__init__.py:230 装配)
cache.set('msg', msg, timeout=60) # 设置
cache_value = cache.get('msg') # 读取
用户行为记录
框架已集成用户行为记录。如果想收集用户对指定函数的访问记录,只需在函数上添加修饰符,会将用户或 API 访问的行为记录到数据表 logs 中:
from myapp import event_logger # myapp/__init__.py:390 装配
@event_logger.log_this
def list(self):
pass
这样就可以在用户行为记录中看到哪个用户、什么时间、访问了哪个路径、做了什么操作、传递了什么参数。
数据库结构升级和回滚
基于 FAB 做 model 的新增或修改,对应数据表的变更。框架基于 Flask-Migrate 集成了数据库结构的升级和回滚。前提是:myapp/__init__.py 引入时能够引入该 model(__init__.py 最下面引入 views,每个 view 又引入对应 model,这样被引入的 model 才能被系统识别,用来判断升级或回滚策略)。
数据库管理文件在 myapp/migrations 下,升级记录在 myapp/migrations/versions 目录下。如果你新增或修改了 model(且可被系统发现),在容器的 /home/myapp/ 目录下执行:
myapp db migrate # 生成对应版本数据库表的升级文件到 versions 文件夹下
系统会对比当前连接数据库的表结构与代码中定义的表结构差异,生成升级文件到 versions 目录。所以数据库一定不能手动修改,否则升级记录文件就不再连贯。
有了升级记录文件,直接执行下面命令进行升级:
myapp db upgrade # 数据库表同步更新到 mysql
完整的"upgrade → migrate → upgrade"三步法见 新增视图页面与数据库升级.md。本仓库
myapp/migrations下另有versions-mysql、versions-postgres两份历史结构。启动时的切换逻辑见install/docker/config.py:476-488:配MYSQL_SERVICE时直接用默认versions(不拷贝);配POSTGRESQL_SERVICE/KINGBASE_SERVICE时用versions-postgres覆盖versions;配DM_SERVICE(达梦)时清空versions,改由db.create_all()建表、不走 Alembic 迁移。
docker 镜像封装
项目包含 docker 镜像封装,为方便代码调试快速迭代,镜像分两部分:
install/docker/Dockerfile-base:基础环境封装,耗时较久但环境部分改动小,封装一次可长期复用(FROM ubuntu:22.04,Python 3.9)。install/docker/Dockerfile:在基础镜像之上加封代码,因为只是 copy 代码,封装较快,适合短周期迭代(FROM ...kubeflow-dashboard:base-python3.9-...)。
本地调试
由于本框架涉及多个容器组件,本地调试使用 docker-compose 启动;也可用 docker-compose 做生产部署,但只能在单机上进行,无法避免单点故障。
单机部署调试的配置在 install/docker/docker-compose.yml,其中包含 redis/myapp/frontend/mysql 服务(以及注释形态的 beat/worker/watch)。需求简单时可注释掉不需要的部分。
为方便调试,一般会将代码目录、配置文件,甚至本地 python 库挂载到容器中:
- ../../myapp/:/home/myapp/myapp/
- ./entrypoint.sh:/entrypoint.sh
- ./config.py:/home/myapp/myapp/config.py
这样你就可以直接在 IDE 中编辑,容器中也能检测到自动加载执行。注意:dev 模式能自动检测文件变更热加载,prod 模式不能自检测,需要手动重启。
k8s 高可用部署
本地调试完成后上生产线。应用简单或仅内部使用时可用上面的 docker-compose 部署;更推荐部署上云,install/kubernetes/ 目录中包含项目所需应用的全部 k8s 部署文件(需要一定的 k8s 基础)。云上部署详见 02-部署安装 段。通用 FAB 版本参考 https://github.com/data-infra/fab/tree/main/install/kubernetes。