All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m17s
755 lines
27 KiB
Python
755 lines
27 KiB
Python
import logging
|
|
|
|
from rest_framework import status
|
|
from rest_framework.decorators import api_view, permission_classes, parser_classes
|
|
from rest_framework.parsers import MultiPartParser, JSONParser
|
|
from rest_framework.permissions import IsAuthenticated, IsAdminUser
|
|
from rest_framework.response import Response
|
|
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 datetime import timedelta
|
|
|
|
from .models import GenerationRecord, QuotaConfig
|
|
from .serializers import (
|
|
VideoGenerateSerializer, QuotaUpdateSerializer,
|
|
UserStatusSerializer, SystemSettingsSerializer,
|
|
AdminCreateUserSerializer,
|
|
)
|
|
from utils.tos_client import upload_file as tos_upload
|
|
from utils.seedance_client import create_task, query_task, extract_video_url, map_status
|
|
|
|
User = get_user_model()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# File validation constants
|
|
ALLOWED_IMAGE_EXTS = {'jpeg', 'jpg', 'png', 'webp', 'bmp', 'tiff', 'gif'}
|
|
ALLOWED_VIDEO_EXTS = {'mp4', 'mov'}
|
|
MAX_IMAGE_SIZE = 30 * 1024 * 1024 # 30MB
|
|
MAX_VIDEO_SIZE = 50 * 1024 * 1024 # 50MB
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Media Upload
|
|
# ──────────────────────────────────────────────
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
@parser_classes([MultiPartParser])
|
|
def upload_media_view(request):
|
|
"""POST /api/v1/media/upload — Upload file to TOS, return public URL."""
|
|
file = request.FILES.get('file')
|
|
if not file:
|
|
return Response({'error': '未上传文件'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
ext = file.name.rsplit('.', 1)[-1].lower() if '.' in file.name else ''
|
|
|
|
if ext in ALLOWED_IMAGE_EXTS:
|
|
media_type = 'image'
|
|
max_size = MAX_IMAGE_SIZE
|
|
elif ext in ALLOWED_VIDEO_EXTS:
|
|
media_type = 'video'
|
|
max_size = MAX_VIDEO_SIZE
|
|
else:
|
|
return Response(
|
|
{'error': f'不支持的文件格式: {ext}'},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
if file.size > max_size:
|
|
limit_mb = max_size // (1024 * 1024)
|
|
return Response(
|
|
{'error': f'{media_type} 文件不能超过 {limit_mb}MB'},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
try:
|
|
url = tos_upload(file, folder=media_type)
|
|
except Exception as e:
|
|
logger.exception('TOS upload failed')
|
|
return Response(
|
|
{'error': f'文件上传失败: {str(e)}'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
)
|
|
|
|
return Response({
|
|
'url': url,
|
|
'type': media_type,
|
|
'filename': file.name,
|
|
'size': file.size,
|
|
})
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Video Generation (with Seedance API)
|
|
# ──────────────────────────────────────────────
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
def video_generate_view(request):
|
|
"""POST /api/v1/video/generate — Create video generation task.
|
|
|
|
Accepts JSON:
|
|
{
|
|
"prompt": "...",
|
|
"mode": "universal" | "keyframe",
|
|
"model": "seedance_2.0" | "seedance_2.0_fast",
|
|
"aspect_ratio": "16:9",
|
|
"duration": 10,
|
|
"references": [
|
|
{"url": "https://...", "type": "image", "role": "reference_image", "label": "图片1"},
|
|
{"url": "https://...", "type": "video", "role": "reference_video", "label": "视频1"}
|
|
]
|
|
}
|
|
"""
|
|
serializer = VideoGenerateSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
user = request.user
|
|
today = timezone.now().date()
|
|
first_of_month = today.replace(day=1)
|
|
duration = serializer.validated_data['duration']
|
|
|
|
# ── Quota check ──
|
|
daily_used = user.generation_records.filter(
|
|
created_at__date=today
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
monthly_used = user.generation_records.filter(
|
|
created_at__date__gte=first_of_month
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
if daily_used + duration > user.daily_seconds_limit:
|
|
return Response({
|
|
'error': 'quota_exceeded',
|
|
'message': '您今日的生成额度已用完',
|
|
'daily_seconds_limit': user.daily_seconds_limit,
|
|
'daily_seconds_used': daily_used,
|
|
'reset_at': (timezone.now() + timedelta(days=1)).replace(
|
|
hour=0, minute=0, second=0, microsecond=0
|
|
).isoformat(),
|
|
}, status=status.HTTP_429_TOO_MANY_REQUESTS)
|
|
|
|
if monthly_used + duration > user.monthly_seconds_limit:
|
|
return Response({
|
|
'error': 'quota_exceeded',
|
|
'message': '您本月的生成额度已用完',
|
|
'monthly_seconds_limit': user.monthly_seconds_limit,
|
|
'monthly_seconds_used': monthly_used,
|
|
}, status=status.HTTP_429_TOO_MANY_REQUESTS)
|
|
|
|
# ── Build Seedance content items from references ──
|
|
references = request.data.get('references', [])
|
|
content_items = []
|
|
reference_snapshots = []
|
|
|
|
for ref in references:
|
|
url = ref.get('url', '')
|
|
ref_type = ref.get('type', 'image')
|
|
role = ref.get('role', '')
|
|
label = ref.get('label', '')
|
|
|
|
reference_snapshots.append({
|
|
'url': url,
|
|
'type': ref_type,
|
|
'role': role,
|
|
'label': label,
|
|
})
|
|
|
|
if ref_type == 'image':
|
|
item = {
|
|
'type': 'image_url',
|
|
'image_url': {'url': url},
|
|
}
|
|
if role:
|
|
item['role'] = role
|
|
content_items.append(item)
|
|
elif ref_type == 'video':
|
|
item = {
|
|
'type': 'video_url',
|
|
'video_url': {'url': url},
|
|
}
|
|
if role:
|
|
item['role'] = role
|
|
content_items.append(item)
|
|
|
|
# ── Create DB record first ──
|
|
prompt = serializer.validated_data['prompt']
|
|
mode = serializer.validated_data['mode']
|
|
model = serializer.validated_data['model']
|
|
aspect_ratio = serializer.validated_data['aspect_ratio']
|
|
|
|
record = GenerationRecord.objects.create(
|
|
user=user,
|
|
prompt=prompt,
|
|
mode=mode,
|
|
model=model,
|
|
aspect_ratio=aspect_ratio,
|
|
duration=duration,
|
|
seconds_consumed=duration,
|
|
reference_urls=reference_snapshots,
|
|
)
|
|
|
|
# ── Call Seedance API (or skip if not enabled) ──
|
|
from django.conf import settings as django_settings
|
|
if django_settings.SEEDANCE_ENABLED and django_settings.ARK_API_KEY:
|
|
try:
|
|
ark_response = create_task(
|
|
prompt=prompt,
|
|
model=model,
|
|
content_items=content_items,
|
|
aspect_ratio=aspect_ratio,
|
|
duration=duration,
|
|
)
|
|
ark_task_id = ark_response.get('id', '')
|
|
record.ark_task_id = ark_task_id
|
|
record.status = 'processing'
|
|
record.save(update_fields=['ark_task_id', 'status'])
|
|
except Exception as e:
|
|
logger.exception('Seedance API create task failed')
|
|
record.status = 'failed'
|
|
record.error_message = str(e)
|
|
record.save(update_fields=['status', 'error_message'])
|
|
else:
|
|
# Seedance not enabled — treat as completed immediately
|
|
record.status = 'completed'
|
|
record.save(update_fields=['status'])
|
|
|
|
remaining = user.daily_seconds_limit - daily_used - duration
|
|
return Response({
|
|
'task_id': str(record.task_id),
|
|
'ark_task_id': record.ark_task_id,
|
|
'status': record.status,
|
|
'estimated_time': 120,
|
|
'seconds_consumed': duration,
|
|
'remaining_seconds_today': max(remaining, 0),
|
|
}, status=status.HTTP_202_ACCEPTED)
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Video Tasks: List + Detail (for frontend polling)
|
|
# ──────────────────────────────────────────────
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
def video_tasks_list_view(request):
|
|
"""GET /api/v1/video/tasks — User's recent generation tasks."""
|
|
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))
|
|
|
|
return Response({'results': results})
|
|
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
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)
|
|
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:
|
|
try:
|
|
ark_resp = query_task(record.ark_task_id)
|
|
new_status = map_status(ark_resp.get('status', ''))
|
|
record.status = new_status
|
|
|
|
if new_status == 'completed':
|
|
video_url = extract_video_url(ark_resp)
|
|
if video_url:
|
|
record.result_url = video_url
|
|
elif new_status == 'failed':
|
|
error = ark_resp.get('error', {})
|
|
record.error_message = (
|
|
error.get('message', '') if isinstance(error, dict) else str(error)
|
|
)
|
|
|
|
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)
|
|
|
|
return Response(_serialize_task(record))
|
|
|
|
|
|
def _serialize_task(record):
|
|
"""Serialize a GenerationRecord for the frontend."""
|
|
return {
|
|
'id': record.id,
|
|
'task_id': str(record.task_id),
|
|
'ark_task_id': record.ark_task_id,
|
|
'prompt': record.prompt,
|
|
'mode': record.mode,
|
|
'model': record.model,
|
|
'aspect_ratio': record.aspect_ratio,
|
|
'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 [],
|
|
'created_at': record.created_at.isoformat(),
|
|
}
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Admin: Dashboard Stats
|
|
# ──────────────────────────────────────────────
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAdminUser])
|
|
def admin_stats_view(request):
|
|
"""GET /api/v1/admin/stats"""
|
|
today = timezone.now().date()
|
|
yesterday = today - timedelta(days=1)
|
|
first_of_month = today.replace(day=1)
|
|
thirty_days_ago = today - timedelta(days=29)
|
|
|
|
total_users = User.objects.count()
|
|
new_users_today = User.objects.filter(date_joined__date=today).count()
|
|
|
|
seconds_today = GenerationRecord.objects.filter(
|
|
created_at__date=today
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
seconds_yesterday = GenerationRecord.objects.filter(
|
|
created_at__date=yesterday
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
seconds_this_month = GenerationRecord.objects.filter(
|
|
created_at__date__gte=first_of_month
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
# Last month same period for comparison
|
|
if first_of_month.month == 1:
|
|
last_month_start = first_of_month.replace(year=first_of_month.year - 1, month=12)
|
|
else:
|
|
last_month_start = first_of_month.replace(month=first_of_month.month - 1)
|
|
days_into_month = (today - first_of_month).days + 1
|
|
last_month_same_day = last_month_start + timedelta(days=days_into_month - 1)
|
|
seconds_last_month_period = GenerationRecord.objects.filter(
|
|
created_at__date__gte=last_month_start,
|
|
created_at__date__lte=last_month_same_day
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
today_change = round(((seconds_today - seconds_yesterday) / max(seconds_yesterday, 1)) * 100, 1) if seconds_yesterday else 0
|
|
month_change = round(((seconds_this_month - seconds_last_month_period) / max(seconds_last_month_period, 1)) * 100, 1) if seconds_last_month_period else 0
|
|
|
|
# Daily trend for past 30 days
|
|
daily_trend_qs = (
|
|
GenerationRecord.objects
|
|
.filter(created_at__date__gte=thirty_days_ago)
|
|
.annotate(date=TruncDate('created_at'))
|
|
.values('date')
|
|
.annotate(seconds=Sum('seconds_consumed'))
|
|
.order_by('date')
|
|
)
|
|
trend_map = {str(item['date']): item['seconds'] or 0 for item in daily_trend_qs}
|
|
daily_trend = []
|
|
for i in range(30):
|
|
d = thirty_days_ago + timedelta(days=i)
|
|
daily_trend.append({'date': str(d), 'seconds': trend_map.get(str(d), 0)})
|
|
|
|
# Top 10 users by seconds consumed this month
|
|
top_users = (
|
|
User.objects.annotate(
|
|
seconds_consumed=Sum(
|
|
'generation_records__seconds_consumed',
|
|
filter=Q(generation_records__created_at__date__gte=first_of_month),
|
|
)
|
|
)
|
|
.filter(seconds_consumed__gt=0)
|
|
.order_by('-seconds_consumed')[:10]
|
|
)
|
|
|
|
return Response({
|
|
'total_users': total_users,
|
|
'new_users_today': new_users_today,
|
|
'seconds_consumed_today': seconds_today,
|
|
'seconds_consumed_this_month': seconds_this_month,
|
|
'today_change_percent': today_change,
|
|
'month_change_percent': month_change,
|
|
'daily_trend': daily_trend,
|
|
'top_users': [
|
|
{'user_id': u.id, 'username': u.username, 'seconds_consumed': u.seconds_consumed or 0}
|
|
for u in top_users
|
|
],
|
|
})
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Admin: User Management
|
|
# ──────────────────────────────────────────────
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAdminUser])
|
|
def admin_users_list_view(request):
|
|
"""GET /api/v1/admin/users"""
|
|
today = timezone.now().date()
|
|
first_of_month = today.replace(day=1)
|
|
|
|
page = int(request.query_params.get('page', 1))
|
|
page_size = min(int(request.query_params.get('page_size', 20)), 100)
|
|
search = request.query_params.get('search', '').strip()
|
|
status_filter = request.query_params.get('status', '').strip()
|
|
|
|
qs = User.objects.annotate(
|
|
seconds_today=Sum(
|
|
'generation_records__seconds_consumed',
|
|
filter=Q(generation_records__created_at__date=today),
|
|
),
|
|
seconds_this_month=Sum(
|
|
'generation_records__seconds_consumed',
|
|
filter=Q(generation_records__created_at__date__gte=first_of_month),
|
|
),
|
|
)
|
|
|
|
if search:
|
|
qs = qs.filter(Q(username__icontains=search) | Q(email__icontains=search))
|
|
if status_filter == 'active':
|
|
qs = qs.filter(is_active=True)
|
|
elif status_filter == 'disabled':
|
|
qs = qs.filter(is_active=False)
|
|
|
|
total = qs.count()
|
|
offset = (page - 1) * page_size
|
|
users = qs.order_by('-date_joined')[offset:offset + page_size]
|
|
|
|
results = []
|
|
for u in users:
|
|
results.append({
|
|
'id': u.id,
|
|
'username': u.username,
|
|
'email': u.email,
|
|
'is_active': u.is_active,
|
|
'date_joined': u.date_joined.isoformat(),
|
|
'daily_seconds_limit': u.daily_seconds_limit,
|
|
'monthly_seconds_limit': u.monthly_seconds_limit,
|
|
'seconds_today': u.seconds_today or 0,
|
|
'seconds_this_month': u.seconds_this_month or 0,
|
|
})
|
|
|
|
return Response({
|
|
'total': total,
|
|
'page': page,
|
|
'page_size': page_size,
|
|
'results': results,
|
|
})
|
|
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAdminUser])
|
|
def admin_user_detail_view(request, user_id):
|
|
"""GET /api/v1/admin/users/:id"""
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
except User.DoesNotExist:
|
|
return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
today = timezone.now().date()
|
|
first_of_month = today.replace(day=1)
|
|
|
|
seconds_today = user.generation_records.filter(
|
|
created_at__date=today
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
seconds_this_month = user.generation_records.filter(
|
|
created_at__date__gte=first_of_month
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
seconds_total = user.generation_records.aggregate(
|
|
total=Sum('seconds_consumed')
|
|
)['total'] or 0
|
|
|
|
recent_records = user.generation_records.order_by('-created_at')[:20]
|
|
|
|
return Response({
|
|
'id': user.id,
|
|
'username': user.username,
|
|
'email': user.email,
|
|
'is_active': user.is_active,
|
|
'is_staff': user.is_staff,
|
|
'date_joined': user.date_joined.isoformat(),
|
|
'daily_seconds_limit': user.daily_seconds_limit,
|
|
'monthly_seconds_limit': user.monthly_seconds_limit,
|
|
'seconds_today': seconds_today,
|
|
'seconds_this_month': seconds_this_month,
|
|
'seconds_total': seconds_total,
|
|
'recent_records': [
|
|
{
|
|
'id': r.id,
|
|
'created_at': r.created_at.isoformat(),
|
|
'seconds_consumed': r.seconds_consumed,
|
|
'prompt': r.prompt,
|
|
'mode': r.mode,
|
|
'model': r.model,
|
|
'status': r.status,
|
|
}
|
|
for r in recent_records
|
|
],
|
|
})
|
|
|
|
|
|
@api_view(['PUT'])
|
|
@permission_classes([IsAdminUser])
|
|
def admin_user_quota_view(request, user_id):
|
|
"""PUT /api/v1/admin/users/:id/quota"""
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
except User.DoesNotExist:
|
|
return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
serializer = QuotaUpdateSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
user.daily_seconds_limit = serializer.validated_data['daily_seconds_limit']
|
|
user.monthly_seconds_limit = serializer.validated_data['monthly_seconds_limit']
|
|
user.save(update_fields=['daily_seconds_limit', 'monthly_seconds_limit'])
|
|
|
|
return Response({
|
|
'user_id': user.id,
|
|
'username': user.username,
|
|
'daily_seconds_limit': user.daily_seconds_limit,
|
|
'monthly_seconds_limit': user.monthly_seconds_limit,
|
|
'updated_at': timezone.now().isoformat(),
|
|
})
|
|
|
|
|
|
@api_view(['PATCH'])
|
|
@permission_classes([IsAdminUser])
|
|
def admin_user_status_view(request, user_id):
|
|
"""PATCH /api/v1/admin/users/:id/status"""
|
|
try:
|
|
user = User.objects.get(id=user_id)
|
|
except User.DoesNotExist:
|
|
return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
serializer = UserStatusSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
user.is_active = serializer.validated_data['is_active']
|
|
user.save(update_fields=['is_active'])
|
|
|
|
return Response({
|
|
'user_id': user.id,
|
|
'username': user.username,
|
|
'is_active': user.is_active,
|
|
'updated_at': timezone.now().isoformat(),
|
|
})
|
|
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([IsAdminUser])
|
|
def admin_create_user_view(request):
|
|
"""POST /api/v1/admin/users — Admin creates a new user"""
|
|
serializer = AdminCreateUserSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
username = serializer.validated_data['username']
|
|
email = serializer.validated_data['email']
|
|
|
|
if User.objects.filter(username=username).exists():
|
|
return Response({'error': '用户名已存在'}, status=status.HTTP_400_BAD_REQUEST)
|
|
if User.objects.filter(email=email).exists():
|
|
return Response({'error': '邮箱已存在'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
user = User.objects.create_user(
|
|
username=username,
|
|
email=email,
|
|
password=serializer.validated_data['password'],
|
|
daily_seconds_limit=serializer.validated_data['daily_seconds_limit'],
|
|
monthly_seconds_limit=serializer.validated_data['monthly_seconds_limit'],
|
|
is_staff=serializer.validated_data['is_staff'],
|
|
)
|
|
|
|
return Response({
|
|
'id': user.id,
|
|
'username': user.username,
|
|
'email': user.email,
|
|
'is_active': user.is_active,
|
|
'is_staff': user.is_staff,
|
|
'daily_seconds_limit': user.daily_seconds_limit,
|
|
'monthly_seconds_limit': user.monthly_seconds_limit,
|
|
'created_at': timezone.now().isoformat(),
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Admin: Consumption Records
|
|
# ──────────────────────────────────────────────
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAdminUser])
|
|
def admin_records_view(request):
|
|
"""GET /api/v1/admin/records"""
|
|
page = int(request.query_params.get('page', 1))
|
|
page_size = min(int(request.query_params.get('page_size', 20)), 100)
|
|
search = request.query_params.get('search', '').strip()
|
|
start_date = request.query_params.get('start_date', '').strip()
|
|
end_date = request.query_params.get('end_date', '').strip()
|
|
|
|
qs = GenerationRecord.objects.select_related('user').order_by('-created_at')
|
|
|
|
if search:
|
|
qs = qs.filter(user__username__icontains=search)
|
|
if start_date:
|
|
qs = qs.filter(created_at__date__gte=start_date)
|
|
if end_date:
|
|
qs = qs.filter(created_at__date__lte=end_date)
|
|
|
|
total = qs.count()
|
|
offset = (page - 1) * page_size
|
|
records = qs[offset:offset + page_size]
|
|
|
|
results = []
|
|
for r in records:
|
|
results.append({
|
|
'id': r.id,
|
|
'created_at': r.created_at.isoformat(),
|
|
'user_id': r.user_id,
|
|
'username': r.user.username,
|
|
'seconds_consumed': r.seconds_consumed,
|
|
'prompt': r.prompt,
|
|
'mode': r.mode,
|
|
'model': r.model,
|
|
'aspect_ratio': r.aspect_ratio,
|
|
'status': r.status,
|
|
})
|
|
|
|
return Response({
|
|
'total': total,
|
|
'page': page,
|
|
'page_size': page_size,
|
|
'results': results,
|
|
})
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Admin: System Settings
|
|
# ──────────────────────────────────────────────
|
|
|
|
@api_view(['GET', 'PUT'])
|
|
@permission_classes([IsAdminUser])
|
|
def admin_settings_view(request):
|
|
"""GET/PUT /api/v1/admin/settings"""
|
|
config, _ = QuotaConfig.objects.get_or_create(pk=1)
|
|
|
|
if request.method == 'GET':
|
|
return Response({
|
|
'default_daily_seconds_limit': config.default_daily_seconds_limit,
|
|
'default_monthly_seconds_limit': config.default_monthly_seconds_limit,
|
|
'announcement': config.announcement,
|
|
'announcement_enabled': config.announcement_enabled,
|
|
})
|
|
|
|
serializer = SystemSettingsSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
config.default_daily_seconds_limit = serializer.validated_data['default_daily_seconds_limit']
|
|
config.default_monthly_seconds_limit = serializer.validated_data['default_monthly_seconds_limit']
|
|
config.announcement = serializer.validated_data.get('announcement', '')
|
|
config.announcement_enabled = serializer.validated_data.get('announcement_enabled', False)
|
|
config.save()
|
|
|
|
return Response({
|
|
'default_daily_seconds_limit': config.default_daily_seconds_limit,
|
|
'default_monthly_seconds_limit': config.default_monthly_seconds_limit,
|
|
'announcement': config.announcement,
|
|
'announcement_enabled': config.announcement_enabled,
|
|
'updated_at': config.updated_at.isoformat(),
|
|
})
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Profile: User's own consumption data
|
|
# ──────────────────────────────────────────────
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
def profile_overview_view(request):
|
|
"""GET /api/v1/profile/overview"""
|
|
user = request.user
|
|
today = timezone.now().date()
|
|
first_of_month = today.replace(day=1)
|
|
period = request.query_params.get('period', '7d')
|
|
days = 30 if period == '30d' else 7
|
|
start_date = today - timedelta(days=days - 1)
|
|
|
|
daily_seconds_used = user.generation_records.filter(
|
|
created_at__date=today
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
monthly_seconds_used = user.generation_records.filter(
|
|
created_at__date__gte=first_of_month
|
|
).aggregate(total=Sum('seconds_consumed'))['total'] or 0
|
|
|
|
total_seconds_used = user.generation_records.aggregate(
|
|
total=Sum('seconds_consumed')
|
|
)['total'] or 0
|
|
|
|
# Daily trend
|
|
trend_qs = (
|
|
user.generation_records
|
|
.filter(created_at__date__gte=start_date)
|
|
.annotate(date=TruncDate('created_at'))
|
|
.values('date')
|
|
.annotate(seconds=Sum('seconds_consumed'))
|
|
.order_by('date')
|
|
)
|
|
trend_map = {str(item['date']): item['seconds'] or 0 for item in trend_qs}
|
|
daily_trend = []
|
|
for i in range(days):
|
|
d = start_date + timedelta(days=i)
|
|
daily_trend.append({'date': str(d), 'seconds': trend_map.get(str(d), 0)})
|
|
|
|
return Response({
|
|
'daily_seconds_limit': user.daily_seconds_limit,
|
|
'daily_seconds_used': daily_seconds_used,
|
|
'monthly_seconds_limit': user.monthly_seconds_limit,
|
|
'monthly_seconds_used': monthly_seconds_used,
|
|
'total_seconds_used': total_seconds_used,
|
|
'daily_trend': daily_trend,
|
|
})
|
|
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
def profile_records_view(request):
|
|
"""GET /api/v1/profile/records"""
|
|
user = request.user
|
|
page = int(request.query_params.get('page', 1))
|
|
page_size = min(int(request.query_params.get('page_size', 20)), 100)
|
|
|
|
qs = user.generation_records.order_by('-created_at')
|
|
total = qs.count()
|
|
offset = (page - 1) * page_size
|
|
records = qs[offset:offset + page_size]
|
|
|
|
results = []
|
|
for r in records:
|
|
results.append({
|
|
'id': r.id,
|
|
'created_at': r.created_at.isoformat(),
|
|
'seconds_consumed': r.seconds_consumed,
|
|
'prompt': r.prompt,
|
|
'mode': r.mode,
|
|
'model': r.model,
|
|
'aspect_ratio': r.aspect_ratio,
|
|
'status': r.status,
|
|
})
|
|
|
|
return Response({
|
|
'total': total,
|
|
'page': page,
|
|
'page_size': page_size,
|
|
'results': results,
|
|
})
|