fix bug report
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m16s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m16s
This commit is contained in:
parent
7aa1788035
commit
060d3a726f
@ -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"]
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
8
backend/entrypoint.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Running database migrations..."
|
||||
python manage.py migrate --noinput
|
||||
|
||||
echo "Starting server..."
|
||||
exec "$@"
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user