diff --git a/src/db_manager.py b/src/db_manager.py index 59dcd8a..d8adc01 100644 --- a/src/db_manager.py +++ b/src/db_manager.py @@ -2,6 +2,9 @@ import psycopg2 from psycopg2.extras import RealDictCursor import datetime import uuid +from utils.logger import get_logger, log_timing + +logger = get_logger("db_manager") class DatabaseManager: def __init__(self): @@ -14,14 +17,16 @@ class DatabaseManager: } self.schema = '"tzwork"' + @log_timing def get_connection(self): try: conn = psycopg2.connect(**self.config) return conn except Exception as e: - print(f"Database connection error: {e}") + logger.error(f"Database connection error: {e}", exc_info=True) return None + @log_timing def insert_analysis_data(self, data_list): conn = self.get_connection() if not conn: @@ -60,6 +65,7 @@ class DatabaseManager: conn.close() return False, str(e) + @log_timing def authenticate_user(self, user_id, password_md5): conn = self.get_connection() if not conn: @@ -93,11 +99,12 @@ class DatabaseManager: } except Exception as e: - print(f"Authentication error: {e}") + logger.error(f"Authentication error: {e}", exc_info=True) if conn: conn.close() return False, f"认证失败: {str(e)}" + @log_timing def get_latest_sample_data(self): """ Retrieves the most recently measured sample data, flattened. @@ -145,7 +152,7 @@ class DatabaseManager: return result except Exception as e: - print(f"Query error: {e}") + logger.error(f"Query error: {e}", exc_info=True) if conn: conn.close() return None @@ -159,6 +166,7 @@ class DatabaseManager: return "合格" return "不合格" + @log_timing def insert_inspection_data(self, inspection_data): """ 插入检验数据到 e_pz_hjqy_import 表 @@ -281,6 +289,7 @@ class DatabaseManager: conn.close() return False, f"插入失败: {str(e)}" + @log_timing def query_by_gch(self, gch): """ 根据工程号查询产地、规格、炉号、材质等信息 @@ -322,6 +331,7 @@ class DatabaseManager: conn.close() return None, f"查询失败: {str(e)}" + @log_timing def get_inspection_list(self, limit=10, offset=0, user_id=''): """ 分页获取检验数据列表 @@ -383,6 +393,7 @@ class DatabaseManager: conn.close() return None, f"查询失败: {str(e)}" + @log_timing def query_standard_range(self, heat_number, heat_number2): """ 根据炉号1(tph)和炉号2(company_ph)查询标准值范围 diff --git a/src/inspection_app.py b/src/inspection_app.py index e2f3479..f7fb1ff 100644 --- a/src/inspection_app.py +++ b/src/inspection_app.py @@ -4,6 +4,7 @@ from ui.inspection_window import InspectionWindow from ui.login_window import LoginWindow from src.session_manager import SessionManager from datetime import datetime +from utils.logger import setup_logging, get_logger def get_mock_data(): return [ @@ -73,6 +74,9 @@ def get_mock_data(): ] def main(): + setup_logging() + logger = get_logger("inspection_app") + logger.info("Application starting...") app = QApplication(sys.argv) font = app.font() diff --git a/src/xml_parser.py b/src/xml_parser.py index f7f9ace..4e609b9 100644 --- a/src/xml_parser.py +++ b/src/xml_parser.py @@ -1,7 +1,10 @@ import xml.etree.ElementTree as ET import os +from utils.logger import get_logger, log_timing +logger = get_logger("xml_parser") class XmlParser: + @log_timing def parse_file(self, file_path): if not os.path.exists(file_path): return None, f"文件未找到: {file_path}" @@ -123,6 +126,5 @@ class XmlParser: return extracted_data, "Success" except Exception as e: - import traceback - traceback.print_exc() + logger.error(f"Parse error: {str(e)}", exc_info=True) return None, f"Parse error: {str(e)}" diff --git a/ui/base_page.py b/ui/base_page.py index 9c9d389..6c6cf66 100644 --- a/ui/base_page.py +++ b/ui/base_page.py @@ -8,6 +8,8 @@ from src.db_manager import DatabaseManager from src.xml_parser import XmlParser from utils.readMTP import MTPHandler from src.session_manager import SessionManager +from utils.logger import get_logger, log_timing_context +logger = get_logger("base_page") class BaseInspectionPage(QDialog): def __init__(self, title, parent=None): @@ -172,6 +174,7 @@ class BaseInspectionPage(QDialog): self.main_layout.addWidget(button_bar) def on_sync(self): + logger.info(f"[{self.page_type}] 开始同步操作") sync_mode = self.settings.value("sync_mode", "local") xml_path = None @@ -189,13 +192,15 @@ class BaseInspectionPage(QDialog): latest_xml = max(xml_files, key=os.path.getmtime) xml_path = latest_xml target_xml_name = os.path.basename(latest_xml) + logger.info(f"本地模式,选定文件: {target_xml_name}") else: device_name = self.settings.value("mtp_device", "Trizeps VII") remote_path = self.settings.value("mtp_path", "") self.show_info("正在同步", f"正在从设备 [{device_name}] 读取最新数据...") - local_cache, filename = self.mtp_handler.get_latest_xml_windows(device_name, remote_path) + with log_timing_context(f"MTP同步 设备={device_name}", "base_page"): + local_cache, filename = self.mtp_handler.get_latest_xml_windows(device_name, remote_path) if not local_cache: self.show_info("同步失败", f"MTP同步错误: {filename}") return @@ -205,7 +210,8 @@ class BaseInspectionPage(QDialog): # 1.解析xml parser = XmlParser() - parsed_data, msg = parser.parse_file(xml_path) + with log_timing_context("XML解析", "base_page"): + parsed_data, msg = parser.parse_file(xml_path) if not parsed_data: self.show_info("同步失败", f"XML解析错误: {msg}") @@ -213,19 +219,21 @@ class BaseInspectionPage(QDialog): # 2. 写入数据 db = DatabaseManager() - success, db_msg = db.insert_analysis_data(parsed_data) + with log_timing_context("DB写入分析数据", "base_page"): + success, db_msg = db.insert_analysis_data(parsed_data) if not success: self.show_info("数据库错误", f"插入失败: {db_msg}\n(请检查数据库连接)") return - latest_data = db.get_latest_sample_data() + with log_timing_context("DB读取最新样本数据", "base_page"): + latest_data = db.get_latest_sample_data() if latest_data: self.update_ui_with_data(latest_data) self.show_info("同步成功", f"已同步 {len(parsed_data)} 条元素数据。\n源文件: {target_xml_name}") else: self.show_info("同步异常", "数据插入成功但无法读取回显。") - + logger.info(f"[{self.page_type}] 同步操作完成") def update_ui_with_data(self, data): ### 更新UI界面,如果不符合,对不符合的元素字体标红,并且 pass @@ -236,6 +244,7 @@ class BaseInspectionPage(QDialog): def on_submit(self): """提交检验数据到 e_pz_hjqy_import 表""" + logger.info(f"[{self.page_type}] 开始提交检验数据") try: # 1. 收集表单数据 gch = self.inputs.get("batch_no").text().strip() if self.inputs.get("batch_no") else "" @@ -296,15 +305,19 @@ class BaseInspectionPage(QDialog): # 6. 插入数据库 db = DatabaseManager() - success, message = db.insert_inspection_data(inspection_data) + with log_timing_context(f"DB提交检验数据 gch={gch}", "base_page"): + success, message = db.insert_inspection_data(inspection_data) if success: + logger.info(f"[{self.page_type}] 提交成功: {message}") self.show_info("提交成功", message) self.clear_data() else: + logger.warning(f"[{self.page_type}] 提交失败: {message}") self.show_info("提交失败", message) except Exception as e: + logger.error(f"[{self.page_type}] 提交异常: {e}", exc_info=True) self.show_info("提交异常", f"发生错误: {str(e)}") def show_info(self, title, message): diff --git a/ui/incoming_inspection_page.py b/ui/incoming_inspection_page.py index f521ea3..ce89fea 100644 --- a/ui/incoming_inspection_page.py +++ b/ui/incoming_inspection_page.py @@ -6,6 +6,8 @@ from openpyxl.styles.builtins import headline_1 from ui.base_page import BaseInspectionPage from src.db_manager import DatabaseManager +from utils.logger import get_logger, log_timing_context +logger = get_logger("incoming_inspection") class IncomingInspectionPage(BaseInspectionPage): def __init__(self, parent=None): @@ -241,8 +243,11 @@ class IncomingInspectionPage(BaseInspectionPage): return self.last_gch = gch - data, err = self.db_manager.query_by_gch(gch) + logger.info(f"查询工程号: {gch}") + with log_timing_context(f"query_by_gch({gch})", "incoming_inspection"): + data, err = self.db_manager.query_by_gch(gch) if err: + logger.warning(f"查询失败: {err}") self.show_info("查询失败", err) return @@ -252,7 +257,6 @@ class IncomingInspectionPage(BaseInspectionPage): self.inputs["spec1"].setText(data.get("qy_czt") or "") self.inputs["info1"].setText(data.get("heat_number2") or "") self.inputs["spec2"].setText(data.get("qy_czw") or "") - def update_ui_with_data(self, data): """ 根据数据库数据更新UI。 @@ -294,7 +298,8 @@ class IncomingInspectionPage(BaseInspectionPage): if not hn1 and not hn2: return - heat1_range, heat2_range, err = self.db_manager.query_standard_range(hn1, hn2) + with log_timing_context("query_standard_range", "incoming_inspection"): + heat1_range, heat2_range, err = self.db_manager.query_standard_range(hn1, hn2) if err: self.show_info("标准值查询失败", err) return @@ -334,7 +339,7 @@ class IncomingInspectionPage(BaseInspectionPage): min_val = float(min_val) if min_val is not None and min_val != '' else None max_val = float(max_val) if max_val is not None and max_val != '' else None except (ValueError, TypeError) as e: - print(e) + logger.debug(f"标准值解析异常: {e}") is_out = False if min_val is not None and value < min_val: is_out = True diff --git a/ui/inspection_window.py b/ui/inspection_window.py index 756943b..4c82e7c 100644 --- a/ui/inspection_window.py +++ b/ui/inspection_window.py @@ -8,6 +8,8 @@ from ui.incoming_inspection_page import IncomingInspectionPage from ui.manual_inspection_page import ManualInspectionPage from src.db_manager import DatabaseManager from src.session_manager import SessionManager +from utils.logger import get_logger, log_timing_context +logger = get_logger("inspection_window") class InspectionWindow(QMainWindow): def __init__(self): @@ -116,6 +118,7 @@ class InspectionWindow(QMainWindow): self.is_loading = True self.loading_label.show() + logger.debug(f"触发加载更多数据, page={self.current_page}") # 使用 QTimer 异步加载,避免阻塞 UI QTimer.singleShot(100, self._fetch_data) @@ -124,9 +127,11 @@ class InspectionWindow(QMainWindow): """从数据库获取数据""" offset = self.current_page * self.page_size user_id = self.session_manager.get_user_id() or '' - rows, error = self.db_manager.get_inspection_list(self.page_size, offset, user_id=user_id) + with log_timing_context(f"get_inspection_list(page={self.current_page})", "inspection_window"): + rows, error = self.db_manager.get_inspection_list(self.page_size, offset, user_id=user_id) if error: + logger.error(f"加载失败: {error}") self.loading_label.setText(f"加载失败: {error}") self.is_loading = False QTimer.singleShot(2000, lambda: self.loading_label.hide()) @@ -141,6 +146,7 @@ class InspectionWindow(QMainWindow): self.add_card(dict(row)) self.current_page += 1 + logger.debug(f"加载了 {len(rows)} 条记录, 当前页={self.current_page}") if len(rows) < self.page_size: self.has_more_data = False diff --git a/ui/login_window.py b/ui/login_window.py index 87d7da7..25e2bab 100644 --- a/ui/login_window.py +++ b/ui/login_window.py @@ -6,6 +6,8 @@ from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QFont import hashlib from src.db_manager import DatabaseManager +from utils.logger import get_logger, log_timing_context +logger = get_logger("login") class LoginWindow(QMainWindow): @@ -117,12 +119,16 @@ class LoginWindow(QMainWindow): QMessageBox.warning(self, "登录失败", "请输入账号和密码") return + logger.info(f"用户登录尝试: {user_id}") password_md5 = hashlib.md5(password.encode()).hexdigest() - success, user_info = self.db.authenticate_user(user_id, password_md5) + with log_timing_context(f"authenticate_user({user_id})", "login"): + success, user_info = self.db.authenticate_user(user_id, password_md5) if success: + logger.info(f"登录成功: {user_id}") self.login_success.emit(user_info) self.close() else: + logger.warning(f"登录失败: {user_id} - {user_info}") QMessageBox.warning(self, "登录失败", user_info) self.password_input.clear() self.password_input.setFocus() diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..d35f498 --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,102 @@ +import os +import time +import logging +import functools +from logging.handlers import RotatingFileHandler +from contextlib import contextmanager + +_logger_initialized = False + +LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "logs") +LOG_FILE = os.path.join(LOG_DIR, "app.log") + +SLOW_THRESHOLD = 1.0 +CRITICAL_THRESHOLD = 5.0 + +LOG_FORMAT = "[%(asctime)s] [%(levelname)-8s] [%(name)s:%(funcName)s:%(lineno)d] %(message)s" +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + + +def setup_logging(level=logging.DEBUG): + global _logger_initialized + if _logger_initialized: + return + _logger_initialized = True + + os.makedirs(LOG_DIR, exist_ok=True) + + root_logger = logging.getLogger("app") + root_logger.setLevel(level) + + if root_logger.handlers: + return + + formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT) + + file_handler = RotatingFileHandler( + LOG_FILE, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8" + ) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(formatter) + + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(formatter) + + root_logger.addHandler(file_handler) + root_logger.addHandler(console_handler) + + +def get_logger(name): + return logging.getLogger(f"app.{name}") + + +def log_timing(func=None, *, logger_name=None): + def decorator(fn): + _logger = get_logger(logger_name or fn.__module__) + + @functools.wraps(fn) + def wrapper(*args, **kwargs): + fn_name = fn.__qualname__ + _logger.debug(f"[ENTER] {fn_name}") + start = time.perf_counter() + try: + result = fn(*args, **kwargs) + return result + except Exception as e: + _logger.error(f"[ERROR] {fn_name}: {e}", exc_info=True) + raise + finally: + elapsed = time.perf_counter() - start + if elapsed >= CRITICAL_THRESHOLD: + _logger.critical(f"[SLOW!] {fn_name} took {elapsed:.3f}s (>={CRITICAL_THRESHOLD}s)") + elif elapsed >= SLOW_THRESHOLD: + _logger.warning(f"[SLOW] {fn_name} took {elapsed:.3f}s (>={SLOW_THRESHOLD}s)") + else: + _logger.debug(f"[EXIT] {fn_name} took {elapsed:.3f}s") + + return wrapper + + if func is not None: + return decorator(func) + return decorator + + +@contextmanager +def log_timing_context(operation, logger_name=None): + _logger = get_logger(logger_name or "timing") + _logger.debug(f"[START] {operation}") + start = time.perf_counter() + try: + yield + except Exception as e: + _logger.error(f"[ERROR] {operation}: {e}", exc_info=True) + raise + finally: + elapsed = time.perf_counter() - start + if elapsed >= CRITICAL_THRESHOLD: + _logger.critical(f"[SLOW!] {operation} took {elapsed:.3f}s (>={CRITICAL_THRESHOLD}s)") + elif elapsed >= SLOW_THRESHOLD: + _logger.warning(f"[SLOW] {operation} took {elapsed:.3f}s (>={SLOW_THRESHOLD}s)") + else: + _logger.debug(f"[DONE] {operation} took {elapsed:.3f}s") diff --git a/utils/readMTP.py b/utils/readMTP.py index a8c2df1..7ef7b4b 100644 --- a/utils/readMTP.py +++ b/utils/readMTP.py @@ -1,5 +1,7 @@ import os import time +from utils.logger import get_logger, log_timing +logger = get_logger("mtp") try: import win32com.client @@ -14,11 +16,12 @@ class MTPHandler: if not os.path.exists(self.cache_dir): os.makedirs(self.cache_dir) + @log_timing def get_latest_xml_windows(self, device_name, path_str): """ 在 Windows MTP 设备上寻找并复制最新的 XML 文件。 """ - print(f"\n[MTP] >>> 开始同步工作: 设备={device_name}") + logger.info(f"开始同步工作: 设备={device_name}") if win32com is None: return None, "系统未安装 pywin32。" @@ -31,7 +34,7 @@ class MTPHandler: device_item = None target_name = device_name.strip().lower() - print(f"[MTP] 正在检索设备,目标名称: '{target_name}'") + logger.debug(f"正在检索设备,目标名称: '{target_name}'") items = computer.Items() available_devices = [] @@ -39,12 +42,12 @@ class MTPHandler: available_devices.append(item.Name) if item.Name.strip().lower() == target_name: device_item = item - print(f"[MTP] 匹配成功: '{item.Name}'") + logger.info(f"匹配成功: '{item.Name}'") break if not device_item: - print(f"[MTP] 未找到设备: '{device_name}'") - print(f"[MTP] 当前电脑识别到的设备列表: {available_devices}") + logger.warning(f"未找到设备: '{device_name}'") + logger.warning(f"当前电脑识别到的设备列表: {available_devices}") return None, f"未找到 MTP 设备: {device_name} (请检查名称)" current_folder = device_item.GetFolder @@ -59,20 +62,20 @@ class MTPHandler: found = True break if not found: - print(f"[MTP] 路径中断,找不到文件夹: '{folder_name}'") + logger.warning(f"路径中断,找不到文件夹: '{folder_name}'") # 打印一下当前目录下有的东西,帮用户排查 content = [i.Name for i in current_folder.Items()] - print(f"[MTP] 当前所在位置 [ {current_folder.Title} ] 下的可选内容: {content}") + logger.debug(f"当前所在位置 [ {current_folder.Title} ] 下的可选内容: {content}") return None, f"路径点不存在: {folder_name}" # 3. 扫描文件并识别“最新” - print(f"[MTP] 已到达目标目录: [ {current_folder.Title} ]") - print(f"[MTP] 正在列出该目录下所有项目...") - print("-" * 40) + logger.info(f"已到达目标目录: [ {current_folder.Title} ]") + logger.debug(f"正在列出该目录下所有项目...") + logger.debug("-" * 40) xml_items = [] all_items = current_folder.Items() - print(f"[MTP] 目录内总项数: {all_items.Count}") + logger.debug(f"目录内总项数: {all_items.Count}") for i in range(all_items.Count): try: @@ -83,7 +86,7 @@ class MTPHandler: # 打印每一个文件的详细信息用于排查 # 尝试打印前 6 个详情列,看看哪一列是类型,哪一列是时间 details = [str(current_folder.GetDetailsOf(item, j)) for j in range(6)] - if i == 0: print(f"[MTP] 属性列参考(0-5): {details}") + if i == 0: logger.debug(f"属性列参考(0-5): {details}") item_type = details[2] # 识别逻辑: @@ -106,16 +109,16 @@ class MTPHandler: 'time': mtime, 'name_ts': name_ts }) - print(f"[MTP] 识别 XML: '{name}' | 创建/修改时间: {mtime} | 文件名时间: {name_ts}") + logger.debug(f"识别 XML: '{name}' | 创建/修改时间: {mtime} | 文件名时间: {name_ts}") else: pass except Exception as e: - print(f"[MTP] 读取第 {i} 个项目时出错: {e}") + logger.error(f"读取第 {i} 个项目时出错: {e}") - print("-" * 40) + logger.debug("-" * 40) if not xml_items: - print(f"[MTP] 报错: 在目录 [ {current_folder.Title} ] 中没有找到任何以 .xml 结尾的文件") + logger.warning(f"在目录 [ {current_folder.Title} ] 中没有找到任何以 .xml 结尾的文件") return None, "该目录中没有 XML 文件" # 排序逻辑优化: @@ -128,9 +131,9 @@ class MTPHandler: latest_item = latest['item'] filename = latest['name'] - print(f"[MTP] 选定最新文件: {filename}") - print(f"[MTP] - 属性时间: {latest['time']}") - print(f"[MTP] - 名称日期: {latest['name_ts']}") + logger.info(f"选定最新文件: {filename}") + logger.debug(f" - 属性时间: {latest['time']}") + logger.debug(f" - 名称日期: {latest['name_ts']}") # 4. 复制到本地 local_path = os.path.abspath(os.path.join(self.cache_dir, filename)) @@ -140,7 +143,7 @@ class MTPHandler: try: os.remove(os.path.join(self.cache_dir, f)) except: pass - print(f"[MTP] 正在拉取文件到本地缓存...") + logger.info(f"正在拉取文件到本地缓存...") dest_shell_folder = shell.NameSpace(os.path.abspath(self.cache_dir)) dest_shell_folder.CopyHere(latest_item, 4 | 16) @@ -153,13 +156,13 @@ class MTPHandler: time.sleep(0.5) if success: - print(f"[MTP] ✨ 同步成功: {filename}") + logger.info(f"同步成功: {filename}") return local_path, filename else: return None, "同步超时" except Exception as e: - print(f"[MTP] 异常: {e}") + logger.error(f"MTP异常: {e}", exc_info=True) return None, str(e) finally: pythoncom.CoUninitialize()