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>
这个提交包含在:
@@ -33,7 +33,7 @@ std::vector<WrongBookEntry> WrongBookService::ListByUser(int64_t user_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT w.user_id,w.problem_id,w.last_submission_id,w.note,w.updated_at,p.title "
|
||||
"SELECT w.user_id,w.problem_id,w.last_submission_id,w.note,w.note_score,w.note_rating,w.note_feedback_md,w.note_images_json,w.note_scored_at,w.updated_at,p.title "
|
||||
"FROM wrong_book w "
|
||||
"JOIN problems p ON p.id=w.problem_id "
|
||||
"WHERE w.user_id=? ORDER BY w.updated_at DESC";
|
||||
@@ -52,8 +52,13 @@ std::vector<WrongBookEntry> WrongBookService::ListByUser(int64_t user_id) {
|
||||
e.item.last_submission_id = sqlite3_column_int64(stmt, 2);
|
||||
}
|
||||
e.item.note = ColText(stmt, 3);
|
||||
e.item.updated_at = sqlite3_column_int64(stmt, 4);
|
||||
e.problem_title = ColText(stmt, 5);
|
||||
e.item.note_score = sqlite3_column_int(stmt, 4);
|
||||
e.item.note_rating = sqlite3_column_int(stmt, 5);
|
||||
e.item.note_feedback_md = ColText(stmt, 6);
|
||||
e.item.note_images_json = ColText(stmt, 7);
|
||||
e.item.note_scored_at = sqlite3_column_int64(stmt, 8);
|
||||
e.item.updated_at = sqlite3_column_int64(stmt, 9);
|
||||
e.problem_title = ColText(stmt, 10);
|
||||
out.push_back(std::move(e));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -81,6 +86,41 @@ void WrongBookService::UpsertNote(int64_t user_id,
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void WrongBookService::UpsertNoteScore(int64_t user_id,
|
||||
int64_t problem_id,
|
||||
int32_t note_score,
|
||||
int32_t note_rating,
|
||||
const std::string& note_feedback_md) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT INTO wrong_book(user_id,problem_id,last_submission_id,note,updated_at,note_score,note_rating,note_feedback_md,note_scored_at) "
|
||||
"VALUES(?,?,?,?,?,?,?,?,?) "
|
||||
"ON CONFLICT(user_id,problem_id) DO UPDATE SET "
|
||||
"note_score=excluded.note_score,"
|
||||
"note_rating=excluded.note_rating,"
|
||||
"note_feedback_md=excluded.note_feedback_md,"
|
||||
"note_scored_at=excluded.note_scored_at,"
|
||||
"updated_at=excluded.updated_at";
|
||||
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare wrong_book upsert note score");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id");
|
||||
CheckSqlite(sqlite3_bind_null(stmt, 3), db, "bind last_submission_id");
|
||||
CheckSqlite(sqlite3_bind_text(stmt, 4, "", -1, SQLITE_TRANSIENT), db, "bind note");
|
||||
const int64_t now = NowSec();
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 5, now), db, "bind updated_at");
|
||||
CheckSqlite(sqlite3_bind_int(stmt, 6, note_score), db, "bind note_score");
|
||||
CheckSqlite(sqlite3_bind_int(stmt, 7, note_rating), db, "bind note_rating");
|
||||
CheckSqlite(sqlite3_bind_text(stmt, 8, note_feedback_md.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind note_feedback_md");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 9, now), db, "bind note_scored_at");
|
||||
CheckSqlite(sqlite3_step(stmt), db, "wrong_book upsert note score");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
|
||||
void WrongBookService::UpsertBySubmission(int64_t user_id,
|
||||
int64_t problem_id,
|
||||
int64_t submission_id,
|
||||
@@ -108,6 +148,41 @@ void WrongBookService::UpsertBySubmission(int64_t user_id,
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
std::string WrongBookService::GetNoteImagesJson(int64_t user_id, int64_t problem_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql = "SELECT note_images_json FROM wrong_book WHERE user_id=? AND problem_id=? LIMIT 1";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare wrong_book get note_images_json");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id");
|
||||
std::string out = "[]";
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
out = ColText(stmt, 0);
|
||||
if (out.empty()) out = "[]";
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
void WrongBookService::SetNoteImagesJson(int64_t user_id, int64_t problem_id, const std::string& note_images_json) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT INTO wrong_book(user_id,problem_id,last_submission_id,note,updated_at,note_images_json) "
|
||||
"VALUES(?,?,?,?,?,?) "
|
||||
"ON CONFLICT(user_id,problem_id) DO UPDATE SET note_images_json=excluded.note_images_json,updated_at=excluded.updated_at";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare wrong_book set note_images_json");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id");
|
||||
CheckSqlite(sqlite3_bind_null(stmt, 3), db, "bind last_submission_id");
|
||||
CheckSqlite(sqlite3_bind_text(stmt, 4, "", -1, SQLITE_TRANSIENT), db, "bind note");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 5, NowSec()), db, "bind updated_at");
|
||||
CheckSqlite(sqlite3_bind_text(stmt, 6, note_images_json.c_str(), -1, SQLITE_TRANSIENT), db, "bind note_images_json");
|
||||
CheckSqlite(sqlite3_step(stmt), db, "wrong_book set note_images_json");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
|
||||
void WrongBookService::Remove(int64_t user_id, int64_t problem_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
|
||||
在新工单中引用
屏蔽一个用户