feat: ADRF async views (phase1) + native async serializers (phase2) + async cache infra
This commit is contained in:
+30
-27
@@ -1,20 +1,23 @@
|
||||
from rest_framework import serializers
|
||||
from adrf.serializers import (
|
||||
ModelSerializer, Serializer, CharField, ChoiceField, FileField,
|
||||
IntegerField, BooleanField, ListField, SerializerMethodField,
|
||||
)
|
||||
from .models import CompressionHistory, ToolCategory, Tool, ColorHistory
|
||||
|
||||
|
||||
class ToolCategorySerializer(serializers.ModelSerializer):
|
||||
tool_count = serializers.SerializerMethodField()
|
||||
class ToolCategorySerializer(ModelSerializer):
|
||||
tool_count = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ToolCategory
|
||||
fields = ['id', 'name', 'icon', 'sort_order', 'tool_count', 'created_at']
|
||||
|
||||
def get_tool_count(self, obj):
|
||||
return obj.tools.filter(is_enabled=True).count()
|
||||
async def get_tool_count(self, obj):
|
||||
return await obj.tools.filter(is_enabled=True).acount()
|
||||
|
||||
|
||||
class ToolSerializer(serializers.ModelSerializer):
|
||||
category_name = serializers.CharField(source='category.name', read_only=True, default='')
|
||||
class ToolSerializer(ModelSerializer):
|
||||
category_name = CharField(source='category.name', read_only=True, default='')
|
||||
|
||||
class Meta:
|
||||
model = Tool
|
||||
@@ -26,10 +29,10 @@ class ToolSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
class CompressionHistorySerializer(serializers.ModelSerializer):
|
||||
original_size_display = serializers.SerializerMethodField()
|
||||
compressed_size_display = serializers.SerializerMethodField()
|
||||
created_at_display = serializers.SerializerMethodField()
|
||||
class CompressionHistorySerializer(ModelSerializer):
|
||||
original_size_display = SerializerMethodField()
|
||||
compressed_size_display = SerializerMethodField()
|
||||
created_at_display = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CompressionHistory
|
||||
@@ -41,13 +44,13 @@ class CompressionHistorySerializer(serializers.ModelSerializer):
|
||||
]
|
||||
read_only_fields = ['id', 'created_at']
|
||||
|
||||
def get_original_size_display(self, obj):
|
||||
async def get_original_size_display(self, obj):
|
||||
return self._format_size(obj.original_size)
|
||||
|
||||
def get_compressed_size_display(self, obj):
|
||||
async def get_compressed_size_display(self, obj):
|
||||
return self._format_size(obj.compressed_size)
|
||||
|
||||
def get_created_at_display(self, obj):
|
||||
async def get_created_at_display(self, obj):
|
||||
return obj.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
@staticmethod
|
||||
@@ -60,29 +63,29 @@ class CompressionHistorySerializer(serializers.ModelSerializer):
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
|
||||
|
||||
class ImageCompressRequestSerializer(serializers.Serializer):
|
||||
file = serializers.FileField()
|
||||
mode = serializers.ChoiceField(choices=['lossy', 'lossless'], default='lossy')
|
||||
quality = serializers.IntegerField(min_value=1, max_value=100, default=80)
|
||||
format = serializers.ChoiceField(
|
||||
class ImageCompressRequestSerializer(Serializer):
|
||||
file = FileField()
|
||||
mode = ChoiceField(choices=['lossy', 'lossless'], default='lossy')
|
||||
quality = IntegerField(min_value=1, max_value=100, default=80)
|
||||
format = ChoiceField(
|
||||
choices=['jpeg', 'png', 'webp', 'avif', 'original'],
|
||||
default='original'
|
||||
)
|
||||
keep_exif = serializers.BooleanField(default=True)
|
||||
width = serializers.IntegerField(required=False, min_value=1, max_value=10000)
|
||||
height = serializers.IntegerField(required=False, min_value=1, max_value=10000)
|
||||
maintain_aspect_ratio = serializers.BooleanField(default=True)
|
||||
keep_exif = BooleanField(default=True)
|
||||
width = IntegerField(required=False, min_value=1, max_value=10000)
|
||||
height = IntegerField(required=False, min_value=1, max_value=10000)
|
||||
maintain_aspect_ratio = BooleanField(default=True)
|
||||
|
||||
|
||||
class ColorHistorySerializer(serializers.ModelSerializer):
|
||||
class ColorHistorySerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = ColorHistory
|
||||
fields = ['id', 'color', 'created_at']
|
||||
read_only_fields = ['id', 'created_at']
|
||||
|
||||
|
||||
class ColorHistorySyncSerializer(serializers.Serializer):
|
||||
colors = serializers.ListField(
|
||||
child=serializers.CharField(max_length=7),
|
||||
class ColorHistorySyncSerializer(Serializer):
|
||||
colors = ListField(
|
||||
child=CharField(max_length=7),
|
||||
help_text='颜色HEX值列表,如 ["#FF5733", "#33FF57"]'
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import re
|
||||
from rest_framework.views import APIView
|
||||
from adrf.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
@@ -17,17 +17,17 @@ def validate_hex_color(color):
|
||||
class ColorHistoryListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
days = int(request.query_params.get('days', 30))
|
||||
cutoff = timezone.now() - timedelta(days=days)
|
||||
histories = ColorHistory.objects.filter(
|
||||
histories = [h async for h in ColorHistory.objects.filter(
|
||||
user=request.user,
|
||||
created_at__gte=cutoff
|
||||
).order_by('-created_at')[:50]
|
||||
).order_by('-created_at')[:50]]
|
||||
serializer = ColorHistorySerializer(histories, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
color = request.data.get('color', '')
|
||||
if not validate_hex_color(color):
|
||||
return Response(
|
||||
@@ -35,16 +35,16 @@ class ColorHistoryListView(APIView):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
color = color.upper()
|
||||
existing = ColorHistory.objects.filter(
|
||||
existing = await ColorHistory.objects.filter(
|
||||
user=request.user,
|
||||
color=color
|
||||
).first()
|
||||
).afirst()
|
||||
if existing:
|
||||
existing.created_at = timezone.now()
|
||||
existing.save()
|
||||
await existing.asave()
|
||||
serializer = ColorHistorySerializer(existing)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
history = ColorHistory.objects.create(
|
||||
history = await ColorHistory.objects.acreate(
|
||||
user=request.user,
|
||||
color=color
|
||||
)
|
||||
@@ -55,10 +55,10 @@ class ColorHistoryListView(APIView):
|
||||
class ColorHistoryDetailView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def delete(self, request, pk):
|
||||
async def delete(self, request, pk):
|
||||
try:
|
||||
history = ColorHistory.objects.get(pk=pk, user=request.user)
|
||||
history.delete()
|
||||
history = await ColorHistory.objects.aget(pk=pk, user=request.user)
|
||||
await history.adelete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except ColorHistory.DoesNotExist:
|
||||
return Response(
|
||||
@@ -70,34 +70,32 @@ class ColorHistoryDetailView(APIView):
|
||||
class ColorHistoryClearView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
deleted_count, _ = ColorHistory.objects.filter(user=request.user).delete()
|
||||
async def post(self, request):
|
||||
deleted_count, _ = await ColorHistory.objects.filter(user=request.user).adelete()
|
||||
return Response({'deleted': deleted_count})
|
||||
|
||||
|
||||
class ColorHistorySyncView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
serializer = ColorHistorySyncSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
colors = serializer.validated_data['colors']
|
||||
valid_colors = [c.upper() for c in colors if validate_hex_color(c)]
|
||||
existing_colors = set(
|
||||
ColorHistory.objects.filter(
|
||||
existing_colors = set([
|
||||
c async for c in ColorHistory.objects.filter(
|
||||
user=request.user,
|
||||
color__in=valid_colors
|
||||
).values_list('color', flat=True)
|
||||
)
|
||||
])
|
||||
new_colors = [c for c in valid_colors if c not in existing_colors]
|
||||
now = timezone.now()
|
||||
for color in new_colors:
|
||||
ColorHistory.objects.create(
|
||||
user=request.user,
|
||||
color=color,
|
||||
created_at=now
|
||||
)
|
||||
await ColorHistory.objects.abulk_create([
|
||||
ColorHistory(user=request.user, color=color, created_at=now)
|
||||
for color in new_colors
|
||||
])
|
||||
return Response({
|
||||
'synced': len(new_colors),
|
||||
'duplicates': len(valid_colors) - len(new_colors),
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from django.http import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
from adrf.views import APIView
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import permission_classes
|
||||
from rest_framework.permissions import AllowAny
|
||||
from asgiref.sync import sync_to_async
|
||||
from ..models import CompressionHistory
|
||||
from ..serializers import CompressionHistorySerializer
|
||||
|
||||
|
||||
@permission_classes([AllowAny])
|
||||
class CompressionHistoryListView(APIView):
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
user = request.user if request.user.is_authenticated else None
|
||||
session_id = request.query_params.get('session_id')
|
||||
|
||||
@@ -29,8 +30,8 @@ class CompressionHistoryListView(APIView):
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
|
||||
total = histories.count()
|
||||
serializer = CompressionHistorySerializer(histories[start:end], many=True)
|
||||
total = await histories.acount()
|
||||
serializer = CompressionHistorySerializer([h async for h in histories[start:end]], many=True)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
@@ -40,7 +41,7 @@ class CompressionHistoryListView(APIView):
|
||||
'page_size': page_size
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
data = request.data.copy()
|
||||
|
||||
if request.user.is_authenticated:
|
||||
@@ -48,7 +49,8 @@ class CompressionHistoryListView(APIView):
|
||||
|
||||
serializer = CompressionHistorySerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
# DRF 序列化器 save() 为同步 ORM 操作,sync_to_async 兜底
|
||||
await sync_to_async(serializer.save)()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'data': serializer.data
|
||||
@@ -62,22 +64,22 @@ class CompressionHistoryListView(APIView):
|
||||
|
||||
@permission_classes([AllowAny])
|
||||
class CompressionHistoryDetailView(APIView):
|
||||
def delete(self, request, pk):
|
||||
async def delete(self, request, pk):
|
||||
user = request.user if request.user.is_authenticated else None
|
||||
session_id = request.query_params.get('session_id')
|
||||
|
||||
try:
|
||||
if user:
|
||||
history = CompressionHistory.objects.get(pk=pk, user=user)
|
||||
history = await CompressionHistory.objects.aget(pk=pk, user=user)
|
||||
elif session_id:
|
||||
history = CompressionHistory.objects.get(pk=pk, session_id=session_id)
|
||||
history = await CompressionHistory.objects.aget(pk=pk, session_id=session_id)
|
||||
else:
|
||||
return JsonResponse(
|
||||
{'error': '无权限删除'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
history.delete()
|
||||
await history.adelete()
|
||||
return JsonResponse({'success': True})
|
||||
except CompressionHistory.DoesNotExist:
|
||||
return JsonResponse(
|
||||
@@ -88,14 +90,14 @@ class CompressionHistoryDetailView(APIView):
|
||||
|
||||
@permission_classes([AllowAny])
|
||||
class CompressionHistoryClearView(APIView):
|
||||
def delete(self, request):
|
||||
async def delete(self, request):
|
||||
user = request.user if request.user.is_authenticated else None
|
||||
session_id = request.query_params.get('session_id')
|
||||
|
||||
if user:
|
||||
count, _ = CompressionHistory.objects.filter(user=user).delete()
|
||||
count, _ = await CompressionHistory.objects.filter(user=user).adelete()
|
||||
elif session_id:
|
||||
count, _ = CompressionHistory.objects.filter(session_id=session_id).delete()
|
||||
count, _ = await CompressionHistory.objects.filter(session_id=session_id).adelete()
|
||||
else:
|
||||
return JsonResponse(
|
||||
{'error': '无权限清空'},
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from PIL import Image
|
||||
from PIL.ExifTags import TAGS
|
||||
from django.http import JsonResponse, FileResponse
|
||||
from rest_framework.views import APIView
|
||||
from adrf.views import APIView
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import permission_classes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import difflib
|
||||
from rest_framework.decorators import permission_classes
|
||||
from rest_framework.views import APIView
|
||||
from adrf.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.http import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
from adrf.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.parsers import JSONParser
|
||||
from django.db import transaction
|
||||
from asgiref.sync import sync_to_async
|
||||
from ..models import Tool, ToolFavorite
|
||||
|
||||
|
||||
@@ -10,60 +10,75 @@ class ToolFavoriteToggleView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, tool_id):
|
||||
try:
|
||||
tool = Tool.objects.get(pk=tool_id, is_enabled=True)
|
||||
except Tool.DoesNotExist:
|
||||
async def post(self, request, tool_id):
|
||||
# 事务段(get_or_create + delete + count)整体放入同步函数,sync_to_async 执行保证原子性
|
||||
def _toggle_in_txn():
|
||||
from django.db import transaction
|
||||
with transaction.atomic():
|
||||
try:
|
||||
tool = Tool.objects.get(pk=tool_id, is_enabled=True)
|
||||
except Tool.DoesNotExist:
|
||||
return None
|
||||
favorite, created = ToolFavorite.objects.get_or_create(
|
||||
user=request.user,
|
||||
tool=tool
|
||||
)
|
||||
if not created:
|
||||
favorite.delete()
|
||||
is_favorite = False
|
||||
message = '已取消收藏'
|
||||
else:
|
||||
is_favorite = True
|
||||
message = '已添加收藏'
|
||||
favorite_count = ToolFavorite.objects.filter(tool=tool).count()
|
||||
return {
|
||||
'tool_id': tool.id,
|
||||
'is_favorite': is_favorite,
|
||||
'favorite_count': favorite_count,
|
||||
'message': message,
|
||||
}
|
||||
|
||||
result = await sync_to_async(_toggle_in_txn)()
|
||||
if result is None:
|
||||
return JsonResponse({'success': False, 'error': '工具不存在'}, status=404)
|
||||
|
||||
favorite, created = ToolFavorite.objects.get_or_create(
|
||||
user=request.user,
|
||||
tool=tool
|
||||
)
|
||||
if not created:
|
||||
favorite.delete()
|
||||
is_favorite = False
|
||||
message = '已取消收藏'
|
||||
else:
|
||||
is_favorite = True
|
||||
message = '已添加收藏'
|
||||
|
||||
favorite_count = ToolFavorite.objects.filter(tool=tool).count()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'data': {
|
||||
'id': tool.id,
|
||||
'is_favorite': is_favorite,
|
||||
'favorite_count': favorite_count
|
||||
'id': result['tool_id'],
|
||||
'is_favorite': result['is_favorite'],
|
||||
'favorite_count': result['favorite_count']
|
||||
},
|
||||
'message': message
|
||||
'message': result['message']
|
||||
})
|
||||
|
||||
|
||||
class ToolFavoriteListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
favorites = ToolFavorite.objects.select_related('tool', 'tool__category').filter(
|
||||
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')
|
||||
).order_by('-created_at')]
|
||||
|
||||
data = [{
|
||||
'id': fav.tool.id,
|
||||
'name': fav.tool.name,
|
||||
'description': fav.tool.description,
|
||||
'icon': fav.tool.icon,
|
||||
'url_path': fav.tool.url_path,
|
||||
'color': fav.tool.color,
|
||||
'category': fav.tool.category.id if fav.tool.category else None,
|
||||
'category_name': fav.tool.category.name if fav.tool.category else '',
|
||||
'is_favorite': True,
|
||||
'favorite_count': ToolFavorite.objects.filter(tool=fav.tool).count(),
|
||||
'created_at': fav.created_at.isoformat()
|
||||
} for fav in favorites]
|
||||
data = []
|
||||
for fav in favorites:
|
||||
favorite_count = await ToolFavorite.objects.filter(tool=fav.tool).acount()
|
||||
data.append({
|
||||
'id': fav.tool.id,
|
||||
'name': fav.tool.name,
|
||||
'description': fav.tool.description,
|
||||
'icon': fav.tool.icon,
|
||||
'url_path': fav.tool.url_path,
|
||||
'color': fav.tool.color,
|
||||
'category': fav.tool.category.id if fav.tool.category else None,
|
||||
'category_name': fav.tool.category.name if fav.tool.category else '',
|
||||
'is_favorite': True,
|
||||
'favorite_count': favorite_count,
|
||||
'created_at': fav.created_at.isoformat()
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
@@ -77,13 +92,13 @@ class ToolFavoriteListView(APIView):
|
||||
class ToolFavoriteStatusView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, tool_id):
|
||||
is_favorite = ToolFavorite.objects.filter(
|
||||
async def get(self, request, tool_id):
|
||||
is_favorite = await ToolFavorite.objects.filter(
|
||||
user=request.user,
|
||||
tool_id=tool_id
|
||||
).exists()
|
||||
).aexists()
|
||||
|
||||
favorite_count = ToolFavorite.objects.filter(tool_id=tool_id).count()
|
||||
favorite_count = await ToolFavorite.objects.filter(tool_id=tool_id).acount()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.http import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
from adrf.views import APIView
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from django.db.models import Count, Q
|
||||
@@ -19,8 +19,8 @@ class ToolCategoryListView(APIView):
|
||||
tags=['工具'],
|
||||
responses={200: success_response}
|
||||
)
|
||||
def get(self, request):
|
||||
categories = ToolCategory.objects.prefetch_related('tools').all()
|
||||
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,
|
||||
@@ -42,7 +42,7 @@ class ToolListView(APIView):
|
||||
],
|
||||
responses={200: success_response}
|
||||
)
|
||||
def get(self, request):
|
||||
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', '')
|
||||
@@ -68,18 +68,19 @@ class ToolListView(APIView):
|
||||
else:
|
||||
tools = tools.order_by('sort_order', '-created_at')
|
||||
|
||||
serializer = ToolSerializer(tools, many=True)
|
||||
tool_list = [t async for t in tools]
|
||||
serializer = ToolSerializer(tool_list, many=True)
|
||||
data = serializer.data
|
||||
|
||||
# 构建 favorites_count 映射
|
||||
favorites_count_map = {
|
||||
t.id: t.annotated_favorites_count for t in tools
|
||||
t.id: t.annotated_favorites_count for t in tool_list
|
||||
}
|
||||
|
||||
if request.user.is_authenticated:
|
||||
favorite_tool_ids = set(
|
||||
ToolFavorite.objects.filter(user=request.user).values_list('tool_id', flat=True)
|
||||
)
|
||||
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)
|
||||
@@ -106,18 +107,18 @@ class ToolDetailView(APIView):
|
||||
],
|
||||
responses={200: success_response, 404: not_found_response}
|
||||
)
|
||||
def get(self, request, pk):
|
||||
async def get(self, request, pk):
|
||||
try:
|
||||
tool = Tool.objects.select_related('category').get(pk=pk, is_enabled=True)
|
||||
tool = await Tool.objects.select_related('category').aget(pk=pk, is_enabled=True)
|
||||
serializer = ToolSerializer(tool)
|
||||
data = serializer.data
|
||||
if request.user.is_authenticated:
|
||||
data['is_favorited'] = ToolFavorite.objects.filter(
|
||||
data['is_favorited'] = await ToolFavorite.objects.filter(
|
||||
user=request.user, tool=tool
|
||||
).exists()
|
||||
).aexists()
|
||||
else:
|
||||
data['is_favorited'] = False
|
||||
data['favorites_count'] = ToolFavorite.objects.filter(tool=tool).count()
|
||||
data['favorites_count'] = await ToolFavorite.objects.filter(tool=tool).acount()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'data': data
|
||||
@@ -141,20 +142,20 @@ class ToolFavoriteToggleView(APIView):
|
||||
],
|
||||
responses={200: success_response, 401: unauthorized_response, 404: not_found_response}
|
||||
)
|
||||
def post(self, request, pk):
|
||||
tool = Tool.objects.filter(pk=pk, is_enabled=True).first()
|
||||
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 = ToolFavorite.objects.get_or_create(user=request.user, tool=tool)
|
||||
fav, created = await ToolFavorite.objects.aget_or_create(user=request.user, tool=tool)
|
||||
if not created:
|
||||
fav.delete()
|
||||
await fav.adelete()
|
||||
is_favorited = False
|
||||
else:
|
||||
is_favorited = True
|
||||
favorites_count = ToolFavorite.objects.filter(tool=tool).count()
|
||||
favorites_count = await ToolFavorite.objects.filter(tool=tool).acount()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'data': {
|
||||
@@ -174,17 +175,19 @@ class ToolFavoriteListView(APIView):
|
||||
tags=['工具'],
|
||||
responses={200: success_response, 401: unauthorized_response}
|
||||
)
|
||||
def get(self, request):
|
||||
favorites = ToolFavorite.objects.select_related('tool', 'tool__category').filter(
|
||||
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')
|
||||
).order_by('-created_at')]
|
||||
|
||||
result = []
|
||||
for fav in favorites:
|
||||
tool_data = ToolSerializer(fav.tool).data
|
||||
tool_data['is_favorited'] = True
|
||||
tool_data['favorites_count'] = ToolFavorite.objects.filter(tool=fav.tool).count()
|
||||
tool_data['favorites_count'] = await ToolFavorite.objects.filter(tool=fav.tool).acount()
|
||||
result.append(tool_data)
|
||||
|
||||
return JsonResponse({
|
||||
@@ -206,13 +209,13 @@ class ToolUsageIncrementView(APIView):
|
||||
],
|
||||
responses={200: success_response, 404: not_found_response}
|
||||
)
|
||||
def post(self, request, pk):
|
||||
async def post(self, request, pk):
|
||||
try:
|
||||
tool = Tool.objects.get(pk=pk, is_enabled=True)
|
||||
tool = await Tool.objects.aget(pk=pk, is_enabled=True)
|
||||
# 使用F()表达式避免竞态条件
|
||||
from django.db.models import F
|
||||
Tool.objects.filter(pk=pk).update(usage_count=F('usage_count') + 1)
|
||||
tool.refresh_from_db()
|
||||
await Tool.objects.filter(pk=pk).aupdate(usage_count=F('usage_count') + 1)
|
||||
await tool.arefresh_from_db()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'data': {
|
||||
|
||||
Reference in New Issue
Block a user