From 060d3a726f46907970e3cdc3ab4e2e74ae0ec0c4 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Fri, 13 Mar 2026 17:17:01 +0800 Subject: [PATCH] fix bug report --- backend/Dockerfile | 4 ++ backend/apps/generation/views.py | 66 ++++++++++++++++++++++------- backend/config/settings.py | 1 + backend/entrypoint.sh | 8 ++++ backend/utils/log_center.py | 71 +++++++++++++++++++++++++++++++- 5 files changed, 133 insertions(+), 17 deletions(-) create mode 100755 backend/entrypoint.sh diff --git a/backend/Dockerfile b/backend/Dockerfile index 3469c83..fa13f05 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -23,6 +23,10 @@ COPY . /app/ # Collect static files RUN python manage.py collectstatic --noinput 2>/dev/null || true +# Make entrypoint executable +RUN chmod +x /app/entrypoint.sh + EXPOSE 8000 +ENTRYPOINT ["/app/entrypoint.sh"] CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "config.wsgi:application"] diff --git a/backend/apps/generation/views.py b/backend/apps/generation/views.py index 22b00b6..3b5e05e 100644 --- a/backend/apps/generation/views.py +++ b/backend/apps/generation/views.py @@ -9,6 +9,7 @@ from django.contrib.auth import get_user_model from django.utils import timezone from django.db.models import Sum, Q from django.db.models.functions import TruncDate +from django.db.utils import OperationalError as DbOperationalError from datetime import timedelta from .models import GenerationRecord, QuotaConfig @@ -29,6 +30,36 @@ ALLOWED_VIDEO_EXTS = {'mp4', 'mov'} MAX_IMAGE_SIZE = 30 * 1024 * 1024 # 30MB MAX_VIDEO_SIZE = 50 * 1024 * 1024 # 50MB +# Columns added in migration 0003; may not exist in production DB yet. +_M0003_COLS = ('ark_task_id', 'result_url', 'error_message', 'reference_urls') +_m0003_ok = None # None = unknown, True = columns exist, False = missing + + +def _eval_qs(qs, limit=None, get_kwargs=None): + """Evaluate a GenerationRecord queryset, deferring migration-0003 columns if missing.""" + global _m0003_ok + + def _run(q, defer): + if defer: + q = q.defer(*_M0003_COLS) + if get_kwargs is not None: + return q.get(**get_kwargs) + if limit is not None: + return list(q[:limit]) + return list(q) + + if _m0003_ok is False: + return _run(qs, defer=True) + try: + result = _run(qs, defer=False) + _m0003_ok = True + return result + except DbOperationalError as e: + if 'ark_task_id' in str(e): + _m0003_ok = False + return _run(qs, defer=True) + raise + # ────────────────────────────────────────────── # Media Upload @@ -238,12 +269,10 @@ def video_tasks_list_view(request): user = request.user page_size = min(int(request.query_params.get('page_size', 50)), 100) - records = user.generation_records.order_by('-created_at')[:page_size] - - results = [] - for r in records: - results.append(_serialize_task(r)) + qs = user.generation_records.order_by('-created_at') + records = _eval_qs(qs, limit=page_size) + results = [_serialize_task(r) for r in records] return Response({'results': results}) @@ -252,14 +281,18 @@ def video_tasks_list_view(request): def video_task_detail_view(request, task_id): """GET /api/v1/video/tasks/ — Get task status, poll Seedance if active.""" try: - record = GenerationRecord.objects.get(task_id=task_id, user=request.user) + record = _eval_qs( + GenerationRecord.objects.filter(user=request.user), + get_kwargs={'task_id': task_id}, + ) except GenerationRecord.DoesNotExist: return Response({'error': '任务不存在'}, status=status.HTTP_404_NOT_FOUND) # If task is still active, poll Seedance API for latest status - if record.status in ('queued', 'processing') and record.ark_task_id: + ark_task_id = record.__dict__.get('ark_task_id', '') + if record.status in ('queued', 'processing') and ark_task_id: try: - ark_resp = query_task(record.ark_task_id) + ark_resp = query_task(ark_task_id) new_status = map_status(ark_resp.get('status', '')) record.status = new_status @@ -275,17 +308,18 @@ def video_task_detail_view(request, task_id): record.save(update_fields=['status', 'result_url', 'error_message']) except Exception as e: - logger.exception('Seedance API query failed for %s', record.ark_task_id) + logger.exception('Seedance API query failed for %s', ark_task_id) return Response(_serialize_task(record)) def _serialize_task(record): """Serialize a GenerationRecord for the frontend.""" + d = record.__dict__ return { 'id': record.id, 'task_id': str(record.task_id), - 'ark_task_id': record.ark_task_id, + 'ark_task_id': d.get('ark_task_id', ''), 'prompt': record.prompt, 'mode': record.mode, 'model': record.model, @@ -293,9 +327,9 @@ def _serialize_task(record): 'duration': record.duration, 'seconds_consumed': record.seconds_consumed, 'status': record.status, - 'result_url': record.result_url, - 'error_message': record.error_message, - 'reference_urls': record.reference_urls or [], + 'result_url': d.get('result_url', ''), + 'error_message': d.get('error_message', ''), + 'reference_urls': d.get('reference_urls') or [], 'created_at': record.created_at.isoformat(), } @@ -469,7 +503,7 @@ def admin_user_detail_view(request, user_id): total=Sum('seconds_consumed') )['total'] or 0 - recent_records = user.generation_records.order_by('-created_at')[:20] + recent_records = _eval_qs(user.generation_records.order_by('-created_at'), limit=20) return Response({ 'id': user.id, @@ -607,7 +641,7 @@ def admin_records_view(request): total = qs.count() offset = (page - 1) * page_size - records = qs[offset:offset + page_size] + records = _eval_qs(qs[offset:offset + page_size]) results = [] for r in records: @@ -731,7 +765,7 @@ def profile_records_view(request): qs = user.generation_records.order_by('-created_at') total = qs.count() offset = (page - 1) * page_size - records = qs[offset:offset + page_size] + records = _eval_qs(qs[offset:offset + page_size]) results = [] for r in records: diff --git a/backend/config/settings.py b/backend/config/settings.py index 0a0f907..aa1e6d2 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -31,6 +31,7 @@ INSTALLED_APPS = [ ] MIDDLEWARE = [ + 'utils.log_center.LogCenterMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100755 index 0000000..287cf29 --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +echo "Running database migrations..." +python manage.py migrate --noinput + +echo "Starting server..." +exec "$@" diff --git a/backend/utils/log_center.py b/backend/utils/log_center.py index c11df3c..d5e08fe 100644 --- a/backend/utils/log_center.py +++ b/backend/utils/log_center.py @@ -1,7 +1,8 @@ -"""Log Center integration — runtime error reporting.""" +"""Log Center integration — runtime error reporting + exception middleware.""" import os import json +import logging import traceback import threading from urllib.request import Request, urlopen @@ -12,6 +13,8 @@ LOG_CENTER_ENABLED = os.getenv('LOG_CENTER_ENABLED', 'false').lower() in ('true' PROJECT_ID = 'video_backend' ENVIRONMENT = os.getenv('ENVIRONMENT', 'development') +logger = logging.getLogger('log_center') + def report_error(exc, context=None): """Report a runtime error to Log Center (async, non-blocking).""" @@ -54,9 +57,64 @@ def _send(payload): pass # Silent failure — never affect main business +# ────────────────────────────────────────────── +# Django Middleware: 捕获 DRF 之外的所有异常 +# (模块导入失败、中间件错误、URL 解析错误等) +# ────────────────────────────────────────────── + +class LogCenterMiddleware: + """Django middleware to catch ALL unhandled exceptions and report to Log Center. + + DRF 的 custom_exception_handler 只能捕获视图内的异常。 + 模块导入失败、中间件异常、URL 解析错误等发生在 DRF 之前, + 不会经过 DRF 的异常处理,需要这个中间件来兜底上报。 + + 异常捕获分工: + - DRF 视图异常 → custom_exception_handler 上报 (DRF 内部 try/except 拦截,不会传播到中间件) + - 非 DRF 视图异常 → process_exception 上报 + - 中间件/导入异常 → __call__ try/except 上报 + 两层互不重复。 + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + try: + response = self.get_response(request) + except Exception as exc: + # 捕获中间件链中的异常(如其他中间件报错、模块导入失败等) + logger.error('Unhandled middleware exception: %s', exc, exc_info=True) + report_error(exc, { + 'source': 'django_middleware', + 'request_path': getattr(request, 'path', 'unknown'), + 'request_method': getattr(request, 'method', 'unknown'), + 'user': str(getattr(request, 'user', 'anonymous')), + }) + raise + return response + + def process_exception(self, request, exception): + """捕获非 DRF 视图的异常(普通 Django 视图、URL 解析错误等)。""" + logger.error('Unhandled view exception: %s', exception, exc_info=True) + report_error(exception, { + 'source': 'django_process_exception', + 'request_path': request.path, + 'request_method': request.method, + 'user': str(getattr(request, 'user', 'anonymous')), + }) + return None # 不处理响应,交给 Django 默认 500 handler + + +# ────────────────────────────────────────────── +# DRF Exception Handler: 捕获视图内异常 + 返回 JSON +# ────────────────────────────────────────────── + def custom_exception_handler(exc, context): """DRF custom exception handler with Log Center reporting.""" from rest_framework.views import exception_handler + from rest_framework.response import Response + from rest_framework import status from rest_framework.exceptions import ( MethodNotAllowed, NotAuthenticated, AuthenticationFailed, NotFound, PermissionDenied, @@ -68,6 +126,7 @@ def custom_exception_handler(exc, context): PermissionDenied)): request = context.get('request') report_error(exc, { + 'source': 'drf_exception_handler', 'view': str(context.get('view', '')), 'request_path': getattr(request, 'path', None), 'request_method': getattr(request, 'method', None), @@ -76,4 +135,14 @@ def custom_exception_handler(exc, context): # Default DRF handling response = exception_handler(exc, context) + + # If DRF didn't handle it (unhandled exception), return JSON 500 + # instead of letting Django render an HTML error page + if response is None: + logger.error('Unhandled DRF exception: %s', exc, exc_info=True) + response = Response( + {'error': f'{type(exc).__name__}: {exc}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + return response