Files
chunyu_project/user/views/tasks.py
T
2026-08-05 23:59:15 +08:00

418 lines
16 KiB
Python

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
)