190 lines
5.7 KiB
Python
190 lines
5.7 KiB
Python
"""
|
|
自定义异常处理
|
|
"""
|
|
import os
|
|
import traceback
|
|
import threading
|
|
import requests
|
|
from rest_framework.views import exception_handler
|
|
from rest_framework.exceptions import AuthenticationFailed as DRFAuthenticationFailed
|
|
from rest_framework.exceptions import NotAuthenticated
|
|
from rest_framework import status
|
|
from rest_framework_simplejwt.exceptions import InvalidToken, AuthenticationFailed as JWTAuthenticationFailed
|
|
from utils.response import APIResponse
|
|
|
|
|
|
# Log Center 配置
|
|
LOG_CENTER_URL = os.environ.get('LOG_CENTER_URL', 'https://qiyuan-log-center-api.airlabs.art')
|
|
LOG_CENTER_ENABLED = os.environ.get('LOG_CENTER_ENABLED', 'true').lower() == 'true'
|
|
|
|
|
|
def report_to_log_center(exc, context):
|
|
"""异步上报错误到 Log Center"""
|
|
if not LOG_CENTER_ENABLED:
|
|
return
|
|
|
|
try:
|
|
# 提取堆栈信息
|
|
tb = traceback.extract_tb(exc.__traceback__) if exc.__traceback__ else []
|
|
last_frame = tb[-1] if tb else None
|
|
|
|
# 获取请求信息
|
|
request = context.get('request')
|
|
request_path = request.path if request else 'unknown'
|
|
request_method = request.method if request else 'unknown'
|
|
|
|
payload = {
|
|
"project_id": "rtc_backend",
|
|
"environment": os.environ.get('ENVIRONMENT', 'development'),
|
|
"level": "ERROR",
|
|
"repo_url": "https://gitea.airlabs.art/zyc/rtc_backend.git",
|
|
"error": {
|
|
"type": type(exc).__name__,
|
|
"message": str(exc),
|
|
"file_path": last_frame.filename if last_frame else "unknown",
|
|
"line_number": last_frame.lineno if last_frame else 0,
|
|
"stack_trace": traceback.format_exception(exc) if exc.__traceback__ else [str(exc)]
|
|
},
|
|
"context": {
|
|
"url": request_path,
|
|
"method": request_method,
|
|
"view": str(context.get('view', '')),
|
|
}
|
|
}
|
|
|
|
# 异步发送,不阻塞响应
|
|
def send_async():
|
|
try:
|
|
requests.post(
|
|
f"{LOG_CENTER_URL}/api/v1/logs/report",
|
|
json=payload,
|
|
timeout=3
|
|
)
|
|
except Exception:
|
|
pass # 静默失败
|
|
|
|
thread = threading.Thread(target=send_async)
|
|
thread.daemon = True
|
|
thread.start()
|
|
|
|
except Exception:
|
|
pass # 上报失败不影响主业务
|
|
|
|
|
|
class BusinessException(Exception):
|
|
"""业务异常"""
|
|
def __init__(self, code=1, message='业务错误', status_code=status.HTTP_400_BAD_REQUEST):
|
|
self.code = code
|
|
self.message = message
|
|
self.status_code = status_code
|
|
super().__init__(message)
|
|
|
|
|
|
class ErrorCode:
|
|
"""错误码定义"""
|
|
# 通用错误 1-99
|
|
SUCCESS = 0
|
|
UNKNOWN_ERROR = 1
|
|
PARAM_ERROR = 2
|
|
NOT_FOUND = 3
|
|
PERMISSION_DENIED = 4
|
|
|
|
# 用户模块 100-199
|
|
USER_NOT_FOUND = 100
|
|
USER_DISABLED = 101
|
|
PHONE_INVALID = 102
|
|
TOKEN_EXPIRED = 103
|
|
SMS_CODE_INVALID = 110
|
|
SMS_CODE_EXPIRED = 111
|
|
SMS_CODE_SEND_LIMIT = 112
|
|
|
|
# 设备模块 200-299
|
|
DEVICE_NOT_FOUND = 200
|
|
DEVICE_ALREADY_BOUND = 201
|
|
DEVICE_MAC_EXISTS = 202
|
|
DEVICE_SN_INVALID = 203
|
|
ROLE_MEMORY_NOT_FOUND = 204
|
|
ROLE_MEMORY_TYPE_MISMATCH = 205
|
|
ROLE_MEMORY_ALREADY_BOUND = 206
|
|
|
|
# 智能体模块 300-399
|
|
SPIRIT_NOT_FOUND = 300
|
|
SPIRIT_LIMIT_EXCEEDED = 301
|
|
|
|
# 批次模块 400-499
|
|
BATCH_NOT_FOUND = 400
|
|
BATCH_SN_EXISTS = 401
|
|
DEVICE_TYPE_NOT_FOUND = 402
|
|
|
|
# 管理员模块 500-599
|
|
ADMIN_NOT_FOUND = 500
|
|
ADMIN_PASSWORD_ERROR = 501
|
|
|
|
# 故事模块 600-699
|
|
STORY_NOT_FOUND = 600
|
|
SHELF_NOT_FOUND = 601
|
|
SHELF_LOCKED = 602
|
|
POINTS_NOT_ENOUGH = 603
|
|
SHELF_FULL = 604
|
|
|
|
# 音乐模块 700-799
|
|
TRACK_NOT_FOUND = 700
|
|
MUSIC_GENERATE_FAILED = 701
|
|
MUSIC_GENERATING = 702
|
|
MUSIC_DEFAULT_UNDELETABLE = 703
|
|
|
|
# 通知模块 800-899
|
|
NOTIFICATION_NOT_FOUND = 800
|
|
|
|
# 系统模块 900-999
|
|
FEEDBACK_SUBMIT_FAILED = 900
|
|
|
|
|
|
def custom_exception_handler(exc, context):
|
|
"""自定义异常处理器"""
|
|
# 上报到 Log Center (排除业务异常和认证异常,这些是正常业务流程)
|
|
if not isinstance(exc, (BusinessException, DRFAuthenticationFailed, NotAuthenticated, InvalidToken, JWTAuthenticationFailed)):
|
|
report_to_log_center(exc, context)
|
|
|
|
# 处理业务异常
|
|
if isinstance(exc, BusinessException):
|
|
return APIResponse(
|
|
code=exc.code,
|
|
message=exc.message,
|
|
status_code=exc.status_code
|
|
)
|
|
|
|
# 调用DRF默认异常处理
|
|
response = exception_handler(exc, context)
|
|
|
|
if response is not None:
|
|
# 统一格式化DRF异常
|
|
error_message = ''
|
|
if isinstance(response.data, dict):
|
|
if 'detail' in response.data:
|
|
error_message = str(response.data['detail'])
|
|
else:
|
|
# 处理字段验证错误
|
|
errors = []
|
|
for field, messages in response.data.items():
|
|
if isinstance(messages, list):
|
|
errors.append(f"{field}: {', '.join(str(m) for m in messages)}")
|
|
else:
|
|
errors.append(f"{field}: {messages}")
|
|
error_message = '; '.join(errors)
|
|
else:
|
|
error_message = str(response.data)
|
|
|
|
return APIResponse(
|
|
code=ErrorCode.UNKNOWN_ERROR,
|
|
message=error_message,
|
|
status_code=response.status_code
|
|
)
|
|
|
|
# 处理未知异常
|
|
return APIResponse(
|
|
code=ErrorCode.UNKNOWN_ERROR,
|
|
message=str(exc),
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|