修复线程bug,进度条结束后崩溃问题

main
risingLee 2026-02-10 16:35:43 +08:00
parent 217290a76e
commit 8d489a499f
2 changed files with 151 additions and 34 deletions

View File

@ -138,38 +138,23 @@ class SQLServerWriter:
start_time = data.get('start_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()
# 构建查询条件
if start_time and end_time:
# 如果有开始和结束时间,使用三个字段组合查询
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 = ?",
"SELECT id, start_time, end_time FROM pump_600_no_load_run_in WHERE order_no = ?",
(order_no,)
)
existing = cursor.fetchone()
if existing:
existing_id = existing[0]
existing_id, existing_start, existing_end = existing
logger.info(
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)
else:
@ -277,10 +262,54 @@ class SQLServerWriter:
return True
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)
try:
self.connection.rollback()
except:
except Exception:
pass
return False

View File

@ -1118,6 +1118,7 @@ class MainWindow(QMainWindow):
# 定义信号用于线程安全的UI更新
state_changed_signal = Signal(str, str) # old_state, new_state
connection_changed_signal = Signal(bool, str) # is_connected, message
reload_experiments_signal = Signal()
def __init__(self) -> None:
super().__init__()
@ -1133,6 +1134,8 @@ class MainWindow(QMainWindow):
# 连接信号到槽函数
self.state_changed_signal.connect(self._handle_state_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.config: AppConfig = AppConfig()
@ -2639,8 +2642,20 @@ class MainWindow(QMainWindow):
thread.quit()
# 使用QTimer延迟等待避免在信号槽中直接调用wait导致死锁
def do_wait():
# 线程还在跑就继续等,避免 “QThread: Destroyed while thread is still running”
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:
self._bg_threads.remove(thread)
@ -2663,7 +2678,10 @@ class MainWindow(QMainWindow):
progress.show()
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:
bridge.updated.emit(msg, cur, total)
@ -2683,6 +2701,11 @@ class MainWindow(QMainWindow):
set_progress_callback(None)
if not progress.wasCanceled() and not cancelled[0]:
progress.close()
# 确保在主线程事件循环中销毁对话框,避免跨线程析构告警
try:
progress.deleteLater()
except Exception:
pass
# 使用安全的清理方法
self._cleanup_thread_safe(thread, worker)
if cancelled[0]:
@ -2703,9 +2726,29 @@ class MainWindow(QMainWindow):
progress.canceled.connect(on_cancel)
worker.finished.connect(lambda res: (None if cancelled[0] else (_cleanup(True), on_success(res))))
worker.failed.connect(lambda msg: (None if cancelled[0] else (_cleanup(), (self.logger.error("Task failed: %s", msg), QMessageBox.critical(self, "错误", msg)))))
worker.cancelled.connect(lambda: (_cleanup() if not cancelled[0] else None))
# 注意worker 的信号在其所属线程中发射。为了避免跨线程操作 Qt UI会导致卡死/警告),
# 这里必须使用 QueuedConnection确保回调在主线程事件循环中执行。
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_workers.append(worker)
thread.start()
@ -3906,6 +3949,18 @@ class MainWindow(QMainWindow):
self._reload_experiments()
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:
try:
@ -4687,9 +4742,11 @@ class MainWindow(QMainWindow):
db.close()
self.logger.info(f"[SQL Server] 更新实验 {exp_id} 状态为: {status}")
# 刷新列表显示
self._reload_experiments()
# 刷新列表显示:可能从后台线程调用,必须通过信号切回主线程
try:
self.reload_experiments_signal.emit()
except Exception:
pass
except Exception as e:
self.logger.error(f"[SQL Server] 更新状态失败: {e}", exc_info=True)
@ -6072,6 +6129,37 @@ def run_app() -> None:
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
splash = SplashScreen()