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 行删除

查看文件

@@ -27,6 +27,9 @@ class AdminController : public drogon::HttpController<AdminController> {
ADD_METHOD_TO(AdminController::listRedeemRecords,
"/api/v1/admin/redeem-records",
drogon::Get);
ADD_METHOD_TO(AdminController::userRatingHistory,
"/api/v1/admin/users/{1}/rating-history",
drogon::Get);
METHOD_LIST_END
void listUsers(const drogon::HttpRequestPtr& req,
@@ -55,6 +58,10 @@ class AdminController : public drogon::HttpController<AdminController> {
void listRedeemRecords(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void userRatingHistory(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t user_id);
};
} // namespace csp::controllers

查看文件

@@ -22,6 +22,12 @@ public:
drogon::Get);
ADD_METHOD_TO(MeController::upsertWrongBookNote, "/api/v1/me/wrong-book/{1}",
drogon::Patch);
ADD_METHOD_TO(MeController::scoreWrongBookNote, "/api/v1/me/wrong-book/{1}/note-score",
drogon::Post);
ADD_METHOD_TO(MeController::uploadWrongBookNoteImages, "/api/v1/me/wrong-book/{1}/note-images",
drogon::Post);
ADD_METHOD_TO(MeController::deleteWrongBookNoteImage, "/api/v1/me/wrong-book/{1}/note-images",
drogon::Delete);
ADD_METHOD_TO(MeController::deleteWrongBookItem, "/api/v1/me/wrong-book/{1}",
drogon::Delete);
ADD_METHOD_TO(MeController::listRatingHistory, "/api/v1/me/rating-history",
@@ -55,6 +61,18 @@ public:
std::function<void(const drogon::HttpResponsePtr &)> &&cb,
int64_t problem_id);
void scoreWrongBookNote(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb,
int64_t problem_id);
void uploadWrongBookNoteImages(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb,
int64_t problem_id);
void deleteWrongBookNoteImage(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb,
int64_t problem_id);
void
deleteWrongBookItem(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb,

查看文件

@@ -0,0 +1,18 @@
#pragma once
#include <drogon/HttpController.h>
namespace csp::controllers {
class NoteImageController : public drogon::HttpController<NoteImageController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(NoteImageController::getNoteImage, "/files/note-images/{1}", drogon::Get);
METHOD_LIST_END
void getNoteImage(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
const std::string& filename);
};
} // namespace csp::controllers

查看文件

@@ -86,6 +86,11 @@ struct WrongBookItem {
int64_t problem_id = 0;
std::optional<int64_t> last_submission_id;
std::string note;
int32_t note_score = 0;
int32_t note_rating = 0;
std::string note_feedback_md;
std::string note_images_json; // JSON array of filenames
int64_t note_scored_at = 0;
int64_t updated_at = 0;
};
@@ -127,6 +132,8 @@ struct GlobalLeaderboardEntry {
std::string username;
int32_t rating = 0;
int64_t created_at = 0;
int32_t total_submissions = 0;
int32_t total_ac = 0;
};
struct ContestLeaderboardEntry {

查看文件

@@ -0,0 +1,29 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include "csp/domain/entities.h"
#include <cstdint>
#include <string>
namespace csp::services {
struct LearningNoteScoreResult {
int32_t score = 0;
int32_t rating = 0;
std::string feedback_md;
std::string model_name;
};
class LearningNoteScoringService {
public:
explicit LearningNoteScoringService(db::SqliteDb& db) : db_(db) {}
LearningNoteScoreResult Score(const std::string& note,
const domain::Problem& problem);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -20,6 +20,13 @@ class WrongBookService {
std::vector<WrongBookEntry> ListByUser(int64_t user_id);
void UpsertNote(int64_t user_id, int64_t problem_id, const std::string& note);
std::string GetNoteImagesJson(int64_t user_id, int64_t problem_id);
void SetNoteImagesJson(int64_t user_id, int64_t problem_id, const std::string& note_images_json);
void UpsertNoteScore(int64_t user_id,
int64_t problem_id,
int32_t note_score,
int32_t note_rating,
const std::string& note_feedback_md);
void UpsertBySubmission(int64_t user_id,
int64_t problem_id,
int64_t submission_id,