feat(reaction): 后台表情统计接口
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ urlpatterns = [
|
|||||||
path("api/admin/", include("utils.urls")),
|
path("api/admin/", include("utils.urls")),
|
||||||
path("api/", include("message.urls.oj")),
|
path("api/", include("message.urls.oj")),
|
||||||
path("api/", include("reaction.urls.oj")),
|
path("api/", include("reaction.urls.oj")),
|
||||||
|
path("api/admin/", include("reaction.urls.admin")),
|
||||||
path("api/", include("comment.urls.oj")),
|
path("api/", include("comment.urls.oj")),
|
||||||
path("api/admin/", include("comment.urls.admin")),
|
path("api/admin/", include("comment.urls.admin")),
|
||||||
path("api/", include("tutorial.urls.tutorial")),
|
path("api/", include("tutorial.urls.tutorial")),
|
||||||
|
|||||||
7
reaction/urls/admin.py
Normal file
7
reaction/urls/admin.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from ..views.admin import ReactionStatsAPI
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("reaction", ReactionStatsAPI.as_view()),
|
||||||
|
]
|
||||||
39
reaction/views/admin.py
Normal file
39
reaction/views/admin.py
Normal file
@@ -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))
|
||||||
Reference in New Issue
Block a user