From 700488a36b5b11ea68fecff2bfa97f6b58baaff3 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Thu, 6 Aug 2026 01:45:33 -0600 Subject: [PATCH] =?UTF-8?q?feat(reaction):=20=E5=90=8E=E5=8F=B0=E8=A1=A8?= =?UTF-8?q?=E6=83=85=E7=BB=9F=E8=AE=A1=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/admin.py | 7 +++++++ reaction/views/admin.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 reaction/urls/admin.py create mode 100644 reaction/views/admin.py diff --git a/oj/urls.py b/oj/urls.py index 09ab4a6..91c7538 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -16,6 +16,7 @@ urlpatterns = [ path("api/admin/", include("utils.urls")), path("api/", include("message.urls.oj")), path("api/", include("reaction.urls.oj")), + path("api/admin/", include("reaction.urls.admin")), path("api/", include("comment.urls.oj")), path("api/admin/", include("comment.urls.admin")), path("api/", include("tutorial.urls.tutorial")), diff --git a/reaction/urls/admin.py b/reaction/urls/admin.py new file mode 100644 index 0000000..9bf6fb8 --- /dev/null +++ b/reaction/urls/admin.py @@ -0,0 +1,7 @@ +from django.urls import path + +from ..views.admin import ReactionStatsAPI + +urlpatterns = [ + path("reaction", ReactionStatsAPI.as_view()), +] diff --git a/reaction/views/admin.py b/reaction/views/admin.py new file mode 100644 index 0000000..9af0285 --- /dev/null +++ b/reaction/views/admin.py @@ -0,0 +1,39 @@ +from django.db.models import Count, F, FloatField, Q +from django.db.models.functions import Cast + +from account.decorators import super_admin_required +from reaction.models import Reaction, ReactionType +from utils.api import APIView + +MIN_USERS_FOR_RATIO = 3 +DEFAULT_ORDERING = "-users" + + +class ReactionStatsAPI(APIView): + @super_admin_required + def get(self, request): + queryset = ( + Reaction.objects.values(pid=F("problem___id"), title=F("problem__title")) + .annotate(users=Count("user", distinct=True)) + .annotate(**{t.value: Count("id", filter=Q(type=t.value)) for t in ReactionType}) + .annotate(**{f"{t.value}_ratio": Cast(t.value, FloatField()) / Cast("users", FloatField()) for t in ReactionType}) + ) + + problem_id = request.GET.get("problem") + if problem_id: + queryset = queryset.filter(problem___id__iexact=problem_id, problem__contest_id__isnull=True) + + ordering = request.GET.get("ordering") or DEFAULT_ORDERING + prefix = "-" if ordering.startswith("-") else "" + field = ordering.lstrip("-") + if field == "users": + order_by = f"{prefix}users" + elif field in ReactionType.values: + # 按占比排序时过滤低样本,避免 1 人点 1 个就冲到 100% + queryset = queryset.filter(users__gte=MIN_USERS_FOR_RATIO) + order_by = f"{prefix}{field}_ratio" + else: + order_by = DEFAULT_ORDERING + + queryset = queryset.order_by(order_by) + return self.success(self.paginate_data(request, queryset))