Files

431 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from rest_framework import status
from adrf.views import APIView
from rest_framework.permissions import IsAuthenticated
from asgiref.sync import sync_to_async
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.async_decorators import async_never_cache_dispatch
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
@async_never_cache_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},
)
async 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 = []
async 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 = await UserTaskProgress.objects.filter(
user=user, task=task, period_key=period_key
).afirst()
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 = await UserLevel.objects.filter(user=user).afirst()
next_threshold = await LevelThreshold.objects.filter(
level__gt=user_level.level if user_level else 1
).order_by('level').afirst() if user_level else None
cur_threshold = await LevelThreshold.objects.filter(
level=user_level.level
).afirst() 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': cur_threshold.title if cur_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,
}
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
)
@async_never_cache_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},
)
async 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:
# track_user_action 为同步 helper(user.py 也以 sync_to_async 调用),线程池兜底
updated_tasks = await sync_to_async(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
)
@async_never_cache_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},
)
async def post(self, request, task_id):
try:
user = request.user
task = await TaskDefinition.objects.filter(id=task_id, is_active=True).afirst()
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 = await UserTaskProgress.objects.filter(
user=user, task=task, period_key=period_key
).afirst()
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
)
# select_for_update + 事务 + refresh_from_db 整体在同步函数内执行,线程池兜底
def _claim_reward():
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 locked_user, user_level, next_threshold
locked_user, user_level, next_threshold = await sync_to_async(_claim_reward)()
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
)
@async_never_cache_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},
)
async def get(self, request):
try:
user = request.user
user_level, _ = await UserLevel.objects.aget_or_create(
user=user, defaults={'xp': 0, 'level': 1}
)
current_threshold = await LevelThreshold.objects.filter(
level=user_level.level
).afirst()
next_threshold = await LevelThreshold.objects.filter(
level__gt=user_level.level
).order_by('level').afirst()
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
)