博客 / 詳情

返回

如何在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接口數據的封裝、前端界面的設計和對於多種輸入控件的支持等方面內容,通過整合這些,可以快速的、彈性的實現多種業務記錄信息的存儲和展示。

以上思路具體實現的過程,拋磚引玉,希望大家有所感悟,並分享一下自己的寶貴經驗和思路。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.