data structure
in product_service.py
def save_simulation_result
关键看这个函数!
其他的都是大包裹式的字典!
n5321 | 2025年12月18日 00:41
in product_service.py
def save_simulation_result
关键看这个函数!
其他的都是大包裹式的字典!
n5321 | 2025年12月18日 00:41
这是一个非常典型的桌面应用程序架构演进问题。目前的架构中,数据在 数据库 (SQLite)、UI 控件状态 和 DataService (内存) 三者之间流转,确实存在“多头管理”和“同步不及时”的风险。
以下我将深入分析你当前的 数据流现状,并给出基于 DataService 作为唯一可信源 (Single Source of Truth) 的重构建议。
目前的流向是混合的,主要痛点在于:数据是谁的?
路径: Database -> ProductController -> CustomerRequirementsPage (UI) -> FieldWidget
问题: 数据直接填入了 UI 控件。虽然 BLDCPage 里也顺手更新了一下 DataService,但主流程是依赖 UI 控件存储数据的。
路径 A (Design Target Page): 用户打字 -> on_text_changed -> 直接调用 DataService.update...。
路径 B (Input Panel): 用户打字 -> _on_text_changed -> 直接调用 DataService.update_maxwell_parameters。
问题: UI 直接操作 Service,耦合度高。而且如果代码里有 setText 操作,会触发信号,导致不必要的递归更新。
路径: InputPanel 点击开始 -> 从 DataService.customer_requirements 和 DataService.maxwell_parameters 拼凑参数 -> 发送给 Controller -> Worker。
问题: maxwell_parameters 和 customer_requirements 之间界限模糊。有些参数(如额定电压)既是客户需求,又是 Maxwell 输入,目前是简单粗暴的 update() 覆盖,容易弄丢数据来源。
路径: Worker -> MaxwellAutomation -> DataService.update_result_detail (写入) -> Controller -> View.on_simulation_success -> View 读取 DataService 或直接使用传参。
问题: 这是一个比较好的部分,已经通过 Service 中转了,但 Worker 内部隐式实例化 DataService 有点依赖全局单例的副作用。
目标:UI 只是数据的展示层,Database 是持久化层,DataService 是运行时的唯一核心。
首先理清 DataService 中那几个字典的具体职责:
| 变量名 | 当前定义 | 建议改进定义 |
|---|---|---|
| product_info | 产品基础信息(ID, Model) | 身份信息:当前选中产品的 ID、型号、系列ID。 |
| customer_requirements | 客户需求(电压、扭矩等) | 设计目标(Target):只读或通过"设计目标页"修改。代表“我想要达到的指标”。 |
| maxwell_parameters | 发送给 Maxwell 的参数 | 仿真输入(Input):实际发给 Maxwell 的几何/物理参数。默认从 Target 继承,但允许在 InputPanel 覆盖修改。 |
| maxwell_para_ranges | 优化范围 | 优化配置:AI 算法的上下限配置。 |
| result_detail | 仿真结果 | 仿真输出(Output):Maxwell 算完回填的标量数据。 |
修改 DataService,使其继承自 QObject,并在数据变更时发送信号。
修改后的 services/data_service.py 结构建议:
codePython
from PyQt6.QtCore import QObject, pyqtSignal, QMutex, QMutexLocker
class DataService(QObject):
_instance = None
_create_mutex = QMutex()
# === 定义信号,通知 UI 刷新 ===
# 这样 UI 不需要知道是谁改了数据,只要数据变了,UI 就自动刷新
data_changed = pyqtSignal(str, dict) # (category, new_data)
def __init__(self):
super().__init__()
if not hasattr(self, '_initialized'):
self._data_mutex = QMutex()
# 数据容器
self._data = {
"product_info": {},
"project_info": {},
"customer_requirements": {}, # 对应设计目标
"maxwell_parameters": {}, # 对应仿真输入
"process_limits": {},
"maxwell_para_ranges": {},
"result_detail": {}
}
self._initialized = True
# === 通用更新方法 ===
def _update_category(self, category: str, data: dict):
with QMutexLocker(self._data_mutex):
self._data[category].update(data)
# 拿到更新后的完整副本
full_data = self._data[category].copy()
# 发射信号 (在锁外部,防止死锁)
self.data_changed.emit(category, full_data)
# === 具体业务方法 ===
def set_active_product(self, product_dict: dict):
"""加载新产品时调用,清空旧状态,填入新状态"""
with QMutexLocker(self._data_mutex):
self._data["product_info"] = product_dict
self._data["customer_requirements"] = product_dict # 初始时,需求即参数
# 关键:仿真参数初始值 = 客户需求
self._data["maxwell_parameters"] = product_dict.copy()
self._data["result_detail"] = {} # 清空旧结果
# 通知所有监听者:产品变了,请刷新
self.data_changed.emit("all", self._data)
def update_maxwell_parameters(self, params: dict):
self._update_category("maxwell_parameters", params)
def update_result_detail(self, results: dict):
self._update_category("result_detail", results)
# ... get 方法保持不变 (返回 copy)
UI 不再直接持有数据,而是作为 DataService 的观察者。
在 InputPanel (示例) 中的改动:
codePython
class InputPanel(QWidget):
def __init__(self, data_service, parent=None):
super().__init__(parent)
self.data_service = data_service
self._init_ui()
# 1. 连接信号:只有 Service 说变了,我才变
self.data_service.data_changed.connect(self.on_data_changed)
def on_data_changed(self, category, data):
"""响应数据变化"""
if category == "maxwell_parameters" or category == "all":
self.block_signals(True) # 防止死循环:UI改->Service改->UI改
self.fill_input_fields(data)
self.block_signals(False)
if category == "result_detail" or category == "all":
self.update_result_labels(data)
def _on_text_changed(self, key, text):
"""用户手动修改时,只负责提交给 Service"""
# ... 解析 val ...
# 只要提交,不要在这里直接 setText,让 on_data_changed 回调来做
self.data_service.update_maxwell_parameters({key: val})
def block_signals(self, block: bool):
# 辅助函数,批量阻塞所有输入框信号
for widgets in self.fields_map.values():
if 'value' in widgets: widgets['value'].blockSignals(block)
采用上述重构后,数据流将变得非常清晰:
Controller: 从 DB 读取 Product 对象。
Controller: 调用 data_service.set_active_product(product_dict)。
DataService: 更新内部状态,发射 data_changed("all", ...) 信号。
UI (TargetPage, InputPanel, DetailPage): 收到信号 -> 自动刷新界面显示。
UI (InputPanel): 用户修改“线径”。
UI: 触发 textChanged -> 调用 data_service.update_maxwell_parameters({'wire_size': 0.5})。
DataService: 更新内存字典,发射 data_changed("maxwell_parameters", ...)。
UI: 收到信号 (如果是其他联动UI则更新,如果是自己则被 blockSignals 忽略)。
UI: 点击“开始”。
Controller: 调用 data_service.get_maxwell_parameters() 获取最新、最全的参数。
Controller: 将参数包传给 Worker。
Worker: 执行 Maxwell 脚本。
Worker: 解析出结果字典。
Worker: 调用 data_service.update_result_detail(result_dict)。
DataService: 发射 data_changed("result_detail", ...)。
UI (ResultPanel, ChartPanel): 收到信号 -> 自动刷新结果显示 (对比值、图表)。
Controller: (可选) 调用 DB Service 将 DataService 中的 maxwell_parameters (输入) 和 result_detail (输出) 存入 SimulationResult 表,形成历史记录。
services/data_service.py:
继承 QObject。
添加 pyqtSignal。
在 update 方法中发射信号。
添加 set_active_product 方法来统一初始化一次仿真的上下文。
ui/pages/bldc_page/widgets/input_panel.py:
删除 fill_input_fields 的直接调用逻辑(由信号触发)。
添加 on_data_changed 槽函数。
在 _on_text_changed 中确保只是“提交数据”,而不是“修改界面”。
controllers/product_controller.py:
在 on_product_selected 中,除了更新 UI,务必调用 self.service.data_service.set_active_product(...)。目前你好像把 data service 混在了 ProductService 或者直接在 View 里调用,建议统一在 Controller 里分发。
utils/property_change.py:
确保它只负责翻译数据,不持有数据。它现在的逻辑是 fields_map (UI控件引用),这不好。
改进: 让它接收 dict (来自 DataService) 而不是 fields_map (UI控件)。这样后台 Worker 可以直接用它,不需要依赖 UI 线程。
通过这种方式,你的应用将遵循 MVVM (Model-View-ViewModel) 的变体模式,DataService 充当 ViewModel,负责持有状态和通知 View 更新,彻底解决数据流混乱的问题。
n5321 | 2025年12月15日 00:18
cd mermaid-live-editor
pnpm dev --open
n5321 | 2025年12月10日 09:18
UML 分为 结构图 + 行为图
这些图用来表示类、模块、系统组件之间的关系。
最常用
展示类、属性、方法、继承、关联、聚合、组合等
是类图的“实例化快照”
展示对象之间当时的关系
描述系统如何分成不同组件(DLL、模块、包等)
展示系统在服务器、硬件上的部署情况
如:Web server、DB server、负载均衡
显示模块/包之间的依赖关系
表示内部结构,如类内部部件、端口、连接器
定义 UML 扩展(刻板构造、标签、元信息)
这些图表示系统的动态动作、逻辑控制、状态转移等。
展示用户(Actor)如何与系统交互
需求阶段常用
类似流程图
用来描述业务流程、算法内部逻辑
描述对象/系统的状态变化
常用于嵌入式、电机控制、协议机
展示对象之间按时间先后的消息交互
最常用的行为图之一
展示对象间的消息传递(更强调关系网络)
活动图 + 序列图的结合
用于大型流程展示
用时间轴展示状态变化
在硬件/协议中常用
Class Diagram 类图
Object Diagram 对象图
Component Diagram 组件图
Deployment Diagram 部署图
Package Diagram 包图
Composite Structure Diagram 复合结构图
Profile Diagram 剖面图
Use Case Diagram 用例图
Activity Diagram 活动图
State Machine Diagram 状态机图
Sequence Diagram 序列图
Communication Diagram 通信图
Interaction Overview Diagram 交互概览图
n5321 | 2025年12月10日 09:16
MVC (Model-View-Controller) 架构的桌面应用程序,涉及数据库操作、UI展示和后台仿真任务。
整体来看,代码结构清晰,分层合理,已经具备了生产级应用的雏形。但是,在 数据流的一致性、ORM的使用细节、MVC解耦 以及 线程安全 方面,存在一些设计隐患和改进空间。
以下是详细的审查报告:
这是当前设计中最大的隐患。
现象:
bldc_page.py 中引入了 DataService,并且通过 _on_text_changed 实时更新 self.data_service。
controller.py 中通过 _on_save_product 负责将数据保存到 SQLite 数据库 (Product Model)。
问题:仿真使用的数据 (DataService) 和 数据库存储的数据 (Product) 是两条平行的线。
如果用户修改了界面的值,DataService 更新了,但没有点“保存”,数据库是旧的。
如果用户点击“保存”,数据库更新了。
风险点:点击“开始仿真”时,_get_property_changes 是从 DataService 取值还是从 UI 控件取值?
代码显示 _get_property_changes 混合了 data_service.customer_requirements 和 maxwell_parameters。
冲突:bldc_page.py 的 display_design_target 方法是从 数据库对象 (product) 加载数据到 UI。如果用户加载了一个产品,修改了输入框(更新了 DataService),然后没保存直接点仿真,仿真是基于新数据的(符合预期)。
但是,如果 DataService 是单例模式(通常 Service 都是单例),而你切换了产品,DataService 里的数据被清空了吗?如果没有,旧产品的残留数据可能会污染新产品的仿真参数。
建议:
确保每次加载新产品时 (display_design_target),不仅要填入 UI,还要同步重置/初始化 DataService 的状态,确保它只包含当前产品的参数。
product.detail 的访问方式不一致 (易报错)在 model.py 中:你使用了 product.detail.get() (这是获取单个对象)。
在 bldc_page.py 中:你使用了 product.detail[0] (这是把 backref 当作列表索引)。
# bldc_page.py
detail_object = product.detail[0] if product.detail else None分析:Peewee 的 backref 默认返回一个 SelectQuery。
product.detail[0] 会触发查询并获取第一条。如果 unique=True 生效,这确实只有一条。
但是,如果 product.detail 为空(即没有详情),if product.detail 在 Peewee 查询中通常会被判定为 True(因为查询对象本身非空),而 [0] 会抛出 IndexError。
建议:统一使用一种安全的方法。
# 推荐写法:利用之前 model.py 里修复过的逻辑,或者统一用 first()
detail_object = product.detail.first() # 如果没有返回 None,有则返回第一个find_product_by_id 中的性能隐患 (副作用)# model.py
def find_product_by_id(product_id: int):
# ... 省略查询 ...
product_dict = model_to_dict(product, backrefs=True, recurse=True) # !!! 昂贵操作
for k, v in product_dict.items():
print(f"{k}: {v}") # !!! I/O 操作
return product问题:model_to_dict 是一个昂贵的操作,尤其是 backrefs=True 时,它会执行额外的 SQL 查询来获取关联数据。你只是为了打印 debug 信息就执行它,这在生产环境中会显著拖慢点击响应速度。
建议:删除或注释掉打印逻辑,或者仅在 logging level 为 DEBUG 时执行。
问题:initialize_database 和 close_connection 是全局函数。在 controller.py 的 shutdown 中调用关闭。
风险:如果在多线程环境(例如 MaxwellWorker 线程)中访问数据库,SQLite 需要特别小心。虽然 Peewee 的 SqliteDatabase 默认有一定的线程安全处理,但建议在 Worker 线程中不要复用主线程的连接实例,或者确保 SQLite 编译时支持多线程且连接模式设置为 WAL。
建议:目前你的 Worker 主要是跑 Maxwell 脚本,如果 Worker 结束后要写回数据库(更新结果),请确保使用 db.connection_context() 或在任务开始时建立连接,结束时关闭。
代码:
# controller.py
self._view.customer_page.product_combo_box.currentIndexChanged...
self._view.customer_page.get_selected_product_id()
self._view.customer_page.load_data_to_form(product)问题:Controller 深入到了 View 的内部组件(product_combo_box)。如果 View 改版了(比如把下拉框换成了列表),Controller 必须重写。
建议:遵循 “迪米特法则” (Law of Demeter)。
View 应该暴露出信号和高级方法。
View 定义:signal_product_selected = pyqtSignal(int)
Controller 连接:view.customer_page.signal_product_selected.connect(self._on_product_selected)
这样 Controller 不需要知道 View 用的是 ComboBox 还是 ListView。
代码:except Exception as e: print(...) 在 _on_series_selection_changed 中。
问题:这会吞掉所有错误,包括代码写错的 AttributeError 或 NameError。这会让调试变得非常困难,你可能看着界面没反应,却不知道是因为数据库错了还是变量名写错了。
建议:只捕获预期的异常(如 PeeweeException),或者在捕获 Exception 后使用 traceback.print_exc() 打印完整堆栈。
问题:bldc_page.py 中有大量的硬编码列表:
self.core_params_fields = [("额定转速(Rpm)", "rated_speed"), ...]同时 model.py 里也有字段定义。
风险:如果你在数据库里改了字段名(比如 rated_speed 改为 speed_rated),你需要修改 model.py 和 bldc_page.py 等多个地方。
建议:虽然完全动态化很难,但建议将这些字段映射配置(Config)提取到一个单独的常量文件(如 constants.py),让 Model 和 View 引用同一个常量定义,减少“魔法字符串”。
display_design_target 的字典合并逻辑# bldc_page.py
detail_object = product.detail[0] if product.detail else None
product_dict = dict(product.__data__)
# ...
combined_dict = {**product_dict, **detail_dict}问题:product.__data__ 是 Peewee 的内部属性,直接访问它是不推荐的,因为它可能不包含继承字段或外键对象的引用。
建议:使用 model_to_dict(product, recurse=False) 更规范,或者直接访问属性 getattr(product, key)。因为你后面是用 key 来遍历 map 的,直接 getattr 其实更安全且支持 IDE 提示。
高优先级(Bug 修复):
修复 bldc_page.py 中 product.detail[0] 的写法,改为 .first() 或使用 try-except,防止没有详情的“孤儿产品”导致界面崩溃。
数据同步:在切换产品时,务必清空或重置 DataService,防止上一个产品的数据残留影响下一个产品的仿真。
中优先级(架构优化):
Controller 解耦:让 View 发射语义化信号(如 productChanged),而不是 Controller 去监听 combo_box。
去除副作用:删除 model.py 中 find_product_by_id 里的 model_to_dict 和 print 循环。
低优先级(代码整洁):
提取字段名称字符串到常量文件。
优化 initialize_database 的调用位置,确保线程安全。
你的代码整体逻辑已经跑通了,特别是解决了之前 ProductDetail
但是,针对你目前 MVC 耦合度高、数据流混乱、逻辑堆积在 Controller 的问题,最成熟、最符合工业界标准的解决方案不是引入一个新库,而是采用 MVVM (Model-View-ViewModel) 架构模式,配合 Repository 模式。
如果非要推荐一个第三方库来辅助,我强烈推荐使用 qt_material 或 PyQt-Fluent-Widgets 来美化 UI,但核心架构必须重构。
以下是为你量身定制的 MVVM 重构蓝图。
这个架构的目标是:View (界面) 只负责显示,Model (数据库) 只负责存取,ViewModel (中间层) 负责业务逻辑和状态同步。
my_app/
│
├── data/
│ ├── db_manager.py # 数据库连接单例
│ ├── models.py # Peewee 模型定义 (Product, ProductDetail)
│ └── repositories.py # ✅ 新增:负责所有数据库 CRUD 操作,隔离 SQL 逻辑
│
├── viewmodels/ # ✅ 新增:业务逻辑层
│ ├── base_viewmodel.py
│ ├── customer_vm.py # 处理客户/产品选择逻辑
│ └── simulation_vm.py # 处理 Maxwell 仿真逻辑 (原 DataService)
│
├── views/ # 界面层 (只包含 UI 代码)
│ ├── main_window.py
│ ├── components/ # 自定义组件
│ └── pages/
│ ├── customer_page.py
│ └── bldc_page.py
│
├── services/ # 后台服务
│ └── maxwell_worker.py # 线程任务
│
└── main.py # 程序入口,负责依赖注入痛点解决:你现在的 model.py 既有类定义又有函数逻辑(find_product_by_id),容易膨胀。
重构:创建一个 ProductRepository,专门管理数据读写。
# data/repositories.py
from typing import Optional
from .models import Product, ProductDetail, ProductSeries, db
class ProductRepository:
"""仓库层:只负责和数据库对话,不包含任何 UI 逻辑"""
def get_by_id(self, product_id: int) -> Optional[Product]:
try:
# 使用 prefetch 预加载,并安全处理 DoesNotExist
return (Product.select(Product, ProductDetail)
.join(ProductDetail, join_type=ProductDetail.LEFT_OUTER)
.where(Product.id == product_id)
.get())
except Product.DoesNotExist:
return None
def create(self, series: ProductSeries, model_number: str) -> Product:
# 事务逻辑封装在这里
with db.atomic():
product = Product.create(series=series, model_number=model_number)
# 自动创建空的 Detail,防止孤儿数据
ProductDetail.create(product=product)
return product
def update(self, product_id: int, data: dict):
"""统一的更新接口"""
# ... 实现安全的更新逻辑,参考之前我们修复的代码 ...
pass
def delete(self, product_id: int):
Product.delete().where(Product.id == product_id).execute()痛点解决:你的 controller.py 管得太宽了,既要知道 ComboBox 怎么选,又要处理数据库。
重构:ViewModel 持有数据状态,View 通过信号监听 ViewModel 的变化。
# viewmodels/customer_vm.py
from PyQt6.QtCore import QObject, pyqtSignal
from data.repositories import ProductRepository
class CustomerViewModel(QObject):
# 定义信号:通知 View 数据变了
series_list_loaded = pyqtSignal(list)
product_list_loaded = pyqtSignal(list)
current_product_changed = pyqtSignal(object) # 传回 Product 对象或 None
status_message = pyqtSignal(str)
error_occurred = pyqtSignal(str)
def __init__(self, repo: ProductRepository):
super().__init__()
self._repo = repo
self._current_product = None
def load_initial_data(self):
"""加载初始数据"""
series = self._repo.get_all_series()
self.series_list_loaded.emit(series)
def select_series(self, series_id):
"""用户选了系列,VM 负责去查产品列表"""
if series_id == 0:
products = self._repo.get_all()
else:
products = self._repo.get_by_series(series_id)
self.product_list_loaded.emit(products)
def select_product(self, product_id):
"""用户选了产品,VM 负责查详情"""
if not product_id:
self._current_product = None
self.current_product_changed.emit(None)
return
product = self._repo.get_by_id(product_id)
self._current_product = product
# 通知 UI 更新,UI 不需要知道是通过 ID 查的还是怎么查的
self.current_product_changed.emit(product)
self.status_message.emit(f"已加载: {product.model_number}")
def save_changes(self, form_data: dict):
"""保存逻辑"""
if not self._current_product:
self.error_occurred.emit("未选择产品")
return
try:
self._repo.update(self._current_product.id, form_data)
self.status_message.emit("保存成功")
except Exception as e:
self.error_occurred.emit(str(e))痛点解决:bldc_page.py 不再直接操作数据库,也不包含 DataService 逻辑。它变得非常“傻”,只负责展示和发信号。
# views/pages/customer_page.py
from PyQt6.QtWidgets import QWidget, QComboBox, ...
from viewmodels.customer_vm import CustomerViewModel
class CustomerPage(QWidget):
def __init__(self, viewModel: CustomerViewModel):
super().__init__()
self._vm = viewModel
self._setup_ui()
self._bind_view_model() # 关键:绑定信号
def _setup_ui(self):
# ... 初始化 UI ...
self.series_combo = QComboBox()
self.product_combo = QComboBox()
# 注意:这里不再连接 Controller,而是连接自身的槽函数,用于转发给 VM
self.series_combo.currentIndexChanged.connect(self._on_series_changed)
def _bind_view_model(self):
"""MVVM 的核心:数据绑定"""
# 1. 监听 VM 的数据变化信号 -> 更新 UI
self._vm.series_list_loaded.connect(self._update_series_list)
self._vm.product_list_loaded.connect(self._update_product_list)
self._vm.current_product_changed.connect(self._display_product_data)
self._vm.error_occurred.connect(lambda msg: QMessageBox.critical(self, "错误", msg))
# --- UI 事件转发给 VM ---
def _on_series_changed(self, index):
series_id = self.series_combo.currentData()
self._vm.select_series(series_id) # 告诉 VM:系列变了,你看着办
# --- VM 通知 UI 更新 ---
def _update_series_list(self, series_list):
self.series_combo.clear()
for s in series_list:
self.series_combo.addItem(s.name, s.id)
def _display_product_data(self, product):
if product is None:
self._clear_form()
return
# 只需要把 product 对象的数据填入输入框
# 这里不需要查数据库,因为 VM 已经把现成的对象给你了
self.model_number_edit.setText(product.model_number)
# ... 填充其他字段 ...在 MVVM 架构中,SimulationViewModel 将替代你目前的 DataService。
SimulationViewModel 内部持有一个字典(即原来的 DataService)。
当用户在界面修改输入框时,界面发送信号 paramChanged(key, value)。
SimulationViewModel 接收信号,更新内部字典。
当切换产品时,CustomerViewModel 会通知 SimulationViewModel:“嘿,换人了”。
SimulationViewModel 立即重置内部字典,并从数据库加载新数据覆盖。
这就形成了一个单一数据源 (Single Source of Truth)。
创建 repositories.py:把 model.py 里的 find_..., create_..., update_... 全部移进去,并加上异常处理和 try-except 保护(如自动创建 Detail)。
创建 viewmodels:
把 controller.py 的逻辑拆分。
Customer 相关的逻辑放入 CustomerViewModel。
Maxwell 仿真相关的逻辑(原来的 DataService)放入 SimulationViewModel。
改造 bldc_page.py
构造函数接收 SimulationViewModel。
删除 self.data_service。
所有的 textChanged 事件都改为调用 self.viewModel.update_param(key, value)。
改造 main.py:
实例化 Repository。
实例化 ViewModels (注入 Repository)。
实例化 Views (注入 ViewModels)。
这种架构虽然前期代码量稍微多一点(多了文件),但是逻辑极其清晰
由于我无法直接接收你的文件包,我将基于你之前提供的代码片段,为你构建一个完整的、经过重构的核心代码骨架。
你可以直接在你的项目中建立文件夹,复制粘贴这些代码。
请按照这个结构整理你的文件:
src/
├── data/
│ ├── __init__.py
│ ├── database.py # 数据库连接与模型定义 (原 model.py)
│ └── repository.py # ✅ 核心:负责所有增删改查,隔离 SQL 细节
├── viewmodels/
│ ├── __init__.py
│ └── product_vm.py # ✅ 核心:业务逻辑,View 和 Repository 的桥梁
├── views/
│ ├── __init__.py
│ └── bldc_page.py # ✅ 界面:只负责显示,不处理逻辑
└── main.py # 程序入口,依赖注入src/data/database.py保持模型定义纯净,去掉所有业务逻辑函数。
import os
from datetime import datetime
from peewee import (
Model, SqliteDatabase, CharField, TextField, IntegerField,
FloatField, DateTimeField, ForeignKeyField
)
# 1. 数据库路径设置
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 回退一级到 src
db_path = os.path.join(base_dir, "..", "products_final.db") # 假设 db 在 src 同级
# 2. 初始化数据库 (开启外键支持)
db = SqliteDatabase(db_path, pragmas={'foreign_keys': 1})
class BaseModel(Model):
class Meta:
database = db
class ProductSeries(BaseModel):
name = CharField(unique=True)
description = TextField(null=True, default="")
class Product(BaseModel):
series = ForeignKeyField(ProductSeries, backref='products', null=True, on_delete='SET NULL')
model_number = CharField(unique=True)
# ... (保留你其他的字段定义,这里为了省流省略,实际代码请补全) ...
rated_voltage = FloatField(null=True)
rated_speed = IntegerField(null=True)
rated_torque = FloatField(null=True)
created_at = DateTimeField(default=datetime.now)
class ProductDetail(BaseModel):
product = ForeignKeyField(Product, backref='detail', unique=True, on_delete='CASCADE')
# ... (保留你其他的字段定义) ...
price = FloatField(null=True)
poles = IntegerField(null=True)
notes = TextField(null=True, default="")
def initialize_db():
db.connect(reuse_if_open=True)
db.create_tables([ProductSeries, Product, ProductDetail], safe=True)
# 确保至少有一个系列
if ProductSeries.select().count() == 0:
ProductSeries.create(name="默认系列", description="系统自动创建")
def close_db():
if not db.is_closed():
db.close()src/data/repository.py (关键文件)这是解决你之前报错的“门神”。它确保了无论数据库里有没有 Detail,代码都不会崩。
import logging
from typing import Optional, Dict, Any
from peewee import IntegrityError, JOIN
from .database import db, Product, ProductDetail, ProductSeries
class ProductRepository:
"""
仓库模式:负责所有直接的数据库操作。
UI 和 ViewModel 不应该知道 SQL 或 Peewee 的存在。
"""
def get_all_series(self):
return list(ProductSeries.select())
def get_products_by_series(self, series_id: Optional[int]):
query = Product.select()
if series_id:
query = query.where(Product.series == series_id)
return list(query.order_by(Product.model_number))
def get_product_by_id(self, product_id: int) -> Optional[Product]:
"""
获取产品及其详情。
✅ 修复点:安全处理 Detail 缺失的情况。
"""
try:
# 1. 尝试查询
product = (Product.select(Product, ProductDetail)
.join(ProductDetail, JOIN.LEFT_OUTER)
.where(Product.id == product_id)
.get())
# 2. ✅ 关键修复:检查 detail 是否真的存在
# 这里的逻辑是:如果数据库查出来 detail 是 None (因为 LEFT JOIN),
# 我们就在内存里给它挂一个临时的空对象,防止 UI 访问报错。
# 或者,你可以选择在这里直接插入一条空记录。
try:
if product.detail is None:
raise ProductDetail.DoesNotExist
except ProductDetail.DoesNotExist:
logging.warning(f"Product {product_id} missing detail. Creating default.")
# 自动在数据库补全缺失的记录
ProductDetail.create(product=product)
# 重新查询以获取完整对象
return self.get_product_by_id(product_id)
return product
except Product.DoesNotExist:
return None
def update_product(self, product_id: int, updates: Dict[str, Any]):
"""
更新产品。
✅ 修复点:使用 _meta.fields 精确匹配字段。
"""
product = self.get_product_by_id(product_id)
if not product:
raise ValueError("Product not found")
detail = product.detail # get_product_by_id 保证了这里一定有值
product_dirty = False
detail_dirty = False
# 获取模型字段定义
p_fields = Product._meta.fields
d_fields = ProductDetail._meta.fields
for key, value in updates.items():
if key in ('id', 'created_at', 'series', 'product'): continue
# 更新主表
if key in p_fields:
if str(getattr(product, key)) != str(value):
setattr(product, key, value)
product_dirty = True
# 更新附表
elif key in d_fields:
if str(getattr(detail, key)) != str(value):
setattr(detail, key, value)
detail_dirty = True
with db.atomic():
if product_dirty: product.save()
if detail_dirty: detail.save()
return productsrc/viewmodels/product_vm.py这是 MVVM 的核心。它持有 UI 的状态,并处理业务逻辑。
from PyQt6.QtCore import QObject, pyqtSignal
from src.data.repository import ProductRepository
class ProductViewModel(QObject):
# --- 信号定义 ---
series_loaded = pyqtSignal(list) # 系列列表已加载
products_loaded = pyqtSignal(list) # 产品列表已加载
current_product_changed = pyqtSignal(object) # 当前选中的产品变了 (传回 Product 对象)
status_message = pyqtSignal(str) # 状态栏消息
error_occurred = pyqtSignal(str) # 错误弹窗
def __init__(self):
super().__init__()
self._repo = ProductRepository()
self._current_product_id = None
def load_initial_data(self):
"""初始化加载系列"""
series = self._repo.get_all_series()
self.series_loaded.emit(series)
# 默认加载所有产品
self.load_products_by_series(None)
def load_products_by_series(self, series_id):
"""根据系列加载产品列表"""
products = self._repo.get_products_by_series(series_id)
self.products_loaded.emit(products)
def select_product(self, product_id):
"""
用户选中了某个产品 ID
"""
self._current_product_id = product_id
if product_id is None:
self.current_product_changed.emit(None)
return
product = self._repo.get_product_by_id(product_id)
if product:
self.current_product_changed.emit(product)
self.status_message.emit(f"已加载: {product.model_number}")
else:
self.error_occurred.emit("无法加载产品数据")
def save_current_product(self, form_data: dict):
"""
保存当前产品的修改
"""
if not self._current_product_id:
self.error_occurred.emit("未选择任何产品")
return
try:
self._repo.update_product(self._current_product_id, form_data)
self.status_message.emit("保存成功!")
# 重新加载一次确保数据一致性
self.select_product(self._current_product_id)
except Exception as e:
self.error_occurred.emit(f"保存失败: {str(e)}")src/views/bldc_page.py彻底解耦。它不知道数据库,也不知道 Repository,只认识 ViewModel。
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QGridLayout,
QLabel, QLineEdit, QPushButton, QComboBox, QMessageBox
)
from PyQt6.QtCore import pyqtSlot
from src.viewmodels.product_vm import ProductViewModel
class BLDCPage(QWidget):
def __init__(self, viewModel: ProductViewModel):
super().__init__()
self._vm = viewModel
self.fields_map = {} # 存储输入框引用
self._setup_ui()
self._bind_view_model() # ✅ 绑定信号
def _setup_ui(self):
layout = QVBoxLayout(self)
# 1. 顶部选择区
top_layout = QHBoxLayout()
self.series_combo = QComboBox()
self.product_combo = QComboBox()
self.save_btn = QPushButton("保存修改")
top_layout.addWidget(QLabel("系列:"))
top_layout.addWidget(self.series_combo)
top_layout.addWidget(QLabel("产品:"))
top_layout.addWidget(self.product_combo)
top_layout.addWidget(self.save_btn)
layout.addLayout(top_layout)
# 2. 参数表单区 (示例)
form_group = QGroupBox("核心参数")
grid = QGridLayout(form_group)
# 定义你要显示的字段 (Label, DB_Field_Key)
fields = [
("型号", "model_number"),
("额定电压(V)", "rated_voltage"),
("额定转速(rpm)", "rated_speed"),
("价格($)", "price"), # 这是 Detail 表的字段,完全没问题
("备注", "notes")
]
for i, (label, key) in enumerate(fields):
grid.addWidget(QLabel(label), i, 0)
edit = QLineEdit()
self.fields_map[key] = edit
grid.addWidget(edit, i, 1)
layout.addWidget(form_group)
layout.addStretch()
# 3. 连接 UI 事件到 VM 方法
# 注意:这里我们不写逻辑,只转发
self.series_combo.currentIndexChanged.connect(self._on_series_changed)
self.product_combo.currentIndexChanged.connect(self._on_product_changed)
self.save_btn.clicked.connect(self._on_save_clicked)
def _bind_view_model(self):
"""绑定 VM 信号到 UI 更新槽函数"""
self._vm.series_loaded.connect(self._update_series_list)
self._vm.products_loaded.connect(self._update_product_list)
self._vm.current_product_changed.connect(self._display_product)
self._vm.error_occurred.connect(lambda msg: QMessageBox.critical(self, "错误", msg))
# 初始加载
self._vm.load_initial_data()
# --- UI 事件处理 ---
def _on_series_changed(self):
sid = self.series_combo.currentData()
self._vm.load_products_by_series(sid)
def _on_product_changed(self):
pid = self.product_combo.currentData()
self._vm.select_product(pid)
def _on_save_clicked(self):
# 收集数据
data = {}
for key, widget in self.fields_map.items():
val = widget.text().strip()
if val: data[key] = val
# 发送给 VM
self._vm.save_current_product(data)
# --- 数据更新槽函数 ---
(list)
def _update_series_list(self, series_list):
self.series_combo.blockSignals(True)
self.series_combo.clear()
self.series_combo.addItem("所有系列", None)
for s in series_list:
self.series_combo.addItem(s.name, s.id)
self.series_combo.blockSignals(False)
(list)
def _update_product_list(self, product_list):
self.product_combo.blockSignals(True)
self.product_combo.clear()
self.product_combo.addItem("请选择...", None)
for p in product_list:
self.product_combo.addItem(p.model_number, p.id)
self.product_combo.blockSignals(False)
(object)
def _display_product(self, product):
"""将 Product 对象的数据填入表单"""
if not product:
for widget in self.fields_map.values(): widget.clear()
return
# 混合 product 和 detail 的属性
# 因为我们在 repository 处理了 detail 可能为空的情况,这里可以直接访问
detail = product.detail
for key, widget in self.fields_map.items():
val = None
# 先找 Product
if hasattr(product, key):
val = getattr(product, key)
# 再找 Detail
elif hasattr(detail, key):
val = getattr(detail, key)
widget.setText(str(val) if val is not None else "")src/main.pyimport sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
from src.data.database import initialize_db, close_db
from src.viewmodels.product_vm import ProductViewModel
from src.views.bldc_page import BLDCPage
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("BLDC Motor Manager (MVVM Refactored)")
self.resize(800, 600)
# 1. 创建 ViewModel
self.product_vm = ProductViewModel()
# 2. 创建 View,并注入 VM
self.main_view = BLDCPage(self.product_vm)
self.setCentralWidget(self.main_view)
# 3. 连接状态栏
self.status_label = QLabel("就绪")
self.statusBar().addWidget(self.status_label)
self.product_vm.status_message.connect(self.status_label.setText)
if __name__ == '__main__':
# 1. 初始化数据库
initialize_db()
# 2. 启动应用
app = QApplication(sys.argv)
window = MainWindow()
window.show()
try:
sys.exit(app.exec())
finally:
# 3. 清理资源
close_db()Repository 处理了“脏活累活”:repository.py 里的 get_product_by_id 自动检测并修复缺失的 ProductDetail。你再也不会看到 product.detail.get() 报错了。
ViewModel 统一了状态:UI 不再自己瞎改数据,而是收集数据传给 VM,VM 保存成功后再通知 UI 刷新。这保证了界面和数据库永远同步。
UI 极其清爽:BLDCPage 里没有一行 SQL 代码,也没有复杂的逻辑判断,只负责“显示”和“发信号”。
易于扩展:如果你想加一个新的仿真功能,只需写一个 SimulationViewModel
由于我无法直接接收你的文件包,我将基于你之前提供的代码片段,为你构建一个完整的、经过重构的核心代码骨架。
你可以直接在你的项目中建立文件夹,复制粘贴这些代码。
请按照这个结构整理你的文件:
src/
├── data/
│ ├── __init__.py
│ ├── database.py # 数据库连接与模型定义 (原 model.py)
│ └── repository.py # ✅ 核心:负责所有增删改查,隔离 SQL 细节
├── viewmodels/
│ ├── __init__.py
│ └── product_vm.py # ✅ 核心:业务逻辑,View 和 Repository 的桥梁
├── views/
│ ├── __init__.py
│ └── bldc_page.py # ✅ 界面:只负责显示,不处理逻辑
└── main.py # 程序入口,依赖注入src/data/database.py保持模型定义纯净,去掉所有业务逻辑函数。
import os
from datetime import datetime
from peewee import (
Model, SqliteDatabase, CharField, TextField, IntegerField,
FloatField, DateTimeField, ForeignKeyField
)
# 1. 数据库路径设置
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 回退一级到 src
db_path = os.path.join(base_dir, "..", "products_final.db") # 假设 db 在 src 同级
# 2. 初始化数据库 (开启外键支持)
db = SqliteDatabase(db_path, pragmas={'foreign_keys': 1})
class BaseModel(Model):
class Meta:
database = db
class ProductSeries(BaseModel):
name = CharField(unique=True)
description = TextField(null=True, default="")
class Product(BaseModel):
series = ForeignKeyField(ProductSeries, backref='products', null=True, on_delete='SET NULL')
model_number = CharField(unique=True)
# ... (保留你其他的字段定义,这里为了省流省略,实际代码请补全) ...
rated_voltage = FloatField(null=True)
rated_speed = IntegerField(null=True)
rated_torque = FloatField(null=True)
created_at = DateTimeField(default=datetime.now)
class ProductDetail(BaseModel):
product = ForeignKeyField(Product, backref='detail', unique=True, on_delete='CASCADE')
# ... (保留你其他的字段定义) ...
price = FloatField(null=True)
poles = IntegerField(null=True)
notes = TextField(null=True, default="")
def initialize_db():
db.connect(reuse_if_open=True)
db.create_tables([ProductSeries, Product, ProductDetail], safe=True)
# 确保至少有一个系列
if ProductSeries.select().count() == 0:
ProductSeries.create(name="默认系列", description="系统自动创建")
def close_db():
if not db.is_closed():
db.close()src/data/repository.py (关键文件)这是解决你之前报错的“门神”。它确保了无论数据库里有没有 Detail,代码都不会崩。
import logging
from typing import Optional, Dict, Any
from peewee import IntegrityError, JOIN
from .database import db, Product, ProductDetail, ProductSeries
class ProductRepository:
"""
仓库模式:负责所有直接的数据库操作。
UI 和 ViewModel 不应该知道 SQL 或 Peewee 的存在。
"""
def get_all_series(self):
return list(ProductSeries.select())
def get_products_by_series(self, series_id: Optional[int]):
query = Product.select()
if series_id:
query = query.where(Product.series == series_id)
return list(query.order_by(Product.model_number))
def get_product_by_id(self, product_id: int) -> Optional[Product]:
"""
获取产品及其详情。
✅ 修复点:安全处理 Detail 缺失的情况。
"""
try:
# 1. 尝试查询
product = (Product.select(Product, ProductDetail)
.join(ProductDetail, JOIN.LEFT_OUTER)
.where(Product.id == product_id)
.get())
# 2. ✅ 关键修复:检查 detail 是否真的存在
# 这里的逻辑是:如果数据库查出来 detail 是 None (因为 LEFT JOIN),
# 我们就在内存里给它挂一个临时的空对象,防止 UI 访问报错。
# 或者,你可以选择在这里直接插入一条空记录。
try:
if product.detail is None:
raise ProductDetail.DoesNotExist
except ProductDetail.DoesNotExist:
logging.warning(f"Product {product_id} missing detail. Creating default.")
# 自动在数据库补全缺失的记录
ProductDetail.create(product=product)
# 重新查询以获取完整对象
return self.get_product_by_id(product_id)
return product
except Product.DoesNotExist:
return None
def update_product(self, product_id: int, updates: Dict[str, Any]):
"""
更新产品。
✅ 修复点:使用 _meta.fields 精确匹配字段。
"""
product = self.get_product_by_id(product_id)
if not product:
raise ValueError("Product not found")
detail = product.detail # get_product_by_id 保证了这里一定有值
product_dirty = False
detail_dirty = False
# 获取模型字段定义
p_fields = Product._meta.fields
d_fields = ProductDetail._meta.fields
for key, value in updates.items():
if key in ('id', 'created_at', 'series', 'product'): continue
# 更新主表
if key in p_fields:
if str(getattr(product, key)) != str(value):
setattr(product, key, value)
product_dirty = True
# 更新附表
elif key in d_fields:
if str(getattr(detail, key)) != str(value):
setattr(detail, key, value)
detail_dirty = True
with db.atomic():
if product_dirty: product.save()
if detail_dirty: detail.save()
return productsrc/viewmodels/product_vm.py这是 MVVM 的核心。它持有 UI 的状态,并处理业务逻辑。
from PyQt6.QtCore import QObject, pyqtSignal
from src.data.repository import ProductRepository
class ProductViewModel(QObject):
# --- 信号定义 ---
series_loaded = pyqtSignal(list) # 系列列表已加载
products_loaded = pyqtSignal(list) # 产品列表已加载
current_product_changed = pyqtSignal(object) # 当前选中的产品变了 (传回 Product 对象)
status_message = pyqtSignal(str) # 状态栏消息
error_occurred = pyqtSignal(str) # 错误弹窗
def __init__(self):
super().__init__()
self._repo = ProductRepository()
self._current_product_id = None
def load_initial_data(self):
"""初始化加载系列"""
series = self._repo.get_all_series()
self.series_loaded.emit(series)
# 默认加载所有产品
self.load_products_by_series(None)
def load_products_by_series(self, series_id):
"""根据系列加载产品列表"""
products = self._repo.get_products_by_series(series_id)
self.products_loaded.emit(products)
def select_product(self, product_id):
"""
用户选中了某个产品 ID
"""
self._current_product_id = product_id
if product_id is None:
self.current_product_changed.emit(None)
return
product = self._repo.get_product_by_id(product_id)
if product:
self.current_product_changed.emit(product)
self.status_message.emit(f"已加载: {product.model_number}")
else:
self.error_occurred.emit("无法加载产品数据")
def save_current_product(self, form_data: dict):
"""
保存当前产品的修改
"""
if not self._current_product_id:
self.error_occurred.emit("未选择任何产品")
return
try:
self._repo.update_product(self._current_product_id, form_data)
self.status_message.emit("保存成功!")
# 重新加载一次确保数据一致性
self.select_product(self._current_product_id)
except Exception as e:
self.error_occurred.emit(f"保存失败: {str(e)}")src/views/bldc_page.py彻底解耦。它不知道数据库,也不知道 Repository,只认识 ViewModel。
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QGridLayout,
QLabel, QLineEdit, QPushButton, QComboBox, QMessageBox
)
from PyQt6.QtCore import pyqtSlot
from src.viewmodels.product_vm import ProductViewModel
class BLDCPage(QWidget):
def __init__(self, viewModel: ProductViewModel):
super().__init__()
self._vm = viewModel
self.fields_map = {} # 存储输入框引用
self._setup_ui()
self._bind_view_model() # ✅ 绑定信号
def _setup_ui(self):
layout = QVBoxLayout(self)
# 1. 顶部选择区
top_layout = QHBoxLayout()
self.series_combo = QComboBox()
self.product_combo = QComboBox()
self.save_btn = QPushButton("保存修改")
top_layout.addWidget(QLabel("系列:"))
top_layout.addWidget(self.series_combo)
top_layout.addWidget(QLabel("产品:"))
top_layout.addWidget(self.product_combo)
top_layout.addWidget(self.save_btn)
layout.addLayout(top_layout)
# 2. 参数表单区 (示例)
form_group = QGroupBox("核心参数")
grid = QGridLayout(form_group)
# 定义你要显示的字段 (Label, DB_Field_Key)
fields = [
("型号", "model_number"),
("额定电压(V)", "rated_voltage"),
("额定转速(rpm)", "rated_speed"),
("价格($)", "price"), # 这是 Detail 表的字段,完全没问题
("备注", "notes")
]
for i, (label, key) in enumerate(fields):
grid.addWidget(QLabel(label), i, 0)
edit = QLineEdit()
self.fields_map[key] = edit
grid.addWidget(edit, i, 1)
layout.addWidget(form_group)
layout.addStretch()
# 3. 连接 UI 事件到 VM 方法
# 注意:这里我们不写逻辑,只转发
self.series_combo.currentIndexChanged.connect(self._on_series_changed)
self.product_combo.currentIndexChanged.connect(self._on_product_changed)
self.save_btn.clicked.connect(self._on_save_clicked)
def _bind_view_model(self):
"""绑定 VM 信号到 UI 更新槽函数"""
self._vm.series_loaded.connect(self._update_series_list)
self._vm.products_loaded.connect(self._update_product_list)
self._vm.current_product_changed.connect(self._display_product)
self._vm.error_occurred.connect(lambda msg: QMessageBox.critical(self, "错误", msg))
# 初始加载
self._vm.load_initial_data()
# --- UI 事件处理 ---
def _on_series_changed(self):
sid = self.series_combo.currentData()
self._vm.load_products_by_series(sid)
def _on_product_changed(self):
pid = self.product_combo.currentData()
self._vm.select_product(pid)
def _on_save_clicked(self):
# 收集数据
data = {}
for key, widget in self.fields_map.items():
val = widget.text().strip()
if val: data[key] = val
# 发送给 VM
self._vm.save_current_product(data)
# --- 数据更新槽函数 ---
(list)
def _update_series_list(self, series_list):
self.series_combo.blockSignals(True)
self.series_combo.clear()
self.series_combo.addItem("所有系列", None)
for s in series_list:
self.series_combo.addItem(s.name, s.id)
self.series_combo.blockSignals(False)
(list)
def _update_product_list(self, product_list):
self.product_combo.blockSignals(True)
self.product_combo.clear()
self.product_combo.addItem("请选择...", None)
for p in product_list:
self.product_combo.addItem(p.model_number, p.id)
self.product_combo.blockSignals(False)
(object)
def _display_product(self, product):
"""将 Product 对象的数据填入表单"""
if not product:
for widget in self.fields_map.values(): widget.clear()
return
# 混合 product 和 detail 的属性
# 因为我们在 repository 处理了 detail 可能为空的情况,这里可以直接访问
detail = product.detail
for key, widget in self.fields_map.items():
val = None
# 先找 Product
if hasattr(product, key):
val = getattr(product, key)
# 再找 Detail
elif hasattr(detail, key):
val = getattr(detail, key)
widget.setText(str(val) if val is not None else "")src/main.py我已经仔细阅读了你提供的所有源代码。代码量很大,逻辑比较完整,但正如之前分析的,存在**Controller 负担过重**、**DataService 与 Database 数据双重来源**、**View 直接处理逻辑**等架构问题。
既然你已经提供了源码,我不需要你再提供任何东西了。我将基于你现有的代码,为你生成一套**核心的重构代码**。
**注意:** 为了保持回答的长度可读且可执行,我不会复制粘贴原本用于“创建布局”的几百行 UI 代码(例如 `_create_form_grid`),因为那些不需要改。我主要重写**数据流向**和**逻辑控制**的部分。
请按照以下步骤,在你的项目中创建文件夹并填入代码。
---
### 📂 1. 新的项目结构
请务必按照这个结构来组织文件:
```text
src/
├── main.py # 程序入口
├── data/ # 数据层
│ ├── __init__.py
│ ├── model.py # (原 model.py) 只保留 Class 定义,删除下面的所有函数
│ └── repository.py # ✅ 新增:负责所有增删改查
├── viewmodels/ # 逻辑层
│ ├── __init__.py
│ ├── product_vm.py # ✅ 新增:管理 CustomerRequirementsPage 的逻辑
│ └── simulation_vm.py # ✅ 新增:替代 DataService,管理 BLDCPage 和 Worker
├── views/ # 界面层
│ ├── __init__.py
│ ├── main_view.py # (原 ui/main_view.py)
│ ├── design_target_page.py # (原 design_target_page.py) 修改对接 VM
│ ├── bldc_page.py # (原 bldc_page.py) 修改对接 VM
│ └── product_detail_page.py # (原 product_detail_page.py)
└── utils/ # 工具层 (保持你现有的不变)
├── worker.py
├── normalize.py
└── maxwell_modules/ ...
```
---
### 🧱 2. Data Layer (数据仓库层)
#### `src/data/model.py`
**操作**:保留你原来的 `BaseModel`, `ProductSeries`, `Product`, `ProductDetail`, `initialize_database`, `close_connection`。
**删除**:删除文件下方所有的 `find_product_by_id`, `create_full_product` 等函数,因为我们要把它们移到 `repository.py` 中。
#### `src/data/repository.py` (新增)
这是解决空指针报错和统一数据访问的关键。
```python
import logging
from peewee import JOIN, IntegrityError, DoesNotExist
from .model import db, Product, ProductDetail, ProductSeries
class ProductRepository:
"""负责所有数据库交互,View 和 ViewModel 不直接操作 Model"""
def get_all_series(self):
return list(ProductSeries.select().order_by(ProductSeries.name))
def get_products_by_series(self, series_id):
query = Product.select()
if series_id and series_id != 0:
query = query.where(Product.series == series_id)
return list(query.order_by(Product.model_number))
def get_product_by_id(self, product_id: int):
"""获取产品,如果详情不存在则自动修复"""
try:
# 左连接查询
product = (Product.select(Product, ProductDetail)
.join(ProductDetail, JOIN.LEFT_OUTER)
.where(Product.id == product_id)
.get())
# ✅ 自动修复逻辑:如果 detail 为空,自动创建
# 注意:Peewee 的 LEFT JOIN 如果没查到,访问 product.detail 可能会抛错或为 None
try:
if product.detail is None:
raise DoesNotExist
except (DoesNotExist, AttributeError):
logging.warning(f"Product {product_id} missing detail. Auto-creating.")
ProductDetail.create(product=product)
# 重新查询以确保对象完整
return self.get_product_by_id(product_id)
return product
except DoesNotExist:
return None
def create_product(self, series_id: int, model_number: str):
"""创建新产品"""
series = ProductSeries.get_by_id(series_id)
with db.atomic():
product = Product.create(series=series, model_number=model_number)
ProductDetail.create(product=product) # 必须同时创建详情
return product
def update_product(self, product_id: int, updates: dict):
"""更新产品,使用 _meta.fields 过滤字段"""
product = self.get_product_by_id(product_id)
if not product: return False
detail = product.detail
p_dirty, d_dirty = False, False
# 字段映射
p_fields = Product._meta.fields
d_fields = ProductDetail._meta.fields
for key, value in updates.items():
if key in ('id', 'created_at', 'series', 'product'): continue
# 转为字符串比较,防止 10 != 10.0 的问题
str_val = str(value) if value is not None else ""
if key in p_fields:
old_val = str(getattr(product, key)) if getattr(product, key) is not None else ""
if old_val != str_val:
setattr(product, key, value)
p_dirty = True
elif key in d_fields:
old_val = str(getattr(detail, key)) if getattr(detail, key) is not None else ""
if old_val != str_val:
setattr(detail, key, value)
d_dirty = True
with db.atomic():
if p_dirty: product.save()
if d_dirty: detail.save()
return True
def delete_product(self, product_id: int):
Product.delete().where(Product.id == product_id).execute()
```
---
### 🧠 3. ViewModel Layer (逻辑层)
#### `src/viewmodels/product_vm.py` (新增)
这个类替代了 `Controller` 中关于“客户需求页”的所有逻辑。
```python
from PyQt6.QtCore import QObject, pyqtSignal
from src.data.repository import ProductRepository
from src.data.model import Product
class ProductViewModel(QObject):
# 信号
series_list_updated = pyqtSignal(list)
product_list_updated = pyqtSignal(list)
current_product_changed = pyqtSignal(object) # 发送 Product 对象
status_msg = pyqtSignal(str)
error_msg = pyqtSignal(str)
def __init__(self):
super().__init__()
self.repo = ProductRepository()
self._current_product_id = None
def load_init_data(self):
series = self.repo.get_all_series()
self.series_list_updated.emit(series)
self.filter_products_by_series(0) # 0 代表全部
def filter_products_by_series(self, series_id):
products = self.repo.get_products_by_series(series_id)
self.product_list_updated.emit(products)
def select_product(self, product_id):
self._current_product_id = product_id
if not product_id:
self.current_product_changed.emit(None)
self.status_msg.emit("就绪")
return
product = self.repo.get_product_by_id(product_id)
if product:
self.current_product_changed.emit(product)
self.status_msg.emit(f"已加载: {product.model_number}")
else:
self.error_msg.emit("加载产品失败")
def create_new_product(self, series_id, model_number):
try:
new_prod = self.repo.create_product(series_id, model_number)
self.filter_products_by_series(series_id) # 刷新列表
self.status_msg.emit(f"产品 {model_number} 创建成功")
return new_prod.id
except Exception as e:
self.error_msg.emit(f"创建失败: {str(e)}")
return None
def save_changes(self, form_data: dict):
if not self._current_product_id:
return
try:
self.repo.update_product(self._current_product_id, form_data)
self.status_msg.emit("保存成功")
# 重新加载以刷新界面显示
self.select_product(self._current_product_id)
except Exception as e:
self.error_msg.emit(f"保存失败: {str(e)}")
def delete_current_product(self):
if self._current_product_id:
self.repo.delete_product(self._current_product_id)
self.filter_products_by_series(0) # 刷新
self.select_product(None)
self.status_msg.emit("产品已删除")
```
#### `src/viewmodels/simulation_vm.py` (新增)
这个类替代了 `DataService` 和 `bldc_page` 中的逻辑部分。它实现了**单一数据源**。
```python
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
from src.utils.worker import MaxwellWorker
from src.utils.maxwell_modules.maxwell_config import MaxwellConfig
from src.data.model import Product
from pathlib import Path
class SimulationViewModel(QObject):
# 信号
parameter_changed = pyqtSignal(str, object) # key, value
log_received = pyqtSignal(str)
progress_updated = pyqtSignal(int)
simulation_finished = pyqtSignal(object) # fig
simulation_error = pyqtSignal(str)
def __init__(self):
super().__init__()
# 替代 DataService,这里是所有参数的唯一真实来源
self.parameters = {}
self.worker = MaxwellWorker()
# 连接 Worker 信号
self.worker.log_message.connect(self.log_received)
self.worker.progress_updated.connect(self.progress_updated)
self.worker.finished.connect(self.simulation_finished)
self.worker.error.connect(self.simulation_error)
def load_product(self, product: Product):
"""当用户在主界面切换产品时调用"""
self.parameters.clear()
if not product: return
# 从 Product 对象加载数据到 parameters 字典
# 这里你可以复用 normalize.py 或者手动映射
d_dict = product.detail.__data__ if product.detail else {}
p_dict = product.__data__
# 合并并存入 self.parameters
self.parameters.update(p_dict)
self.parameters.update(d_dict)
# 触发 UI 更新(如果有需要的话,或者 View 直接监听 ProductVM)
def update_parameter(self, key, value):
"""UI 输入框变化时调用"""
self.parameters[key] = value
# 这里可以加逻辑:如果 value 变了,是否需要通知其他地方?
def start_simulation(self, project_path, project_name):
if self.worker.isRunning():
return
config = MaxwellConfig(project_path, Path(project_path).parent,
project_name, "RMxprtDesign", "Setup")
# 准备数据:self.parameters 就是最新的数据,因为 input 改动时同步更新了
self.worker.start()
self.worker.execute_simulation_task(config, self.parameters)
def cleanup(self):
self.worker.cleanup()
```
---
### 🎨 4. View Layer (界面层改造)
#### `src/views/design_target_page.py` (改造要点)
1. **构造函数**:接收 `ProductViewModel`。
2. **删除**:删除 `DataService`。
3. **连接**:
* 下拉框 `currentIndexChanged` -> 调用 `vm.select_product(id)`。
* 保存按钮 `clicked` -> 收集 `self.fields_map` 数据 -> 调用 `vm.save_changes(data)`。
* VM 信号 `current_product_changed` -> 触发 `load_data_to_form(product)`。
```python
# 代码片段示例
class CustomerRequirementsPage(QWidget):
def __init__(self, viewModel):
super().__init__()
self.vm = viewModel
# ... UI 初始化 ...
# 绑定
self.vm.product_list_updated.connect(self.update_product_combo_box)
self.vm.current_product_changed.connect(self.load_data_to_form)
self.save_button.clicked.connect(self._on_save)
def _on_save(self):
data, _ = self.get_data_from_form() # 复用你现有的收集逻辑
# 把两个字典合二为一传给 VM
full_data = {**data[0], **data[1]}
self.vm.save_changes(full_data)
```
#### `src/views/bldc_page.py` (改造要点)
1. **构造函数**:接收 `SimulationViewModel`。
2. **删除**:删除 `DataService`, `MaxwellWorker` (因为移到了 VM)。
3. **修改输入框**:`textChanged` 事件不再更新 `DataService`,而是调用 `self.vm.update_parameter(key, value)`。
4. **修改开始按钮**:调用 `self.vm.start_simulation(...)`。
```python
# 代码片段示例
class MaxwellApp(QWidget):
def __init__(self, sim_vm):
super().__init__()
self.vm = sim_vm
# ... UI 初始化 ...
# 绑定
self.vm.log_received.connect(self.log_output.append)
self.vm.simulation_finished.connect(self._on_sim_finished)
def _on_text_changed(self, key, text):
# ... 类型转换 ...
self.vm.update_parameter(key, value)
def _start_simulation(self):
# ... 检查路径 ...
self.vm.start_simulation(self.selected_project_path, self.project_name)
```
---
### 🚀 5. Main Entry (入口)
#### `src/main.py`
这是组装所有组件的地方。
```python
import sys
from PyQt6.QtWidgets import QApplication
from src.data.model import initialize_database, close_connection
from src.viewmodels.product_vm import ProductViewModel
from src.viewmodels.simulation_vm import SimulationViewModel
from src.views.main_view import MainView
def main():
app = QApplication(sys.argv)
# 1. 初始化 DB
initialize_database()
# 2. 创建 ViewModels
product_vm = ProductViewModel()
sim_vm = SimulationViewModel()
# 3. 关联 VM:当产品改变时,通知仿真 VM 加载新数据
product_vm.current_product_changed.connect(sim_vm.load_product)
# 4. 创建 MainView (需要修改 MainView 构造函数来接收 VMs)
# 或者 MainView 创建子页面时,把 vm 传进去
main_window = MainView(product_vm, sim_vm)
main_window.show()
# 5. 初始加载数据
product_vm.load_init_data()
exit_code = app.exec()
# 6. 清理
sim_vm.cleanup()
close_connection()
sys.exit(exit_code)
if __name__ == "__main__":
main()
```
### 修改 `MainView` (src/views/main_view.py)
你需要稍微改一下 `MainView` 的 `__init__` 和 `_create_main_content_stack`:
```python
class MainView(QMainWindow):
def __init__(self, product_vm, sim_vm): # 接收 VMs
super().__init__()
self.product_vm = product_vm
self.sim_vm = sim_vm
# ... 其他初始化 ...
self.main_content_stack = self._create_main_content_stack()
# ...
def _create_main_content_stack(self):
stack = QStackedWidget()
# 注入 VM 到子页面
self.customer_page = CustomerRequirementsPage(self.product_vm)
self.bldc_page = MaxwellApp(self.sim_vm)
self.detail_page = ProductDetailPage() # 这个页面比较简单,可以后续再加绑定
stack.addWidget(self.customer_page)
stack.addWidget(self.bldc_page)
stack.addWidget(self.detail_page)
return stack
```
### 总结
这套方案直接使用了你提供的 Peewee Model 定义和 PyQt 界面代码,但重构了**连接方式**。
1. **Repository** 帮你自动处理了 `product.detail` 为空的情况,消灭了空指针 BUG。
2. **ProductViewModel** 统一了数据库读写逻辑,不再依赖 `controller.py`。
3. **SimulationViewModel** 统一了仿真参数,替代了 `DataService`,解决了“数据不同步”的问题。
4. **MainView** 负责依赖注入,让各个组件松耦合。Repository 处理了“脏活累活”:repository.py 里的 get_product_by_id 自动检测并修复缺失的 ProductDetail。你再也不会看到 product.detail.get() 报错了。
ViewModel 统一了状态:UI 不再自己瞎改数据,而是收集数据传给 VM,VM 保存成功后再通知 UI 刷新。这保证了界面和数据库永远同步。
UI 极其清爽:BLDCPage
易于扩展:如果你想加一个新的仿真功能,只需写一个 SimulationViewModel
n5321 | 2025年11月22日 00:25
目标:增加核心参数的一次性罗列!在每一次call Ansys maxwell 完成计算之后,获取结果参数,call back 到UI上!
方式:核心参数还是用qgrid生成,保持动态特性。价值是维护方便

数据存在df_for_detail 的变量里面!
暂时的问题是对数据结构的设计,需要呈现哪些data还需要仔细的思考!
n5321 | 2025年11月2日 23:29
目标:在matplotlib的曲线图里添加对照组。
方式:在UI里添加button,选择,读取csv文档 add traces,沿用原始的chart设置!
测试idea: 写了一个try_compare.py,UI, path齐全,分分钟搞定了!
实操:添加进AutoEM。
完全不是那么回事!
try_compare写的logic 跟AutoEM完全不同!try_compare在一段script把所有的东西都包括进去了,所以debug 也极其简单!
在AutoEM中有一个很复杂的操作,因为数据和控制要传到到N个object!他具体的logi大概如下:
page默认生成一个chart! chart是在fig 里面!
UI提供chart的框子——准备填matplot的figure,设置成canva
worker get data
plotting tool 升成figure(用新曲线完全替代)
再返回到worker
UI清楚原来的figure,添加新的figure
尝试实操!
更改logic!不在替换figure,考虑用同一个figure,在figure的框架里做删改traces!
问题是figure里面对于chart的设置有大量的代码,把设置代码从plotting_tool改到init之中,后续recall的时候又会用默认设置。
放弃新idea,继续用figure替换的思路。
未做深入思考,考虑增加一个function,plotting_tool生成sim chart,增加一个ref_chart的功能几乎一致的function,来生成ref chart。然后卡住了。
晚上睡觉才想起本质上是替换的,把设置全部堆到plotting_tool才是正常操作!
增加新功能,UI里添加button,click以后,可以看到ref chart!
需要把UI get 到的data 传递到worker,worker生成fig,再传递会UI
本质上我对signal and slot的逻辑没有搞清楚。
问题:在这种细节繁复的装配体结构中,数据的传递,logic线的设计,是架构师内容。还蛮麻烦的!
自己对QT的框架理解本质上不够!
还是需要深入阅读代码!
n5321 | 2025年11月2日 10:42
matlab 有invoke 函数来探测COM!
在python 下面用的dir
dir(self.maxwell_app) ['Design', 'Desktop', 'Editor', 'Module', 'Project', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'analyze_design', 'connect_maxwell', 'export_reports', 'iMaxwell', 'open_project_and_design', 'release_maxwell', 'set_properties']
具体来看:
| 属性名 | 类型/用途 | 说明 |
|---|---|---|
Desktop | AEDT 桌面对象(oDesktop) | 控制 Maxwell AEDT 会话、项目打开、退出等操作。 |
Project | AEDT 工程对象(oProject) | 当前打开的 Maxwell 工程。 |
Design | AEDT 设计对象(oDesign) | 当前工程下的具体设计,如“RMxprtDesign”或“Maxwell3DDesign”。 |
Editor | AEDT 编辑器对象(oEditor) | 用于修改几何、材料、属性、参数。通常是 "3D Modeler" 编辑器。 |
Module | AEDT 模块对象(oModule) | 可调用报告、求解、场后处理等功能模块,如 "ReportSetup"、"FieldsReporter" 等。 |
iMaxwell | COM 接口对象 | 由 win32com.client.Dispatch("Ansoft.ElectronicsDesktop") 获取,用于连接 AEDT。 |
| 方法名 | 主要功能 | 典型调用示例 |
|---|---|---|
__init__() | 初始化 MaxwellAutomation 实例 | app = MaxwellAutomation() |
connect_maxwell() | 建立与 Maxwell AEDT 的 COM 连接 | app.connect_maxwell() |
open_project_and_design(config) | 打开指定工程与设计 | app.open_project_and_design(config) |
set_properties(property_changes) | 批量修改参数属性 | app.set_properties(property_changes) |
analyze_design(setup_name) | 启动指定 Setup 的仿真 | app.analyze_design("Setup1") |
export_reports(config) | 导出仿真结果报告或数据文件 | app.export_reports(config) |
release_maxwell() | 关闭并释放 Maxwell 会话 | app.release_maxwell() |
这些是 Python 自动提供的类方法,用于对象描述、比较、属性访问等,一般不手动使用:
| 方法 | 功能 |
|---|---|
__init__ | 构造函数 |
__str__ / __repr__ | 控制打印输出格式 |
__eq__, __lt__, __gt__ | 对象比较 |
__getattribute__, __setattr__ | 控制属性访问 |
__dict__, __doc__ | 存放实例属性与文档说明 |
__sizeof__, __reduce__, __module__ | 内部运行支持 |
n5321 | 2025年10月30日 22:20
python通过com32 来更改ansys 参数的logic 跟matlab 确实不同!
一个简单的事情,居然还花了几个小时的时间!
背景:
1. matlab基本上copy record vbscript 就可以了!但是python 用的是不一样的logic!
通过com32, 他是直接插入到Ansys对象里面去的!一个com又已经被封装了! 我熟悉的是actxserver下面对ansys 的manipulate!
2. Ansys Maxwell下面的数据结构对于不同的参数来说,是不一样的!所以他需要设计不同的控制结构!开始考虑是做成一个数组结构,然后添加索引,match 到了以后直接调用。
问题是中间debug的过程比较麻烦!因为封装了,所以有点难把问题点拎出来!
logic! 暂时用的这个code 实现!
def set_properties(self, prop_changes):
"""Applies a list of property changes to the active design or editor."""
for prop_change in prop_changes:
changed_props = ["NAME:" + prop_change['name']]
print('更改的参数', changed_props)
if prop_change['name'] == 'Wire Size':
# Wire Size 复合属性
changed_props += [
"WireSizeWireDiameter:=", prop_change['value'],
"WireSizeGauge:=", "0",
"WireSizeWireWidth:=", "0mm",
"WireSizeWireThickness:=", "0mm",
"WireSizeMixedWireRectType:=", False,
["NAME:WireSizeMixedDiameter"],
["NAME:WireSizeMixedWidth"],
["NAME:WireSizeMixedThickness"],
["NAME:WireSizeMixedThicknessMixedFillet"],
["NAME:WireSizeMixedThicknessMixedNumber"]
]
print('wiresize data is', changed_props)
# 不同类型的参数修改方式
if 'type' in prop_change:
if prop_change['type'] == 'Value':
changed_props += ["Value:=", prop_change['value']]
elif prop_change['type'] == 'Material':
changed_props += ["Material:=", prop_change['value']]
elif prop_change['type'] == 'MustBeInt':
changed_props += [
"MustBeInt:=", prop_change.get('must_be_int', False),
"Value:=", prop_change['value']
]
else:
# 默认只写 Value
changed_props += ["Value:=", prop_change['value']]
# 选择目标对象
if prop_change['target'] == 'Editor':
self.Editor.ChangeProperty([
"NAME:AllTabs",
[
f"NAME:{prop_change['tab']}",
["NAME:PropServers", prop_change['prop_server']],
["NAME:ChangedProps", changed_props]
]
])
elif prop_change['target'] == 'Design':
self.Design.ChangeProperty([
"NAME:AllTabs",
[
f"NAME:{prop_change['tab']}",
["NAME:PropServers", prop_change['prop_server']],
["NAME:ChangedProps", [
"NAME:" + prop_change['name'],
"Value:=", prop_change['value']
]]
]
])
几个点记录一下!
1, 考虑更改的参数,暂时用的prop_changes来统一。里面去changed_props来做微调。
最后通过self.editer or self.design来更改。
2, 更改的参数点,需要把所以的参数都记录进来!
更改文档
maxwell_automation.py
ai_search_page.py
n5321 | 2025年10月26日 11:00
properties setup暂时用的这个function!
感觉是一个坑
def _get_property_changes_from_table(self) -> List[dict]:
"""
从参数表格中获取用户输入的属性修改。
返回一个字典列表,每个字典包含一个属性的修改信息。
"""
prop_changes = []
prop_map = {
"Reference Speed": {'target': 'Editor', 'tab': 'Machine', 'prop_server': 'Machine', 'type': 'Value'},
"Steel Type (Stator)": {'target': 'Editor', 'tab': 'Stator', 'prop_server': 'Stator', 'type': 'Material'},
"Steel Type (Rotor)": {'target': 'Editor', 'tab': 'Rotor', 'prop_server': 'Rotor', 'type': 'Material'},
"Embrace": {'target': 'Editor', 'tab': 'Pole', 'prop_server': 'Rotor:Pole', 'type': 'Value'},
"Magnet Thickness": {'target': 'Editor', 'tab': 'Pole', 'prop_server': 'Rotor:Pole', 'type': 'Value'},
"Rated Output Power": {'target': 'Design', 'tab': 'General', 'prop_server': 'AnalysisSetup:all', 'type': 'Value'},
"Rated Speed": {'target': 'Design', 'tab': 'General', 'prop_server': 'AnalysisSetup:Setup1', 'type': 'Value'},
}
for row in range(self.param_table.rowCount()):
name_item = self.param_table.item(row, 0)
value_item = self.param_table.item(row, 1)
if name_item and value_item:
name = name_item.text().strip()
value = value_item.text().strip()
if name in prop_map:
info = prop_map[name].copy()
info['name'] = name
info['value'] = value
prop_changes.append(info)
elif name: # 如果参数名不为空但不在映射中
self.log_output.append(f"<span style='color:orange;'>警告: 未知参数 '{name}',将跳过。</span>")
logging.warning(f"未知参数 '{name}',将跳过。")
logging.debug(f"Collected property changes: {prop_changes}")
return prop_changes
n5321 | 2025年10月21日 00:13