Files
OnlineJudge/docs/superpowers/specs/2026-05-11-problem-yearly-ac-rate-design.md

125 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Problem Yearly AC Rate — Design Spec
**Date:** 2026-05-11
**Status:** Approved
---
## Overview
Add a per-problem "historical AC rate by year" feature. A new backend API returns yearly submission statistics for a given problem; the frontend displays this as a line chart on the problem stats page, alongside the existing submission-result pie chart.
---
## Backend
### Endpoint
```
GET /api/problem/yearly_ac?problem_id=<_id>
```
- Public (user-facing), no authentication required
- `problem_id`: the problem's display ID (`Problem._id`, string)
### Query Logic
```python
Submission.objects.filter(
problem_id=problem.id,
contest_id__isnull=True, # exclude contest submissions
result__not_in=[JudgeStatus.PENDING, JudgeStatus.JUDGING], # exclude in-flight submissions
)
.annotate(year=ExtractYear('create_time'))
.values('year')
.annotate(
total=Count('id'),
accepted=Count('id', filter=Q(result=JudgeStatus.ACCEPTED))
)
.order_by('year')
```
### Response Format
```json
{
"error": null,
"data": [
{"year": 2023, "total": 120, "accepted": 54, "ac_rate": 45.0},
{"year": 2024, "total": 88, "accepted": 31, "ac_rate": 35.23}
]
}
```
- `ac_rate`: float, two decimal places, percentage (0100)
- If `total == 0` for a year: `ac_rate = 0` (guards against division by zero)
- Empty list returned if no judged submissions exist
### Error Cases
| Condition | Response |
|---|---|
| `problem_id` missing | `error("problem_id is required")` |
| Problem not found / not visible | `error("Problem does not exist")` |
### Caching
- Redis cache key: `problem_yearly_ac:{problem._id}`
- TTL: 3600 seconds (1 hour), consistent with other problem caches
### Implementation Location
- View class: `ProblemYearlyACRateAPI` in `OnlineJudge/problem/views/oj.py`
- URL: `path("problem/yearly_ac", ProblemYearlyACRateAPI.as_view())` in `OnlineJudge/problem/urls/oj.py`
---
## Frontend
### New Component
`ojnext/src/oj/problem/components/ProblemYearlyChart.vue`
- Uses `vue-chartjs` Line chart (same library as `oj/contest/components/LineChart.vue`)
- Registers: `CategoryScale`, `LinearScale`, `PointElement`, `LineElement`, `Title`, `Tooltip`, `Filler`
- Props: `data: Array<{ year: number; total: number; accepted: number; ac_rate: number }>`
- X-axis: year (string labels)
- Y-axis: AC rate 0100%
- Tooltip per point: year, AC rate, accepted/total counts
- Shows nothing (v-if) when data is empty or has only one point
### Chart Options
- `responsive: true`, `maintainAspectRatio: false`, height `250px`
- Dataset: `fill: true` (area chart feel), single line
- Y-axis: `min: 0`, `max: 100`, tick suffix `%`
### API Call
Add to `ojnext/src/oj/api.ts`:
```ts
export function getProblemYearlyAC(problemId: string) {
return http.get<Array<{ year: number; total: number; accepted: number; ac_rate: number }>>(
"/problem/yearly_ac",
{ params: { problem_id: problemId } }
)
}
```
### Integration
In `ojnext/src/oj/problem/components/ProblemInfo.vue`:
- Import `getProblemYearlyAC` and call it on mount (alongside existing `getProblemBeatRate`)
- Render `<ProblemYearlyChart>` below the existing pie chart, with a section title "历年 AC 率"
- No loading spinner needed (chart simply absent until data arrives)
---
## Assumptions
- Contest submissions excluded; they represent a different access context
- PENDING (6) and JUDGING (7) excluded; PARTIALLY_ACCEPTED (8) and SYSTEM_ERROR (5) are included as final verdicts
- No migrations needed (query only, no model changes)