From 203a9a48d0af8b8532423b113dd85e367a794105 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Thu, 6 Aug 2026 01:40:11 -0600 Subject: [PATCH] =?UTF-8?q?feat(reaction):=20=E7=94=A8=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- oj/urls.py | 1 + reaction/urls/oj.py | 7 +++++ reaction/views/oj.py | 69 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 reaction/urls/oj.py create mode 100644 reaction/views/oj.py diff --git a/oj/urls.py b/oj/urls.py index fa77037..09ab4a6 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ path("api/admin/", include("submission.urls.admin")), path("api/admin/", include("utils.urls")), path("api/", include("message.urls.oj")), + path("api/", include("reaction.urls.oj")), path("api/", include("comment.urls.oj")), path("api/admin/", include("comment.urls.admin")), path("api/", include("tutorial.urls.tutorial")), diff --git a/reaction/urls/oj.py b/reaction/urls/oj.py new file mode 100644 index 0000000..0a4620d --- /dev/null +++ b/reaction/urls/oj.py @@ -0,0 +1,7 @@ +from django.urls import path + +from ..views.oj import ReactionAPI + +urlpatterns = [ + path("reaction", ReactionAPI.as_view()), +] diff --git a/reaction/views/oj.py b/reaction/views/oj.py new file mode 100644 index 0000000..b8246af --- /dev/null +++ b/reaction/views/oj.py @@ -0,0 +1,69 @@ +from asgiref.sync import sync_to_async +from django.db import transaction +from django.db.models import Count, Q + +from account.decorators import login_required +from problem.models import Problem +from reaction.models import Reaction, ReactionType +from reaction.serializers import SetReactionSerializer +from submission.models import JudgeStatus, Submission +from utils.api import AsyncAPIView +from utils.api.api import validate_serializer +from utils.async_helpers import async_cache_delete, async_cache_get, async_cache_set +from utils.constants import CacheKey + +ACCEPTED_RESULTS = [JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED] + + +class ReactionAPI(AsyncAPIView): + async def get_counts(self, problem_id): + """返回该题七个表情的计数,带 Redis 缓存。""" + cache_key = f"{CacheKey.reaction_stats}:{problem_id}" + cached = await async_cache_get(cache_key) + if cached is not None: + return cached + counts = await Reaction.objects.filter(problem_id=problem_id).aaggregate(**{t.value: Count("id", filter=Q(type=t.value)) for t in ReactionType}) + await async_cache_set(cache_key, counts, 3600) + return counts + + @login_required + async def get(self, request): + problem_id = request.GET.get("problem_id") + if not problem_id: + return self.error("problem_id is required") + mine = [r.type async for r in Reaction.objects.filter(user=request.user, problem_id=problem_id)] + if not mine: + return self.success({"mine": [], "counts": None}) + return self.success({"mine": mine, "counts": await self.get_counts(problem_id)}) + + @login_required + @validate_serializer(SetReactionSerializer) + async def post(self, request): + data = request.data + try: + problem = await Problem.objects.aget(id=data["problem_id"], visible=True) + except Problem.DoesNotExist: + return self.error("problem is not exists") + + solved = await Submission.objects.filter( + user_id=request.user.id, + problem_id=problem.id, + result__in=ACCEPTED_RESULTS, + ).aexists() + if not solved: + return self.error("submission is not exists or not accepted") + + types = data["types"] + user = request.user + + def overwrite(): + with transaction.atomic(): + Reaction.objects.filter(user=user, problem=problem).delete() + Reaction.objects.bulk_create([Reaction(user=user, problem=problem, type=t) for t in types]) + + await sync_to_async(overwrite)() + await async_cache_delete(f"{CacheKey.reaction_stats}:{problem.id}") + + if not types: + return self.success({"mine": [], "counts": None}) + return self.success({"mine": types, "counts": await self.get_counts(problem.id)})