feat: rebuild CSP practice workflow, UX and automation

这个提交包含在:
Codex CLI
2026-02-13 15:49:05 +08:00
父节点 d33deed4c5
当前提交 e2ab522b78
修改 105 个文件,包含 15669 行新增428 行删除

查看文件

@@ -0,0 +1,34 @@
#pragma once
#include <drogon/HttpController.h>
#include <cstdint>
namespace csp::controllers {
class ContestController : public drogon::HttpController<ContestController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(ContestController::list, "/api/v1/contests", drogon::Get);
ADD_METHOD_TO(ContestController::getById, "/api/v1/contests/{1}", drogon::Get);
ADD_METHOD_TO(ContestController::registerForContest, "/api/v1/contests/{1}/register", drogon::Post);
ADD_METHOD_TO(ContestController::leaderboard, "/api/v1/contests/{1}/leaderboard", drogon::Get);
METHOD_LIST_END
void list(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void getById(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id);
void registerForContest(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id);
void leaderboard(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,33 @@
#pragma once
#include <drogon/HttpController.h>
#include <cstdint>
namespace csp::controllers {
class ImportController : public drogon::HttpController<ImportController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(ImportController::latestJob, "/api/v1/import/jobs/latest", drogon::Get);
ADD_METHOD_TO(ImportController::jobById, "/api/v1/import/jobs/{1}", drogon::Get);
ADD_METHOD_TO(ImportController::jobItems, "/api/v1/import/jobs/{1}/items", drogon::Get);
ADD_METHOD_TO(ImportController::runJob, "/api/v1/import/jobs/run", drogon::Post);
METHOD_LIST_END
void latestJob(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void jobById(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t job_id);
void jobItems(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t job_id);
void runJob(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,24 @@
#pragma once
#include <drogon/HttpController.h>
#include <string>
namespace csp::controllers {
class KbController : public drogon::HttpController<KbController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(KbController::listArticles, "/api/v1/kb/articles", drogon::Get);
ADD_METHOD_TO(KbController::getArticle, "/api/v1/kb/articles/{1}", drogon::Get);
METHOD_LIST_END
void listArticles(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void getArticle(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
std::string slug);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,17 @@
#pragma once
#include <drogon/HttpController.h>
namespace csp::controllers {
class LeaderboardController : public drogon::HttpController<LeaderboardController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(LeaderboardController::global, "/api/v1/leaderboard/global", drogon::Get);
METHOD_LIST_END
void global(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,33 @@
#pragma once
#include <drogon/HttpController.h>
#include <cstdint>
namespace csp::controllers {
class MeController : public drogon::HttpController<MeController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(MeController::profile, "/api/v1/me", drogon::Get);
ADD_METHOD_TO(MeController::listWrongBook, "/api/v1/me/wrong-book", drogon::Get);
ADD_METHOD_TO(MeController::upsertWrongBookNote, "/api/v1/me/wrong-book/{1}", drogon::Patch);
ADD_METHOD_TO(MeController::deleteWrongBookItem, "/api/v1/me/wrong-book/{1}", drogon::Delete);
METHOD_LIST_END
void profile(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void listWrongBook(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void upsertWrongBookNote(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,
int64_t problem_id);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,21 @@
#pragma once
#include <drogon/HttpController.h>
namespace csp::controllers {
class MetaController : public drogon::HttpController<MetaController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(MetaController::openapi, "/api/openapi.json", drogon::Get);
ADD_METHOD_TO(MetaController::mcp, "/api/v1/mcp", drogon::Post);
METHOD_LIST_END
void openapi(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void mcp(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,44 @@
#pragma once
#include <drogon/HttpController.h>
#include <cstdint>
namespace csp::controllers {
class ProblemController : public drogon::HttpController<ProblemController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(ProblemController::list, "/api/v1/problems", drogon::Get);
ADD_METHOD_TO(ProblemController::getById, "/api/v1/problems/{1}", drogon::Get);
ADD_METHOD_TO(ProblemController::getDraft, "/api/v1/problems/{1}/draft", drogon::Get);
ADD_METHOD_TO(ProblemController::saveDraft, "/api/v1/problems/{1}/draft", drogon::Put);
ADD_METHOD_TO(ProblemController::listSolutions, "/api/v1/problems/{1}/solutions", drogon::Get);
ADD_METHOD_TO(ProblemController::generateSolutions, "/api/v1/problems/{1}/solutions/generate", drogon::Post);
METHOD_LIST_END
void list(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void getById(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id);
void getDraft(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id);
void saveDraft(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id);
void listSolutions(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id);
void generateSolutions(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,21 @@
#pragma once
#include <drogon/HttpController.h>
namespace csp::controllers {
class ProblemGenController : public drogon::HttpController<ProblemGenController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(ProblemGenController::status, "/api/v1/problem-gen/status", drogon::Get);
ADD_METHOD_TO(ProblemGenController::run, "/api/v1/problem-gen/run", drogon::Post);
METHOD_LIST_END
void status(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void run(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,33 @@
#pragma once
#include <drogon/HttpController.h>
#include <cstdint>
namespace csp::controllers {
class SubmissionController : public drogon::HttpController<SubmissionController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(SubmissionController::submitProblem, "/api/v1/problems/{1}/submit", drogon::Post);
ADD_METHOD_TO(SubmissionController::listSubmissions, "/api/v1/submissions", drogon::Get);
ADD_METHOD_TO(SubmissionController::getSubmission, "/api/v1/submissions/{1}", drogon::Get);
ADD_METHOD_TO(SubmissionController::runCpp, "/api/v1/run/cpp", drogon::Post);
METHOD_LIST_END
void submitProblem(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id);
void listSubmissions(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void getSubmission(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t submission_id);
void runCpp(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};
} // namespace csp::controllers

查看文件

@@ -33,5 +33,6 @@ class SqliteDb {
// Apply SQL migrations in order. For now we ship a single init migration.
void ApplyMigrations(SqliteDb& db);
void SeedDemoData(SqliteDb& db);
} // namespace csp::db

查看文件

@@ -33,6 +33,10 @@ struct Problem {
std::string statement_md;
int32_t difficulty = 1;
std::string source;
std::string statement_url;
std::string llm_profile_json;
std::string sample_input;
std::string sample_output;
int64_t created_at = 0;
};
@@ -64,12 +68,15 @@ struct Submission {
int64_t id = 0;
int64_t user_id = 0;
int64_t problem_id = 0;
std::optional<int64_t> contest_id;
Language language = Language::Cpp;
std::string code;
SubmissionStatus status = SubmissionStatus::Pending;
int32_t score = 0;
int32_t time_ms = 0;
int32_t memory_kb = 0;
std::string compile_log;
std::string runtime_log;
int64_t created_at = 0;
};
@@ -81,4 +88,51 @@ struct WrongBookItem {
int64_t updated_at = 0;
};
struct Contest {
int64_t id = 0;
std::string title;
int64_t starts_at = 0;
int64_t ends_at = 0;
std::string rule_json;
};
struct ContestProblem {
int64_t contest_id = 0;
int64_t problem_id = 0;
int32_t idx = 0;
};
struct ContestRegistration {
int64_t contest_id = 0;
int64_t user_id = 0;
int64_t registered_at = 0;
};
struct KbArticle {
int64_t id = 0;
std::string slug;
std::string title;
std::string content_md;
int64_t created_at = 0;
};
struct KbArticleLink {
int64_t article_id = 0;
int64_t problem_id = 0;
};
struct GlobalLeaderboardEntry {
int64_t user_id = 0;
std::string username;
int32_t rating = 0;
int64_t created_at = 0;
};
struct ContestLeaderboardEntry {
int64_t user_id = 0;
std::string username;
int32_t solved = 0;
int64_t penalty_sec = 0;
};
} // namespace csp::domain

查看文件

@@ -14,5 +14,9 @@ Json::Value ToPublicJson(const User& u);
Json::Value ToJson(const Problem& p);
Json::Value ToJson(const Submission& s);
Json::Value ToJson(const WrongBookItem& w);
Json::Value ToJson(const Contest& c);
Json::Value ToJson(const KbArticle& a);
Json::Value ToJson(const GlobalLeaderboardEntry& e);
Json::Value ToJson(const ContestLeaderboardEntry& e);
} // namespace csp::domain

查看文件

@@ -0,0 +1,36 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include "csp/domain/entities.h"
#include <cstdint>
#include <optional>
#include <vector>
namespace csp::services {
struct ContestDetail {
domain::Contest contest;
std::vector<domain::Problem> problems;
};
class ContestService {
public:
explicit ContestService(db::SqliteDb& db) : db_(db) {}
std::vector<domain::Contest> ListContests();
std::optional<domain::Contest> GetContest(int64_t contest_id);
std::vector<domain::Problem> ListContestProblems(int64_t contest_id);
void Register(int64_t contest_id, int64_t user_id);
bool IsRegistered(int64_t contest_id, int64_t user_id);
bool ContainsProblem(int64_t contest_id, int64_t problem_id);
bool IsRunning(int64_t contest_id);
std::vector<domain::ContestLeaderboardEntry> Leaderboard(int64_t contest_id);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,40 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <optional>
#include <string>
namespace csp::services {
struct ImportRunOptions {
bool clear_all_problems = false;
};
class ImportRunner {
public:
static ImportRunner& Instance();
void Configure(std::string db_path);
bool TriggerAsync(const std::string& trigger, const ImportRunOptions& options);
void AutoStartIfEnabled();
bool IsRunning() const;
std::string LastCommand() const;
std::optional<int> LastExitCode() const;
int64_t LastStartedAt() const;
int64_t LastFinishedAt() const;
private:
ImportRunner() = default;
std::string db_path_;
mutable std::mutex mu_;
bool running_ = false;
std::string last_command_;
std::optional<int> last_exit_code_;
int64_t last_started_at_ = 0;
int64_t last_finished_at_ = 0;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,61 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace csp::services {
struct ImportJob {
int64_t id = 0;
std::string status;
std::string trigger;
int32_t total_count = 0;
int32_t processed_count = 0;
int32_t success_count = 0;
int32_t failed_count = 0;
std::string options_json;
std::string last_error;
int64_t started_at = 0;
std::optional<int64_t> finished_at;
int64_t updated_at = 0;
int64_t created_at = 0;
};
struct ImportJobItem {
int64_t id = 0;
int64_t job_id = 0;
std::string source_path;
std::string status;
std::string title;
int32_t difficulty = 0;
std::optional<int64_t> problem_id;
std::string error_text;
std::optional<int64_t> started_at;
std::optional<int64_t> finished_at;
int64_t updated_at = 0;
int64_t created_at = 0;
};
struct ImportJobItemQuery {
std::string status;
int page = 1;
int page_size = 50;
};
class ImportService {
public:
explicit ImportService(db::SqliteDb& db) : db_(db) {}
std::optional<ImportJob> GetLatestJob();
std::optional<ImportJob> GetById(int64_t job_id);
std::vector<ImportJobItem> ListItems(int64_t job_id, const ImportJobItemQuery& query);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,29 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include "csp/domain/entities.h"
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace csp::services {
struct KbArticleDetail {
domain::KbArticle article;
std::vector<std::pair<int64_t, std::string>> related_problems;
};
class KbService {
public:
explicit KbService(db::SqliteDb& db) : db_(db) {}
std::vector<domain::KbArticle> ListArticles();
std::optional<KbArticleDetail> GetBySlug(const std::string& slug);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <optional>
#include <string>
namespace csp::services {
class ProblemGenRunner {
public:
static ProblemGenRunner& Instance();
void Configure(std::string db_path);
bool TriggerAsync(const std::string& trigger, int count = 1);
void AutoStartIfEnabled();
bool IsRunning() const;
std::string LastCommand() const;
std::optional<int> LastExitCode() const;
int64_t LastStartedAt() const;
int64_t LastFinishedAt() const;
private:
ProblemGenRunner() = default;
mutable std::mutex mu_;
std::string db_path_;
bool running_ = false;
std::string last_command_;
std::optional<int> last_exit_code_;
int64_t last_started_at_ = 0;
int64_t last_finished_at_ = 0;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,41 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include "csp/domain/entities.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace csp::services {
struct ProblemQuery {
std::string q;
std::string tag;
std::vector<std::string> tags;
std::string source_prefix;
int difficulty = 0;
int page = 1;
int page_size = 20;
std::string order_by = "id";
std::string order = "asc";
};
struct ProblemListResult {
std::vector<domain::Problem> items;
int total_count = 0;
};
class ProblemService {
public:
explicit ProblemService(db::SqliteDb& db) : db_(db) {}
ProblemListResult List(const ProblemQuery& query);
std::optional<domain::Problem> GetById(int64_t id);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <optional>
#include <set>
#include <string>
namespace csp::services {
class ProblemSolutionRunner {
public:
static ProblemSolutionRunner& Instance();
void Configure(std::string db_path);
bool TriggerAsync(int64_t problem_id, int64_t job_id, int max_solutions);
bool IsRunning(int64_t problem_id) const;
private:
ProblemSolutionRunner() = default;
std::string db_path_;
mutable std::mutex mu_;
std::set<int64_t> running_problem_ids_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,69 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace csp::services {
struct ProblemDraft {
std::string language;
std::string code;
std::string stdin_text;
int64_t updated_at = 0;
};
struct ProblemSolution {
int64_t id = 0;
int64_t problem_id = 0;
int variant = 1;
std::string title;
std::string idea_md;
std::string explanation_md;
std::string code_cpp;
std::string complexity;
std::string tags_json;
std::string source;
int64_t created_at = 0;
int64_t updated_at = 0;
};
struct ProblemSolutionJob {
int64_t id = 0;
int64_t problem_id = 0;
std::string status;
int progress = 0;
std::string message;
int64_t created_by = 0;
int max_solutions = 3;
int64_t created_at = 0;
std::optional<int64_t> started_at;
std::optional<int64_t> finished_at;
int64_t updated_at = 0;
};
class ProblemWorkspaceService {
public:
explicit ProblemWorkspaceService(db::SqliteDb& db) : db_(db) {}
bool ProblemExists(int64_t problem_id);
void SaveDraft(int64_t user_id,
int64_t problem_id,
const std::string& language,
const std::string& code,
const std::string& stdin_text);
std::optional<ProblemDraft> GetDraft(int64_t user_id, int64_t problem_id);
int64_t CreateSolutionJob(int64_t problem_id, int64_t created_by, int max_solutions);
std::optional<ProblemSolutionJob> GetLatestSolutionJob(int64_t problem_id);
std::vector<ProblemSolution> ListSolutions(int64_t problem_id);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,48 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include "csp/domain/entities.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace csp::services {
struct SubmissionCreateRequest {
int64_t user_id = 0;
int64_t problem_id = 0;
std::optional<int64_t> contest_id;
std::string language = "cpp";
std::string code;
};
struct RunOnlyResult {
domain::SubmissionStatus status = domain::SubmissionStatus::Unknown;
int time_ms = 0;
std::string stdout_text;
std::string stderr_text;
std::string compile_log;
};
class SubmissionService {
public:
explicit SubmissionService(db::SqliteDb& db) : db_(db) {}
domain::Submission CreateAndJudge(const SubmissionCreateRequest& req);
std::vector<domain::Submission> List(std::optional<int64_t> user_id,
std::optional<int64_t> problem_id,
std::optional<int64_t> contest_id,
int page,
int page_size);
std::optional<domain::Submission> GetById(int64_t id);
RunOnlyResult RunOnlyCpp(const std::string& code, const std::string& input);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,23 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include "csp/domain/entities.h"
#include <cstdint>
#include <optional>
#include <vector>
namespace csp::services {
class UserService {
public:
explicit UserService(db::SqliteDb& db) : db_(db) {}
std::optional<domain::User> GetById(int64_t id);
std::vector<domain::GlobalLeaderboardEntry> GlobalLeaderboard(int limit = 100);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,33 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include "csp/domain/entities.h"
#include <cstdint>
#include <string>
#include <vector>
namespace csp::services {
struct WrongBookEntry {
domain::WrongBookItem item;
std::string problem_title;
};
class WrongBookService {
public:
explicit WrongBookService(db::SqliteDb& db) : db_(db) {}
std::vector<WrongBookEntry> ListByUser(int64_t user_id);
void UpsertNote(int64_t user_id, int64_t problem_id, const std::string& note);
void UpsertBySubmission(int64_t user_id,
int64_t problem_id,
int64_t submission_id,
const std::string& note);
void Remove(int64_t user_id, int64_t problem_id);
private:
db::SqliteDb& db_;
};
} // namespace csp::services