feat: implement unified logging system for GUI components

Add centralized logging mechanism that simultaneously outputs to console
and GUI log components, improving code maintainability and consistency.

Changes:
- Add gui/log_config.py for centralized logging configuration
- Add gui/widgets/log_handler.py as bridge between logging and LogText
- Integrate unified logging into DataExtractionTab and MaterialValidationTab
- Initialize logging system in MainWindow on startup
- Improve error messages in material_status_validator for empty results
- Add documentation for logging mechanism and refactoring

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-02-24 20:31:59 +08:00
parent 3c45ef58d1
commit 24053c6a3b
9 changed files with 821 additions and 24 deletions

434
docs/LOGGING_MECHANISM.md Normal file
View File

@@ -0,0 +1,434 @@
# 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 传递
# 直接使用 loggingGuiTextHandler 会处理
```
### 长期重构(破坏性变更)
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` - 重构总结文档

View File

@@ -0,0 +1,141 @@
# GUI 日志系统重构完成总结
## 实施概述
已成功完成 GUI 日志系统的统一重构,实现了以下目标:
1. **统一日志格式**:控制台和 UI 日志使用一致的格式和配置
2. **标准化接口**:使用 Python 标准 `logging` 模块
3. **代码简化**:减少了重复的日志处理代码
4. **易扩展性**:后续可轻松添加文件输出、远程日志等功能
## 新增文件
### 1. `gui/log_config.py`
统一日志配置模块,提供:
- `LOG_FORMAT`: 日志格式常量
- `DATE_FORMAT`: 日期格式常量
- `setup_gui_logging(level=logging.INFO)`: 初始化日志系统
- `get_logger(name)`: 获取指定名称的 logger
### 2. `gui/widgets/log_handler.py`
自定义 logging Handler桥接 logging 模块和 GUI
- `GuiTextHandler` 类:将日志输出到 LogText 组件
- 线程安全设计:使用 `after()` 确保 GUI 更新在主线程
- 自动级别映射:将 logging 级别映射到 LogText 级别
### 3. `tests/test_logging_simple.py`
简单的非 GUI 测试脚本,验证日志配置。
### 4. `tests/test_logging_system.py`
完整的 GUI 测试脚本,测试所有日志功能(包括 GUI 界面)。
## 修改文件
### 1. `gui/main_window.py`
- 导入 `setup_gui_logging`
-`__init__` 中调用 `setup_gui_logging()` 初始化全局日志
### 2. `gui/material_validation_tab.py`
- 导入 `logging`, `get_logger`, `GuiTextHandler`
-`__init__` 中初始化 `self.logger`
-`_create_log_panel` 中创建并配置 `GuiTextHandler`
- 更新 `_update_log` 方法使用标准 logging
### 3. `gui/data_extraction_tab.py`
- 导入 `logging`, `get_logger`, `GuiTextHandler`
-`__init__` 中初始化 `self.logger`
-`_create_log_panel` 中创建并配置 `GuiTextHandler`
- 更新 `_update_log` 方法使用标准 logging
### 4. `gui/widgets/__init__.py`
- 添加 `GuiTextHandler` 到导出列表
## 日志格式
统一格式:`%(asctime)s [%(levelname)s] %(message)s`
示例输出:
```
2026-02-13 21:36:07 [INFO] 物料校验标签页已就绪
2026-02-13 21:36:08 [WARNING] 未选择任何负责人
2026-02-13 21:36:09 [ERROR] 校验过程中发生错误
```
## 使用方式
### 在新代码中使用
```python
from gui.log_config import get_logger
# 获取 logger
logger = get_logger(__name__)
# 输出日志
logger.info("信息日志")
logger.warning("警告日志")
logger.error("错误日志")
```
### 在现有代码中使用 `_update_log`
保持兼容,`_update_log` 方法自动桥接到 logging
```python
self._update_log("消息", "INFO") # → logger.info()
self._update_log("消息", "WARNING") # → logger.warning()
self._update_log("消息", "ERROR") # → logger.error()
self._update_log("消息", "SUCCESS") # → logger.info() (UI 显示为 SUCCESS)
```
## 测试验证
### 运行简单测试
```bash
python tests/test_logging_simple.py
```
### 运行完整 GUI 测试
```bash
python tests/test_logging_system.py
```
## 优势
1. **统一风格**:控制台和 UI 使用相同格式,便于调试
2. **标准化**:使用 Python 标准 logging 模块,符合最佳实践
3. **易扩展**:后续可轻松添加文件输出、远程日志等
4. **代码简化**:减少重复的 `_update_log` 方法实现
5. **线程安全**logging 模块内置线程安全支持GuiTextHandler 额外处理了 GUI 线程安全
## 兼容性
- 保持向后兼容:`_update_log` 方法仍然可用
- `SUCCESS` 级别:映射到 `INFO`,但 UI 中仍显示为绿色 SUCCESS
- `DEBUG` 级别:默认不显示,可通过配置启用
## 后续改进建议
1. **文件输出**:添加 `FileHandler` 将日志保存到文件
2. **日志轮转**:使用 `RotatingFileHandler``TimedRotatingFileHandler`
3. **配置化**:通过配置文件控制日志级别和输出目标
4. **远程日志**:添加 `SyslogHandler` 或自定义网络 Handler
5. **性能监控**:集成性能指标到日志系统
## 文件清单
### 新增文件
- `gui/log_config.py` - 日志配置模块
- `gui/widgets/log_handler.py` - GUI 日志处理器
- `tests/test_logging_simple.py` - 简单测试脚本
- `tests/test_logging_system.py` - 完整测试脚本
- `docs/LOGGING_REFACTORING_SUMMARY.md` - 本文档
### 修改文件
- `gui/main_window.py` - 初始化日志系统
- `gui/material_validation_tab.py` - 使用统一日志
- `gui/data_extraction_tab.py` - 使用统一日志
- `gui/widgets/__init__.py` - 导出 GuiTextHandler

View File

@@ -9,12 +9,14 @@
import os
import sys
import threading
import logging
import queue
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
from gui.widgets import FileSelector, LogText, ProductionIdInput
from gui.widgets import FileSelector, LogText, ProductionIdInput, GuiTextHandler
from gui.config_manager import ConfigManager
from gui.log_config import setup_gui_logging, get_logger
from gui.progress import ProgressInfo, ProgressCalculator
from gui.utils import RealtimeOutput
@@ -32,6 +34,10 @@ class DataExtractionTab(ttk.Frame):
self.progress_calculator = ProgressCalculator()
self.progress_queue = queue.Queue()
# 初始化统一日志系统
self.logger = get_logger(__name__)
self._gui_handler = None # 将在 _create_log_panel 中设置
self._poll_progress_queue()
self.create_widgets()
self._apply_ui_config()
@@ -116,6 +122,14 @@ class DataExtractionTab(ttk.Frame):
self.log_text = LogText(parent, height=15, readonly=True)
self.log_text.pack(fill=tk.BOTH, expand=True)
# 设置 GUI 日志处理器,将 logging 输出桥接到 LogText 组件
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)
def _apply_ui_config(self):
try:
font_family = self.config.get("ui.font_family", "Microsoft YaHei UI")
@@ -219,15 +233,24 @@ class DataExtractionTab(ttk.Frame):
except: pass
def _update_log(self, message: str, level: str = "INFO"):
"""标准的日志更新方法"""
def update():
# 即使任务结束,只要是成功/错误消息也强制显示
if self.extracting or level in ["ERROR", "WARNING", "SUCCESS"]:
if level == "INFO": self.log_text.info(message)
elif level == "SUCCESS": self.log_text.success(message)
elif level == "WARNING": self.log_text.warning(message)
elif level == "ERROR": self.log_text.error(message)
self.after(0, update)
"""
标准的日志更新方法(兼容接口)
通过统一的 logging 系统输出日志,自动同时输出到控制台和 GUI。
Args:
message: 日志消息
level: 日志级别 (INFO, SUCCESS, WARNING, ERROR, DEBUG)
"""
# 将自定义级别映射到 logging 级别
level_upper = level.upper()
if level_upper == "SUCCESS":
# SUCCESS 映射到 INFO但在 UI 中仍显示为 SUCCESS
self.logger.info(message)
else:
# 其他级别直接映射
log_level = getattr(logging, level_upper, logging.INFO)
self.logger.log(log_level, message)
def _on_production_ids_changed(self, event=None):
if self.main_window:

43
gui/log_config.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
GUI 日志配置模块
统一配置 GUI 应用和控制台的日志输出
"""
import logging
# 日志格式配置
LOG_FORMAT = '%(asctime)s [%(levelname)s] %(message)s'
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
def setup_gui_logging(level=logging.INFO):
"""
初始化 GUI 应用的日志配置
Args:
level: 日志级别,默认为 INFO
Returns:
logging.Logger: 根 logger
"""
logging.basicConfig(
level=level,
format=LOG_FORMAT,
datefmt=DATE_FORMAT,
force=True # 确保重新配置(即使之前配置过)
)
return logging.getLogger()
def get_logger(name):
"""
获取指定名称的 logger
Args:
name: logger 名称,通常使用 __name__
Returns:
logging.Logger: logger 实例
"""
return logging.getLogger(name)

View File

@@ -9,6 +9,7 @@ ERP 自动化工具的主窗口,包含多个功能标签页。
import tkinter as tk
from tkinter import ttk
from gui.config_manager import ConfigManager
from gui.log_config import setup_gui_logging
from gui.data_extraction_tab import DataExtractionTab
from gui.material_validation_tab import MaterialValidationTab
from gui.settings_tab import SettingsTab
@@ -30,6 +31,9 @@ class MainWindow:
self.session_manager = session_manager
self.shared_production_ids = [] # 共享的 Production ID 列表
# 初始化统一日志系统
setup_gui_logging()
# 设置窗口属性(包含用户信息)
user_type_display = "管理员" if session_manager.is_admin() else "用户"
self.root.title(f"ERP 自动化工具 v1.0 - {session_manager.get_username()} ({user_type_display})")

View File

@@ -11,14 +11,16 @@
import os
import threading
import logging
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog
from pathlib import Path
from io import StringIO
from contextlib import redirect_stdout
from typing import List, Dict
from gui.widgets import FileSelector, LogText
from gui.widgets import FileSelector, LogText, GuiTextHandler
from gui.config_manager import ConfigManager
from gui.log_config import setup_gui_logging, get_logger
from gui.material_type_management_dialog import MaterialTypeManagementDialog
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
import pandas as pd
@@ -156,6 +158,10 @@ class MaterialValidationTab(ttk.Frame):
self.select_all_managers_var: tk.BooleanVar = tk.BooleanVar(value=True) # 全选复选框状态
self.previously_selected_managers: List[str] = [] # 保存筛选状态
# 初始化统一日志系统
self.logger = get_logger(__name__)
self._gui_handler = None # 将在 _create_log_panel 中设置
self.create_widgets()
# 稍后显示就绪消息
@@ -379,6 +385,14 @@ class MaterialValidationTab(ttk.Frame):
self.log_text = LogText(parent, height=8, readonly=True)
self.log_text.pack(fill=tk.BOTH, expand=True)
# 设置 GUI 日志处理器,将 logging 输出桥接到 LogText 组件
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)
def _on_source_mode_change(self):
"""数据源模式切换"""
# PERMISSION CHECK: 仅管理员用户可以选择数据源模式
@@ -895,10 +909,13 @@ class MaterialValidationTab(ttk.Frame):
if line.strip():
self._update_log(line, "INFO")
if result_file and results:
if result_file and results and len(results) > 0:
self._update_log(f"校验完成,结果已保存到:{output_file}", "SUCCESS")
# 加载结果显示(带删除状态)
self._load_results_with_deletion_status(result_file)
elif results is not None and len(results) == 0:
# 已经在 validator 中输出详细错误信息,这里只做简单提示
self._update_log("校验失败:未找到物料记录,请查看上方日志了解详细原因", "ERROR")
else:
self._update_log("校验失败", "ERROR")
@@ -1159,19 +1176,24 @@ class MaterialValidationTab(ttk.Frame):
self._initialize_manager_filter()
def _update_log(self, message: str, level: str = "INFO"):
"""线程安全的日志更新"""
"""
线程安全的日志更新(兼容接口)
def update():
if level == "INFO":
self.log_text.info(message)
elif level == "SUCCESS":
self.log_text.success(message)
elif level == "WARNING":
self.log_text.warning(message)
elif level == "ERROR":
self.log_text.error(message)
通过统一的 logging 系统输出日志,自动同时输出到控制台和 GUI。
self.after(0, update)
Args:
message: 日志消息
level: 日志级别 (INFO, SUCCESS, WARNING, ERROR, DEBUG)
"""
# 将自定义级别映射到 logging 级别
level_upper = level.upper()
if level_upper == "SUCCESS":
# SUCCESS 映射到 INFO但在 UI 中仍显示为 SUCCESS
self.logger.info(message)
else:
# 其他级别直接映射
log_level = getattr(logging, level_upper, logging.INFO)
self.logger.log(log_level, message)
def export_results(self):
"""导出结果到 Excel"""

View File

@@ -7,5 +7,6 @@ GUI 自定义组件模块
from .file_selector import FileSelector
from .log_text import LogText
from .production_id_input import ProductionIdInput
from .log_handler import GuiTextHandler
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput']
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput', 'GuiTextHandler']

120
gui/widgets/log_handler.py Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
自定义 logging Handler将日志输出到 LogText 组件
"""
import logging
import re
from typing import Optional
from gui.widgets.log_text import LogText
class GuiTextHandler(logging.Handler):
"""
将日志输出到 GUI LogText 组件的 Handler
这个 Handler 桥接了 Python 标准 logging 模块和 GUI 的 LogText 组件,
使得使用 logging 模块的代码可以自动将日志输出到 GUI 界面。
"""
def __init__(self, log_text: Optional[LogText] = None):
"""
初始化 Handler
Args:
log_text: LogText 组件实例,可以为 None稍后通过 set_log_text 设置
"""
super().__init__()
self.log_text = log_text
# 映射 logging 级别到 LogText 级别
self.level_map = {
logging.INFO: 'INFO',
logging.WARNING: 'WARNING',
logging.ERROR: 'ERROR',
logging.DEBUG: 'DEBUG',
logging.CRITICAL: 'ERROR'
}
def set_log_text(self, log_text: LogText):
"""
设置或更新 LogText 组件引用
Args:
log_text: LogText 组件实例
"""
self.log_text = log_text
def emit(self, record: logging.LogRecord):
"""
实现日志输出
Args:
record: logging.LogRecord 对象
"""
if not self.log_text:
return
try:
# 获取日志级别
level = self.level_map.get(record.levelno, 'INFO')
# 只获取消息内容不包含时间戳和级别LogText.log() 会添加)
message = record.getMessage()
# 移除消息中可能存在的冗余级别标记(如 "[INFO] "、"[ERROR] " 等)
# 这是因为有些代码在消息中已经包含了级别标记
message = self._strip_redundant_level_prefix(message)
# 定义更新函数
def update():
"""在主线程中更新 GUI"""
try:
# LogText.log() 会自动添加时间戳和级别
self.log_text.log(message, level)
except Exception:
# 如果 log 失败,忽略错误避免递归
pass
# 尝试使用 after 确保在主线程更新
import tkinter as tk
try:
# 尝试获取主窗口
widget = self.log_text
while widget and widget.master:
if isinstance(widget.master, tk.Tk):
# 找到主窗口,使用 after 调度更新
widget.master.after(0, update)
return
widget = widget.master
# 如果找不到主窗口,直接调用(适用于非 GUI 模式或测试)
update()
except Exception:
# 如果线程调度失败,直接调用
update()
except Exception:
# 处理错误,避免影响主程序
self.handleError(record)
def _strip_redundant_level_prefix(self, message: str) -> str:
"""
移除消息开头的冗余级别标记
例如:"[INFO] 读取 ProductionID 文件" -> "读取 ProductionID 文件"
"[ERROR] 错误信息" -> "错误信息"
Args:
message: 原始消息
Returns:
清理后的消息
"""
# 常见的日志级别标记模式
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

View File

@@ -466,6 +466,15 @@ class MaterialStatusValidator:
dao = DiscreteMaterialPlanDAO()
material_records = dao.query_by_source_numbers(source_numbers)
self._print(f"[INFO] 获取到 {len(material_records)} 条记录")
# 如果没有找到物料记录,给出友好提示
if not material_records:
self._print("\n[ERROR] 校验失败:未找到物料记录")
self._print("[ERROR] 可能原因:")
self._print("[ERROR] 1. 这些生产订单的物料数据还没有提取到数据库")
self._print("[ERROR] 2. 请先运行【正式备料计划数据提取】工具")
self._print("[ERROR] 3. 提取时勾选【持久化到数据库】选项")
self._print("[ERROR] 4. 将这 10 个 ProductionID 的物料数据保存到 DiscreteMaterialPlanData 表")
else:
raise ValueError("必须指定 full_table=True 或提供 production_id_file 参数")