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:
434
docs/LOGGING_MECHANISM.md
Normal file
434
docs/LOGGING_MECHANISM.md
Normal 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 传递
|
||||||
|
# 直接使用 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` - 重构总结文档
|
||||||
141
docs/LOGGING_REFACTORING_SUMMARY.md
Normal file
141
docs/LOGGING_REFACTORING_SUMMARY.md
Normal 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
|
||||||
@@ -9,12 +9,14 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import logging
|
||||||
import queue
|
import queue
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, filedialog, messagebox
|
from tkinter import ttk, filedialog, messagebox
|
||||||
from pathlib import Path
|
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.config_manager import ConfigManager
|
||||||
|
from gui.log_config import setup_gui_logging, get_logger
|
||||||
from gui.progress import ProgressInfo, ProgressCalculator
|
from gui.progress import ProgressInfo, ProgressCalculator
|
||||||
from gui.utils import RealtimeOutput
|
from gui.utils import RealtimeOutput
|
||||||
|
|
||||||
@@ -32,6 +34,10 @@ class DataExtractionTab(ttk.Frame):
|
|||||||
self.progress_calculator = ProgressCalculator()
|
self.progress_calculator = ProgressCalculator()
|
||||||
self.progress_queue = queue.Queue()
|
self.progress_queue = queue.Queue()
|
||||||
|
|
||||||
|
# 初始化统一日志系统
|
||||||
|
self.logger = get_logger(__name__)
|
||||||
|
self._gui_handler = None # 将在 _create_log_panel 中设置
|
||||||
|
|
||||||
self._poll_progress_queue()
|
self._poll_progress_queue()
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
self._apply_ui_config()
|
self._apply_ui_config()
|
||||||
@@ -116,6 +122,14 @@ class DataExtractionTab(ttk.Frame):
|
|||||||
self.log_text = LogText(parent, height=15, readonly=True)
|
self.log_text = LogText(parent, height=15, readonly=True)
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=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):
|
def _apply_ui_config(self):
|
||||||
try:
|
try:
|
||||||
font_family = self.config.get("ui.font_family", "Microsoft YaHei UI")
|
font_family = self.config.get("ui.font_family", "Microsoft YaHei UI")
|
||||||
@@ -219,15 +233,24 @@ class DataExtractionTab(ttk.Frame):
|
|||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
def _update_log(self, message: str, level: str = "INFO"):
|
def _update_log(self, message: str, level: str = "INFO"):
|
||||||
"""标准的日志更新方法"""
|
"""
|
||||||
def update():
|
标准的日志更新方法(兼容接口)
|
||||||
# 即使任务结束,只要是成功/错误消息也强制显示
|
|
||||||
if self.extracting or level in ["ERROR", "WARNING", "SUCCESS"]:
|
通过统一的 logging 系统输出日志,自动同时输出到控制台和 GUI。
|
||||||
if level == "INFO": self.log_text.info(message)
|
|
||||||
elif level == "SUCCESS": self.log_text.success(message)
|
Args:
|
||||||
elif level == "WARNING": self.log_text.warning(message)
|
message: 日志消息
|
||||||
elif level == "ERROR": self.log_text.error(message)
|
level: 日志级别 (INFO, SUCCESS, WARNING, ERROR, DEBUG)
|
||||||
self.after(0, update)
|
"""
|
||||||
|
# 将自定义级别映射到 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):
|
def _on_production_ids_changed(self, event=None):
|
||||||
if self.main_window:
|
if self.main_window:
|
||||||
|
|||||||
43
gui/log_config.py
Normal file
43
gui/log_config.py
Normal 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)
|
||||||
@@ -9,6 +9,7 @@ ERP 自动化工具的主窗口,包含多个功能标签页。
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from gui.config_manager import ConfigManager
|
from gui.config_manager import ConfigManager
|
||||||
|
from gui.log_config import setup_gui_logging
|
||||||
from gui.data_extraction_tab import DataExtractionTab
|
from gui.data_extraction_tab import DataExtractionTab
|
||||||
from gui.material_validation_tab import MaterialValidationTab
|
from gui.material_validation_tab import MaterialValidationTab
|
||||||
from gui.settings_tab import SettingsTab
|
from gui.settings_tab import SettingsTab
|
||||||
@@ -30,6 +31,9 @@ class MainWindow:
|
|||||||
self.session_manager = session_manager
|
self.session_manager = session_manager
|
||||||
self.shared_production_ids = [] # 共享的 Production ID 列表
|
self.shared_production_ids = [] # 共享的 Production ID 列表
|
||||||
|
|
||||||
|
# 初始化统一日志系统
|
||||||
|
setup_gui_logging()
|
||||||
|
|
||||||
# 设置窗口属性(包含用户信息)
|
# 设置窗口属性(包含用户信息)
|
||||||
user_type_display = "管理员" if session_manager.is_admin() else "用户"
|
user_type_display = "管理员" if session_manager.is_admin() else "用户"
|
||||||
self.root.title(f"ERP 自动化工具 v1.0 - {session_manager.get_username()} ({user_type_display})")
|
self.root.title(f"ERP 自动化工具 v1.0 - {session_manager.get_username()} ({user_type_display})")
|
||||||
|
|||||||
@@ -11,14 +11,16 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox, filedialog, simpledialog
|
from tkinter import ttk, messagebox, filedialog, simpledialog
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
from typing import List, Dict
|
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.config_manager import ConfigManager
|
||||||
|
from gui.log_config import setup_gui_logging, get_logger
|
||||||
from gui.material_type_management_dialog import MaterialTypeManagementDialog
|
from gui.material_type_management_dialog import MaterialTypeManagementDialog
|
||||||
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
|
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -156,6 +158,10 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
self.select_all_managers_var: tk.BooleanVar = tk.BooleanVar(value=True) # 全选复选框状态
|
self.select_all_managers_var: tk.BooleanVar = tk.BooleanVar(value=True) # 全选复选框状态
|
||||||
self.previously_selected_managers: List[str] = [] # 保存筛选状态
|
self.previously_selected_managers: List[str] = [] # 保存筛选状态
|
||||||
|
|
||||||
|
# 初始化统一日志系统
|
||||||
|
self.logger = get_logger(__name__)
|
||||||
|
self._gui_handler = None # 将在 _create_log_panel 中设置
|
||||||
|
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
|
||||||
# 稍后显示就绪消息
|
# 稍后显示就绪消息
|
||||||
@@ -379,6 +385,14 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
self.log_text = LogText(parent, height=8, readonly=True)
|
self.log_text = LogText(parent, height=8, readonly=True)
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=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):
|
def _on_source_mode_change(self):
|
||||||
"""数据源模式切换"""
|
"""数据源模式切换"""
|
||||||
# PERMISSION CHECK: 仅管理员用户可以选择数据源模式
|
# PERMISSION CHECK: 仅管理员用户可以选择数据源模式
|
||||||
@@ -895,10 +909,13 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
if line.strip():
|
if line.strip():
|
||||||
self._update_log(line, "INFO")
|
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._update_log(f"校验完成,结果已保存到:{output_file}", "SUCCESS")
|
||||||
# 加载结果显示(带删除状态)
|
# 加载结果显示(带删除状态)
|
||||||
self._load_results_with_deletion_status(result_file)
|
self._load_results_with_deletion_status(result_file)
|
||||||
|
elif results is not None and len(results) == 0:
|
||||||
|
# 已经在 validator 中输出详细错误信息,这里只做简单提示
|
||||||
|
self._update_log("校验失败:未找到物料记录,请查看上方日志了解详细原因", "ERROR")
|
||||||
else:
|
else:
|
||||||
self._update_log("校验失败", "ERROR")
|
self._update_log("校验失败", "ERROR")
|
||||||
|
|
||||||
@@ -1159,19 +1176,24 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
self._initialize_manager_filter()
|
self._initialize_manager_filter()
|
||||||
|
|
||||||
def _update_log(self, message: str, level: str = "INFO"):
|
def _update_log(self, message: str, level: str = "INFO"):
|
||||||
"""线程安全的日志更新"""
|
"""
|
||||||
|
线程安全的日志更新(兼容接口)
|
||||||
|
|
||||||
def update():
|
通过统一的 logging 系统输出日志,自动同时输出到控制台和 GUI。
|
||||||
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)
|
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):
|
def export_results(self):
|
||||||
"""导出结果到 Excel"""
|
"""导出结果到 Excel"""
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ GUI 自定义组件模块
|
|||||||
from .file_selector import FileSelector
|
from .file_selector import FileSelector
|
||||||
from .log_text import LogText
|
from .log_text import LogText
|
||||||
from .production_id_input import ProductionIdInput
|
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
120
gui/widgets/log_handler.py
Normal 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
|
||||||
@@ -466,6 +466,15 @@ class MaterialStatusValidator:
|
|||||||
dao = DiscreteMaterialPlanDAO()
|
dao = DiscreteMaterialPlanDAO()
|
||||||
material_records = dao.query_by_source_numbers(source_numbers)
|
material_records = dao.query_by_source_numbers(source_numbers)
|
||||||
self._print(f"[INFO] 获取到 {len(material_records)} 条记录")
|
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:
|
else:
|
||||||
raise ValueError("必须指定 full_table=True 或提供 production_id_file 参数")
|
raise ValueError("必须指定 full_table=True 或提供 production_id_file 参数")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user