# GUI 日志工作机制说明 ## 概述 本文档说明了 ERP 自动化工具中 GUI 日志系统的工作机制,包括日志从产生到显示的完整流程。 ## 架构概览 ```mermaid graph TB subgraph Utils["Utils 脚本层"] A1[离散备料计划维护数据提取.py] A2[离散备料计划维护数据清理.py] A3[material_status_validator.py] A4[_log 方法] end subgraph GUI["GUI 层"] B1[DataExtractionTab] B2[MaterialValidationTab] B3[_update_log 方法] B4[LogText 组件] end subgraph Logging["Logging 框架"] C1[Python logging 模块] C2[GuiTextHandler] end A1 -->|logger.info| C1 A2 -->|logger.info| C1 A3 -->|logger.info| C1 A1 -->|progress_callback| B1 A2 -->|progress_callback| B2 A3 -->|progress_callback| B2 B1 -->|_update_log| C1 B2 -->|_update_log| C1 B1 -->|直接调用| B4 B2 -->|直接调用| B4 C1 -->|日志记录| C2 C2 -->|清理消息| B4 B4 -->|添加格式| Display[用户界面] style A1 fill:#e1f5ff style A2 fill:#e1f5ff style A3 fill:#e1f5ff style B1 fill:#fff4e1 style B2 fill:#fff4e1 style B4 fill:#e8f5e9 style C1 fill:#f3e5f5 style C2 fill:#f3e5f5 ``` ## 组件职责 ### 1. Utils 脚本层 **职责**: 业务逻辑执行和日志产生 **主要文件**: - `utils/离散备料计划维护数据提取.py` - `utils/离散备料计划维护数据清理.py` - `utils/material_status_validator.py` **日志输出方式**: ```python def _log(self, message, level="info"): """统一日志出口:同步分发到控制台和 UI 回调""" level = level.lower() # 方式1: 输出到控制台(添加级别标记) log_map = { "info": logger.info, "warn": logger.warning, "error": logger.error } log_func = log_map.get(level, logger.info) log_func(message) # 输出: "2026-02-13 21:42:03 [INFO] message" # 方式2: 同步到 UI(通过回调) if self.progress_callback: self._report_progress("log", 0, 0, message, log_level=level.upper()) ``` **问题**: 消息中可能包含 `[INFO]`、`[ERROR]` 等级别前缀 ### 2. GUI 层 #### 2.1 Tab 组件 (DataExtractionTab, MaterialValidationTab) **职责**: 用户交互和业务逻辑调用 **日志处理**: ```python def _update_log(self, message: str, level: str = "INFO"): """线程安全的日志更新""" # 将自定义级别映射到 logging 级别 level_upper = level.upper() if level_upper == "SUCCESS": self.logger.info(message) else: log_level = getattr(logging, level_upper, logging.INFO) self.logger.log(log_level, message) ``` **初始化**: ```python def __init__(self, parent, config: ConfigManager, main_window=None): # ... self.logger = get_logger(__name__) self._gui_handler = None # 将在 _create_log_panel 中设置 def _create_log_panel(self, parent): self.log_text = LogText(parent, height=15, readonly=True) self.log_text.pack(fill=tk.BOTH, expand=True) # 设置 GUI 日志处理器 self._gui_handler = GuiTextHandler(self.log_text) self._gui_handler.setFormatter(logging.Formatter( '%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' )) self.logger.addHandler(self._gui_handler) ``` #### 2.2 LogText 组件 **职责**: 日志显示和格式化 **核心方法**: ```python def log(self, message: str, level: str = 'INFO') -> None: """添加日志消息""" timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') log_message = f"[{timestamp}] [{level}] {message}\n" # 插入文本并设置颜色 tag = level.lower() self.text.insert('end', log_message, (tag,)) self.text.see('end') # 自动滚动到底部 ``` **级别颜色映射**: ```python LOG_COLORS = { 'INFO': '#000000', # 黑色 'SUCCESS': '#008000', # 绿色 'WARNING': '#FF8C00', # 深橙色 'ERROR': '#FF0000', # 红色 'DEBUG': '#808080', # 灰色 } ``` ### 3. Logging 框架层 #### 3.1 Log Config (gui/log_config.py) **职责**: 全局日志配置 ```python def setup_gui_logging(level=logging.INFO): """初始化 GUI 应用的日志配置""" logging.basicConfig( level=level, format=LOG_FORMAT, # '%(asctime)s [%(levelname)s] %(message)s' datefmt=DATE_FORMAT, # '%Y-%m-%d %H:%M:%S' force=True ) return logging.getLogger() ``` #### 3.2 GuiTextHandler (gui/widgets/log_handler.py) **职责**: 桥接 logging 模块和 GUI **核心逻辑**: ```python class GuiTextHandler(logging.Handler): def emit(self, record: logging.LogRecord): """实现日志输出""" if not self.log_text: return try: # 1. 获取日志级别 level = self.level_map.get(record.levelno, 'INFO') # 2. 获取纯消息内容(不含格式) message = record.getMessage() # 3. 移除冗余级别前缀(如 "[INFO] ") message = self._strip_redundant_level_prefix(message) # 4. 线程安全地更新 GUI def update(): self.log_text.log(message, level) # 5. 使用 after 确保在主线程更新 widget.master.after(0, update) except Exception: self.handleError(record) ``` **清理冗余级别前缀**: ```python def _strip_redundant_level_prefix(self, message: str) -> str: """移除消息开头的冗余级别标记""" level_pattern = r'^\[(?:INFO|WARNING|ERROR|DEBUG|CRITICAL|WARN|SUCCESS)\]\s*' match = re.match(level_pattern, message) if match: return message[match.end():] return message ``` ## 日志流程详解 ### 场景 1: Utils 脚本 → 控制台 → GUI ```mermaid sequenceDiagram participant U as Utils 脚本 participant L as Logger participant C as Console participant G as GuiTextHandler participant T as LogText participant UI as 用户界面 U->>U: _log("读取文件", "info") Note over U: 业务逻辑执行 U->>L: logger.info("[INFO] 读取文件") Note over U,L: 1. 控制台输出 L->>C: 2026-02-13 21:42:03 [INFO] [INFO] 读取文件 Note over C: 控制台显示(可能有冗余级别) U->>G: progress_callback(log, message, log_level="INFO") Note over U,G: 2. UI 回调 G->>G: _strip_redundant_level_prefix("[INFO] 读取文件") Note over G: 清理: "[INFO] " -> "" G->>T: log("读取文件", "INFO") Note over G,T: 纯净消息 T->>UI: [2026-02-13 21:42:03] [INFO] 读取文件 Note over UI: GUI 显示(格式统一) ``` ### 场景 2: GUI 直接调用 → Logging → GUI ```mermaid sequenceDiagram participant Tab as Tab 组件 participant L as Logger participant G as GuiTextHandler participant T as LogText participant UI as 用户界面 Tab->>L: _update_log("开始校验", "INFO") Note over Tab,L: 用户操作触发 L->>L: logger.info("开始校验") L->>G: emit(LogRecord) Note over L,G: Logging 框架分发 G->>G: record.getMessage() = "开始校验" Note over G: 获取纯消息 G->>G: _strip_redundant_level_prefix("开始校验") Note over G: 检查并清理(此处无冗余) G->>T: log("开始校验", "INFO") Note over G,T: 跨线程调用 T->>UI: [2026-02-13 21:42:03] [INFO] 开始校验 Note over UI: GUI 显示 ``` ## 数据流转分析 ### 消息内容的变化 | 阶段 | 消息内容 | 说明 | |------|---------|------| | Utils 原始消息 | `"读取 ProductionID 文件"` | 业务逻辑产生 | | logger.info() 后 | `"2026-02-13 21:42:03 [INFO] 读取 ProductionID 文件"` | 控制台格式化 | | progress_callback 传递 | `"读取 ProductionID 文件"` | 原始消息(可能含 `[INFO] ` 前缀) | | GuiTextHandler 处理后 | `"读取 ProductionID 文件"` | 移除冗余前缀 | | LogText.log() 添加 | `"[2026-02-13 21:42:03] [INFO] 读取 ProductionID 文件"` | GUI 格式化 | | 用户界面显示 | `[2026-02-13 21:42:03] [INFO] 读取 ProductionID 文件` | 最终显示 | ### 级别映射 | 层级 | 级别值 | 说明 | |------|--------|------| | Utils _log() | `"info"` / `"warn"` / `"error"` | 小写字符串 | | progress_callback | `"INFO"` / `"WARNING"` / `"ERROR"` | 大写字符串 | | logging 模块 | `logging.INFO` / `logging.WARNING` / `logging.ERROR` | 整数常量 | | LogText 组件 | `"INFO"` / `"WARNING"` / `"ERROR"` / `"SUCCESS"` | 字符串 | | GuiTextHandler level_map | 字典映射 `logging.INFO -> 'INFO'` | 转换逻辑 | ## 当前问题分析 ### 问题 1: 双重输出路径 **现状**: Utils 脚本同时通过两种方式输出日志 1. `logger.info(message)` → 控制台 2. `progress_callback(log, message)` → GUI **影响**: - 控制台日志和 GUI 日志可能不一致 - 增加维护复杂度 **建议**: - 统一使用 logging 模块 - GuiTextHandler 自动输出到控制台和 GUI ### 问题 2: 消息中包含级别前缀 **现状**: ```python # Utils 代码 logger.info("[INFO] 读取 ProductionID 文件") ``` **影响**: - 消息格式不统一 - 需要额外的清理逻辑 **建议**: ```python # 推荐做法 logger.info("读取 ProductionID 文件") # 不包含级别前缀 ``` ### 问题 3: 线程同步复杂度 **现状**: - Utils 脚本在后台线程执行 - 使用 `progress_callback` 线程安全地更新 GUI - GuiTextHandler 也使用 `after()` 确保主线程更新 **影响**: - 两次线程转换 - 代码路径复杂 **建议**: - 统一使用 logging 模块 - 利用 logging 的线程安全特性 - GuiTextHandler 内部处理线程同步 ## 改进建议 ### 短期优化(保持兼容) 1. **统一 Utils 脚本的日志格式** ```python # 当前 def _log(self, message, level="info"): log_func(message) # 可能包含 "[INFO] " 前缀 # 改进 def _log(self, message, level="info"): # 确保消息不包含级别前缀 clean_message = self._strip_level_prefix(message) log_func(clean_message) ``` 2. **简化 progress_callback** ```python # 当前 self._report_progress("log", 0, 0, message, log_level=level.upper()) # 改进:移除 log 级别通过 progress_callback 传递 # 直接使用 logging,GuiTextHandler 会处理 ``` ### 长期重构(破坏性变更) 1. **移除 progress_callback 中的日志路径** - Utils 脚本只使用 logging 模块 - GuiTextHandler 统一处理控制台和 GUI 输出 2. **配置化日志目标** ```python # config.py LOGGING = { 'version': 1, 'handlers': { 'console': {'class': 'logging.StreamHandler'}, 'gui': {'class': 'GuiTextHandler', 'log_text': ...} }, 'root': { 'handlers': ['console', 'gui'] } } ``` 3. **统一级别系统** - 移除自定义的 "SUCCESS" 级别 - 使用标准的 logging.INFO + 额外的元数据 ## 附录 ### 相关文件清单 | 文件路径 | 职责 | |---------|------| | `gui/log_config.py` | 日志配置 | | `gui/widgets/log_handler.py` | GuiTextHandler | | `gui/widgets/log_text.py` | LogText 组件 | | `gui/main_window.py` | 初始化日志系统 | | `gui/material_validation_tab.py` | 物料校验标签页 | | `gui/data_extraction_tab.py` | 数据提取标签页 | | `utils/离散备料计划维护数据提取.py` | 业务逻辑 + _log | | `utils/离散备料计划维护数据清理.py` | 业务逻辑 + _log | | `utils/material_status_validator.py` | 业务逻辑 + _log | ### 测试文件 | 文件路径 | 说明 | |---------|------| | `tests/test_logging_simple.py` | 简单日志测试 | | `tests/test_logging_system.py` | 完整 GUI 测试 | | `tests/test_log_handler_fix.py` | 冗余级别清理测试 | ### 参考文档 - [Python logging 模块文档](https://docs.python.org/3/library/logging.html) - [Tkinter 线程安全最佳实践](https://docs.python.org/3/library/tkinter.html#thread-safety) - `docs/LOGGING_REFACTORING_SUMMARY.md` - 重构总结文档