From c775ae4b22bdd352d76102658a0fcf5c80196e18 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 5 Aug 2026 04:18:41 -0600 Subject: [PATCH] =?UTF-8?q?feat(problem):=20=E6=96=B0=E5=A2=9E=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E7=AE=A1=E7=90=86=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 Sonnet 5 --- problem/serializers.py | 13 +++++++++ problem/urls/admin.py | 2 ++ problem/views/admin.py | 61 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/problem/serializers.py b/problem/serializers.py index 617acc1..a4d8929 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -109,6 +109,19 @@ class TagSerializer(serializers.ModelSerializer): fields = "__all__" +class TagAdminSerializer(serializers.ModelSerializer): + problem_count = serializers.IntegerField(read_only=True) + + class Meta: + model = ProblemTag + fields = ["id", "name", "problem_count"] + + +class EditTagSerializer(serializers.Serializer): + id = serializers.IntegerField() + name = serializers.CharField(max_length=32) + + class BaseProblemSerializer(serializers.ModelSerializer): tags = serializers.SlugRelatedField(many=True, slug_field="name", read_only=True) created_by = UsernameSerializer() diff --git a/problem/urls/admin.py b/problem/urls/admin.py index c850894..56dff4e 100644 --- a/problem/urls/admin.py +++ b/problem/urls/admin.py @@ -11,6 +11,7 @@ from ..views.admin import ( SQLTestCasePreviewAPI, SQLTestCaseScriptsAPI, StuckProblemsAPI, + TagAdminAPI, TestCaseAPI, TopACTrendAPI, ) @@ -25,6 +26,7 @@ urlpatterns = [ path("problem/stuck", StuckProblemsAPI.as_view()), path("problem/top_ac_trend", TopACTrendAPI.as_view()), path("problem/flowchart", ProblemFlowchartAIGen.as_view()), + path("problem/tag", TagAdminAPI.as_view()), path("contest/problem", ContestProblemAPI.as_view()), path("contest_problem/make_public", MakeContestProblemPublicAPIView.as_view()), path("contest/add_problem_from_public", AddContestProblemAPI.as_view()), diff --git a/problem/views/admin.py b/problem/views/admin.py index 9a4b436..c8e6a69 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -19,7 +19,7 @@ from utils.api import APIError, APIView, CSRFExemptAPIView, validate_serializer from utils.openai import get_ai_client from utils.shortcuts import natural_sort_key, rand_str -from ..models import Problem +from ..models import Problem, ProblemTag from ..serializers import ( AddContestProblemSerializer, ContestProblemMakePublicSerializer, @@ -27,12 +27,14 @@ from ..serializers import ( CreateProblemSerializer, EditContestProblemSerializer, EditProblemSerializer, + EditTagSerializer, ProblemAdminListSerializer, ProblemAdminSerializer, SQLTestCasePreviewSerializer, + TagAdminSerializer, TestCaseUploadForm, ) -from ..services import resolve_tags +from ..services import clear_tag_cache, resolve_tags from ..utils import generate_sql_display @@ -497,6 +499,61 @@ class AddContestProblemAPI(APIView): return self.success() +class TagAdminAPI(APIView): + @problem_permission_required + def get(self, request): + tags = ProblemTag.objects.annotate(problem_count=Count("problem")) + keyword = request.GET.get("keyword", "").strip() + if keyword: + tags = tags.filter(name__icontains=keyword) + tags = tags.order_by("-problem_count", "name") + return self.success(TagAdminSerializer(tags, many=True).data) + + @problem_permission_required + @validate_serializer(EditTagSerializer) + def put(self, request): + data = request.data + try: + tag = ProblemTag.objects.get(id=data["id"]) + except ProblemTag.DoesNotExist: + return self.error("标签不存在,请刷新后重试") + + name = data["name"].strip() + if not name: + return self.error("标签名不能为空") + + target = ProblemTag.objects.filter(name__iexact=name).exclude(id=tag.id).first() + if target is None: + tag.name = name + tag.save() + clear_tag_cache() + return self.success({"merged": False, "id": tag.id, "name": tag.name, "affected_count": 0}) + + # 改名撞上已有标签,视为合并:题目关系转移过去,原标签删除 + affected_count = 0 + for problem in Problem.objects.filter(tags=tag): + problem.tags.add(target) + problem.tags.remove(tag) + affected_count += 1 + tag.delete() + clear_tag_cache() + return self.success({"merged": True, "id": target.id, "name": target.name, "affected_count": affected_count}) + + @problem_permission_required + def delete(self, request): + tag_id = request.GET.get("id") + if not tag_id: + return self.error("Invalid parameter, id is required") + try: + tag = ProblemTag.objects.get(id=tag_id) + except ProblemTag.DoesNotExist: + return self.error("标签不存在,请刷新后重试") + # 删除标签行的同时,Django 会级联清掉 problem_tags 中间表里的关系 + tag.delete() + clear_tag_cache() + return self.success() + + class ProblemVisibleAPI(APIView): @problem_permission_required def put(self, request):