feat(C-02):工具热榜周榜+最近使用

新增ToolUsageRecord使用日志(tool+created_at索引);/tool/top/总榜按累计排序、周榜按近7天聚合(无日志回退总榜,兼容period旧参);/tool/recent/登录去重倒序;seed新增AI图片放大/代码运行两个工具。
This commit is contained in:
chunyu
2026-09-15 15:20:27 +08:00
parent 37cad3947d
commit 24874aae23
7 changed files with 300 additions and 2 deletions
+119 -1
View File
@@ -3,7 +3,7 @@ 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
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
@@ -216,6 +216,15 @@ class ToolUsageIncrementView(APIView):
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': {
@@ -228,3 +237,112 @@ class ToolUsageIncrementView(APIView):
{'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})