新增ToolUsageRecord使用日志(tool+created_at索引);/tool/top/总榜按累计排序、周榜按近7天聚合(无日志回退总榜,兼容period旧参);/tool/recent/登录去重倒序;seed新增AI图片放大/代码运行两个工具。
349 lines
14 KiB
Python
349 lines
14 KiB
Python
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})
|