import json import logging import random import time import requests import threading from django.shortcuts import render, redirect, get_object_or_404 from django.http import JsonResponse, HttpResponse from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie from django.core.cache import cache from django.contrib.auth.models import User from django.conf import settings from django.db.models import Q, Avg from django.utils import timezone import uuid from .models import Competition, CompetitionEnrollment, Project, Score, ScoreDimension, Comment, ProjectFile from shop.models import WeChatUser from shop.sms_utils import send_sms from ai_services.models import TranscriptionTask from ai_services.services import AliyunTingwuService logger = logging.getLogger(__name__) # --- Helper Functions --- def get_client_ip(request): x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0] else: ip = request.META.get('REMOTE_ADDR') return ip def log_audit(request, action, target, result="SUCCESS", details=""): judge_id = request.session.get('judge_id') phone = request.session.get('judge_phone', 'Unknown') role = request.session.get('judge_role', 'unknown') ip = get_client_ip(request) timestamp = timezone.now().strftime("%Y-%m-%d %H:%M:%S") log_entry = f"[{timestamp}] IP:{ip} | Phone:{phone} | Role:{role} | Action:{action} | Target:{target} | Result:{result} | Details:{details}\n" # Write to a file try: with open(settings.BASE_DIR / 'judge_audit.log', 'a', encoding='utf-8') as f: f.write(log_entry) except Exception as e: logger.error(f"Failed to write audit log: {e}") def judge_required(view_func): def wrapper(request, *args, **kwargs): if not request.session.get('judge_id'): return redirect('judge_login') return view_func(request, *args, **kwargs) return wrapper def check_contestant_access(view_func): """ Check if the user is allowed to access. Contestants have limited access. """ def wrapper(request, *args, **kwargs): if not request.session.get('judge_id'): return redirect('judge_login') role = request.session.get('judge_role') if role == 'contestant': # Some views might be restricted for contestants # For now, this decorator just ensures login, but specific views handle logic pass return view_func(request, *args, **kwargs) return wrapper # --- Views --- def admin_entry(request): """Entry point for /competition/admin""" if request.session.get('judge_id'): return redirect('judge_dashboard') return redirect('judge_login') @csrf_exempt def login_view(request): if request.method == 'GET': return render(request, 'judge/login.html') phone = request.POST.get('phone') code = request.POST.get('code') if not phone or not code: return render(request, 'judge/login.html', {'error': '请输入手机号和验证码'}) # Verify Code cached_code = cache.get(f"sms_code_{phone}") # Universal pass code for development/testing if code != cached_code and code != '888888': return render(request, 'judge/login.html', {'error': '验证码错误 or expired'}) # Check User try: user = WeChatUser.objects.filter(phone_number=phone).first() if not user: return render(request, 'judge/login.html', {'error': '该手机号未绑定用户'}) # Check roles # Priority: Judge > Guest > Contestant (if allowed) is_judge = CompetitionEnrollment.objects.filter(user=user, role='judge').exists() is_guest = CompetitionEnrollment.objects.filter(user=user, role='guest').exists() role = None if is_judge: role = 'judge' elif is_guest: role = 'guest' else: # Check if contestant in any competition with allow_contestant_grading=True contestant_enrollments = CompetitionEnrollment.objects.filter( user=user, role='contestant', competition__allow_contestant_grading=True ) if contestant_enrollments.exists(): role = 'contestant' if not role: return render(request, 'judge/login.html', {'error': '您没有权限登录系统'}) # Login Success request.session['judge_id'] = user.id request.session['judge_phone'] = phone request.session['judge_name'] = user.nickname request.session['judge_role'] = role log_audit(request, 'LOGIN', 'System', 'SUCCESS', f"User {user.nickname} logged in as {role}") return redirect('judge_dashboard') except Exception as e: logger.error(f"Login error: {e}") return render(request, 'judge/login.html', {'error': '系统错误'}) @csrf_exempt def send_code(request): if request.method != 'POST': return JsonResponse({'success': False, 'message': 'Method not allowed'}) try: data = json.loads(request.body) phone = data.get('phone') if not phone or len(phone) != 11: return JsonResponse({'success': False, 'message': 'Invalid phone number'}) # Generate Code code = str(random.randint(100000, 999999)) # 6 digits to match typical SMS cache.set(f"sms_code_{phone}", code, timeout=300) # 5 mins # Send SMS using the specified API def _send_async(): try: api_url = "https://data.tangledup-ai.com/api/send-sms" payload = { "phone_number": phone, "code": code, "template_code": "SMS_493295002", "sign_name": "叠加态科技云南", "additionalProp1": {} } headers = { "Content-Type": "application/json", "accept": "application/json" } response = requests.post(api_url, json=payload, headers=headers, timeout=15) logger.info(f"SMS Response for {phone}: {response.status_code} - {response.text}") except Exception as e: logger.error(f"发送短信异常: {str(e)}") threading.Thread(target=_send_async).start() return JsonResponse({'success': True}) except Exception as e: return JsonResponse({'success': False, 'message': str(e)}) def logout_view(request): log_audit(request, 'LOGOUT', 'System') request.session.flush() return redirect('judge_login') @judge_required def dashboard(request): judge_id = request.session['judge_id'] role = request.session.get('judge_role', 'judge') user = WeChatUser.objects.get(id=judge_id) # Get competitions if role == 'judge': enrollments = CompetitionEnrollment.objects.filter(user=user, role='judge') elif role == 'guest': enrollments = CompetitionEnrollment.objects.filter(user=user, role='guest') else: # Contestant: only competitions allowing grading enrollments = CompetitionEnrollment.objects.filter( user=user, role='contestant', competition__allow_contestant_grading=True ) competition_ids = enrollments.values_list('competition_id', flat=True) # Get Projects projects = Project.objects.filter( competition_id__in=competition_ids, status='submitted' ).select_related('contestant__user') # Format for template project_list = [] for p in projects: # Check current score/grading status for this user # Note: Score model links to 'judge' which is a CompetitionEnrollment # We need the enrollment for this user in this competition user_enrollment = enrollments.filter(competition=p.competition).first() project_list.append({ 'id': p.id, 'title': p.title, 'cover_image_url': p.cover_image_url or (p.cover_image.url if p.cover_image else ''), 'contestant_name': p.contestant.user.nickname, 'current_score': p.final_score, # Global score 'status_class': 'status-submitted', 'get_status_display': p.get_status_display() }) return render(request, 'judge/dashboard.html', { 'projects': project_list, 'user_role': role, 'user_name': request.session.get('judge_name', '用户') }) @judge_required def project_detail_api(request, project_id): judge_id = request.session['judge_id'] role = request.session.get('judge_role', 'judge') user = WeChatUser.objects.get(id=judge_id) project = get_object_or_404(Project, id=project_id) # Check permission # User must be enrolled in the project's competition with correct role/settings if role == 'judge': enrollment = CompetitionEnrollment.objects.filter(user=user, role='judge', competition=project.competition).first() elif role == 'guest': enrollment = CompetitionEnrollment.objects.filter(user=user, role='guest', competition=project.competition).first() else: enrollment = CompetitionEnrollment.objects.filter( user=user, role='contestant', competition=project.competition, competition__allow_contestant_grading=True ).first() if not enrollment: return JsonResponse({'error': 'No permission'}, status=403) # Get Dimensions dimensions = ScoreDimension.objects.filter(competition=project.competition, is_public=True).order_by('order') # Get existing scores by THIS user scores = Score.objects.filter(project=project, judge=enrollment) score_map = {s.dimension_id: s.score for s in scores} dim_data = [] for d in dimensions: dim_data.append({ 'id': d.id, 'name': d.name, 'weight': float(d.weight), 'max_score': d.max_score, 'current_score': float(score_map.get(d.id, 0)) }) # Get Comments # If role is contestant, they CANNOT see other people's comments history = [] current_comment = "" if role in ['judge', 'guest']: comments = Comment.objects.filter(project=project).order_by('-created_at') for c in comments: history.append({ 'judge_name': c.judge.user.nickname, 'content': c.content, 'created_at': c.created_at.strftime("%Y-%m-%d %H:%M") }) if c.judge.id == enrollment.id: current_comment = c.content else: # Contestant: only see their own comment my_comment = Comment.objects.filter(project=project, judge=enrollment).first() if my_comment: current_comment = my_comment.content history.append({ 'judge_name': user.nickname, # Self 'content': my_comment.content, 'created_at': my_comment.created_at.strftime("%Y-%m-%d %H:%M") }) # Include AI results latest_task = TranscriptionTask.objects.filter(project=project, status='SUCCEEDED').order_by('-created_at').first() ai_data = None if latest_task: ai_data = { 'transcription': latest_task.transcription, 'summary': latest_task.summary, 'auto_chapters_data': latest_task.auto_chapters_data } data = { 'id': project.id, 'title': project.title, 'description': project.description, 'contestant_name': project.contestant.user.nickname, 'dimensions': dim_data, 'history_comments': history, 'current_comment': current_comment, 'ai_result': ai_data, 'can_grade': role == 'judge' or (role == 'contestant' and project.contestant.user != user) # Contestant can grade others if allowed } # Specifically for guest: can_grade is False if role == 'guest': data['can_grade'] = False return JsonResponse(data) @judge_required @csrf_exempt def submit_score(request): if request.method != 'POST': return JsonResponse({'success': False, 'message': 'Method not allowed'}) try: data = json.loads(request.body) project_id = data.get('project_id') comment_content = data.get('comment') judge_id = request.session['judge_id'] role = request.session.get('judge_role', 'judge') if role == 'guest': return JsonResponse({'success': False, 'message': '嘉宾无权评分'}) user = WeChatUser.objects.get(id=judge_id) project = get_object_or_404(Project, id=project_id) enrollment = None if role == 'judge': enrollment = CompetitionEnrollment.objects.filter(user=user, role='judge', competition=project.competition).first() else: enrollment = CompetitionEnrollment.objects.filter( user=user, role='contestant', competition=project.competition, competition__allow_contestant_grading=True ).first() if not enrollment: return JsonResponse({'success': False, 'message': 'No permission'}) # Save Scores dimensions = ScoreDimension.objects.filter(competition=project.competition, is_public=True) for d in dimensions: score_key = f'score_{d.id}' if score_key in data: val = data[score_key] Score.objects.update_or_create( project=project, judge=enrollment, dimension=d, defaults={'score': val} ) # Save Comment if comment_content: Comment.objects.update_or_create( project=project, judge=enrollment, defaults={'content': comment_content} ) # Recalculate Project Score project.calculate_score() log_audit(request, 'SCORE_UPDATE', f"Project {project.id}", 'SUCCESS') return JsonResponse({'success': True}) except Exception as e: logger.error(f"Submit score error: {e}") return JsonResponse({'success': False, 'message': str(e)}) @judge_required @csrf_exempt def upload_audio(request): # Contestants cannot upload, but Guests can role = request.session.get('judge_role') if role not in ['judge', 'guest']: return JsonResponse({'success': False, 'message': 'Permission denied'}) if request.method != 'POST': return JsonResponse({'success': False, 'message': 'Method not allowed'}) judge_id = request.session['judge_id'] file_obj = request.FILES.get('file') project_id = request.POST.get('project_id') if not file_obj or not project_id: return JsonResponse({'success': False, 'message': 'Missing file or project_id'}) try: # Check permission user = WeChatUser.objects.get(id=judge_id) project = Project.objects.get(id=project_id) # Verify judge/guest has access to this project's competition enrollment = CompetitionEnrollment.objects.filter( user=user, role=role, competition=project.competition ).first() if not enrollment: return JsonResponse({'success': False, 'message': 'No permission for this project'}) # Upload to OSS & Create Task service = AliyunTingwuService() if not service.bucket: return JsonResponse({'success': False, 'message': 'OSS not configured'}) file_extension = file_obj.name.split('.')[-1] file_name = f"transcription/{uuid.uuid4()}.{file_extension}" oss_url = service.upload_to_oss(file_obj, file_name) # Create Task Record task = TranscriptionTask.objects.create( project=project, file_url=oss_url, status=TranscriptionTask.Status.PENDING ) # Call Tingwu try: tingwu_response = service.create_transcription_task(oss_url) # Handle response format if 'Data' in tingwu_response and isinstance(tingwu_response['Data'], dict): task_id = tingwu_response['Data'].get('TaskId') else: task_id = tingwu_response.get('TaskId') if task_id: task.task_id = task_id task.status = TranscriptionTask.Status.PROCESSING task.save() log_audit(request, 'UPLOAD_AUDIO', f"Task {task.id}", 'SUCCESS') return JsonResponse({'success': True, 'task_id': task.id, 'file_url': oss_url}) else: task.status = TranscriptionTask.Status.FAILED task.error_message = "No TaskId returned" task.save() return JsonResponse({'success': False, 'message': 'Failed to create Tingwu task'}) except Exception as e: task.status = TranscriptionTask.Status.FAILED task.error_message = str(e) task.save() return JsonResponse({'success': False, 'message': f'Tingwu Error: {e}'}) except Exception as e: logger.error(f"Upload error: {e}") return JsonResponse({'success': False, 'message': str(e)}) @judge_required def ai_manage(request): # Contestants cannot access AI manage role = request.session.get('judge_role') if role not in ['judge', 'guest']: return redirect('judge_dashboard') judge_id = request.session['judge_id'] user = WeChatUser.objects.get(id=judge_id) enrollments = CompetitionEnrollment.objects.filter(user=user, role=role) competition_ids = enrollments.values_list('competition_id', flat=True) # Get tasks for projects in these competitions tasks = TranscriptionTask.objects.filter( project__competition_id__in=competition_ids ).select_related('project').order_by('-created_at') task_list = [] for t in tasks: # Get Evaluation Score # AIEvaluation is linked to Task evals = t.ai_evaluations.all() score = evals[0].score if evals else None task_list.append({ 'id': t.id, 'project': t.project, 'file_url': t.file_url, 'file_name': t.file_url.split('/')[-1] if t.file_url else 'Unknown', 'status': t.status, 'status_class': 'status-' + t.status.lower(), # CSS class 'get_status_display': t.get_status_display(), 'ai_score': score }) return render(request, 'judge/ai_manage.html', { 'tasks': task_list, 'user_name': request.session.get('judge_name', '用户'), 'user_role': role }) @judge_required @csrf_exempt def delete_ai_task(request, task_id): role = request.session.get('judge_role') if role not in ['judge', 'guest']: return JsonResponse({'success': False, 'message': 'Permission denied'}) if request.method != 'POST': return JsonResponse({'success': False, 'message': 'Method not allowed'}) try: task = get_object_or_404(TranscriptionTask, id=task_id) # Permission check # ... task.delete() log_audit(request, 'DELETE_TASK', f"Task {task_id}", 'SUCCESS') return JsonResponse({'success': True}) except Exception as e: return JsonResponse({'success': False, 'message': str(e)})