fix bug report
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m16s

This commit is contained in:
zyc 2026-03-13 17:17:01 +08:00
parent 7aa1788035
commit 060d3a726f
5 changed files with 133 additions and 17 deletions

View File

@ -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"]

View File

@ -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/<task_id> — 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:

View File

@ -31,6 +31,7 @@ INSTALLED_APPS = [
]
MIDDLEWARE = [
'utils.log_center.LogCenterMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',

8
backend/entrypoint.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -e
echo "Running database migrations..."
python manage.py migrate --noinput
echo "Starting server..."
exec "$@"

View File

@ -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