a13rollcall/exceptions.py
2026-10-06 18:38:55 +08:00

549 行
19 KiB
Python

# -*- coding: utf-8 -*-
"""
A13课堂点名系统 - 异常层级设计模块
定义项目统一的异常类层级,便于错误处理和排查
"""
import traceback
from datetime import datetime
from typing import Optional, Dict, Any
# ==================== 基础异常 ====================
class A13BaseError(Exception):
"""A13系统基础异常类"""
def __init__(self, message: str = "", error_code: int = -1, details: Optional[Dict[str, Any]] = None):
self.message = message
self.error_code = error_code
self.details = details or {}
self.timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
super().__init__(message)
def __str__(self):
return f"[{self.error_code}] {self.message}"
def to_dict(self) -> Dict[str, Any]:
return {
"error_type": self.__class__.__name__,
"message": self.message,
"error_code": self.error_code,
"details": self.details,
"timestamp": self.timestamp,
}
def get_full_traceback(self) -> str:
return traceback.format_exc()
# ==================== 配置相关异常 ====================
class ConfigError(A13BaseError):
"""配置相关异常基类"""
pass
class ConfigNotFoundError(ConfigError):
"""配置文件不存在"""
def __init__(self, config_path: str = "", message: str = ""):
super().__init__(
message=message or f"配置文件不存在: {config_path}",
error_code=-200,
details={"config_path": config_path}
)
class ConfigParseError(ConfigError):
"""配置解析错误"""
def __init__(self, config_path: str = "", parse_error: str = "", message: str = ""):
super().__init__(
message=message or f"配置文件解析失败: {config_path}\n错误: {parse_error}",
error_code=-201,
details={"config_path": config_path, "parse_error": parse_error}
)
class ConfigSchemaError(ConfigError):
"""配置Schema验证错误"""
def __init__(self, schema_errors: list = None, message: str = ""):
super().__init__(
message=message or f"配置Schema验证失败: {len(schema_errors or [])} 个错误",
error_code=-202,
details={"schema_errors": schema_errors or []}
)
class ConfigValueError(ConfigError):
"""配置值错误"""
def __init__(self, key: str = "", value: Any = None, expected: str = "", message: str = ""):
super().__init__(
message=message or f"配置值错误: {key} = {value}, 期望: {expected}",
error_code=-203,
details={"key": key, "value": value, "expected": expected}
)
# ==================== 文件相关异常 ====================
class FileError(A13BaseError):
"""文件相关异常基类"""
pass
class FileNotFoundError(FileError):
"""文件不存在"""
def __init__(self, file_path: str = "", message: str = ""):
super().__init__(
message=message or f"文件不存在: {file_path}",
error_code=-100,
details={"file_path": file_path}
)
class FileReadError(FileError):
"""文件读取错误"""
def __init__(self, file_path: str = "", error: str = "", message: str = ""):
super().__init__(
message=message or f"文件读取失败: {file_path}\n错误: {error}",
error_code=-101,
details={"file_path": file_path, "error": error}
)
class FileWriteError(FileError):
"""文件写入错误"""
def __init__(self, file_path: str = "", error: str = "", message: str = ""):
super().__init__(
message=message or f"文件写入失败: {file_path}\n错误: {error}",
error_code=-102,
details={"file_path": file_path, "error": error}
)
class FileCorruptedError(FileError):
"""文件损坏"""
def __init__(self, file_path: str = "", message: str = ""):
super().__init__(
message=message or f"文件已损坏: {file_path}",
error_code=-103,
details={"file_path": file_path}
)
class FileLockedError(FileError):
"""文件被锁定"""
def __init__(self, file_path: str = "", message: str = ""):
super().__init__(
message=message or f"文件被锁定,无法访问: {file_path}",
error_code=-104,
details={"file_path": file_path}
)
class FileEncodingError(FileError):
"""文件编码错误"""
def __init__(self, file_path: str = "", encoding: str = "", message: str = ""):
super().__init__(
message=message or f"文件编码错误: {file_path}, 编码: {encoding}",
error_code=-105,
details={"file_path": file_path, "encoding": encoding}
)
# ==================== 插件相关异常 ====================
class PluginError(A13BaseError):
"""插件相关异常基类"""
pass
class PluginNotFoundError(PluginError):
"""插件不存在"""
def __init__(self, plugin_name: str = "", message: str = ""):
super().__init__(
message=message or f"插件不存在: {plugin_name}",
error_code=-300,
details={"plugin_name": plugin_name}
)
class PluginLoadError(PluginError):
"""插件加载错误"""
def __init__(self, plugin_name: str = "", error: str = "", message: str = ""):
super().__init__(
message=message or f"插件加载失败: {plugin_name}\n错误: {error}",
error_code=-301,
details={"plugin_name": plugin_name, "error": error}
)
class PluginExecuteError(PluginError):
"""插件执行错误"""
def __init__(self, plugin_name: str = "", function_name: str = "", error: str = "", message: str = ""):
super().__init__(
message=message or f"插件执行失败: {plugin_name}.{function_name}\n错误: {error}",
error_code=-302,
details={"plugin_name": plugin_name, "function_name": function_name, "error": error}
)
class PluginDependencyMissingError(PluginError):
"""插件依赖缺失"""
def __init__(self, plugin_name: str = "", missing_deps: list = None, message: str = ""):
super().__init__(
message=message or f"插件依赖缺失: {plugin_name}\n缺失: {', '.join(missing_deps or [])}",
error_code=-303,
details={"plugin_name": plugin_name, "missing_deps": missing_deps or []}
)
class PluginVersionIncompatibleError(PluginError):
"""插件版本不兼容"""
def __init__(self, plugin_name: str = "", required_version: str = "", current_version: str = "", message: str = ""):
super().__init__(
message=message or f"插件版本不兼容: {plugin_name}\n要求: {required_version}, 当前: {current_version}",
error_code=-304,
details={"plugin_name": plugin_name, "required_version": required_version, "current_version": current_version}
)
class PluginPermissionDeniedError(PluginError):
"""插件权限被拒绝"""
def __init__(self, plugin_name: str = "", permission: str = "", message: str = ""):
super().__init__(
message=message or f"插件权限被拒绝: {plugin_name}\n权限: {permission}",
error_code=-305,
details={"plugin_name": plugin_name, "permission": permission}
)
class PluginCrashedError(PluginError):
"""插件崩溃"""
def __init__(self, plugin_name: str = "", crash_info: str = "", message: str = ""):
super().__init__(
message=message or f"插件崩溃: {plugin_name}\n崩溃信息: {crash_info}",
error_code=-306,
details={"plugin_name": plugin_name, "crash_info": crash_info}
)
class PluginFormatError(PluginError):
"""插件格式错误"""
def __init__(self, file_path: str = "", error: str = "", message: str = ""):
super().__init__(
message=message or f"插件格式错误: {file_path}\n错误: {error}",
error_code=-307,
details={"file_path": file_path, "error": error}
)
# ==================== 网络相关异常 ====================
class NetworkError(A13BaseError):
"""网络相关异常基类"""
pass
class NetworkTimeoutError(NetworkError):
"""网络超时"""
def __init__(self, url: str = "", timeout: float = 0, message: str = ""):
super().__init__(
message=message or f"网络请求超时: {url} (超时: {timeout}s)",
error_code=-400,
details={"url": url, "timeout": timeout}
)
class NetworkConnectionError(NetworkError):
"""网络连接错误"""
def __init__(self, url: str = "", error: str = "", message: str = ""):
super().__init__(
message=message or f"网络连接失败: {url}\n错误: {error}",
error_code=-401,
details={"url": url, "error": error}
)
class NetworkHttpError(NetworkError):
"""HTTP错误"""
def __init__(self, url: str = "", status_code: int = 0, message: str = ""):
super().__init__(
message=message or f"HTTP请求错误: {url} (状态码: {status_code})",
error_code=-402,
details={"url": url, "status_code": status_code}
)
class NetworkSslError(NetworkError):
"""SSL错误"""
def __init__(self, url: str = "", error: str = "", message: str = ""):
super().__init__(
message=message or f"SSL错误: {url}\n错误: {error}",
error_code=-403,
details={"url": url, "error": error}
)
# ==================== 数据相关异常 ====================
class DataError(A13BaseError):
"""数据相关异常基类"""
pass
class DataEmptyError(DataError):
"""数据为空"""
def __init__(self, data_name: str = "", message: str = ""):
super().__init__(
message=message or f"数据为空: {data_name}",
error_code=-500,
details={"data_name": data_name}
)
class DataInvalidError(DataError):
"""数据无效"""
def __init__(self, data_name: str = "", invalid_value: Any = None, message: str = ""):
super().__init__(
message=message or f"数据无效: {data_name} = {invalid_value}",
error_code=-501,
details={"data_name": data_name, "invalid_value": invalid_value}
)
class DataDuplicateError(DataError):
"""数据重复"""
def __init__(self, data_name: str = "", duplicate_value: Any = None, message: str = ""):
super().__init__(
message=message or f"数据重复: {data_name} = {duplicate_value}",
error_code=-502,
details={"data_name": data_name, "duplicate_value": duplicate_value}
)
class DataNotFoundError(DataError):
"""数据不存在"""
def __init__(self, data_name: str = "", search_key: Any = None, message: str = ""):
super().__init__(
message=message or f"数据不存在: {data_name} (查找: {search_key})",
error_code=-503,
details={"data_name": data_name, "search_key": search_key}
)
# ==================== 抽取相关异常 ====================
class DrawError(A13BaseError):
"""抽取相关异常基类"""
pass
class DrawNoStudentsError(DrawError):
"""没有学生可抽取"""
def __init__(self, message: str = ""):
super().__init__(
message=message or "没有学生可抽取,请先添加学生名单",
error_code=-600
)
class DrawAllDrawnError(DrawError):
"""所有学生已抽取完毕"""
def __init__(self, message: str = ""):
super().__init__(
message=message or "所有学生已抽取完毕,请重置本轮",
error_code=-601
)
class DrawAlreadyRunningError(DrawError):
"""抽取正在进行中"""
def __init__(self, message: str = ""):
super().__init__(
message=message or "抽取正在进行中,请稍候",
error_code=-602
)
class DrawNotRunningError(DrawError):
"""抽取未在进行中"""
def __init__(self, message: str = ""):
super().__init__(
message=message or "当前没有正在进行的抽取",
error_code=-603
)
# ==================== UI相关异常 ====================
class UIError(A13BaseError):
"""UI相关异常基类"""
pass
class WidgetNotFoundError(UIError):
"""控件不存在"""
def __init__(self, widget_name: str = "", message: str = ""):
super().__init__(
message=message or f"控件不存在: {widget_name}",
error_code=-700,
details={"widget_name": widget_name}
)
class LayoutError(UIError):
"""布局错误"""
def __init__(self, layout_name: str = "", error: str = "", message: str = ""):
super().__init__(
message=message or f"布局错误: {layout_name}\n错误: {error}",
error_code=-701,
details={"layout_name": layout_name, "error": error}
)
# ==================== 权限相关异常 ====================
class PermissionError(A13BaseError):
"""权限相关异常基类"""
pass
class PermissionDeniedError(PermissionError):
"""权限被拒绝"""
def __init__(self, permission: str = "", message: str = ""):
super().__init__(
message=message or f"权限被拒绝: {permission}",
error_code=-4,
details={"permission": permission}
)
class OperationNotAllowedError(PermissionError):
"""操作不被允许"""
def __init__(self, operation: str = "", reason: str = "", message: str = ""):
super().__init__(
message=message or f"操作不被允许: {operation}\n原因: {reason}",
error_code=-5,
details={"operation": operation, "reason": reason}
)
# ==================== 异常工具函数 ====================
def get_error_category(error_code: int) -> str:
"""根据错误码获取错误类别"""
if error_code == 0:
return "成功"
elif -100 > error_code >= -200:
return "配置错误"
elif -100 > error_code >= -100:
return "文件错误"
elif -300 > error_code >= -400:
return "插件错误"
elif -400 > error_code >= -500:
return "网络错误"
elif -500 > error_code >= -600:
return "数据错误"
elif -600 > error_code >= -700:
return "抽取错误"
elif -700 > error_code >= -800:
return "UI错误"
else:
return "未知错误"
def format_error_message(error: Exception) -> str:
"""格式化错误消息,用户友好"""
if isinstance(error, A13BaseError):
return error.message
elif isinstance(error, FileNotFoundError):
return f"文件不存在: {error.filename}"
elif isinstance(error, PermissionError):
return "权限不足,无法执行此操作"
elif isinstance(error, TimeoutError):
return "操作超时,请重试"
elif isinstance(error, MemoryError):
return "内存不足,请关闭其他程序后重试"
else:
return f"发生错误: {str(error)}"
def get_user_friendly_suggestion(error: Exception) -> str:
"""获取用户友好的错误解决建议"""
if isinstance(error, ConfigNotFoundError):
return "配置文件将在下次保存时自动创建"
elif isinstance(error, ConfigParseError):
return "配置文件格式错误,建议删除配置文件后重启程序,将自动恢复默认配置"
elif isinstance(error, FileNotFoundError):
return "请检查文件路径是否正确,或重新创建该文件"
elif isinstance(error, FileCorruptedError):
return "文件已损坏,建议从备份恢复,或重新生成该文件"
elif isinstance(error, PluginLoadError):
return "插件加载失败,请检查插件文件是否完整,或尝试重新安装插件"
elif isinstance(error, PluginDependencyMissingError):
return "插件缺少依赖库,请安装所需的Python库后重试"
elif isinstance(error, PluginVersionIncompatibleError):
return "插件版本与当前程序不兼容,请更新程序或插件"
elif isinstance(error, NetworkTimeoutError):
return "网络连接超时,请检查网络连接后重试"
elif isinstance(error, DrawNoStudentsError):
return "请先在学生名单文件中添加学生信息"
elif isinstance(error, DrawAllDrawnError):
return "点击'重置本轮'按钮可以重新开始抽取"
elif isinstance(error, PermissionDeniedError):
return "请以管理员身份运行程序,或检查文件权限"
else:
return "如问题持续存在,请查看日志文件或联系技术支持"
# ==================== 异常上下文管理器 ====================
class ExceptionHandler:
"""异常处理器,用于统一捕获和处理异常"""
def __init__(self, raise_errors: bool = False, log_errors: bool = True):
self.raise_errors = raise_errors
self.log_errors = log_errors
self.last_error = None
def handle(self, error: Exception, context: str = "") -> bool:
"""处理异常,返回是否成功处理"""
self.last_error = error
if self.log_errors:
try:
import logging
logging.error(f"[{context}] {type(error).__name__}: {str(error)}")
logging.error(traceback.format_exc())
except Exception:
pass
if self.raise_errors:
raise error
return True
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val:
return self.handle(exc_val, "context")
return False
# ==================== 安全执行装饰器 ====================
def safe_execute(default_return=None, log_error: bool = True, show_error: bool = False):
"""安全执行装饰器,捕获异常并返回默认值"""
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
if log_error:
try:
import logging
logging.error(f"函数 {func.__name__} 执行失败: {str(e)}")
logging.error(traceback.format_exc())
except Exception:
pass
if show_error:
try:
from PyQt5.QtWidgets import QMessageBox
QMessageBox.critical(None, "错误", format_error_message(e))
except Exception:
pass
return default_return
return wrapper
return decorator