如何在Python开发中实现无代码、纯配置的业务界面展示和常规数据操作的处理分析过程

要实现无代码、纯配置的业务界面展示和常规数据操作,最佳的方式是通过实体-属性-值的设计方式,也就是常说的EAV模式,通过动态构建实体类型、动态构建对应的属性列表,以及根据类型的不同对属性值进行存储,从而构建一系列的处理规则,实现业务模块的动态化,本篇随笔探讨一下,如何在Python开发中实现无代码、纯配置的业务界面展示,以及实现常规数据操作的过程,抛砖引玉,共同探讨。

1、何为实体-属性-值的设计方式

EAV(Entity-Attribute-Value)模型,我们先来了解一下。

EAV 把所有业务抽象成:

image

数据结构示例,如下所示:

Entity:   Customer
Attributes:
  name        string
  phone       string
  level       enum

Values:
  (001, name, "张三")
  (001, phone, "138****")
  (001, level, "VIP")

分别 包括定义实体表,对应的属性类别(名称、类型等),以及每个属性的值记录。

一句话说明就是:每个实体都有唯一的标识符,每个实体都可以有多个属性与之关联,每个属性都有唯一的标识符,每个属性都可以具有多个值。

我们对属性值表基于数据类型进行分割,每个不同的数据类型拆为一个单独的表,同时通过 属性表(Attribute) 添加 类型决定去哪里存取数据。

 我们可以借鉴magento的eav模型,它是EAV设计的最优参考了。Magento 2中的EAV属性类型有下面这些表:

  • eav_entity_int
  • eav_entity_varchar
  • eav_entity_text
  • eav_entity_decimal
  • eav_entity_datetime

属性元数据驱动 UI 控件选择,我们也根据属性的类型进行定义,如可以定义不同的输入控件。

image

 一句话,EAV + 元数据 + 通用UI + CRUD引擎 = 无代码业务系统

本质就是:

  • 业务结构:EAV建模

  • 界面构建:属性→控件自动映射

  • 数据交互:通用CRUD

  • 规则校验:DSL配置

  • 查询过滤:条件模板驱动

 

2、在Python开发中实现无代码、纯配置的业务界面展示和常规数据操作

有了上面的EAV知识的介绍,我们可以来进一步探讨在Python开发中实现无代码、纯配置的业务界面展示和常规数据操作过程了。

我们先来看一个界面下效果。

image

对于这样一个常规的列表展示界面,包括有条件查询、分页列表记录展示,属性类型,包括文本、整数、浮点小数、日期、备注长文本等类型录入,以及对应不同的数据类型,有下拉列表(固定的、动态字典的)、复选框、评分、弹出选择、映射关联属性等多种方式的录入处理,也是比较常见的情况。

而对于条件,可以展开多个条件,展开效果如下所示。

image

 而对于数据的录入,有弹出界面处理方式,也有对应直接编辑列表的方式,直接编辑输入比较快捷,如果能够丰富录入的控件处理,那么也是非常好的一种数据编辑方式。

因此我们直接在列表中进行数据的编辑处理,提供不同类型、不同方式的输入处理,如下面是动态字典,通过下拉列表或者单选框的方式进行录入。

image

而对于一些系统用户、角色、机构,我们应该也可以弹出来选择记录,并更新关联的字段信息

image

列表界面一般为了方便,会提供相关的右键菜单,提供常规的操作处理。

image

 而有些业务表是主从表的方式进行展示,如对应报价单、订单等常规的数据,通过主从表的方式会更加合适。

image

 主表可以直接录入外,明细表也通过直接录入的方式,通过选择产品,可以快速的实现数据记录的选择,以及对相关字段属性值的复制,非常方便。

 

 3、配置业务界面处理

如果要实现上面业务界面的展示处理,那么我们需要如何配置业务界面元素呢。

通过上面的EAV介绍

image

那么我们至少围绕上面几个信息来定义和存储数据内容,在更高的层次上定义好相关的信息:

1)实体类是否分页、是否有主从表关系。

2)属性定义,需要包括是否可以查询(作为条件)、是否可用(显示与否)、属性类型(决定存储位置)、控件输入方式(日期、数值、文本),而其中文本最为灵活,可能是通过配置字典(动态或者固定列表),选择系统表方式获取,选择动态业务表对象,通过编码规则生成编码等方式,数值可能是常规数值输入、评分输入、或者复选框等方式。另外还有是否必填、是否只读,排序顺序等关键定义

3)属性值的存储,根据不同的数据类型,存储在不同的表中,提高处理效率的同时不会降低精度。

4)属性信息的提取,这个非常关键,如果把这些数据每次组合起来,那么常规的做法就是关联多个表来实现数据的联合,但是效率会非常低下。好的做法可以利用NoSQL的动态文档的特点,对数据的组合通过MogoDB的方式实现快速检索处理,存储的时候,一份完整的记录存储在MongoDB,另外一份数据写入具体的属性值表中,必要时可以随时实现同步即可。

有了上面的几点介绍,我们来看看具体在Python中如何管理这些内容。

image

如上面顶部为实体(或实体类型)的定义信息,主要包括名称、模型类名、是否分页几个属性。

下面是对应实体类型的属性列表,其中属性名称、模型类型名、存储类型,为核心信息,其他必填、排序、字典类型、只读、隐藏、可查询 等等属性定义为一些构建界面必须的相关属性。

这两个表可以通过直接编辑模式进行快速录入,从而方便动态定义实体类型和相关的属性列表。

另外通过定义从表,可以从系统的动态定义实体类型中选择业务表作为从表信息,如下对于订单或者报价单的业务,通过主从表的方式显示的,定义界面如下所示。

image

 而对于一些属性字段的输入类型,我们提供一些内置的选项供选择。

image

如前面介绍的选择用户方式,就从基础用户表中选择记录,更新关联的字段信息。

而如果选择类似系统业务编码的,那么也提供一个编码生成的方式(结合业务编码模块规则生成编码),如订单中的订单编码记录,新增的时候,提供一个按钮可以结合订单编码规则生成编码。

image

 而对于常规的字典,我们可以通过配置字典类型,就可以实现字段和系统字典项目的关联了。

image

 这样就可以在实际记录的界面中选择字典项目了,如下所示对于支付方式的字典项目,可以从中选择。

image

而对于选择用户表、角色表、机构表,以及选择动态EAV生成的表,那么也需要进行一些属性值的关联复制处理,那么需要通过映射源字段和目标字段的名称,实现关联。

image

 这样就可以再选择产品信息的记录的时候,把它的属性值带到目标记录上。

如在订单明细记录中,通过产品选择的方式,可以带过来对应的属性值。

image

  

3、数据的查询和MongoDB 

使用EAV(Entity-Attribute-Value)模式来存储完整的数据结构信息以及NoSQL数据库来存储完整的记录是一种灵活的方法,特别适用于需要存储动态结构数据的场景。

EAV的常规关系型数据库表存储常规的设计表,如实体类型、属性定义、属性值(多个)表的相关信息,而利用MongoDB数据库的大数据处理灵活性和高性能的响应,能够存储我们实际变化的文档信息。在检索的时候,并提供了常规关系型数据库的联合查询、JSON查询无法得到的灵活性和高性能。 

有了字段的定义,我们就可以在业务列表中显示相关的字段,并从MongoDB总检索指定类型的数据,由于MongoDB本身支持非常好的查询处理,因此对于查询来说非常简单。

表的数据在MongoDB中存储的,如下界面所示。

 

对于在Python界面中展示相关的记录,我们根据配置,构建一个窗体界面,适配条件动态展示、列表展示、是否分页,以及对于各种属性定义好对应的前端输入类型,就能很好的实现数据的展示和直接录入的处理了。

由于我们前端后端通过WebAPI进行交互,后端在FastAPI服务中提供对应MongoDB的数据常规CRUD的接口来处理数据的增删改查操作,如下所示。

image

 对于条件查询的处理,这个和我们常规方式设计业务表,生成的查询接口类似,如下所示。

@router.post(
    "/mongo-list-post",
    response_model=AjaxResponse[PagedResult[dict] | None],
    summary="分页获取实体类型的记录",
    dependencies=[DependsJwtAuth],
)
async def mongo_get_list_post(
    request: Request,
    input: Annotated[EAVPagedDto, Body(description="查询条件")],
    db: AsyncSession = Depends(get_db),
):
    logger.info(f"EAVPagedDto:{input.model_dump()}")

    item = await attribute_crud.mongo_get_list(db, input)
    return AjaxResponse(item)

而在Python的前端,我们这里以PySide6的界面实现为例,通过实体类型的定义和对应属性的列表信息,我们可以进行界面的动态构建,如下是PySide6的界面实现代码。

    async def _create_content_panel(self) -> QWidget:
        """创建右侧主要内容面板"""

        # 创建一个主面板
        panel = QWidget(self)
        # 创建一个垂直布局
        main_layout = QVBoxLayout()
                
        # 创建一个折叠的查询条件框
        search_bar = self._create_search_bar(panel)
        main_layout.addWidget(search_bar)

        # 创建显示数据的表格
        table_widget = self._create_grid(panel)
        main_layout.addWidget(table_widget, 1)  # 拉伸占用高度

        # 如果是分页,创建一个分页控件
        if self.ispaging:
            self.pager_bar = ctrl.MyPager(panel, self.items_per_page, self.update_grid)
            main_layout.addWidget(self.pager_bar)

        # 创建从表数据表格
        sub_table_widget = await self._create_sub_content(panel)
        if sub_table_widget is not None:
            main_layout.addWidget(sub_table_widget, 1)  # 拉伸占用高度

        # 设置布局
        panel.setLayout(main_layout)
        return panel

在查询条件的展示处理中,我们根据属性列表来统一构建显示的控件信息,如下代码所示。

    def CreateConditionsWithSizer(self, parent: QWidget = None) -> QGridLayout:
        """子类可重写该方法,创建折叠面板中的查询条件,包括布局 QGridLayout"""

        layout = QGridLayout()
        layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
        layout.setSpacing(2)  # 增加间距

        # 统一处理查询条件控件的添加,使用默认的布局方式
        cols = self.CONDITIONS_PER_ROW * 2 # 每行显示的条件数,*2表示包含标签和输入控件
        self.condition_widgets = list = EAVUtil.CreateConditions(self.attribute_list, parent)

        for i in range(len(list)):
            control = list[i]
            # print(type(control))
            if not isinstance(control, QWidget):
                print("错误!control 不是 QWidget:", control)
                break
            control.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed))
            layout.addWidget(control, i // cols, i % cols)

        return layout

而对于数据的直接编辑处理,我们通过控件属性的定义,统一转换为对应自定义视图委托的配置信息,从而构建不同的输入控件方式。

        config = EAVUtil.convert_attributes_to_config(self.attribute_list, self.entity_type_info)
        # 设置QTableView视图的委托处理对象
        self.delegate = CustomDelegate(config, self.table_model, self.table_view)
        #对特殊字段进行自定义的处理,如弹出选择对话框等
        self.delegate.customTriggered.connect(self.on_custom_triggered)

其中的config例子可能是其中下面的定义。

        #实际字段测试
        config = {
            "ProductName": {"type": "text"},
            "Color": {"type": "combo", "dict_type_name":"产品颜色"}, #字典类型名称
            "Size": {"type": "int"},
            "Style": {"type": "combo", "dict_type_name":"产品款式"},
            "Height": {"type": "double", "min": 0, "max": 1000, "decimals": 2, "format": "{0} cm"},
            "Width": {"type": "double", "min": 0, "max": 1000, "decimals": 2, "format": "{0} cm"},
            "Note": {"type": "text"},
            "Test": {"type": "text"},
            "Tag": {"type": "text"},
            "CreateDate": {"type": "datetime", "format": "yyyy-MM-dd HH:mm:ss"},
            "Price": {"type": "double", "decimals": 2,"format": "{0:C2}"},
            "Status": {"type": "check", "true": "1", "false": "0"},
            "Dealer": {"type": "radio", "dict_type_name":"送货区域", "width": 280},
            "Rating": {"type": "rating"},
            "Tag": {"type": "text"},
            "User": {"type": "text", "readonly":True},
            "Organ": {"type": "text", "readonly":True},
            "Role": {"type": "text", "readonly":True},
            "Attach": {"type": "text", "readonly":True},
        }

这需要我们根据实际的配置信息进行构建config即可。

我们为了支持直接录入多种控件类型,那么需要自定义视图委托。

class CustomDelegate(QStyledItemDelegate):

它支持的类型有:

    - text: 普通文本 (QLineEdit)
    - int: 整数 (QSpinBox)
    - double: 浮点数 (QDoubleSpinBox)
    - date: 日期 (QDateEdit)
    - combo: 下拉选择 (QComboBox)
    - check: 复选框 (直接显示)
    - radio: 单选按钮组 (QRadioButton)
    - slider: 滑动条 (QSlider)
    - multiline: 多行文本 (QTextEdit)
    - password: 密码文本 (QLineEdit)
    - percent: 百分比 (QDoubleSpinBox)
    - currency: 货币 (QDoubleSpinBox)
    - time: 时间 (QTimeEdit)
    - datetime: 日期时间 (QDateTimeEdit)
    - color: 颜色选择 (QPushButton)
    - icon: 图标选择 (QPushButton)
    - bitmap: 位图选择 (QPushButton)
    - rating: 评分 (StarRating)
    - tablenumber: 表号生成器 (QLineEdit + QPushButton)
    - select_entity: 选择EAV自定义实体记录,以及字段复制映射
    - select_system: 选择系统类型, 如:用户、角色、组织架构等,以及字段复制映射
    - custom: 自定义不可编辑控件,同时触发 customTriggered 信号,传出单元格索引和字段名称

这个我曾经在文章《在PySide6/PyQt6的开发框架中,增加对表格多种格式录入的处理,以及主从表的数据显示和保存操作》有所介绍。

表格中多种格式录入的效果示例如下。

image

 

以上就是对于如何在Python开发中实现无代码、纯配置的业务界面展示和常规数据操作的处理分析过程,其中设计到EAV相关基础表的设计,MongoDB数据的查询处理、FastAPI接口数据的封装、前端界面的设计和对于多种输入控件的支持等方面内容,通过整合这些,可以快速的、弹性的实现多种业务记录信息的存储和展示。

以上思路具体实现的过程,抛砖引玉,希望大家有所感悟,并分享一下自己的宝贵经验和思路。

posted on 2025-12-03 15:03  伍华聪  阅读(495)  评论(0)    收藏  举报

导航