feat: problems local stats, user status, admin panel enhancements, rating text

- Problems page: replace Luogu pass rate with local submission stats
  (local_submit_count, local_ac_count)
- Problems page: add user AC/fail status column (user_ac, user_fail_count)
- Admin users: add total_submissions and total_ac columns
- Admin users: add detail panel with submissions/rating/redeem tabs
- Admin: new endpoint GET /api/v1/admin/users/{id}/rating-history
- Rating history: note field includes problem title via JOIN
- Me page: translate task codes to friendly labels with icons
- Me page: problem links in rating history are clickable
- Wrong book service, learning note scoring, note image controller
- Backend SQL uses batch queries for performance

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
这个提交包含在:
cryptocommuniums-afk
2026-02-16 17:35:22 +08:00
父节点 7860414ae5
当前提交 cfbe9a0363
修改 22 个文件,包含 1366 行新增26 行删除

查看文件

@@ -362,6 +362,10 @@ CREATE TABLE IF NOT EXISTS wrong_book (
problem_id INTEGER NOT NULL,
last_submission_id INTEGER,
note TEXT NOT NULL DEFAULT "",
note_score INTEGER NOT NULL DEFAULT 0,
note_rating INTEGER NOT NULL DEFAULT 0,
note_feedback_md TEXT NOT NULL DEFAULT "",
note_scored_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL,
PRIMARY KEY(user_id, problem_id),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
@@ -577,6 +581,11 @@ CREATE TABLE IF NOT EXISTS daily_task_logs (
EnsureColumn(db, "problem_drafts", "stdin", "stdin TEXT NOT NULL DEFAULT ''");
EnsureColumn(db, "problem_solution_jobs", "max_solutions",
"max_solutions INTEGER NOT NULL DEFAULT 3");
EnsureColumn(db, "wrong_book", "note_score", "note_score INTEGER NOT NULL DEFAULT 0");
EnsureColumn(db, "wrong_book", "note_rating", "note_rating INTEGER NOT NULL DEFAULT 0");
EnsureColumn(db, "wrong_book", "note_feedback_md", "note_feedback_md TEXT NOT NULL DEFAULT ''");
EnsureColumn(db, "wrong_book", "note_scored_at", "note_scored_at INTEGER NOT NULL DEFAULT 0");
EnsureColumn(db, "wrong_book", "note_images_json", "note_images_json TEXT NOT NULL DEFAULT '[]'");
EnsureColumn(db, "problem_solutions", "variant", "variant INTEGER NOT NULL DEFAULT 1");
EnsureColumn(db, "problem_solutions", "idea_md", "idea_md TEXT NOT NULL DEFAULT ''");
EnsureColumn(db, "problem_solutions", "explanation_md",
@@ -727,6 +736,154 @@ void SeedDemoData(SqliteDb& db) {
0,
now);
}
// Always seed C++基础课程任务(幂等:按 slug 检测存在)。
{
const auto existing = QueryOneId(raw, "SELECT id FROM problems WHERE slug='cpp-basic-01-hello' LIMIT 1");
if (!existing.has_value()) {
const int64_t created = now;
struct CourseItem {
const char* slug;
const char* title;
int diff;
const char* source;
const char* md;
const char* tags[6];
};
const CourseItem items[] = {
{
"cpp-basic-01-hello",
"C++基础01环境配置与Hello WorldVSCode",
1,
"course:cpp-basic:01",
R"MD(# C++基础01环境配置与Hello WorldVSCode
## 学习目标
- VSCode C++14
-
- `main()``#include <iostream>``cout`
## 推荐视频(观看后写笔记)
- VSCode + mingw64 C/C++BV1tg411N7Fq
- https://www.bilibili.com/video/BV1tg411N7Fq/
- C++01 BV1dK4y137bk
- https://www.bilibili.com/video/BV1dK4y137bk/
## 参考图文
- LoongBa GCC/VSCode/HelloWorld
- https://github.com/LoongBa/Cpp_Beginner_Guide
## 练习(完成至少 2 题)
- B2002 Hello, World! https://www.luogu.com.cn/problem/B2002
- P1000 https://www.luogu.com.cn/problem/P1000
## 提交要求
-
1)
2) HelloWorld
3) 3
)MD",
{"cpp-basic", "vscode", "io", "", "", ""}
},
{
"cpp-basic-02-io",
"C++基础02输入输出与变量",
1,
"course:cpp-basic:02",
R"MD(# C++基础02输入输出与变量
## 学习目标
- `cin` `cout`
- `int / long long / double / char / string`
## 推荐视频
- C++02 03
- https://www.bilibili.com/video/BV1dK4y137bk/
## 练习
- P1001 A+B Problem https://www.luogu.com.cn/problem/P1001
- B2008 (a+b)×c https://www.luogu.com.cn/problem/B2008
- P5704 https://www.luogu.com.cn/problem/P5704
## 提交要求
- / `cin/cout` /
)MD",
{"cpp-basic", "io", "types", "", "", ""}
},
{
"cpp-basic-03-branch",
"C++基础03分支结构if / switch",
2,
"course:cpp-basic:03",
R"MD(# C++基础03分支结构if / switch
## 学习目标
- `if / else if / else`
-
## 练习
- B2035 https://www.luogu.com.cn/problem/B2035
- P5711 https://www.luogu.com.cn/problem/P5711
- P1909 https://www.luogu.com.cn/problem/P1909
## 提交要求
- 0/
)MD",
{"cpp-basic", "branch", "logic", "", "", ""}
},
{
"cpp-basic-04-loop",
"C++基础04循环结构for / while",
2,
"course:cpp-basic:04",
R"MD(# C++基础04循环结构for / while
## 学习目标
-
## 练习
- B2083 https://www.luogu.com.cn/problem/B2083
- P1421 https://www.luogu.com.cn/problem/P1421
## 提交要求
- //+
)MD",
{"cpp-basic", "loop", "debug", "", "", ""}
},
{
"cpp-basic-05-array",
"C++基础05数组入门一维",
3,
"course:cpp-basic:05",
R"MD(# C++基础05数组入门一维
## 学习目标
-
## 练习
- P1427 https://www.luogu.com.cn/problem/P1427
- P1428 https://www.luogu.com.cn/problem/P1428
## 提交要求
- 0/1
)MD",
{"cpp-basic", "array", "", "", "", ""}
}
};
for (const auto& it : items) {
InsertProblem(raw, it.slug, it.title, it.md, it.diff, it.source, "", "", created);
const auto pid = QueryOneId(raw, std::string("SELECT id FROM problems WHERE slug='") + it.slug + "' LIMIT 1");
if (pid.has_value()) {
for (const char* tag : it.tags) {
if (!tag || !*tag) continue;
InsertProblemTag(raw, *pid, tag);
}
}
}
}
}
}
} // namespace csp::db