修复线程bug,进度条结束后崩溃问题
parent
217290a76e
commit
8d489a499f
|
|
@ -138,38 +138,23 @@ class SQLServerWriter:
|
||||||
start_time = data.get('start_time')
|
start_time = data.get('start_time')
|
||||||
end_time = data.get('end_time')
|
end_time = data.get('end_time')
|
||||||
|
|
||||||
# 检查记录是否已存在(基于 order_no, start_time, end_time 组合)
|
# 检查记录是否已存在
|
||||||
|
# 目前数据库在 pump_600_no_load_run_in 上的唯一键约束是基于工单号(order_no),
|
||||||
|
# 所以这里优先按 order_no 检查是否存在,避免与数据库唯一键不一致导致插入冲突。
|
||||||
cursor = self.connection.cursor()
|
cursor = self.connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
# 构建查询条件
|
"SELECT id, start_time, end_time FROM pump_600_no_load_run_in WHERE order_no = ?",
|
||||||
if start_time and end_time:
|
(order_no,)
|
||||||
# 如果有开始和结束时间,使用三个字段组合查询
|
)
|
||||||
cursor.execute(
|
|
||||||
"""SELECT id FROM pump_600_no_load_run_in
|
|
||||||
WHERE order_no = ? AND start_time = ? AND end_time = ?""",
|
|
||||||
(order_no, start_time, end_time)
|
|
||||||
)
|
|
||||||
elif start_time:
|
|
||||||
# 只有开始时间
|
|
||||||
cursor.execute(
|
|
||||||
"""SELECT id FROM pump_600_no_load_run_in
|
|
||||||
WHERE order_no = ? AND start_time = ?""",
|
|
||||||
(order_no, start_time)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 只有工单号(向后兼容)
|
|
||||||
cursor.execute(
|
|
||||||
"SELECT id FROM pump_600_no_load_run_in WHERE order_no = ?",
|
|
||||||
(order_no,)
|
|
||||||
)
|
|
||||||
|
|
||||||
existing = cursor.fetchone()
|
existing = cursor.fetchone()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
existing_id = existing[0]
|
existing_id, existing_start, existing_end = existing
|
||||||
logger.info(
|
logger.info(
|
||||||
f"找到已存在的记录 (id={existing_id}, order_no={order_no}, "
|
f"找到已存在的记录 (id={existing_id}, order_no={order_no}, "
|
||||||
f"start_time={start_time}, end_time={end_time}),将更新数据"
|
f"start_time={existing_start}, end_time={existing_end}),将更新数据 "
|
||||||
|
f"(新 start_time={start_time}, 新 end_time={end_time})"
|
||||||
)
|
)
|
||||||
return self._update_pump_600_data_by_id(existing_id, data)
|
return self._update_pump_600_data_by_id(existing_id, data)
|
||||||
else:
|
else:
|
||||||
|
|
@ -277,10 +262,54 @@ class SQLServerWriter:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# 处理唯一键冲突:如果工单号已存在,则回退为更新逻辑
|
||||||
|
import pyodbc as _pyodbc # 局部导入以避免类型检查器告警
|
||||||
|
is_integrity_error = isinstance(e, _pyodbc.IntegrityError)
|
||||||
|
error_text = str(e)
|
||||||
|
if is_integrity_error and ("2627" in error_text or "2601" in error_text):
|
||||||
|
order_no = data.get('order_no')
|
||||||
|
logger.warning(
|
||||||
|
f"插入 SQL Server 数据时发生唯一键冲突,将尝试改为更新记录 "
|
||||||
|
f"(order_no={order_no}, 错误={error_text})"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# 查找已存在记录的ID(按唯一键 order_no)
|
||||||
|
cursor = self.connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM pump_600_no_load_run_in WHERE order_no = ?",
|
||||||
|
(order_no,)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
record_id = row[0]
|
||||||
|
logger.info(
|
||||||
|
f"找到冲突记录 (id={record_id}, order_no={order_no}),执行更新替代插入"
|
||||||
|
)
|
||||||
|
# 回滚当前事务后执行更新
|
||||||
|
try:
|
||||||
|
self.connection.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return self._update_pump_600_data_by_id(record_id, data)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"唯一键冲突但未能找到已存在记录 (order_no={order_no}),放弃更新"
|
||||||
|
)
|
||||||
|
except Exception as e2:
|
||||||
|
logger.error(
|
||||||
|
f"处理唯一键冲突为更新时失败: {e2}",
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.connection.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
logger.error(f"插入 SQL Server 数据失败: {e}", exc_info=True)
|
logger.error(f"插入 SQL Server 数据失败: {e}", exc_info=True)
|
||||||
try:
|
try:
|
||||||
self.connection.rollback()
|
self.connection.rollback()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
104
ui_main.py
104
ui_main.py
|
|
@ -1118,6 +1118,7 @@ class MainWindow(QMainWindow):
|
||||||
# 定义信号用于线程安全的UI更新
|
# 定义信号用于线程安全的UI更新
|
||||||
state_changed_signal = Signal(str, str) # old_state, new_state
|
state_changed_signal = Signal(str, str) # old_state, new_state
|
||||||
connection_changed_signal = Signal(bool, str) # is_connected, message
|
connection_changed_signal = Signal(bool, str) # is_connected, message
|
||||||
|
reload_experiments_signal = Signal()
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -1133,6 +1134,8 @@ class MainWindow(QMainWindow):
|
||||||
# 连接信号到槽函数
|
# 连接信号到槽函数
|
||||||
self.state_changed_signal.connect(self._handle_state_change_in_main_thread)
|
self.state_changed_signal.connect(self._handle_state_change_in_main_thread)
|
||||||
self.connection_changed_signal.connect(self._handle_connection_change_in_main_thread)
|
self.connection_changed_signal.connect(self._handle_connection_change_in_main_thread)
|
||||||
|
# 刷新实验列表必须在主线程执行(后台线程通过 emit 请求刷新)
|
||||||
|
self.reload_experiments_signal.connect(self._reload_experiments, Qt.QueuedConnection)
|
||||||
|
|
||||||
self.template_path: Path | None = None
|
self.template_path: Path | None = None
|
||||||
self.config: AppConfig = AppConfig()
|
self.config: AppConfig = AppConfig()
|
||||||
|
|
@ -2639,8 +2642,20 @@ class MainWindow(QMainWindow):
|
||||||
thread.quit()
|
thread.quit()
|
||||||
# 使用QTimer延迟等待,避免在信号槽中直接调用wait导致死锁
|
# 使用QTimer延迟等待,避免在信号槽中直接调用wait导致死锁
|
||||||
def do_wait():
|
def do_wait():
|
||||||
|
# 线程还在跑就继续等,避免 “QThread: Destroyed while thread is still running”
|
||||||
if thread.isRunning():
|
if thread.isRunning():
|
||||||
thread.wait(100) # 最多等待100ms
|
# 不在这里强行 deleteLater,等线程真正结束后再清理
|
||||||
|
QTimer.singleShot(50, do_wait)
|
||||||
|
return
|
||||||
|
# 线程已结束:请求在其所属线程中安全销毁
|
||||||
|
try:
|
||||||
|
worker.deleteLater()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
thread.deleteLater()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# 从列表中移除
|
# 从列表中移除
|
||||||
try:
|
try:
|
||||||
self._bg_threads.remove(thread)
|
self._bg_threads.remove(thread)
|
||||||
|
|
@ -2663,7 +2678,10 @@ class MainWindow(QMainWindow):
|
||||||
progress.show()
|
progress.show()
|
||||||
|
|
||||||
bridge = ProgressBridge()
|
bridge = ProgressBridge()
|
||||||
bridge.updated.connect(lambda msg, cur, total: progress.setLabelText(f"{msg} ({cur}/{total})"))
|
# 进度更新也可能来自后台线程,必须使用 QueuedConnection
|
||||||
|
def _on_progress_updated(msg: str, cur: int, total: int) -> None:
|
||||||
|
progress.setLabelText(f"{msg} ({cur}/{total})")
|
||||||
|
bridge.updated.connect(_on_progress_updated, Qt.QueuedConnection)
|
||||||
|
|
||||||
def _progress_text(msg: str, cur: int, total: int) -> None:
|
def _progress_text(msg: str, cur: int, total: int) -> None:
|
||||||
bridge.updated.emit(msg, cur, total)
|
bridge.updated.emit(msg, cur, total)
|
||||||
|
|
@ -2683,6 +2701,11 @@ class MainWindow(QMainWindow):
|
||||||
set_progress_callback(None)
|
set_progress_callback(None)
|
||||||
if not progress.wasCanceled() and not cancelled[0]:
|
if not progress.wasCanceled() and not cancelled[0]:
|
||||||
progress.close()
|
progress.close()
|
||||||
|
# 确保在主线程事件循环中销毁对话框,避免跨线程析构告警
|
||||||
|
try:
|
||||||
|
progress.deleteLater()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# 使用安全的清理方法
|
# 使用安全的清理方法
|
||||||
self._cleanup_thread_safe(thread, worker)
|
self._cleanup_thread_safe(thread, worker)
|
||||||
if cancelled[0]:
|
if cancelled[0]:
|
||||||
|
|
@ -2703,9 +2726,29 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
progress.canceled.connect(on_cancel)
|
progress.canceled.connect(on_cancel)
|
||||||
|
|
||||||
worker.finished.connect(lambda res: (None if cancelled[0] else (_cleanup(True), on_success(res))))
|
# 注意:worker 的信号在其所属线程中发射。为了避免跨线程操作 Qt UI(会导致卡死/警告),
|
||||||
worker.failed.connect(lambda msg: (None if cancelled[0] else (_cleanup(), (self.logger.error("Task failed: %s", msg), QMessageBox.critical(self, "错误", msg)))))
|
# 这里必须使用 QueuedConnection,确保回调在主线程事件循环中执行。
|
||||||
worker.cancelled.connect(lambda: (_cleanup() if not cancelled[0] else None))
|
def on_finished(res):
|
||||||
|
if cancelled[0]:
|
||||||
|
return
|
||||||
|
_cleanup(True)
|
||||||
|
on_success(res)
|
||||||
|
|
||||||
|
def on_failed(msg):
|
||||||
|
if cancelled[0]:
|
||||||
|
return
|
||||||
|
_cleanup()
|
||||||
|
self.logger.error("Task failed: %s", msg)
|
||||||
|
QMessageBox.critical(self, "错误", msg)
|
||||||
|
|
||||||
|
def on_cancelled():
|
||||||
|
if cancelled[0]:
|
||||||
|
return
|
||||||
|
_cleanup()
|
||||||
|
|
||||||
|
worker.finished.connect(on_finished, Qt.QueuedConnection)
|
||||||
|
worker.failed.connect(on_failed, Qt.QueuedConnection)
|
||||||
|
worker.cancelled.connect(on_cancelled, Qt.QueuedConnection)
|
||||||
self._bg_threads.append(thread)
|
self._bg_threads.append(thread)
|
||||||
self._bg_workers.append(worker)
|
self._bg_workers.append(worker)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
@ -3906,6 +3949,18 @@ class MainWindow(QMainWindow):
|
||||||
self._reload_experiments()
|
self._reload_experiments()
|
||||||
|
|
||||||
def _reload_experiments(self) -> None:
|
def _reload_experiments(self) -> None:
|
||||||
|
# 保护:如果被后台线程误调用,立即切回主线程执行
|
||||||
|
try:
|
||||||
|
if QThread.currentThread() != self.thread():
|
||||||
|
try:
|
||||||
|
self.reload_experiments_signal.emit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
# 线程检查失败时继续尝试执行(但正常情况下不会发生)
|
||||||
|
pass
|
||||||
|
|
||||||
# 更新配置对话框中的实验表格(如果存在)
|
# 更新配置对话框中的实验表格(如果存在)
|
||||||
if hasattr(self, "exp_table") and self.exp_table is not None:
|
if hasattr(self, "exp_table") and self.exp_table is not None:
|
||||||
try:
|
try:
|
||||||
|
|
@ -4687,9 +4742,11 @@ class MainWindow(QMainWindow):
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
self.logger.info(f"[SQL Server] 更新实验 {exp_id} 状态为: {status}")
|
self.logger.info(f"[SQL Server] 更新实验 {exp_id} 状态为: {status}")
|
||||||
|
# 刷新列表显示:可能从后台线程调用,必须通过信号切回主线程
|
||||||
# 刷新列表显示
|
try:
|
||||||
self._reload_experiments()
|
self.reload_experiments_signal.emit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"[SQL Server] 更新状态失败: {e}", exc_info=True)
|
self.logger.error(f"[SQL Server] 更新状态失败: {e}", exc_info=True)
|
||||||
|
|
@ -6072,6 +6129,37 @@ def run_app() -> None:
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 安装 Qt 消息处理器:当出现 “different thread” 等警告时打印 Python 调用栈与线程信息,
|
||||||
|
# 用于精确定位是哪段代码在后台线程操作了 UI。
|
||||||
|
try:
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
from PySide6.QtCore import qInstallMessageHandler
|
||||||
|
from logger import get_logger
|
||||||
|
|
||||||
|
_qt_logger = get_logger("qt")
|
||||||
|
|
||||||
|
def _qt_message_handler(msg_type, context, message):
|
||||||
|
try:
|
||||||
|
text = str(message)
|
||||||
|
# 只对高风险跨线程告警输出堆栈,避免日志爆炸
|
||||||
|
if "different thread" in text or "Timers cannot be started from another thread" in text:
|
||||||
|
_qt_logger.error(
|
||||||
|
"QtThreadWarning: %s | thread=%s (%s)",
|
||||||
|
text,
|
||||||
|
threading.current_thread().name,
|
||||||
|
threading.get_ident(),
|
||||||
|
)
|
||||||
|
_qt_logger.error("QtThreadWarning stack:\n%s", "".join(traceback.format_stack(limit=40)))
|
||||||
|
else:
|
||||||
|
_qt_logger.warning("Qt: %s", text)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
qInstallMessageHandler(_qt_message_handler)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 显示启动界面
|
# 显示启动界面
|
||||||
from splash_screen import SplashScreen
|
from splash_screen import SplashScreen
|
||||||
splash = SplashScreen()
|
splash = SplashScreen()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue