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
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
new page
chart的字体太大了!已经从12改到10号,怎么好像还是有点不好看!
核心参数的内容需要思考一下!
重点是要把对比体现出来!user的目标是设计最好的电机,最好是比较出来的!
日志的位置可以考虑调整,参数设置的地方需要更改!chart 那里考虑做tab!同时需要看的还有其他好几个图,比如齿槽扭力的chart就值得看一眼!这个改动感觉需要一两周才搞得完啊!
debug的工作量有点大!
n5321 | 2025年10月20日 00:30
问题:
数据获取已经搞定!
有了一个最简单的可以work的GUI版本!

问题:
结果图表需要整理!
Log,目前的log未填充文本进文档。需要添加进去!
参数不用table,用edit_line!_后续更改!
button keep one!
文档加密的问题!后续添加?keep MVP框架?!
n5321 | 2025年10月14日 00:00