# 挑战分数系统 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`,在 `