zyc ffe92f7b15 Initial commit: 即梦视频生成平台
- web/: React + Vite + TypeScript 前端
- backend/: Django + DRF + SimpleJWT 后端
- prototype/: HTML 设计原型
- docs/: PRD 和设计评审文档
- test: 单元测试 + E2E 极限测试
2026-03-13 09:59:33 +08:00

536 lines
20 KiB
Python

from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
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,
)
User = get_user_model()
# ──────────────────────────────────────────────
# Video Generation
# ──────────────────────────────────────────────
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def video_generate_view(request):
"""POST /api/v1/video/generate — Phase 3: seconds-based quota"""
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']
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)
record = GenerationRecord.objects.create(
user=user,
prompt=serializer.validated_data['prompt'],
mode=serializer.validated_data['mode'],
model=serializer.validated_data['model'],
aspect_ratio=serializer.validated_data['aspect_ratio'],
duration=duration,
seconds_consumed=duration,
)
remaining = user.daily_seconds_limit - daily_used - duration
return Response({
'task_id': str(record.task_id),
'status': 'queued',
'estimated_time': 120,
'seconds_consumed': duration,
'remaining_seconds_today': max(remaining, 0),
}, status=status.HTTP_202_ACCEPTED)
# ──────────────────────────────────────────────
# 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,
})