deepdata: core product code (backend, frontend, runtime, deepflow module)

This commit is contained in:
figmar
2026-08-30 19:23:38 +08:00
commit bd3db30be9
1405 changed files with 271727 additions and 0 deletions
@@ -0,0 +1,7 @@
# app/core/__init__.py
"""
Core module
"""
+120
View File
@@ -0,0 +1,120 @@
from pydantic_settings import BaseSettings
from pydantic import model_validator
from typing import Optional
class Settings(BaseSettings):
"""应用程序配置"""
class Config:
env_file = ".env"
case_sensitive = False
extra = 'ignore'
# Service
app_name: str = "DeepData Python Backend"
app_version: str = "1.0.0"
app_description: str = "Adapter for integrating Data Management System with Label Studio"
host: str = "0.0.0.0"
port: int = 18000
# CORS
# allowed_origins: List[str] = ["*"]
# allowed_methods: List[str] = ["*"]
# allowed_headers: List[str] = ["*"]
# Log
log_level: str = "INFO"
debug: bool = True
log_file_dir: str = "/var/log/deepdata/backend-python"
log_pvc_monitor_enabled: bool = True
log_pvc_monitor_path: str = "/var/log/deepdata"
log_pvc_monitor_threshold: float = 0.9
log_pvc_monitor_interval_seconds: int = 300
log_pvc_monitor_delete_batch_size: int = 10
log_pvc_monitor_file_suffixes: str = "log,out,err"
log_rotation_max_size: str = "100MB"
log_rotation_backup_count: int = 30
rag_storage_dir: str = "/data/rag_storage"
# Database
pgsql_host: str = "deepdata-database"
pgsql_port: int = 5432
pgsql_user: str = "postgres"
pgsql_password: str = ""
pgsql_database: str = "deepdata"
# Database
mysql_host: str = "deepdata-database"
mysql_port: int = 3306
mysql_user: str = "root"
mysql_password: str = ""
mysql_database: str = "deepdata"
database_url: str = "" # Will be overridden by build_database_url() if not provided
@model_validator(mode='after')
def build_database_url(self):
"""如果没有提供 database_url,则根据 MySQL 配置构建"""
if not self.database_url:
if self.pgsql_host:
if self.pgsql_password and self.pgsql_user:
self.database_url = f"postgresql+asyncpg://{self.pgsql_user}:{self.pgsql_password}@{self.pgsql_host}:{self.pgsql_port}/{self.pgsql_database}"
else:
self.database_url = f"postgresql+asyncpg://{self.pgsql_host}:{self.pgsql_port}/{self.pgsql_database}"
elif self.mysql_password and self.mysql_user:
self.database_url = f"mysql+aiomysql://{self.mysql_user}:{self.mysql_password}@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
else:
self.database_url = f"mysql+aiomysql://{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
return self
# Label Studio
label_studio_base_url: str = "http://label-studio:8000"
label_studio_username: Optional[str] = None
label_studio_password: Optional[str] = None
label_studio_user_token: Optional[str] = None # Legacy Token
label_studio_local_document_root: str = "/label-studio/local" # Label Studio local file storage path
label_studio_file_path_prefix: str = "/data/local-files/?d=" # Label Studio local file serving URL prefix
ls_task_page_size: int = 1000
# DeepData
dm_file_path_prefix: str = "/dataset" # DM存储文件夹前缀
deepdata_jwt_enable: bool = False
# Milvus 配置
milvus_uri: str = "http://milvus:19530"
milvus_token: str = ""
# 文件存储配置(共享文件系统)
file_storage_path: str = "/data/files"
# ==================== 配比任务并行复制配置 ====================
# 动态并发计算参数(全闪存储高性能场景默认值)
# 并发下限(最少并发数)
ratio_copy_min_concurrent: int = 8
# 并发上限(最多并发数,防止资源耗尽)
ratio_copy_max_concurrent: int = 128
# CPU核心系数(每个核心贡献的并发数,全闪存储建议4.0)
ratio_copy_cpu_factor: float = 4.0
# 每并发任务预估内存占用(MB)
ratio_copy_memory_per_task_mb: int = 32
# 内存安全保留比例(保留给其他进程)
ratio_copy_memory_reserve_ratio: float = 0.2
# 是否启用动态计算(False则使用固定值)
ratio_copy_dynamic_concurrent: bool = True
# 固定并发数(当 dynamic_concurrent=False 时使用)
ratio_copy_fixed_concurrent: int = 10
# 全局设置实例
settings = Settings()
@@ -0,0 +1,240 @@
"""
核心异常处理模块
为应用程序提供统一、优雅的异常处理系统,确保所有 API 响应都符合标准化格式。
## 快速开始
### 1. 注册异常处理器(在 main.py 中)
```python
from app.core.exception import register_exception_handlers, ExceptionHandlingMiddleware
app = FastAPI()
# 注册全局异常捕获中间件(最外层)
app.add_middleware(ExceptionHandlingMiddleware)
# 注册异常处理器
register_exception_handlers(app)
```
### 2. 在代码中抛出业务异常
```python
from app.core.exception import ErrorCodes, BusinessError
# 资源不存在
async def get_user(user_id: str):
user = await db.get_user(user_id)
if not user:
raise BusinessError(ErrorCodes.NOT_FOUND)
return user
# 参数验证失败
async def create_user(name: str):
if not name:
raise BusinessError(ErrorCodes.BAD_REQUEST)
# ...
```
### 3. 返回成功响应
```python
from app.core.exception import SuccessResponse
@router.get("/users/{user_id}")
async def get_user(user_id: str):
user = await service.get_user(user_id)
return SuccessResponse(data=user)
# 返回格式: {"code": "0", "message": "success", "data": {...}}
```
### 4. 使用事务管理
```python
from app.core.exception import transaction, BusinessError, ErrorCodes
@router.post("/users")
async def create_user(request: CreateUserRequest, db: AsyncSession = Depends(get_db)):
async with transaction(db):
# 所有数据库操作
user = User(name=request.name)
db.add(user)
await db.flush()
# 如果抛出异常,事务会自动回滚
if check_duplicate(user.name):
raise BusinessError(ErrorCodes.OPERATION_FAILED)
# 事务已自动提交
return SuccessResponse(data=user)
```
## 异常类型说明
### BusinessError(业务异常)
用于预期的业务错误,如资源不存在、权限不足、参数错误等。
特点:
- 不会记录完整的堆栈跟踪(因为这是预期内的错误)
- 返回对应的 HTTP 状态码(400、404 等)
- 客户端会收到标准化的错误响应
使用场景:
- 资源不存在
- 参数验证失败
- 权限不足
- 业务规则违反
### SystemError(系统异常)
用于意外的系统错误,如数据库错误、网络错误、配置错误等。
特点:
- 记录完整的堆栈跟踪
- 返回 HTTP 500
- 不暴露敏感的系统信息给客户端
使用场景:
- 数据库连接失败
- 网络超时
- 配置错误
- 编程错误
## 错误码定义
所有错误码在 `ErrorCodes` 类中集中定义,遵循规范:`{module}.{sequence}`
### 通用错误码
- `SUCCESS` (0): 操作成功
- `BAD_REQUEST` (common.0001): 请求参数错误
- `NOT_FOUND` (common.0002): 资源不存在
- `FORBIDDEN` (common.0003): 权限不足
- `UNAUTHORIZED` (common.0004): 未授权访问
- `VALIDATION_ERROR` (common.0005): 数据验证失败
### 系统级错误码
- `INTERNAL_ERROR` (system.0001): 服务器内部错误
- `DATABASE_ERROR` (system.0002): 数据库错误
- `NETWORK_ERROR` (system.0003): 网络错误
### 模块错误码
- `annotation.*`: 标注模块相关错误
- `collection.*`: 归集模块相关错误
- `evaluation.*`: 评估模块相关错误
- `generation.*`: 生成模块相关错误
- `rag.*`: RAG 模块相关错误
- `ratio.*`: 配比模块相关错误
## Result 类型(可选的函数式错误处理)
如果你不喜欢使用异常,可以使用 Result 类型进行函数式错误处理:
```python
from app.core.exception import Result, Ok, Err, ErrorCodes
def get_user(user_id: str) -> Result[User]:
user = db.find_user(user_id)
if user:
return Ok(user)
return Err(ErrorCodes.NOT_FOUND)
# 使用结果
result = get_user("123")
if result.is_ok():
user = result.unwrap()
print(f"User: {user.name}")
else:
error = result.unwrap_err()
print(f"Error: {error.message}")
# 链式操作
result = get_user("123")
.map(lambda user: user.name)
.and_then(validate_name)
```
## 响应格式
### 成功响应
```json
{
"code": "0",
"message": "success",
"data": {
"id": 123,
"name": "张三"
}
}
```
### 错误响应
```json
{
"code": "common.0002",
"message": "资源不存在",
"data": null
}
```
## 最佳实践
1. **始终使用业务异常**:对于可预见的业务错误,使用 `BusinessError` 而不是 HTTPException
2. **集中定义错误码**:所有错误码在 `ErrorCodes` 中定义,不要硬编码
3. **提供有用的数据**:在抛出异常时,可以通过 `data` 参数传递额外的错误信息
4. **使用事务管理**:涉及多个数据库操作时,使用 `transaction` 上下文管理器
5. **不要捕获 SystemError**:让系统错误由全局处理器统一处理
## 迁移指南
如果你有旧的异常处理代码,迁移步骤:
1. 删除旧的异常类定义
2. 将 `raise OldException(...)` 替换为 `raise BusinessError(ErrorCodes.XXX)`
3. 移除 try-except 中的异常转换逻辑,让全局处理器处理
4. 更新导入语句:`from app.core.exception import ErrorCodes, BusinessError`
## 测试
可以使用测试端点验证异常处理:
```bash
curl http://localhost:8000/test-success
curl http://localhost:8000/test-business-error
curl http://localhost:8000/test-system-error
```
"""
from .base import BaseError, ErrorCode, SystemError, BusinessError
from .codes import ErrorCodes
from .handlers import (
register_exception_handlers,
ErrorResponse,
SuccessResponse
)
from .middleware import ExceptionHandlingMiddleware
from .result import Result, Ok, Err
from .transaction import transaction, ensure_transaction_rollback
__all__ = [
# 基础异常类
'BaseError',
'ErrorCode',
'SystemError',
'BusinessError',
# 错误码
'ErrorCodes',
# 处理器
'register_exception_handlers',
'ErrorResponse',
'SuccessResponse',
# 中间件
'ExceptionHandlingMiddleware',
# Result 类型
'Result',
'Ok',
'Err',
# 事务管理
'transaction',
'ensure_transaction_rollback',
]
@@ -0,0 +1,121 @@
"""
基础异常类和错误码定义
"""
from typing import Any
from dataclasses import dataclass
@dataclass(frozen=True)
class ErrorCode:
"""
不可变的错误码定义
属性:
code: 错误码字符串(如 "annotation.0001")
message: 人类可读的错误消息
http_status: HTTP状态码(业务错误默认为400)
"""
code: str
message: str
http_status: int = 400
def __post_init__(self):
"""验证错误码格式"""
if not isinstance(self.code, str):
raise ValueError(f"错误码必须是字符串,实际类型: {type(self.code)}")
if not self.code:
raise ValueError("错误码不能为空")
class BaseError(Exception):
"""
所有应用异常的基类
所有自定义异常都应该继承此类。
提供自动的错误码和消息处理。
使用示例:
raise BusinessError(ErrorCodes.TASK_NOT_FOUND)
客户端将收到:
{
"code": "annotation.0001",
"message": "任务不存在",
"data": null
}
"""
def __init__(
self,
error_code: ErrorCode,
data: Any = None,
*args: Any
):
"""
使用错误码和可选数据初始化异常
Args:
error_code: ErrorCode 实例
data: 附加错误数据(会包含在响应中)
*args: 额外参数(用于兼容性)
"""
self.error_code = error_code
self.data = data
super().__init__(error_code.message, *args)
@property
def code(self) -> str:
"""获取错误码字符串"""
return self.error_code.code
@property
def message(self) -> str:
"""获取错误消息"""
return self.error_code.message
@property
def http_status(self) -> int:
"""获取HTTP状态码"""
return self.error_code.http_status
def to_dict(self) -> dict:
"""将异常转换为响应字典"""
return {
"code": self.code,
"message": self.message,
"data": self.data
}
class SystemError(BaseError):
"""
系统级异常(意外错误)
用于:
- 数据库错误
- 网络错误
- 配置错误
- 编程错误(bug)
系统错误会记录完整的堆栈跟踪
"""
def __init__(self, error_code: ErrorCode, data: Any = None):
super().__init__(error_code, data)
class BusinessError(BaseError):
"""
业务逻辑异常(预期内的错误)
用于:
- 验证失败
- 资源不存在
- 权限不足
- 业务规则违反
业务错误不记录堆栈跟踪(因为它们是预期的)
"""
def __init__(self, error_code: ErrorCode, data: Any = None):
super().__init__(error_code, data)
@@ -0,0 +1,212 @@
"""
集中式错误码定义
所有错误码都在这里定义,遵循规范:{module}.{sequence}
模块代码:
- common: 通用错误
- system: 系统级错误
- annotation: 标注模块
- collection: 归集模块
- evaluation: 评估模块
- generation: 生成模块
- rag: RAG模块
- ratio: 配比模块
"""
from typing import Final
from .base import ErrorCode
class ErrorCodes:
def __init__(self):
self.message = None
self.code = None
"""
集中式错误码定义
所有错误码在此一次性定义,使用时直接通过类属性访问。
使用示例:
from app.core.exception import ErrorCodes, BusinessError
raise BusinessError(ErrorCodes.TASK_NOT_FOUND)
"""
# ========== 通用错误码 ==========
SUCCESS: Final = ErrorCode("0", "Success", 200)
BAD_REQUEST: Final = ErrorCode("common.0001", "Bad request", 400)
NOT_FOUND: Final = ErrorCode("common.0002", "Resource not found", 404)
FORBIDDEN: Final = ErrorCode("common.0003", "Forbidden", 403)
UNAUTHORIZED: Final = ErrorCode("common.0004", "Unauthorized", 401)
VALIDATION_ERROR: Final = ErrorCode("common.0005", "Validation error", 422)
OPERATION_FAILED: Final = ErrorCode("common.0006", "Operation failed", 500)
# ========== 系统级错误码 ==========
INTERNAL_ERROR: Final = ErrorCode("system.0001", "Internal server error", 500)
DATABASE_ERROR: Final = ErrorCode("system.0002", "Database error", 500)
NETWORK_ERROR: Final = ErrorCode("system.0003", "Network error", 500)
CONFIG_ERROR: Final = ErrorCode("system.0004", "Configuration error", 500)
SERVICE_UNAVAILABLE: Final = ErrorCode("system.0005", "Service unavailable", 503)
# ========== 标注模块 ==========
ANNOTATION_TASK_NOT_FOUND: Final = ErrorCode(
"annotation.0001", "Annotation task not found", 404
)
ANNOTATION_PROJECT_NOT_FOUND: Final = ErrorCode(
"annotation.0002", "Annotation project not found", 404
)
ANNOTATION_TEMPLATE_NOT_FOUND: Final = ErrorCode(
"annotation.0003", "Annotation template not found", 404
)
ANNOTATION_FILE_NOT_FOUND: Final = ErrorCode(
"annotation.0004", "File not found", 404
)
ANNOTATION_TAG_UPDATE_FAILED: Final = ErrorCode(
"annotation.0005", "Failed to update tags", 500
)
# ========== 归集模块 ==========
COLLECTION_TASK_NOT_FOUND: Final = ErrorCode(
"collection.0001", "Collection task not found", 404
)
COLLECTION_TEMPLATE_NOT_FOUND: Final = ErrorCode(
"collection.0002", "Collection template not found", 404
)
COLLECTION_EXECUTION_NOT_FOUND: Final = ErrorCode(
"collection.0003", "Execution record not found", 404
)
COLLECTION_LOG_NOT_FOUND: Final = ErrorCode(
"collection.0004", "Log file not found", 404
)
# ========== 评估模块 ==========
EVALUATION_TASK_NOT_FOUND: Final = ErrorCode(
"evaluation.0001", "Evaluation task not found", 404
)
EVALUATION_TASK_TYPE_ERROR: Final = ErrorCode(
"evaluation.0002", "Invalid task type", 400
)
EVALUATION_MODEL_NOT_FOUND: Final = ErrorCode(
"evaluation.0003", "Evaluation model not found", 404
)
# ========== 生成模块 ==========
GENERATION_TASK_NOT_FOUND: Final = ErrorCode(
"generation.0001", "Generation task not found", 404
)
GENERATION_FILE_NOT_FOUND: Final = ErrorCode(
"generation.0002", "Generation file not found", 404
)
GENERATION_CHUNK_NOT_FOUND: Final = ErrorCode(
"generation.0003", "Data chunk not found", 404
)
GENERATION_DATA_NOT_FOUND: Final = ErrorCode(
"generation.0004", "Generation data not found", 404
)
# ========== RAG 模块 ==========
RAG_CONFIG_ERROR: Final = ErrorCode("rag.0001", "RAG configuration error", 400)
RAG_KNOWLEDGE_BASE_NOT_FOUND: Final = ErrorCode(
"rag.0002", "Knowledge base not found", 404
)
RAG_KNOWLEDGE_BASE_ALREADY_EXISTS: Final = ErrorCode(
"rag.0003", "Knowledge base already exists", 400
)
RAG_KNOWLEDGE_BASE_NAME_INVALID: Final = ErrorCode(
"rag.0004", "Knowledge base name is invalid", 400
)
RAG_FILE_NOT_FOUND: Final = ErrorCode("rag.0005", "RAG file not found", 404)
RAG_FILE_PROCESS_FAILED: Final = ErrorCode(
"rag.0006", "File processing failed", 500
)
RAG_FILE_PARSE_FAILED: Final = ErrorCode("rag.0007", "File parsing failed", 500)
RAG_CHUNK_NOT_FOUND: Final = ErrorCode("rag.0008", "Chunk not found", 404)
RAG_MODEL_NOT_FOUND: Final = ErrorCode("rag.0009", "RAG model not found", 404)
RAG_QUERY_FAILED: Final = ErrorCode("rag.0010", "RAG query failed", 500)
RAG_MILVUS_ERROR: Final = ErrorCode("rag.0011", "Milvus operation failed", 500)
RAG_COLLECTION_NOT_FOUND: Final = ErrorCode(
"rag.0012", "Milvus collection not found", 404
)
RAG_EMBEDDING_FAILED: Final = ErrorCode(
"rag.0013", "Embedding generation failed", 500
)
RAG_UNSUPPORTED_TYPE: Final = ErrorCode(
"rag.0014", "Unsupported RAG type", 400
)
RAG_INVALID_REQUEST: Final = ErrorCode(
"rag.0015", "Invalid request", 400
)
# ========== 配比模块 ==========
RATIO_TASK_NOT_FOUND: Final = ErrorCode("ratio.0001", "Ratio task not found", 404)
RATIO_NAME_REQUIRED: Final = ErrorCode("ratio.0002", "Task name is required", 400)
RATIO_ALREADY_EXISTS: Final = ErrorCode("ratio.0003", "Task already exists", 400)
RATIO_DELETE_FAILED: Final = ErrorCode("ratio.0004", "Failed to delete task", 500)
# ========== 清洗模块 ==========
CLEANING_TASK_NOT_FOUND: Final = ErrorCode(
"cleaning.0001", "Cleaning task not found", 404
)
CLEANING_NAME_DUPLICATED: Final = ErrorCode(
"cleaning.0002", "Cleaning task name is duplicated", 400
)
CLEANING_TEMPLATE_NOT_FOUND: Final = ErrorCode(
"cleaning.0003", "Cleaning template not found", 404
)
CLEANING_TEMPLATE_NAME_DUPLICATED: Final = ErrorCode(
"cleaning.0004", "Cleaning template name is duplicated", 400
)
CLEANING_INVALID_OPERATOR_INPUT: Final = ErrorCode(
"cleaning.0005", "Invalid operator input/output types", 400
)
CLEANING_INVALID_EXECUTOR_TYPE: Final = ErrorCode(
"cleaning.0006", "Invalid executor type", 400
)
CLEANING_DATASET_NOT_FOUND: Final = ErrorCode(
"cleaning.0007", "Dataset not found", 404
)
CLEANING_FILE_SYSTEM_ERROR: Final = ErrorCode(
"cleaning.0008", "File system error", 500
)
CLEANING_SETTINGS_PARSE_ERROR: Final = ErrorCode(
"cleaning.0009", "Settings parse error", 400
)
CLEANING_TASK_ID_REQUIRED: Final = ErrorCode(
"cleaning.0010", "Task ID is required", 400
)
CLEANING_CANNOT_DELETE_PRESET_TEMPLATE: Final = ErrorCode(
"cleaning.0011", "Cannot delete preset template", 400
)
CLEANING_TASK_LOG_NOT_FOUND: Final = ErrorCode(
"cleaning.0012", "Cleaning task log file not found", 404
)
CLEANING_TASK_STATUS_INVALID: Final = ErrorCode(
"cleaning.0013", "Cleaning task status is invalid for this operation", 400
)
# ========== 算子市场模块 ==========
OPERATOR_NOT_FOUND: Final = ErrorCode("operator.0001", "Operator not found", 404)
OPERATOR_IN_INSTANCE: Final = ErrorCode("operator.0002", "Operator is in use", 400)
OPERATOR_CANNOT_DELETE_PREDEFINED: Final = ErrorCode(
"operator.0003", "Cannot delete predefined operator", 400
)
OPERATOR_UNSUPPORTED_FILE_TYPE: Final = ErrorCode(
"operator.0004", "Unsupported file type", 400
)
OPERATOR_PARSE_FAILED: Final = ErrorCode(
"operator.0005", "Failed to parse operator package", 400
)
OPERATOR_FIELD_NOT_FOUND: Final = ErrorCode(
"operator.0006", "Required field is missing", 400
)
# ========== 系统模块 ==========
SYSTEM_MODEL_NOT_FOUND: Final = ErrorCode(
"system.0006", "Model configuration not found", 404
)
SYSTEM_MODEL_HEALTH_CHECK_FAILED: Final = ErrorCode(
"system.0007", "Model health check failed", 500
)
@@ -0,0 +1,203 @@
"""
异常处理器和响应构建器
提供异常的自动转换,将异常转换为标准化的 JSON 响应。
"""
from typing import Any, Optional
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from .base import BaseError, BusinessError, SystemError
from .codes import ErrorCodes
from ..logging import get_logger
logger = get_logger(__name__)
def SuccessResponse(data: Any = None, message: str = "success", code: str = "0") -> dict:
"""
构建成功响应
Args:
data: 响应数据
message: 成功消息
code: 自定义成功码(默认为 "0")
Returns:
响应字典,符合 StandardResponse 格式
使用示例:
return SuccessResponse(data={"id": 123})
# 或
return SuccessResponse({"id": 123})
"""
return {
"code": code,
"message": message,
"data": data
}
def ErrorResponse(
error_code: ErrorCodes,
data: Any = None,
custom_message: Optional[str] = None
) -> dict:
"""
构建错误响应
Args:
error_code: 错误码(来自 ErrorCodes)
data: 附加错误数据
custom_message: 覆盖默认错误消息
Returns:
响应字典,符合 StandardResponse 格式
使用示例:
return ErrorResponse(ErrorCodes.TASK_NOT_FOUND)
"""
return {
"code": error_code.code,
"message": custom_message or error_code.message,
"data": data
}
async def business_error_handler(request: Request, exc: BusinessError) -> JSONResponse:
"""
处理业务逻辑异常
业务异常是预期的错误,不需要记录堆栈跟踪。
返回对应的 HTTP 状态码(400、404 等),错误信息在响应体的 code 字段中。
"""
return JSONResponse(
status_code=exc.http_status,
content=exc.to_dict()
)
async def system_error_handler(request: Request, exc: SystemError) -> JSONResponse:
"""
处理系统异常
系统异常是意外的错误,需要记录完整的堆栈跟踪。
返回 HTTP 500 和经过净化的错误消息。
"""
logger.error(
f"System error occurred at {request.method} {request.url.path}: {exc.message}",
exc_info=True
)
return JSONResponse(
status_code=exc.http_status,
content={
"code": exc.code,
"message": exc.message,
"data": None # 绝不暴露系统错误详情
}
)
async def validation_error_handler(
request: Request,
exc: RequestValidationError
) -> JSONResponse:
"""处理请求验证错误"""
errors = exc.errors()
simplified_errors = [
err.get("msg", "Validation error")
for err in errors
]
return JSONResponse(
status_code=422,
content={
"code": ErrorCodes.VALIDATION_ERROR.code,
"message": ErrorCodes.VALIDATION_ERROR.message,
"data": {
"detail": "请求验证失败",
"errors": simplified_errors
}
}
)
async def http_exception_handler(
request: Request,
exc: StarletteHTTPException
) -> JSONResponse:
"""处理 HTTP 异常(404、500 等)"""
# 根据 HTTP 状态码映射到错误码
error_code = ErrorCodes.INTERNAL_ERROR
if exc.status_code == 404:
error_code = ErrorCodes.NOT_FOUND
elif exc.status_code == 401:
error_code = ErrorCodes.UNAUTHORIZED
elif exc.status_code == 403:
error_code = ErrorCodes.FORBIDDEN
elif exc.status_code == 422:
error_code = ErrorCodes.VALIDATION_ERROR
elif exc.status_code == 400:
error_code = ErrorCodes.BAD_REQUEST
return JSONResponse(
status_code=exc.status_code,
content={
"code": error_code.code,
"message": "error",
"data": {
"detail": str(exc.detail)
}
}
)
async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""
通用异常处理器(最后的兜底处理)
这是最后一道防线 - 记录完整的堆栈跟踪
并返回经过净化的错误给客户端。
"""
logger.error(
f"Unhandled exception occurred at {request.method} {request.url.path}: {str(exc)}",
exc_info=True
)
return JSONResponse(
status_code=500,
content={
"code": ErrorCodes.INTERNAL_ERROR.code,
"message": ErrorCodes.INTERNAL_ERROR.message,
"data": {
"detail": "服务器内部错误"
}
}
)
def register_exception_handlers(app) -> None:
"""
注册所有异常处理器到 FastAPI 应用
Args:
app: FastAPI 应用实例
使用示例:
from app.core.exception import register_exception_handlers
app = FastAPI()
register_exception_handlers(app)
"""
from .base import BusinessError, SystemError
# 按照特异性顺序注册异常处理器(最具体的在前)
app.add_exception_handler(BusinessError, business_error_handler)
app.add_exception_handler(SystemError, system_error_handler)
app.add_exception_handler(RequestValidationError, validation_error_handler)
app.add_exception_handler(StarletteHTTPException, http_exception_handler)
app.add_exception_handler(Exception, generic_exception_handler)
@@ -0,0 +1,96 @@
"""
全局异常处理中间件,支持自动响应包装
此中间件提供自动地返回值和异常转换,确保所有响应都符合标准化的 JSON 格式。
关键特性:
- 自动包装 dict/list 响应为 StandardResponse 格式
- 异常处理,并记录适当的日志
- 堆栈跟踪隔离(永远不会暴露给客户端)
"""
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response as StarletteResponse
from .base import BaseError, BusinessError, SystemError
from .codes import ErrorCodes
from ..logging import get_logger
logger = get_logger(__name__)
class ExceptionHandlingMiddleware(BaseHTTPMiddleware):
"""
全局异常捕获中间件
这是最外层的中间件,捕获所有未处理的异常,
并将它们转换为标准化的响应格式。
堆栈跟踪信息只记录到日志文件,永远不会发送给客户端。
"""
async def dispatch(self, request: Request, call_next):
"""
处理请求并处理任何异常
异常会被捕获、记录(带堆栈跟踪),并转换为标准化响应。
业务异常会被重新抛出,由专门的处理器处理。
"""
try:
response = await call_next(request)
return response
except BusinessError:
# 让业务异常处理器处理
raise
except SystemError as exc:
# 记录系统错误及其完整堆栈跟踪
logger.error(
f"System error occurred at {request.method} {request.url.path}",
exc_info=True
)
return self._error_response(
code=exc.code,
message=exc.message,
http_status=exc.http_status
)
except BaseError as exc:
# 处理其他自定义错误
logger.warning(f"BaseError occurred at {request.url.path}: {exc.message}")
return self._error_response(
code=exc.code,
message=exc.message,
http_status=exc.http_status
)
except Exception as exc:
# 捕获所有未处理的异常
logger.error(
f"Unhandled exception occurred at {request.method} {request.url.path}",
exc_info=True
)
return self._error_response(
code=ErrorCodes.INTERNAL_ERROR.code,
message=ErrorCodes.INTERNAL_ERROR.message,
http_status=500
)
@staticmethod
def _error_response(
code: str,
message: str,
http_status: int
) -> StarletteResponse:
"""构建错误响应"""
return JSONResponse(
status_code=http_status,
content={
"code": code,
"message": message,
"data": None
}
)
@@ -0,0 +1,170 @@
"""
Result 类型用于优雅的错误处理
提供受 Rust 启发的 Result<T, E> 类型,用于处理可能失败的操作而无需使用异常。
使用示例:
# 成功情况
def get_user(user_id: str) -> Result[User]:
user = db.find_user(user_id)
if user:
return Ok(user)
return Err(ErrorCodes.USER_NOT_FOUND)
# 使用结果
result = get_user("123")
if result.is_ok():
user = result.unwrap()
print(f"User: {user.name}")
else:
error = result.unwrap_err()
print(f"Error: {error.message}")
"""
from typing import Generic, TypeVar, Optional, Any
from .base import ErrorCode
from .codes import ErrorCodes
T = TypeVar('T') # 成功类型
E = TypeVar('E', bound=ErrorCode) # 错误类型
class Result(Generic[T, E]):
"""
表示成功(Ok)或失败(Err)的 Result 类型
此类型允许在不需要异常的情况下进行显式错误处理
"""
def __init__(self, value: Optional[T], error: Optional[E], is_ok: bool):
self._value = value
self._error = error
self._is_ok = is_ok
@staticmethod
def ok(value: T) -> 'Result[T, E]':
"""创建一个包含值的成功结果"""
return Result(value, None, True)
@staticmethod
def err(error: E) -> 'Result[T, E]':
"""创建一个包含错误码的失败结果"""
return Result(None, error, False)
@property
def is_ok(self) -> bool:
"""检查结果是否成功"""
return self._is_ok
@property
def is_err(self) -> bool:
"""检查结果是否失败"""
return not self._is_ok
def unwrap(self) -> T:
"""
获取成功值
Returns:
成功值
Raises:
ValueError: 如果结果是错误
"""
if self._is_ok:
return self._value
raise ValueError(
f"Cannot unwrap error result: {self._error.message}"
)
def unwrap_err(self) -> E:
"""
获取错误码
Returns:
错误码
Raises:
ValueError: 如果结果是成功的
"""
if not self._is_ok:
return self._error
raise ValueError("Cannot unwrap error from successful result")
def unwrap_or(self, default: T) -> T:
"""
获取成功值,如果出错则返回默认值
Args:
default: 出错时返回的默认值
Returns:
成功值或默认值
"""
return self._value if self._is_ok else default
def map(self, func) -> 'Result[Any, E]':
"""
如果存在成功值,则应用函数
Args:
func: 要应用的函数
Returns:
包含映射值的新结果或相同的错误
"""
if self._is_ok:
try:
return Result.ok(func(self._value))
except Exception:
# 如果映射失败,转换为错误
return Result.err(ErrorCodes.INTERNAL_ERROR)
return self
def and_then(self, func) -> 'Result[Any, E]':
"""
链式调用返回 Result 的操作
Args:
func: 接收成功值并返回新 Result 的函数
Returns:
来自函数的新结果或相同的错误
"""
if self._is_ok:
return func(self._value)
return self
def or_else(self, func) -> 'Result[T, Any]':
"""
在出错时提供备用结果
Args:
func: 接收错误并返回新 Result 的函数
Returns:
相同的结果或来自函数的新结果
"""
if not self._is_ok:
return func(self._error)
return self
def Ok(value: T) -> Result[T, ErrorCode]:
"""
创建一个成功的结果
使用示例:
return Ok(user_data)
"""
return Result.ok(value)
def Err(error_code: ErrorCode) -> Result[Any, ErrorCode]:
"""
创建一个失败的结果
使用示例:
return Err(ErrorCodes.USER_NOT_FOUND)
"""
return Result.err(error_code)
@@ -0,0 +1,76 @@
"""
数据库事务管理工具
提供自动事务管理的上下文管理器和依赖注入函数,
确保在异常情况下正确回滚事务。
"""
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.logging import get_logger
logger = get_logger(__name__)
@asynccontextmanager
async def transaction(db: AsyncSession) -> AsyncGenerator[None, None]:
"""
数据库事务上下文管理器
自动处理事务的提交和回滚:
- 如果代码块正常执行完成,则自动提交
- 如果代码块抛出异常,则自动回滚
使用示例:
@router.post("/tasks")
async def create_task(
request: CreateTaskRequest,
db: AsyncSession = Depends(get_db)
):
async with transaction(db):
# 数据库操作
db.add(task)
await db.flush()
# 事务已自动提交
return SuccessResponse(data=task)
Args:
db: SQLAlchemy 异步会话
Yields:
None
Raises:
Exception: 重新抛出代码块中的任何异常
"""
try:
yield
# 如果没有异常,提交事务
await db.commit()
logger.debug("Transaction committed successfully")
except Exception as e:
# 发生异常,回滚事务
try:
await db.rollback()
logger.warning(f"Transaction rolled back due to exception: {e}")
except Exception as rollback_error:
logger.error(f"Failed to rollback transaction: {rollback_error}")
# 重新抛出异常,让上层异常处理器处理
raise
async def ensure_transaction_rollback(db: AsyncSession) -> None:
"""
确保事务回滚(用于错误处理)
Args:
db: SQLAlchemy 异步会话
"""
try:
await db.rollback()
logger.debug("Transaction rolled back")
except Exception as e:
logger.error(f"Failed to rollback transaction: {e}")
+121
View File
@@ -0,0 +1,121 @@
import logging
import re
import sys
from logging.handlers import RotatingFileHandler
from pathlib import Path
from app.core.config import settings
_SIZE_PATTERN = re.compile(r"^\s*(\d+)\s*([KMG]?B?)?\s*$", re.IGNORECASE)
class CenteredLevelNameFormatter(logging.Formatter):
"""Center the level name in the log output"""
def format(self, record):
# 将 levelname 居中对齐到8个字符
record.levelname = record.levelname.center(8)
return super().format(record)
def _parse_size_to_bytes(value: str) -> int:
match = _SIZE_PATTERN.match(value)
if not match:
return 100 * 1024 * 1024
amount = int(match.group(1))
unit = (match.group(2) or "B").upper()
if unit in {"G", "GB"}:
return amount * 1024 * 1024 * 1024
if unit in {"M", "MB"}:
return amount * 1024 * 1024
if unit in {"K", "KB"}:
return amount * 1024
return amount
def _rotated_log_namer(default_name: str) -> str:
path = Path(default_name)
index_suffix = path.suffix
if not index_suffix.lstrip(".").isdigit():
return default_name
base_path = Path(str(path)[: -len(index_suffix)])
if not base_path.suffix:
return default_name
return str(base_path.with_name(f"{base_path.stem}{index_suffix}{base_path.suffix}"))
def _remove_managed_handlers(logger: logging.Logger) -> None:
for handler in list(logger.handlers):
if getattr(handler, "_deepdata_managed", False):
logger.removeHandler(handler)
handler.close()
def _mark_managed(handler: logging.Handler) -> logging.Handler:
setattr(handler, "_deepdata_managed", True)
return handler
def setup_logging():
log_format = "%(asctime)s [%(levelname)s] - %(name)s - %(message)s"
date_format = "%Y-%m-%d %H:%M:%S"
console_handler = _mark_managed(logging.StreamHandler(sys.stdout))
console_handler.setLevel(getattr(logging, settings.log_level.upper()))
log_dir = Path(settings.log_file_dir)
log_dir.mkdir(exist_ok=True)
file_handler = _mark_managed(RotatingFileHandler(
log_dir / "python-backend.log",
maxBytes=_parse_size_to_bytes(settings.log_rotation_max_size),
backupCount=max(1, settings.log_rotation_backup_count),
encoding="utf-8"
))
file_handler.namer = _rotated_log_namer
file_handler.setLevel(getattr(logging, settings.log_level.upper()))
# Style setting - Centered level names
formatter = CenteredLevelNameFormatter(log_format, date_format)
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# Root Logger
root_logger = logging.getLogger()
_remove_managed_handlers(root_logger)
root_logger.setLevel(getattr(logging, settings.log_level.upper()))
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
# Uvicorn
uvicorn_logger = logging.getLogger("uvicorn")
uvicorn_logger.handlers.clear()
uvicorn_logger.addHandler(console_handler)
uvicorn_logger.setLevel(logging.INFO)
uvicorn_access = logging.getLogger("uvicorn.access")
uvicorn_access.handlers.clear()
uvicorn_access.addHandler(console_handler)
uvicorn_access.setLevel(logging.DEBUG)
uvicorn_error = logging.getLogger("uvicorn.error")
uvicorn_error.handlers.clear()
uvicorn_error.addHandler(console_handler)
uvicorn_error.setLevel(logging.ERROR)
# SQLAlchemy (ERROR only)
sqlalchemy_logger = logging.getLogger("sqlalchemy.engine")
_remove_managed_handlers(sqlalchemy_logger)
sqlalchemy_logger.setLevel(logging.ERROR)
sqlalchemy_logger.addHandler(console_handler)
sqlalchemy_logger.setLevel(logging.ERROR)
# Minimize noise from HTTPX and HTTPCore
logging.getLogger("httpx").setLevel(logging.ERROR)
logging.getLogger("httpcore").setLevel(logging.ERROR)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
@@ -0,0 +1,159 @@
"""Security utilities for handling sensitive data"""
import re
from typing import Any, Dict
SENSITIVE_FIELDS = [
"secretKey",
"accessKey",
"password",
"passwd",
"pwd",
"secret",
"token",
"apiKey",
"api_key",
"access_key",
"secret_key",
]
MASK_PATTERN = "**********"
def mask_sensitive_value(value: str) -> str:
"""Mask a single sensitive value"""
return MASK_PATTERN
def is_masked_value(value: str) -> bool:
"""Check if a value is already masked"""
return value == MASK_PATTERN
def preserve_sensitive_values(
new_config: Dict[str, Any],
original_config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Preserve original sensitive values if masked pattern is detected in update request.
When frontend receives masked values and sends them back in update request,
this function detects masked values and replaces them with original values
from database.
Args:
new_config: Config from update request (may contain masked values)
original_config: Original config from database (contains real values)
Returns:
Config with original sensitive values preserved
"""
if not isinstance(new_config, dict) or not isinstance(original_config, dict):
return new_config
preserved_config = {}
for key, new_value in new_config.items():
# Check if key is a sensitive field
key_lower = key.lower()
is_sensitive = any(
field.lower() == key_lower or field.lower() in key_lower
for field in SENSITIVE_FIELDS
)
# If value is masked and field is sensitive, use original value
if is_sensitive and isinstance(new_value, str) and is_masked_value(new_value):
original_value = original_config.get(key)
preserved_config[key] = original_value if original_value else new_value
elif isinstance(new_value, dict):
# Recursively process nested dictionaries
original_nested = original_config.get(key, {})
preserved_config[key] = preserve_sensitive_values(new_value, original_nested)
elif isinstance(new_value, list):
original_list = original_config.get(key, [])
preserved_list = []
for i, new_item in enumerate(new_value):
if i < len(original_list):
orig_item = original_list[i]
if isinstance(new_item, dict) and isinstance(orig_item, dict):
preserved_list.append(preserve_sensitive_values(new_item, orig_item))
else:
preserved_list.append(new_item)
else:
preserved_list.append(new_item)
preserved_config[key] = preserved_list
else:
# Keep non-sensitive/non-masked values
preserved_config[key] = new_value
return preserved_config
def mask_sensitive_dict(data: Dict[str, Any]) -> Dict[str, Any]:
"""
Recursively mask sensitive fields in a dictionary.
Args:
data: Dictionary that may contain sensitive fields
Returns:
Dictionary with sensitive values masked
"""
if not isinstance(data, dict):
return data
masked_data = {}
for key, value in data.items():
# Check if the key is a sensitive field (case-insensitive)
key_lower = key.lower()
is_sensitive = any(
field.lower() == key_lower or field.lower() in key_lower
for field in SENSITIVE_FIELDS
)
if is_sensitive and isinstance(value, str):
# Mask the sensitive value
masked_data[key] = MASK_PATTERN
elif isinstance(value, dict):
# Recursively mask nested dictionaries
masked_data[key] = mask_sensitive_dict(value)
elif isinstance(value, list):
# Process list items
masked_data[key] = [
mask_sensitive_dict(item) if isinstance(item, dict) else item
for item in value
]
else:
# Keep non-sensitive values as-is
masked_data[key] = value
return masked_data
def mask_sensitive_info(text: str) -> str:
"""
Mask sensitive information in text by replacing values with **********
Args:
text: Original text that may contain sensitive information
Returns:
Text with sensitive values masked
"""
masked_text = text
for field in SENSITIVE_FIELDS:
patterns = [
rf'"{field}"\s*:\s*"[^"]*"',
rf'{field}\s*=\s*[^\s,\]]+',
rf"'{field}'\s*:\s*'[^']*'",
]
for pattern in patterns:
if '"' in pattern:
masked_text = re.sub(pattern, f'"{field}": "{MASK_PATTERN}"', masked_text, flags=re.IGNORECASE)
elif "'" in pattern:
masked_text = re.sub(pattern, f"'{field}': '{MASK_PATTERN}'", masked_text, flags=re.IGNORECASE)
else:
masked_text = re.sub(pattern, f'{field}={MASK_PATTERN}', masked_text, flags=re.IGNORECASE)
return masked_text