# 挑战分数系统 Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 让挑战评分真正可见可用——激活 Profile 总分、新增排行榜/我的成绩页面、挑战卡片显示最高得分、管理员可导出 CSV。 **Architecture:** 后端激活 `Profile.total_score` 自动重算逻辑,在 `account` 和 `submission` 路由各新增 2 个端点;前端新增两个独立页面(排行榜、我的成绩),并在挑战卡片和提交列表页做小改动。 **Tech Stack:** Django + django-ninja(后端);Vue 3 + TypeScript + Naive UI(前端);`uv run` 执行 Django 命令,`npm run start` 启动前端 --- ### Task 1: 激活 Profile 总分自动重算 **Files:** - Modify: `api/account/models.py` - Modify: `api/submission/models.py` **Step 1: 替换 update_total_score 为 recalculate_total_score** 打开 `api/account/models.py`,将 `Profile.update_total_score` 方法替换为: ```python def recalculate_total_score(self): from django.db.models import Max, Sum from submission.models import Submission total = ( Submission.objects .filter(user=self.user, task__task_type="challenge", score__gt=0) .values("task_id") .annotate(best=Max("score")) .aggregate(total=Sum("best"))["total"] ) or 0.0 self.total_score = total self.save(update_fields=["total_score"]) ``` **Step 2: 在 Rating post_save 信号里追加调用** 打开 `api/submission/models.py`,找到 `update_submission_score_on_save` 函数,在 `instance.submission.update_score()` 之后追加: ```python instance.submission.user.profile.recalculate_total_score() ``` 完整信号函数变为: ```python @receiver(post_save, sender=Rating) def update_submission_score_on_save(sender, instance, **kwargs): instance.submission.update_score() instance.submission.user.profile.recalculate_total_score() ``` **Step 3: 验证** ```bash cd api uv run manage.py check ``` 期望:`System check identified no issues (0 silenced).` **Step 4: Commit** ```bash git add api/account/models.py api/submission/models.py git commit -m "feat: activate Profile.total_score auto-recalculation on rating" ``` --- ### Task 2: Challenge 模型添加 pass_score 字段 **Files:** - Modify: `api/task/models.py` **Step 1: 添加字段** 打开 `api/task/models.py`,在 `Challenge` 类的 `score = models.IntegerField(default=0)` 后追加: ```python pass_score = models.FloatField(null=True, blank=True, verbose_name="通过分数线") ``` **Step 2: 生成并应用迁移** ```bash cd api uv run manage.py makemigrations task uv run manage.py migrate ``` 期望:迁移成功,无报错。 **Step 3: Commit** ```bash git add api/task/migrations/ api/task/models.py git commit -m "feat: add pass_score field to Challenge for future unlock mechanism" ``` --- ### Task 3: 排行榜 API **Files:** - Modify: `api/account/schemas.py` - Modify: `api/account/api.py` **Step 1: 添加 LeaderboardEntry schema** 打开 `api/account/schemas.py`,在文件末尾追加: ```python class LeaderboardEntry(Schema): rank: int username: str total_score: float ``` **Step 2: 添加端点** 打开 `api/account/api.py`,在文件末尾追加: ```python from typing import List as TypingList @router.get("/leaderboard", response=TypingList["LeaderboardEntry"]) def leaderboard(request): """ 排行榜:所有用户按总分降序 """ from .schemas import LeaderboardEntry from .models import Profile profiles = ( Profile.objects .select_related("user") .filter(total_score__gt=0) .order_by("-total_score") ) return [ LeaderboardEntry(rank=i + 1, username=p.user.username, total_score=p.total_score) for i, p in enumerate(profiles) ] ``` 注意:`LeaderboardEntry` 需要在 `api/account/schemas.py` 中导入,在 `api/account/api.py` 顶部的 import 处补充: ```python from .schemas import ( BatchUsersIn, UserListSchema, UserRegistrationSchema, UserLoginSchema, LeaderboardEntry, ) ``` 并将端点函数里的局部 import 去掉。 **Step 3: 验证** ```bash cd api uv run manage.py check ``` **Step 4: Commit** ```bash git add api/account/schemas.py api/account/api.py git commit -m "feat: add leaderboard API endpoint" ``` --- ### Task 4: 我的成绩 API **Files:** - Modify: `api/submission/schemas.py` - Modify: `api/submission/api.py` **Step 1: 添加 MyScoreOut schema** 打开 `api/submission/schemas.py`,末尾追加: ```python class MyScoreOut(Schema): task_id: int task_display: int task_title: str score: float created: str ``` **Step 2: 添加端点** 打开 `api/submission/api.py`,末尾追加(注意顶部要导入 `MyScoreOut`): 在顶部 imports 的 schemas 导入里加 `MyScoreOut`: ```python from .schemas import ( FlagIn, SubmissionFilter, SubmissionIn, SubmissionOut, RatingScoreIn, MyScoreOut, ) ``` 末尾追加端点: ```python from django.db.models import Max @router.get("/my-scores", response=List[MyScoreOut]) @login_required def my_scores(request): """ 当前用户每道挑战的最高分提交 """ from django.db.models import OuterRef, Subquery # 取每个 task 的最高分提交 best_submissions = ( Submission.objects .filter(user=request.user, task__task_type="challenge") .order_by("task_id", "-score") .distinct("task_id") .select_related("task") ) return [ MyScoreOut( task_id=s.task_id, task_display=s.task.display, task_title=s.task.title, score=s.score, created=s.created.isoformat(), ) for s in best_submissions ] ``` > **注意**:`distinct("task_id")` 是 PostgreSQL 专用语法(`DISTINCT ON`)。开发环境使用 SQLite 时这行会报错。需用 Python 端去重替代: > > ```python > seen = {} > for s in Submission.objects.filter( > user=request.user, task__task_type="challenge" > ).order_by("-score").select_related("task"): > if s.task_id not in seen: > seen[s.task_id] = s > best_submissions = seen.values() > ``` > > 根据实际运行环境选择。开发用 SQLite 请用 Python 端去重版本。 **Step 3: 验证** ```bash cd api uv run manage.py check ``` **Step 4: Commit** ```bash git add api/submission/schemas.py api/submission/api.py git commit -m "feat: add my-scores API endpoint" ``` --- ### Task 5: CSV 导出 API **Files:** - Modify: `api/submission/api.py` **Step 1: 添加端点** 在 `api/submission/api.py` 末尾追加: ```python import csv from django.http import StreamingHttpResponse @router.get("/export") @login_required def export_submissions(request, filters: SubmissionFilter = Query(...)): """ 导出提交成绩 CSV(仅管理员/超级管理员) """ if request.user.role not in (RoleChoices.SUPER, RoleChoices.ADMIN): raise HttpError(403, "没有权限") from django.db.models import Max qs = ( Submission.objects .filter(task__task_type="challenge") .select_related("task", "user") ) if filters.task_id: qs = qs.filter(task_id=filters.task_id) if filters.username: qs = qs.filter(user__username__icontains=filters.username) # 每个用户每道题取最高分(Python 端聚合,兼容 SQLite) best: dict[tuple, dict] = {} for s in qs.order_by("-score"): key = (s.user.username, s.task_id) if key not in best: best[key] = { "username": s.user.username, "task_display": s.task.display, "task_title": s.task.title, "score": s.score, "created": s.created.isoformat(), } def rows(): yield "用户名,题号,标题,最高分,提交时间\n" for r in best.values(): yield f"{r['username']},{r['task_display']},{r['task_title']},{r['score']:.2f},{r['created']}\n" response = StreamingHttpResponse(rows(), content_type="text/csv; charset=utf-8") response["Content-Disposition"] = 'attachment; filename="submissions.csv"' return response ``` **Step 2: 验证** ```bash cd api uv run manage.py check ``` **Step 3: Commit** ```bash git add api/submission/api.py git commit -m "feat: add CSV export API for admin" ``` --- ### Task 6: 前端 API 方法 **Files:** - Modify: `client/src/api.ts` **Step 1: 添加 Account.leaderboard** 在 `api.ts` 的 `Account` 对象里,`batchCreate` 方法之后追加: ```typescript async leaderboard() { const res = await http.get("/account/leaderboard") return res.data as { rank: number; username: string; total_score: number }[] }, ``` **Step 2: 添加 Submission.myScores 和 Submission.export** 在 `api.ts` 的 `Submission` 对象里,`updateFlag` 之后追加: ```typescript async myScores() { const res = await http.get("/submission/my-scores") return res.data as { task_id: number task_display: number task_title: string score: number created: string }[] }, async exportCsv(params: { task_id?: number; username?: string }) { const res = await http.get("/submission/export", { params, responseType: "blob", }) return res.data as Blob }, ``` **Step 3: Commit** ```bash git add client/src/api.ts git commit -m "feat: add leaderboard, myScores, exportCsv API methods" ``` --- ### Task 7: 排行榜页面和路由 **Files:** - Create: `client/src/pages/Leaderboard.vue` - Modify: `client/src/router.ts` **Step 1: 创建 Leaderboard.vue** ```vue ``` **Step 2: 添加路由** 打开 `client/src/router.ts`,在 `routes` 数组里(submission 路由之后)追加: ```typescript { path: "/leaderboard", name: "leaderboard", component: () => import("./pages/Leaderboard.vue"), }, ``` **Step 3: Commit** ```bash git add client/src/pages/Leaderboard.vue client/src/router.ts git commit -m "feat: add leaderboard page and route" ``` --- ### Task 8: 我的成绩页面和路由 **Files:** - Create: `client/src/pages/MyScores.vue` - Modify: `client/src/router.ts` **Step 1: 创建 MyScores.vue** ```vue ``` **Step 2: 添加路由(需要登录)** 在 `client/src/router.ts` 里追加: ```typescript { path: "/my-scores", name: "my-scores", component: () => import("./pages/MyScores.vue"), meta: { auth: true }, }, ``` **Step 3: Commit** ```bash git add client/src/pages/MyScores.vue client/src/router.ts git commit -m "feat: add my-scores page and route" ``` --- ### Task 9: 挑战卡片显示用户最高得分 **Files:** - Modify: `client/src/components/Challenge.vue` **Step 1: 加载 my-scores 并映射** 打开 `client/src/components/Challenge.vue`,在 `