fix
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
> 2026-08-06 规则更新:当前实现已改为单选、点击即提交、提交后不可修改,并通过 `(problem, user)` 数据库唯一约束保证一人一题一条。新接口使用单值 `type` / `mine_type`;为支持前后端错序部署,过渡期仍接受单元素 `types` 并返回数组 `mine`。统计直接查询数据库,不再使用 reaction 缓存。本文中的多选及缓存步骤是早期实施记录。
|
> 2026-08-06 规则更新:当前实现已改为单选、点击即提交、提交后不可修改,并通过 `(problem, user)` 数据库唯一约束保证一人一题一条。新接口使用单值 `type` / `mine_type`;为支持前后端错序部署,过渡期仍接受单元素 `types` 并返回数组 `mine`。统计直接查询数据库,不再使用 reaction 缓存。本文中的多选及缓存步骤是早期实施记录。
|
||||||
|
|
||||||
|
> 2026-08-06 后台形态变更:Task 3(管理接口 `/api/admin/reaction`)与 Task 6(后台反馈统计页)的产物已整体删除。后台反馈现在只是**题目列表的一列**:显示票数最高的那个表情图标,tooltip 出「标签名 + 人数」,无评价留空,并列取 `ReactionType` 定义序靠前的一个。数据由 `GET /api/admin/problem` 的 `top_reaction` 字段下发,聚合逻辑在 `reaction/services.py:get_top_reactions()`。详见 spec 顶部同日说明。
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
**Goal:** 把题目点评从「三维评分 + 文字」重写为「一排七个表情按钮,点击即表态」,并把后台从逐条评论管理改为按题目聚合的反馈统计表。
|
**Goal:** 把题目点评从「三维评分 + 文字」重写为「一排七个表情按钮,点击即表态」,并把后台从逐条评论管理改为按题目聚合的反馈统计表。
|
||||||
@@ -1090,6 +1092,8 @@ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>"
|
|||||||
|
|
||||||
实现全部完成并通过终审。以下是评审过程中判定「可以带着合并」的次要项,都不阻塞上线,记在这里免得日后重新发现一遍。
|
实现全部完成并通过终审。以下是评审过程中判定「可以带着合并」的次要项,都不阻塞上线,记在这里免得日后重新发现一遍。
|
||||||
|
|
||||||
|
其中涉及后台统计页与统计缓存的四条(题号筛选返回空结果、缓存快照少算一票、`ReactionStatsRow` 缺 `_ratio`、后台筛选不重置页码与 `listStats` 无序号守卫)随后台页面下线与缓存移除一并作废,代码已不存在,保留仅作记录。
|
||||||
|
|
||||||
**后端**
|
**后端**
|
||||||
|
|
||||||
- `problem_id` 传非数字(如 `?problem_id=abc`)会走兜底 except 返回泛化 500 并打一条 stack trace。前端始终传数字,实际影响只是日志噪音。与原 `comment/views/oj.py` 的写法一致。
|
- `problem_id` 传非数字(如 `?problem_id=abc`)会走兜底 except 返回泛化 500 并打一条 stack trace。前端始终传数字,实际影响只是日志噪音。与原 `comment/views/oj.py` 的写法一致。
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
> 2026-08-06 规则更新:评价已改为单选,点击一个表情后立即提交,且提交后不可修改。一人一题在数据库中最多保留一条记录。新请求字段为 `type`,新响应字段为 `mine_type: ReactionKey | null`;为支持前后端错序部署,过渡期仍接受单元素 `types` 并返回数组 `mine`。计数直接查询数据库,不使用 Redis 缓存。本文后续关于“最多选 3 个、可取消、整份覆盖、统计缓存”的内容仅保留为早期设计记录,不再代表当前行为。
|
> 2026-08-06 规则更新:评价已改为单选,点击一个表情后立即提交,且提交后不可修改。一人一题在数据库中最多保留一条记录。新请求字段为 `type`,新响应字段为 `mine_type: ReactionKey | null`;为支持前后端错序部署,过渡期仍接受单元素 `types` 并返回数组 `mine`。计数直接查询数据库,不使用 Redis 缓存。本文后续关于“最多选 3 个、可取消、整份覆盖、统计缓存”的内容仅保留为早期设计记录,不再代表当前行为。
|
||||||
|
|
||||||
|
> 2026-08-06 后台形态变更:独立的后台反馈统计页与 `GET /api/admin/reaction` 已全部下线(`reaction/views/admin.py`、`reaction/urls/admin.py`、`src/admin/reaction/list.vue`、路由与菜单项均已删除)。取而代之,**后台题目列表**(`src/admin/problem/list.vue`)多一列「反馈」,只显示该题票数最高的那一个表情图标,tooltip 出「标签名 + 人数」;无人评价则留空,并列第一时按 `ReactionType` 的定义顺序取靠前的一个。数据来自 `GET /api/admin/problem` 新增的 `top_reaction: {type, count} | null` 字段,由 `reaction/services.py:get_top_reactions()` 对当页题目做一次聚合查询算出(比赛题目列表不返回该字段,前端也不显示该列)。本文「管理接口」「后台页面」两节仅为早期设计记录,占比排序、七列计数表、按题号筛选均已不存在。
|
||||||
|
|
||||||
日期:2026-08-06
|
日期:2026-08-06
|
||||||
涉及仓库:`OnlineJudge`(后端)、`ojnext`(前端)
|
涉及仓库:`OnlineJudge`(后端)、`ojnext`(前端)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ 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("tutorial.urls.tutorial")),
|
path("api/", include("tutorial.urls.tutorial")),
|
||||||
path("api/admin/", include("tutorial.urls.admin")),
|
path("api/admin/", include("tutorial.urls.admin")),
|
||||||
path("api/", include("ai.urls.oj")),
|
path("api/", include("ai.urls.oj")),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from django.http import StreamingHttpResponse
|
|||||||
from account.decorators import ensure_created_by, problem_permission_required, teacher_admin_required
|
from account.decorators import ensure_created_by, problem_permission_required, teacher_admin_required
|
||||||
from contest.models import Contest, ContestStatus
|
from contest.models import Contest, ContestStatus
|
||||||
from judge.sql_runner import SQLCaseError, build_display
|
from judge.sql_runner import SQLCaseError, build_display
|
||||||
|
from reaction.services import get_top_reactions
|
||||||
from submission.models import Submission
|
from submission.models import Submission
|
||||||
from utils.api import APIError, APIView, CSRFExemptAPIView, validate_serializer
|
from utils.api import APIError, APIView, CSRFExemptAPIView, validate_serializer
|
||||||
from utils.openai import get_ai_client
|
from utils.openai import get_ai_client
|
||||||
@@ -283,7 +284,12 @@ class ProblemAPI(ProblemBase):
|
|||||||
|
|
||||||
if not user.can_mgmt_all_problem():
|
if not user.can_mgmt_all_problem():
|
||||||
problems = problems.filter(created_by=user)
|
problems = problems.filter(created_by=user)
|
||||||
return self.success(self.paginate_data(request, problems, ProblemAdminListSerializer))
|
|
||||||
|
data = self.paginate_data(request, problems, ProblemAdminListSerializer)
|
||||||
|
top_reactions = get_top_reactions([row["id"] for row in data["results"]])
|
||||||
|
for row in data["results"]:
|
||||||
|
row["top_reaction"] = top_reactions.get(row["id"])
|
||||||
|
return self.success(data)
|
||||||
|
|
||||||
@problem_permission_required
|
@problem_permission_required
|
||||||
@validate_serializer(EditProblemSerializer)
|
@validate_serializer(EditProblemSerializer)
|
||||||
|
|||||||
19
reaction/services.py
Normal file
19
reaction/services.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
from reaction.models import Reaction, ReactionType
|
||||||
|
|
||||||
|
# 并列票数时按 ReactionType 的定义顺序取靠前的那个,跟前端 REACTIONS 的顺序一致
|
||||||
|
TYPE_ORDER = {t.value: index for index, t in enumerate(ReactionType)}
|
||||||
|
|
||||||
|
|
||||||
|
def get_top_reactions(problem_ids):
|
||||||
|
"""返回 {problem_id: {"type": ..., "count": ...}},只包含有评价的题目。"""
|
||||||
|
if not problem_ids:
|
||||||
|
return {}
|
||||||
|
top = {}
|
||||||
|
rows = Reaction.objects.filter(problem_id__in=problem_ids).values("problem_id", "type").annotate(count=Count("id"))
|
||||||
|
for row in rows:
|
||||||
|
current = top.get(row["problem_id"])
|
||||||
|
if current is None or (-row["count"], TYPE_ORDER[row["type"]]) < (-current["count"], TYPE_ORDER[current["type"]]):
|
||||||
|
top[row["problem_id"]] = {"type": row["type"], "count": row["count"]}
|
||||||
|
return top
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
|
|
||||||
from ..views.admin import ReactionStatsAPI
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("reaction", ReactionStatsAPI.as_view()),
|
|
||||||
]
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
from django.db.models import Count, F, Max, Q
|
|
||||||
|
|
||||||
from account.decorators import super_admin_required
|
|
||||||
from reaction.models import Reaction, ReactionType
|
|
||||||
from utils.api import APIView
|
|
||||||
|
|
||||||
|
|
||||||
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(last_time=Max("create_time"))
|
|
||||||
.annotate(**{t.value: Count("id", filter=Q(type=t.value)) for t in ReactionType})
|
|
||||||
.order_by("-users")
|
|
||||||
)
|
|
||||||
|
|
||||||
problem_id = request.GET.get("problem")
|
|
||||||
if problem_id:
|
|
||||||
queryset = queryset.filter(problem___id__iexact=problem_id, problem__contest_id__isnull=True)
|
|
||||||
|
|
||||||
data = self.paginate_data(request, queryset)
|
|
||||||
data["results"] = list(data["results"])
|
|
||||||
return self.success(data)
|
|
||||||
Reference in New Issue
Block a user