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
|
# Collect static files
|
||||||
RUN python manage.py collectstatic --noinput 2>/dev/null || true
|
RUN python manage.py collectstatic --noinput 2>/dev/null || true
|
||||||
|
|
||||||
|
# Make entrypoint executable
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 8000
|
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"]
|
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.utils import timezone
|
||||||
from django.db.models import Sum, Q
|
from django.db.models import Sum, Q
|
||||||
from django.db.models.functions import TruncDate
|
from django.db.models.functions import TruncDate
|
||||||
|
from django.db.utils import OperationalError as DbOperationalError
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from .models import GenerationRecord, QuotaConfig
|
from .models import GenerationRecord, QuotaConfig
|
||||||
@ -29,6 +30,36 @@ ALLOWED_VIDEO_EXTS = {'mp4', 'mov'}
|
|||||||
MAX_IMAGE_SIZE = 30 * 1024 * 1024 # 30MB
|
MAX_IMAGE_SIZE = 30 * 1024 * 1024 # 30MB
|
||||||
MAX_VIDEO_SIZE = 50 * 1024 * 1024 # 50MB
|
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
|
# Media Upload
|
||||||
@ -238,12 +269,10 @@ def video_tasks_list_view(request):
|
|||||||
user = request.user
|
user = request.user
|
||||||
page_size = min(int(request.query_params.get('page_size', 50)), 100)
|
page_size = min(int(request.query_params.get('page_size', 50)), 100)
|
||||||
|
|
||||||
records = user.generation_records.order_by('-created_at')[:page_size]
|
qs = user.generation_records.order_by('-created_at')
|
||||||
|
records = _eval_qs(qs, limit=page_size)
|
||||||
results = []
|
|
||||||
for r in records:
|
|
||||||
results.append(_serialize_task(r))
|
|
||||||
|
|
||||||
|
results = [_serialize_task(r) for r in records]
|
||||||
return Response({'results': results})
|
return Response({'results': results})
|
||||||
|
|
||||||
|
|
||||||
@ -252,14 +281,18 @@ def video_tasks_list_view(request):
|
|||||||
def video_task_detail_view(request, task_id):
|
def video_task_detail_view(request, task_id):
|
||||||
"""GET /api/v1/video/tasks/<task_id> — Get task status, poll Seedance if active."""
|
"""GET /api/v1/video/tasks/<task_id> — Get task status, poll Seedance if active."""
|
||||||
try:
|
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:
|
except GenerationRecord.DoesNotExist:
|
||||||
return Response({'error': '任务不存在'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': '任务不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# If task is still active, poll Seedance API for latest status
|
# 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:
|
try:
|
||||||
ark_resp = query_task(record.ark_task_id)
|
ark_resp = query_task(ark_task_id)
|
||||||
new_status = map_status(ark_resp.get('status', ''))
|
new_status = map_status(ark_resp.get('status', ''))
|
||||||
record.status = new_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'])
|
record.save(update_fields=['status', 'result_url', 'error_message'])
|
||||||
except Exception as e:
|
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))
|
return Response(_serialize_task(record))
|
||||||
|
|
||||||
|
|
||||||
def _serialize_task(record):
|
def _serialize_task(record):
|
||||||
"""Serialize a GenerationRecord for the frontend."""
|
"""Serialize a GenerationRecord for the frontend."""
|
||||||
|
d = record.__dict__
|
||||||
return {
|
return {
|
||||||
'id': record.id,
|
'id': record.id,
|
||||||
'task_id': str(record.task_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,
|
'prompt': record.prompt,
|
||||||
'mode': record.mode,
|
'mode': record.mode,
|
||||||
'model': record.model,
|
'model': record.model,
|
||||||
@ -293,9 +327,9 @@ def _serialize_task(record):
|
|||||||
'duration': record.duration,
|
'duration': record.duration,
|
||||||
'seconds_consumed': record.seconds_consumed,
|
'seconds_consumed': record.seconds_consumed,
|
||||||
'status': record.status,
|
'status': record.status,
|
||||||
'result_url': record.result_url,
|
'result_url': d.get('result_url', ''),
|
||||||
'error_message': record.error_message,
|
'error_message': d.get('error_message', ''),
|
||||||
'reference_urls': record.reference_urls or [],
|
'reference_urls': d.get('reference_urls') or [],
|
||||||
'created_at': record.created_at.isoformat(),
|
'created_at': record.created_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,7 +503,7 @@ def admin_user_detail_view(request, user_id):
|
|||||||
total=Sum('seconds_consumed')
|
total=Sum('seconds_consumed')
|
||||||
)['total'] or 0
|
)['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({
|
return Response({
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
@ -607,7 +641,7 @@ def admin_records_view(request):
|
|||||||
|
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
records = qs[offset:offset + page_size]
|
records = _eval_qs(qs[offset:offset + page_size])
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for r in records:
|
for r in records:
|
||||||
@ -731,7 +765,7 @@ def profile_records_view(request):
|
|||||||
qs = user.generation_records.order_by('-created_at')
|
qs = user.generation_records.order_by('-created_at')
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
records = qs[offset:offset + page_size]
|
records = _eval_qs(qs[offset:offset + page_size])
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for r in records:
|
for r in records:
|
||||||
|
|||||||
@ -31,6 +31,7 @@ INSTALLED_APPS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'utils.log_center.LogCenterMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
'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 os
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import threading
|
import threading
|
||||||
from urllib.request import Request, urlopen
|
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'
|
PROJECT_ID = 'video_backend'
|
||||||
ENVIRONMENT = os.getenv('ENVIRONMENT', 'development')
|
ENVIRONMENT = os.getenv('ENVIRONMENT', 'development')
|
||||||
|
|
||||||
|
logger = logging.getLogger('log_center')
|
||||||
|
|
||||||
|
|
||||||
def report_error(exc, context=None):
|
def report_error(exc, context=None):
|
||||||
"""Report a runtime error to Log Center (async, non-blocking)."""
|
"""Report a runtime error to Log Center (async, non-blocking)."""
|
||||||
@ -54,9 +57,64 @@ def _send(payload):
|
|||||||
pass # Silent failure — never affect main business
|
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):
|
def custom_exception_handler(exc, context):
|
||||||
"""DRF custom exception handler with Log Center reporting."""
|
"""DRF custom exception handler with Log Center reporting."""
|
||||||
from rest_framework.views import exception_handler
|
from rest_framework.views import exception_handler
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.exceptions import (
|
from rest_framework.exceptions import (
|
||||||
MethodNotAllowed, NotAuthenticated, AuthenticationFailed,
|
MethodNotAllowed, NotAuthenticated, AuthenticationFailed,
|
||||||
NotFound, PermissionDenied,
|
NotFound, PermissionDenied,
|
||||||
@ -68,6 +126,7 @@ def custom_exception_handler(exc, context):
|
|||||||
PermissionDenied)):
|
PermissionDenied)):
|
||||||
request = context.get('request')
|
request = context.get('request')
|
||||||
report_error(exc, {
|
report_error(exc, {
|
||||||
|
'source': 'drf_exception_handler',
|
||||||
'view': str(context.get('view', '')),
|
'view': str(context.get('view', '')),
|
||||||
'request_path': getattr(request, 'path', None),
|
'request_path': getattr(request, 'path', None),
|
||||||
'request_method': getattr(request, 'method', None),
|
'request_method': getattr(request, 'method', None),
|
||||||
@ -76,4 +135,14 @@ def custom_exception_handler(exc, context):
|
|||||||
|
|
||||||
# Default DRF handling
|
# Default DRF handling
|
||||||
response = exception_handler(exc, context)
|
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
|
return response
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user