from rest_framework import status from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from django.db import transaction from django.db.models import F from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from datetime import timedelta import logging from utils.response_codes import ( ResponseCode, create_standardized_response, create_standardized_error_response ) from ..models import ( FUser, TaskDefinition, UserTaskProgress, UserLevel, LevelThreshold, PointTransaction, ) from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response logger = logging.getLogger(__name__) def get_period_key(task_type): now = timezone.localtime(timezone.now()) if task_type == 'daily': return now.strftime('%Y-%m-%d') elif task_type == 'weekly': return now.strftime('%Y-W%W') return 'permanent' def get_or_create_progress(user, task, period_key): progress, created = UserTaskProgress.objects.get_or_create( user=user, task=task, period_key=period_key, defaults={'current_count': 0} ) return progress def track_user_action(user, action_type, count=1): """追踪用户行为并更新任务进度,返回更新的任务列表""" now = timezone.localtime(timezone.now()) daily_period = now.strftime('%Y-%m-%d') weekly_period = now.strftime('%Y-W%W') tasks = TaskDefinition.objects.filter( action_type=action_type, is_active=True, ) updated_tasks = [] for task in tasks: if task.task_type == 'daily': period_key = daily_period elif task.task_type == 'weekly': period_key = weekly_period else: period_key = 'permanent' progress = get_or_create_progress(user, task, period_key) if progress.is_completed and progress.is_claimed: continue progress.current_count = min( progress.current_count + count, task.target_count ) if progress.current_count >= task.target_count and not progress.is_completed: progress.is_completed = True progress.completed_at = now progress.save() updated_tasks.append({ 'id': task.id, 'name': task.name, 'task_type': task.task_type, 'current_count': progress.current_count, 'target_count': task.target_count, 'is_completed': progress.is_completed, 'is_claimed': progress.is_claimed, }) return updated_tasks def check_level_up(user_level): next_threshold = LevelThreshold.objects.filter( level__gt=user_level.level ).order_by('level').first() leveled_up = False while next_threshold and user_level.xp >= next_threshold.xp_required: user_level.level = next_threshold.level leveled_up = True next_threshold = LevelThreshold.objects.filter( level__gt=user_level.level ).order_by('level').first() if leveled_up: user_level.save(update_fields=['level', 'updated_at']) return leveled_up @method_decorator(never_cache, name='dispatch') class TaskListAPIView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['任务'], operation_summary='获取任务列表', operation_description='获取所有活跃任务列表及当前用户的进度和等级信息', responses={200: success_response, 401: unauthorized_response, 500: error_response}, ) def get(self, request): try: user = request.user now = timezone.localtime(timezone.now()) daily_period = now.strftime('%Y-%m-%d') weekly_period = now.strftime('%Y-W%W') tasks = TaskDefinition.objects.filter(is_active=True) task_list = [] for task in tasks: if task.task_type == 'daily': period_key = daily_period elif task.task_type == 'weekly': period_key = weekly_period else: period_key = 'permanent' progress = UserTaskProgress.objects.filter( user=user, task=task, period_key=period_key ).first() task_list.append({ 'id': task.id, 'name': task.name, 'task_type': task.task_type, 'action_type': task.action_type, 'description': task.description, 'target_count': task.target_count, 'reward_points': task.reward_points, 'reward_coins': task.reward_coins, 'reward_xp': task.reward_xp, 'icon': task.icon, 'redirect_url': task.redirect_url, 'current_count': progress.current_count if progress else 0, 'is_completed': progress.is_completed if progress else False, 'is_claimed': progress.is_claimed if progress else False, }) user_level = UserLevel.objects.filter(user=user).first() next_threshold = LevelThreshold.objects.filter( level__gt=user_level.level if user_level else 1 ).order_by('level').first() if user_level else None level_data = { 'level': user_level.level if user_level else 1, 'xp': user_level.xp if user_level else 0, 'title': (LevelThreshold.objects.filter(level=user_level.level).first().title if user_level and LevelThreshold.objects.filter(level=user_level.level).exists() else '新手'), 'next_level_xp': next_threshold.xp_required if next_threshold else None, 'next_level_title': next_threshold.title if next_threshold else None, } return create_standardized_response( data={ 'tasks': task_list, 'level': level_data, }, code=ResponseCode.SUCCESS, status_code=status.HTTP_200_OK ) except Exception as e: logger.error(f'获取任务列表失败: {e}') return create_standardized_error_response( message='获取任务列表失败', code=ResponseCode.SERVER_INTERNAL_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) @method_decorator(never_cache, name='dispatch') class TaskTrackAPIView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['任务'], operation_summary='上报任务进度', operation_description='上报用户行为进度,自动匹配并更新相关任务的完成状态', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['action_type'], properties={ 'action_type': openapi.Schema(type=openapi.TYPE_STRING, description='行为类型'), 'count': openapi.Schema(type=openapi.TYPE_INTEGER, description='行为次数,默认1'), } ), responses={200: success_response, 400: error_response, 401: unauthorized_response, 500: error_response}, ) def post(self, request): action_type = request.data.get('action_type') count = int(request.data.get('count', 1)) if not action_type: return create_standardized_error_response( message='缺少 action_type 参数', code=ResponseCode.PARAMETER_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) valid_actions = [choice[0] for choice in TaskDefinition.ACTION_TYPE_CHOICES] if action_type not in valid_actions: return create_standardized_error_response( message=f'无效的行为类型: {action_type}', code=ResponseCode.PARAMETER_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) try: updated_tasks = track_user_action(request.user, action_type, count) return create_standardized_response( data={'updated_tasks': updated_tasks}, code=ResponseCode.SUCCESS, status_code=status.HTTP_200_OK ) except Exception as e: logger.error(f'任务进度更新失败: {e}') return create_standardized_error_response( message='任务进度更新失败', code=ResponseCode.SERVER_INTERNAL_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) @method_decorator(never_cache, name='dispatch') class TaskClaimAPIView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['任务'], operation_summary='领取任务奖励', operation_description='领取已完成任务的奖励,包括积分、y币和经验值', manual_parameters=[ openapi.Parameter('task_id', openapi.IN_PATH, description='任务ID', type=openapi.TYPE_INTEGER), ], responses={200: success_response, 400: error_response, 401: unauthorized_response, 500: error_response}, ) def post(self, request, task_id): try: user = request.user task = TaskDefinition.objects.filter(id=task_id, is_active=True).first() if not task: return create_standardized_error_response( message='任务不存在', code=ResponseCode.PARAMETER_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) now = timezone.localtime(timezone.now()) if task.task_type == 'daily': period_key = now.strftime('%Y-%m-%d') elif task.task_type == 'weekly': period_key = now.strftime('%Y-W%W') else: period_key = 'permanent' progress = UserTaskProgress.objects.filter( user=user, task=task, period_key=period_key ).first() if not progress or not progress.is_completed: return create_standardized_error_response( message='任务尚未完成', code=ResponseCode.PARAMETER_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) if progress.is_claimed: return create_standardized_error_response( message='奖励已领取', code=ResponseCode.PARAMETER_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) with transaction.atomic(): locked_user = FUser.objects.select_for_update().get(pk=user.pk) user_level, _ = UserLevel.objects.select_for_update().get_or_create( user=locked_user, defaults={'xp': 0, 'level': 1} ) if task.reward_points > 0: locked_user.points = F('points') + task.reward_points locked_user.save(update_fields=['points']) locked_user.refresh_from_db() PointTransaction.objects.create( user=locked_user, transaction_type='earn', currency_type='points', amount=task.reward_points, balance_after=locked_user.points, description=f'完成任务: {task.name}', ) if task.reward_coins > 0: locked_user.coins = F('coins') + task.reward_coins locked_user.save(update_fields=['coins']) locked_user.refresh_from_db() PointTransaction.objects.create( user=locked_user, transaction_type='earn', currency_type='coins', amount=task.reward_coins, balance_after=locked_user.coins, description=f'完成任务: {task.name}', ) if task.reward_xp > 0: user_level.xp = F('xp') + task.reward_xp user_level.save(update_fields=['xp', 'updated_at']) user_level.refresh_from_db() check_level_up(user_level) progress.is_claimed = True progress.claimed_at = now progress.save(update_fields=['is_claimed', 'claimed_at']) user_level.refresh_from_db() next_threshold = LevelThreshold.objects.filter( level__gt=user_level.level ).order_by('level').first() return create_standardized_response( data={ 'points': locked_user.points, 'coins': locked_user.coins, 'xp': user_level.xp, 'level': user_level.level, 'next_level_xp': next_threshold.xp_required if next_threshold else None, 'reward_points': task.reward_points, 'reward_coins': task.reward_coins, 'reward_xp': task.reward_xp, }, code=ResponseCode.SUCCESS, status_code=status.HTTP_200_OK ) except Exception as e: logger.error(f'领取任务奖励失败: {e}') return create_standardized_error_response( message='领取奖励失败', code=ResponseCode.SERVER_INTERNAL_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) @method_decorator(never_cache, name='dispatch') class UserLevelAPIView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['任务'], operation_summary='获取用户等级信息', operation_description='获取当前用户的等级、经验值、等级称号和升级进度', responses={200: success_response, 401: unauthorized_response, 500: error_response}, ) def get(self, request): try: user = request.user user_level, _ = UserLevel.objects.get_or_create( user=user, defaults={'xp': 0, 'level': 1} ) current_threshold = LevelThreshold.objects.filter( level=user_level.level ).first() next_threshold = LevelThreshold.objects.filter( level__gt=user_level.level ).order_by('level').first() current_xp = current_threshold.xp_required if current_threshold else 0 next_xp = next_threshold.xp_required if next_threshold else user_level.xp progress_pct = 0 if next_xp > current_xp: progress_pct = round((user_level.xp - current_xp) / (next_xp - current_xp) * 100, 1) progress_pct = max(0, min(100, progress_pct)) return create_standardized_response( data={ 'level': user_level.level, 'xp': user_level.xp, 'title': current_threshold.title if current_threshold else '新手', 'next_level_xp': next_threshold.xp_required if next_threshold else None, 'next_level_title': next_threshold.title if next_threshold else None, 'progress_pct': progress_pct, }, code=ResponseCode.SUCCESS, status_code=status.HTTP_200_OK ) except Exception as e: logger.error(f'获取等级信息失败: {e}') return create_standardized_error_response( message='获取等级信息失败', code=ResponseCode.SERVER_INTERNAL_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR )