from django.http import JsonResponse from adrf.views import APIView from rest_framework import status from rest_framework.permissions import AllowAny, IsAuthenticated from django.db.models import Count, Q from ..models import ToolCategory, Tool, ToolFavorite, ToolUsageRecord from ..serializers import ToolCategorySerializer, ToolSerializer 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 class ToolCategoryListView(APIView): permission_classes = [AllowAny] @swagger_auto_schema( operation_summary='获取工具分类列表', operation_description='获取所有工具分类及其关联的工具列表', tags=['工具'], responses={200: success_response} ) async def get(self, request): categories = [c async for c in ToolCategory.objects.prefetch_related('tools').all()] serializer = ToolCategorySerializer(categories, many=True) return JsonResponse({ 'success': True, 'data': await serializer.adata }) class ToolListView(APIView): permission_classes = [AllowAny] @swagger_auto_schema( operation_summary='获取工具列表', operation_description='按分类获取工具列表,支持启用状态过滤', tags=['工具'], manual_parameters=[ openapi.Parameter('category_id', openapi.IN_QUERY, description='分类ID', type=openapi.TYPE_INTEGER), openapi.Parameter('enabled_only', openapi.IN_QUERY, description='是否只显示启用的工具', type=openapi.TYPE_STRING), openapi.Parameter('ordering', openapi.IN_QUERY, description='排序字段', type=openapi.TYPE_STRING), ], responses={200: success_response} ) async def get(self, request): category_id = request.query_params.get('category_id') enabled_only = request.query_params.get('enabled_only', 'true') ordering = request.query_params.get('ordering', '') tools = Tool.objects.select_related('category').annotate( annotated_favorites_count=Count('tool_favorites') ) if enabled_only == 'true': tools = tools.filter(is_enabled=True) if category_id: tools = tools.filter(category_id=category_id) if ordering: # 支持按 favorites_count 排序 if ordering == '-favorites_count': tools = tools.order_by('-annotated_favorites_count') elif ordering == 'favorites_count': tools = tools.order_by('annotated_favorites_count') else: tools = tools.order_by(ordering) else: tools = tools.order_by('sort_order', '-created_at') tool_list = [t async for t in tools] serializer = ToolSerializer(tool_list, many=True) data = await serializer.adata # 构建 favorites_count 映射 favorites_count_map = { t.id: t.annotated_favorites_count for t in tool_list } if request.user.is_authenticated: favorite_tool_ids = set([ tid async for tid in ToolFavorite.objects.filter(user=request.user).values_list('tool_id', flat=True) ]) for item in data: item['is_favorited'] = item['id'] in favorite_tool_ids item['favorites_count'] = favorites_count_map.get(item['id'], 0) else: for item in data: item['is_favorited'] = False item['favorites_count'] = favorites_count_map.get(item['id'], 0) return JsonResponse({ 'success': True, 'data': data }) class ToolDetailView(APIView): permission_classes = [AllowAny] @swagger_auto_schema( operation_summary='获取工具详情', operation_description='获取单个工具的详细信息', tags=['工具'], manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='工具ID', type=openapi.TYPE_INTEGER, required=True) ], responses={200: success_response, 404: not_found_response} ) async def get(self, request, pk): try: tool = await Tool.objects.select_related('category').aget(pk=pk, is_enabled=True) serializer = ToolSerializer(tool) data = await serializer.adata if request.user.is_authenticated: data['is_favorited'] = await ToolFavorite.objects.filter( user=request.user, tool=tool ).aexists() else: data['is_favorited'] = False data['favorites_count'] = await ToolFavorite.objects.filter(tool=tool).acount() return JsonResponse({ 'success': True, 'data': data }) except Tool.DoesNotExist: return JsonResponse( {'success': False, 'error': '工具不存在'}, status=status.HTTP_404_NOT_FOUND ) class ToolFavoriteToggleView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( operation_summary='切换工具收藏状态', operation_description='添加或取消工具收藏', tags=['工具'], manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='工具ID', type=openapi.TYPE_INTEGER, required=True) ], responses={200: success_response, 401: unauthorized_response, 404: not_found_response} ) async def post(self, request, pk): tool = await Tool.objects.filter(pk=pk, is_enabled=True).afirst() if not tool: return JsonResponse( {'success': False, 'error': '工具不存在'}, status=status.HTTP_404_NOT_FOUND ) fav, created = await ToolFavorite.objects.aget_or_create(user=request.user, tool=tool) if not created: await fav.adelete() is_favorited = False else: is_favorited = True favorites_count = await ToolFavorite.objects.filter(tool=tool).acount() return JsonResponse({ 'success': True, 'data': { 'id': tool.id, 'is_favorited': is_favorited, 'favorites_count': favorites_count, } }) class ToolFavoriteListView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( operation_summary='获取我的收藏工具列表', operation_description='获取当前用户收藏的所有工具', tags=['工具'], responses={200: success_response, 401: unauthorized_response} ) async def get(self, request): favorites = [fav async for fav in ToolFavorite.objects.select_related( 'tool', 'tool__category' ).filter( user=request.user, tool__is_enabled=True ).order_by('-created_at')] result = [] for fav in favorites: tool_data = ToolSerializer(fav.tool).data tool_data['is_favorited'] = True tool_data['favorites_count'] = await ToolFavorite.objects.filter(tool=fav.tool).acount() result.append(tool_data) return JsonResponse({ 'success': True, 'data': result }) class ToolUsageIncrementView(APIView): """工具使用次数递增接口""" permission_classes = [AllowAny] @swagger_auto_schema( operation_summary='递增工具使用次数', operation_description='用户点击工具时调用,递增该工具的使用次数(基于会话去重,同一会话5分钟内不重复计数)', tags=['工具'], manual_parameters=[ openapi.Parameter('pk', openapi.IN_PATH, description='工具ID', type=openapi.TYPE_INTEGER, required=True) ], responses={200: success_response, 404: not_found_response} ) async def post(self, request, pk): try: tool = await Tool.objects.aget(pk=pk, is_enabled=True) # 使用F()表达式避免竞态条件 from django.db.models import F await Tool.objects.filter(pk=pk).aupdate(usage_count=F('usage_count') + 1) await tool.arefresh_from_db() # 写入使用日志(周榜统计用),失败不影响主流程 try: await ToolUsageRecord.objects.acreate( tool=tool, user=request.user if request.user.is_authenticated else None, session_id=request.COOKIES.get('sessionid', '')[:64] or (request.session.session_key or '')[:64], ) except Exception: pass return JsonResponse({ 'success': True, 'data': { 'id': tool.id, 'usage_count': tool.usage_count, } }) except Tool.DoesNotExist: return JsonResponse( {'success': False, 'error': '工具不存在'}, status=status.HTTP_404_NOT_FOUND ) class ToolTopView(APIView): """工具热榜:总榜(累计使用次数)/ 周榜(近7天使用日志聚合)""" permission_classes = [AllowAny] @swagger_auto_schema( operation_summary='工具热榜', operation_description='获取热门工具排行榜。range=all 按累计使用次数排序;range=week 按近7天使用日志聚合排序(无日志时回退到总榜)。兼容旧参数 period=total|week。返回项含 id/name/icon/usage_count/weekly_count/rank。', tags=['工具'], manual_parameters=[ openapi.Parameter('range', openapi.IN_QUERY, description='榜单周期: all / week', type=openapi.TYPE_STRING), openapi.Parameter('period', openapi.IN_QUERY, description='兼容旧参数: total / week', type=openapi.TYPE_STRING), openapi.Parameter('limit', openapi.IN_QUERY, description='返回数量,默认10,最大50', type=openapi.TYPE_INTEGER), ], responses={200: success_response} ) async def get(self, request): from django.utils import timezone from datetime import timedelta raw_range = request.query_params.get('range') raw_period = request.query_params.get('period') period = (raw_range or raw_period or 'all').lower() # 兼容映射:all <-> total if period == 'total': period = 'all' if period not in ('all', 'week'): period = 'all' try: limit = int(request.query_params.get('limit', 10)) except (TypeError, ValueError): limit = 10 limit = max(1, min(limit, 50)) tools = Tool.objects.filter(is_enabled=True).select_related('category') weekly_map: dict = {} if period == 'week': week_ago = timezone.now() - timedelta(days=7) weekly_counts = ( ToolUsageRecord.objects.filter(created_at__gte=week_ago) .values_list('tool_id') .annotate(cnt=Count('id')) ) weekly_map = {tid: cnt async for tid, cnt in weekly_counts} if weekly_map: tool_list = [t async for t in tools] tool_list.sort(key=lambda t: (weekly_map.get(t.id, 0), t.usage_count), reverse=True) serializer = ToolSerializer(tool_list[:limit], many=True) data = await serializer.adata for rank, item in enumerate(data, start=1): item['weekly_count'] = weekly_map.get(item['id'], 0) item['weekly_usage'] = item['weekly_count'] item['rank'] = rank return JsonResponse({'success': True, 'data': data}) # 无周日志时回退总榜 period = 'all' tools = tools.order_by('-usage_count', 'sort_order') tool_list = [t async for t in tools[:limit]] serializer = ToolSerializer(tool_list, many=True) data = await serializer.adata for rank, item in enumerate(data, start=1): item['weekly_count'] = None item['weekly_usage'] = None item['rank'] = rank return JsonResponse({'success': True, 'data': data}) class ToolRecentView(APIView): """本人最近使用工具(登录,去重按最近倒序)""" permission_classes = [IsAuthenticated] @swagger_auto_schema( operation_summary='最近使用的工具', operation_description='返回当前登录用户最近使用过的工具(按工具去重,取最近一条记录倒序)。', tags=['工具'], manual_parameters=[ openapi.Parameter('limit', openapi.IN_QUERY, description='返回数量,默认10,最大50', type=openapi.TYPE_INTEGER), ], responses={200: success_response, 401: unauthorized_response} ) async def get(self, request): try: limit = int(request.query_params.get('limit', 10)) except (TypeError, ValueError): limit = 10 limit = max(1, min(limit, 50)) records = [ r async for r in ToolUsageRecord.objects.filter(user=request.user) .select_related('tool', 'tool__category') .order_by('-created_at', '-id')[: limit * 5] ] seen = set() tools = [] for r in records: if r.tool_id in seen: continue if not getattr(r.tool, 'is_enabled', False): continue seen.add(r.tool_id) tools.append(r.tool) if len(tools) >= limit: break serializer = ToolSerializer(tools, many=True) data = await serializer.adata return JsonResponse({'success': True, 'data': data})