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, })