feat: rebuild CSP practice workflow, UX and automation
这个提交包含在:
@@ -14,6 +14,17 @@ add_library(csp_core
|
||||
src/app_state.cc
|
||||
src/services/crypto.cc
|
||||
src/services/auth_service.cc
|
||||
src/services/problem_service.cc
|
||||
src/services/user_service.cc
|
||||
src/services/wrong_book_service.cc
|
||||
src/services/kb_service.cc
|
||||
src/services/contest_service.cc
|
||||
src/services/submission_service.cc
|
||||
src/services/problem_workspace_service.cc
|
||||
src/services/problem_solution_runner.cc
|
||||
src/services/problem_gen_runner.cc
|
||||
src/services/import_service.cc
|
||||
src/services/import_runner.cc
|
||||
src/domain/enum_strings.cc
|
||||
src/domain/json.cc
|
||||
)
|
||||
@@ -30,6 +41,15 @@ target_link_libraries(csp_core PUBLIC
|
||||
|
||||
add_library(csp_web
|
||||
src/controllers/auth_controller.cc
|
||||
src/controllers/problem_controller.cc
|
||||
src/controllers/submission_controller.cc
|
||||
src/controllers/me_controller.cc
|
||||
src/controllers/contest_controller.cc
|
||||
src/controllers/leaderboard_controller.cc
|
||||
src/controllers/kb_controller.cc
|
||||
src/controllers/import_controller.cc
|
||||
src/controllers/meta_controller.cc
|
||||
src/controllers/problem_gen_controller.cc
|
||||
src/health_controller.cc
|
||||
)
|
||||
|
||||
@@ -53,7 +73,7 @@ target_include_directories(csp_server PRIVATE
|
||||
target_link_libraries(csp_server PRIVATE
|
||||
Drogon::Drogon
|
||||
csp_core
|
||||
csp_web
|
||||
"$<LINK_LIBRARY:WHOLE_ARCHIVE,csp_web>"
|
||||
)
|
||||
|
||||
enable_testing()
|
||||
@@ -64,6 +84,18 @@ add_executable(csp_tests
|
||||
tests/auth_service_test.cc
|
||||
tests/auth_http_test.cc
|
||||
tests/domain_test.cc
|
||||
tests/problem_service_test.cc
|
||||
tests/kb_service_test.cc
|
||||
tests/contest_service_test.cc
|
||||
tests/submission_service_test.cc
|
||||
tests/me_http_test.cc
|
||||
tests/problem_http_test.cc
|
||||
tests/problem_workspace_service_test.cc
|
||||
tests/problem_workspace_http_test.cc
|
||||
tests/contest_http_test.cc
|
||||
tests/submission_http_test.cc
|
||||
tests/import_service_test.cc
|
||||
tests/import_http_test.cc
|
||||
)
|
||||
|
||||
target_include_directories(csp_tests PRIVATE
|
||||
@@ -74,7 +106,7 @@ target_link_libraries(csp_tests PRIVATE
|
||||
Catch2::Catch2WithMain
|
||||
Drogon::Drogon
|
||||
csp_core
|
||||
csp_web
|
||||
"$<LINK_LIBRARY:WHOLE_ARCHIVE,csp_web>"
|
||||
)
|
||||
|
||||
include(CTest)
|
||||
|
||||
@@ -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
|
||||
@@ -24,7 +24,11 @@ CREATE TABLE IF NOT EXISTS problems (
|
||||
title TEXT NOT NULL,
|
||||
statement_md TEXT NOT NULL,
|
||||
difficulty INTEGER NOT NULL DEFAULT 1,
|
||||
source TEXT NOT NULL DEFAULT "",
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
statement_url TEXT NOT NULL DEFAULT '',
|
||||
llm_profile_json TEXT NOT NULL DEFAULT '{}',
|
||||
sample_input TEXT NOT NULL DEFAULT '',
|
||||
sample_output TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
@@ -39,22 +43,26 @@ CREATE TABLE IF NOT EXISTS submissions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
problem_id INTEGER NOT NULL,
|
||||
contest_id INTEGER,
|
||||
language TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
score INTEGER NOT NULL DEFAULT 0,
|
||||
time_ms INTEGER NOT NULL DEFAULT 0,
|
||||
memory_kb INTEGER NOT NULL DEFAULT 0,
|
||||
compile_log TEXT NOT NULL DEFAULT '',
|
||||
runtime_log TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wrong_book (
|
||||
user_id INTEGER NOT NULL,
|
||||
problem_id INTEGER NOT NULL,
|
||||
last_submission_id INTEGER,
|
||||
note TEXT NOT NULL DEFAULT "",
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY(user_id, problem_id),
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -62,5 +70,133 @@ CREATE TABLE IF NOT EXISTS wrong_book (
|
||||
FOREIGN KEY(last_submission_id) REFERENCES submissions(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
starts_at INTEGER NOT NULL,
|
||||
ends_at INTEGER NOT NULL,
|
||||
rule_json TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contest_problems (
|
||||
contest_id INTEGER NOT NULL,
|
||||
problem_id INTEGER NOT NULL,
|
||||
idx INTEGER NOT NULL,
|
||||
PRIMARY KEY(contest_id, problem_id),
|
||||
FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contest_registrations (
|
||||
contest_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
registered_at INTEGER NOT NULL,
|
||||
PRIMARY KEY(contest_id, user_id),
|
||||
FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_articles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
content_md TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_article_links (
|
||||
article_id INTEGER NOT NULL,
|
||||
problem_id INTEGER NOT NULL,
|
||||
PRIMARY KEY(article_id, problem_id),
|
||||
FOREIGN KEY(article_id) REFERENCES kb_articles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS import_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
status TEXT NOT NULL,
|
||||
trigger TEXT NOT NULL DEFAULT 'manual',
|
||||
total_count INTEGER NOT NULL DEFAULT 0,
|
||||
processed_count INTEGER NOT NULL DEFAULT 0,
|
||||
success_count INTEGER NOT NULL DEFAULT 0,
|
||||
failed_count INTEGER NOT NULL DEFAULT 0,
|
||||
options_json TEXT NOT NULL DEFAULT '{}',
|
||||
last_error TEXT NOT NULL DEFAULT '',
|
||||
started_at INTEGER NOT NULL,
|
||||
finished_at INTEGER,
|
||||
updated_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS import_job_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id INTEGER NOT NULL,
|
||||
source_path TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'queued',
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
difficulty INTEGER NOT NULL DEFAULT 0,
|
||||
problem_id INTEGER,
|
||||
error_text TEXT NOT NULL DEFAULT '',
|
||||
started_at INTEGER,
|
||||
finished_at INTEGER,
|
||||
updated_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(job_id) REFERENCES import_jobs(id) ON DELETE CASCADE,
|
||||
UNIQUE(job_id, source_path)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problem_drafts (
|
||||
user_id INTEGER NOT NULL,
|
||||
problem_id INTEGER NOT NULL,
|
||||
language TEXT NOT NULL DEFAULT 'cpp',
|
||||
code TEXT NOT NULL DEFAULT '',
|
||||
stdin TEXT NOT NULL DEFAULT '',
|
||||
updated_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY(user_id, problem_id),
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problem_solution_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
problem_id INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'queued',
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
message TEXT NOT NULL DEFAULT '',
|
||||
created_by INTEGER NOT NULL DEFAULT 0,
|
||||
max_solutions INTEGER NOT NULL DEFAULT 3,
|
||||
created_at INTEGER NOT NULL,
|
||||
started_at INTEGER,
|
||||
finished_at INTEGER,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problem_solutions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
problem_id INTEGER NOT NULL,
|
||||
variant INTEGER NOT NULL DEFAULT 1,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
idea_md TEXT NOT NULL DEFAULT '',
|
||||
explanation_md TEXT NOT NULL DEFAULT '',
|
||||
code_cpp TEXT NOT NULL DEFAULT '',
|
||||
complexity TEXT NOT NULL DEFAULT '',
|
||||
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||
source TEXT NOT NULL DEFAULT 'llm',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_user_created_at ON submissions(user_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_problem_created_at ON submissions(problem_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_contest_user_created_at ON submissions(contest_id, user_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_problem_tags_tag ON problem_tags(tag);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_article_links_problem_id ON kb_article_links(problem_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_import_jobs_created_at ON import_jobs(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_import_jobs_status ON import_jobs(status, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_import_job_items_job_status ON import_job_items(job_id, status, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_problem_drafts_updated ON problem_drafts(updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_problem_solution_jobs_problem ON problem_solution_jobs(problem_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_problem_solutions_problem ON problem_solutions(problem_id, variant, id);
|
||||
|
||||
@@ -16,6 +16,7 @@ void AppState::Init(const std::string& sqlite_path) {
|
||||
db_ = std::make_unique<db::SqliteDb>(db::SqliteDb::OpenFile(sqlite_path));
|
||||
}
|
||||
csp::db::ApplyMigrations(*db_);
|
||||
csp::db::SeedDemoData(*db_);
|
||||
}
|
||||
|
||||
csp::db::SqliteDb& AppState::db() {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "csp/controllers/contest_controller.h"
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/domain/json.h"
|
||||
#include "csp/services/contest_service.h"
|
||||
#include "http_auth.h"
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
|
||||
const std::string& msg) {
|
||||
Json::Value j;
|
||||
j["ok"] = false;
|
||||
j["error"] = msg;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(code);
|
||||
return resp;
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
|
||||
Json::Value j;
|
||||
j["ok"] = true;
|
||||
j["data"] = data;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(drogon::k200OK);
|
||||
return resp;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ContestController::list(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
services::ContestService svc(csp::AppState::Instance().db());
|
||||
const auto contests = svc.ListContests();
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& c : contests) arr.append(domain::ToJson(c));
|
||||
cb(JsonOk(arr));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ContestController::getById(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t contest_id) {
|
||||
try {
|
||||
services::ContestService svc(csp::AppState::Instance().db());
|
||||
const auto contest = svc.GetContest(contest_id);
|
||||
if (!contest.has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "contest not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
Json::Value data;
|
||||
data["contest"] = domain::ToJson(*contest);
|
||||
|
||||
Json::Value problems(Json::arrayValue);
|
||||
for (const auto& p : svc.ListContestProblems(contest_id)) {
|
||||
problems.append(domain::ToJson(p));
|
||||
}
|
||||
data["problems"] = problems;
|
||||
|
||||
std::string auth_error;
|
||||
const auto user_id = GetAuthedUserId(req, auth_error);
|
||||
if (user_id.has_value()) {
|
||||
data["registered"] = svc.IsRegistered(contest_id, *user_id);
|
||||
}
|
||||
|
||||
cb(JsonOk(data));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ContestController::registerForContest(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t contest_id) {
|
||||
try {
|
||||
std::string auth_error;
|
||||
const auto user_id = GetAuthedUserId(req, auth_error);
|
||||
if (!user_id.has_value()) {
|
||||
cb(JsonError(drogon::k401Unauthorized, auth_error));
|
||||
return;
|
||||
}
|
||||
|
||||
services::ContestService svc(csp::AppState::Instance().db());
|
||||
if (!svc.GetContest(contest_id).has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "contest not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
svc.Register(contest_id, *user_id);
|
||||
|
||||
Json::Value data;
|
||||
data["contest_id"] = Json::Int64(contest_id);
|
||||
data["user_id"] = Json::Int64(*user_id);
|
||||
data["registered"] = true;
|
||||
cb(JsonOk(data));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ContestController::leaderboard(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t contest_id) {
|
||||
try {
|
||||
services::ContestService svc(csp::AppState::Instance().db());
|
||||
if (!svc.GetContest(contest_id).has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "contest not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto rows = svc.Leaderboard(contest_id);
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& r : rows) arr.append(domain::ToJson(r));
|
||||
cb(JsonOk(arr));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/services/auth_service.h"
|
||||
|
||||
#include <drogon/HttpRequest.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
inline std::optional<int64_t> GetAuthedUserId(const drogon::HttpRequestPtr& req,
|
||||
std::string& error) {
|
||||
const std::string authz = req->getHeader("Authorization");
|
||||
const std::string prefix = "Bearer ";
|
||||
if (authz.rfind(prefix, 0) != 0) {
|
||||
error = "missing Authorization: Bearer <token>";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::string token = authz.substr(prefix.size());
|
||||
if (token.empty()) {
|
||||
error = "empty bearer token";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
services::AuthService auth(csp::AppState::Instance().db());
|
||||
const auto user_id = auth.VerifyToken(token);
|
||||
if (!user_id.has_value()) {
|
||||
error = "invalid or expired token";
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<int64_t>(*user_id);
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -0,0 +1,190 @@
|
||||
#include "csp/controllers/import_controller.h"
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/services/import_runner.h"
|
||||
#include "csp/services/import_service.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
|
||||
const std::string& msg) {
|
||||
Json::Value j;
|
||||
j["ok"] = false;
|
||||
j["error"] = msg;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(code);
|
||||
return resp;
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
|
||||
Json::Value j;
|
||||
j["ok"] = true;
|
||||
j["data"] = data;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(drogon::k200OK);
|
||||
return resp;
|
||||
}
|
||||
|
||||
int ParsePositiveInt(const std::string& s,
|
||||
int default_value,
|
||||
int min_value,
|
||||
int max_value) {
|
||||
if (s.empty()) return default_value;
|
||||
const int v = std::stoi(s);
|
||||
return std::max(min_value, std::min(max_value, v));
|
||||
}
|
||||
|
||||
Json::Value ToJson(const services::ImportJob& job) {
|
||||
Json::Value j;
|
||||
j["id"] = Json::Int64(job.id);
|
||||
j["status"] = job.status;
|
||||
j["trigger"] = job.trigger;
|
||||
j["total_count"] = job.total_count;
|
||||
j["processed_count"] = job.processed_count;
|
||||
j["success_count"] = job.success_count;
|
||||
j["failed_count"] = job.failed_count;
|
||||
j["options_json"] = job.options_json;
|
||||
j["last_error"] = job.last_error;
|
||||
j["started_at"] = Json::Int64(job.started_at);
|
||||
if (job.finished_at.has_value()) {
|
||||
j["finished_at"] = Json::Int64(*job.finished_at);
|
||||
} else {
|
||||
j["finished_at"] = Json::nullValue;
|
||||
}
|
||||
j["updated_at"] = Json::Int64(job.updated_at);
|
||||
j["created_at"] = Json::Int64(job.created_at);
|
||||
return j;
|
||||
}
|
||||
|
||||
Json::Value ToJson(const services::ImportJobItem& item) {
|
||||
Json::Value j;
|
||||
j["id"] = Json::Int64(item.id);
|
||||
j["job_id"] = Json::Int64(item.job_id);
|
||||
j["source_path"] = item.source_path;
|
||||
j["status"] = item.status;
|
||||
j["title"] = item.title;
|
||||
j["difficulty"] = item.difficulty;
|
||||
if (item.problem_id.has_value()) {
|
||||
j["problem_id"] = Json::Int64(*item.problem_id);
|
||||
} else {
|
||||
j["problem_id"] = Json::nullValue;
|
||||
}
|
||||
j["error_text"] = item.error_text;
|
||||
if (item.started_at.has_value()) {
|
||||
j["started_at"] = Json::Int64(*item.started_at);
|
||||
} else {
|
||||
j["started_at"] = Json::nullValue;
|
||||
}
|
||||
if (item.finished_at.has_value()) {
|
||||
j["finished_at"] = Json::Int64(*item.finished_at);
|
||||
} else {
|
||||
j["finished_at"] = Json::nullValue;
|
||||
}
|
||||
j["updated_at"] = Json::Int64(item.updated_at);
|
||||
j["created_at"] = Json::Int64(item.created_at);
|
||||
return j;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ImportController::latestJob(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
services::ImportService svc(csp::AppState::Instance().db());
|
||||
Json::Value payload;
|
||||
const auto job = svc.GetLatestJob();
|
||||
payload["runner_running"] = services::ImportRunner::Instance().IsRunning();
|
||||
if (job.has_value()) {
|
||||
payload["job"] = ToJson(*job);
|
||||
} else {
|
||||
payload["job"] = Json::nullValue;
|
||||
}
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ImportController::jobById(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t job_id) {
|
||||
try {
|
||||
services::ImportService svc(csp::AppState::Instance().db());
|
||||
const auto job = svc.GetById(job_id);
|
||||
if (!job.has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "import job not found"));
|
||||
return;
|
||||
}
|
||||
Json::Value payload = ToJson(*job);
|
||||
payload["runner_running"] = services::ImportRunner::Instance().IsRunning();
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ImportController::jobItems(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t job_id) {
|
||||
try {
|
||||
services::ImportJobItemQuery query;
|
||||
query.status = req->getParameter("status");
|
||||
query.page = ParsePositiveInt(req->getParameter("page"), 1, 1, 100000);
|
||||
query.page_size = ParsePositiveInt(req->getParameter("page_size"), 100, 1, 500);
|
||||
|
||||
services::ImportService svc(csp::AppState::Instance().db());
|
||||
const auto rows = svc.ListItems(job_id, query);
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& row : rows) arr.append(ToJson(row));
|
||||
|
||||
Json::Value payload;
|
||||
payload["items"] = arr;
|
||||
payload["page"] = query.page;
|
||||
payload["page_size"] = query.page_size;
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::invalid_argument&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "invalid query parameter"));
|
||||
} catch (const std::out_of_range&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "query parameter out of range"));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ImportController::runJob(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
services::ImportRunOptions opts;
|
||||
const auto json = req->getJsonObject();
|
||||
if (json) {
|
||||
opts.clear_all_problems =
|
||||
(*json).isMember("clear_all_problems") &&
|
||||
(*json)["clear_all_problems"].asBool();
|
||||
}
|
||||
const bool started =
|
||||
services::ImportRunner::Instance().TriggerAsync("manual", opts);
|
||||
if (!started) {
|
||||
cb(JsonError(drogon::k409Conflict, "import job already running"));
|
||||
return;
|
||||
}
|
||||
|
||||
Json::Value payload;
|
||||
payload["started"] = true;
|
||||
payload["running"] = true;
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "csp/controllers/kb_controller.h"
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/domain/json.h"
|
||||
#include "csp/services/kb_service.h"
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
|
||||
const std::string& msg) {
|
||||
Json::Value j;
|
||||
j["ok"] = false;
|
||||
j["error"] = msg;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(code);
|
||||
return resp;
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
|
||||
Json::Value j;
|
||||
j["ok"] = true;
|
||||
j["data"] = data;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(drogon::k200OK);
|
||||
return resp;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void KbController::listArticles(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
services::KbService svc(csp::AppState::Instance().db());
|
||||
const auto rows = svc.ListArticles();
|
||||
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& a : rows) arr.append(domain::ToJson(a));
|
||||
|
||||
cb(JsonOk(arr));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void KbController::getArticle(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
std::string slug) {
|
||||
try {
|
||||
services::KbService svc(csp::AppState::Instance().db());
|
||||
const auto detail = svc.GetBySlug(slug);
|
||||
if (!detail.has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "article not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
Json::Value data;
|
||||
data["article"] = domain::ToJson(detail->article);
|
||||
|
||||
Json::Value rel(Json::arrayValue);
|
||||
for (const auto& p : detail->related_problems) {
|
||||
Json::Value item;
|
||||
item["problem_id"] = Json::Int64(p.first);
|
||||
item["title"] = p.second;
|
||||
rel.append(item);
|
||||
}
|
||||
data["related_problems"] = rel;
|
||||
|
||||
cb(JsonOk(data));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "csp/controllers/leaderboard_controller.h"
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/domain/json.h"
|
||||
#include "csp/services/user_service.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
|
||||
const std::string& msg) {
|
||||
Json::Value j;
|
||||
j["ok"] = false;
|
||||
j["error"] = msg;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(code);
|
||||
return resp;
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr JsonOkArray(const Json::Value& arr) {
|
||||
Json::Value j;
|
||||
j["ok"] = true;
|
||||
j["data"] = arr;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(drogon::k200OK);
|
||||
return resp;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LeaderboardController::global(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
int limit = 100;
|
||||
const auto limit_str = req->getParameter("limit");
|
||||
if (!limit_str.empty()) {
|
||||
limit = std::max(1, std::min(500, std::stoi(limit_str)));
|
||||
}
|
||||
|
||||
services::UserService users(csp::AppState::Instance().db());
|
||||
const auto rows = users.GlobalLeaderboard(limit);
|
||||
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& r : rows) arr.append(domain::ToJson(r));
|
||||
cb(JsonOkArray(arr));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "csp/controllers/me_controller.h"
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/domain/json.h"
|
||||
#include "csp/services/user_service.h"
|
||||
#include "csp/services/wrong_book_service.h"
|
||||
#include "http_auth.h"
|
||||
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
|
||||
const std::string& msg) {
|
||||
Json::Value j;
|
||||
j["ok"] = false;
|
||||
j["error"] = msg;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(code);
|
||||
return resp;
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
|
||||
Json::Value j;
|
||||
j["ok"] = true;
|
||||
j["data"] = data;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(drogon::k200OK);
|
||||
return resp;
|
||||
}
|
||||
|
||||
std::optional<int64_t> RequireAuth(const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>& cb) {
|
||||
std::string auth_error;
|
||||
const auto user_id = GetAuthedUserId(req, auth_error);
|
||||
if (!user_id.has_value()) {
|
||||
cb(JsonError(drogon::k401Unauthorized, auth_error));
|
||||
return std::nullopt;
|
||||
}
|
||||
return user_id;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MeController::profile(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
const auto user_id = RequireAuth(req, cb);
|
||||
if (!user_id.has_value()) return;
|
||||
|
||||
services::UserService users(csp::AppState::Instance().db());
|
||||
const auto user = users.GetById(*user_id);
|
||||
if (!user.has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "user not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
cb(JsonOk(domain::ToPublicJson(*user)));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void MeController::listWrongBook(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
const auto user_id = RequireAuth(req, cb);
|
||||
if (!user_id.has_value()) return;
|
||||
|
||||
services::WrongBookService wrong_book(csp::AppState::Instance().db());
|
||||
const auto rows = wrong_book.ListByUser(*user_id);
|
||||
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& row : rows) {
|
||||
Json::Value item = domain::ToJson(row.item);
|
||||
item["problem_title"] = row.problem_title;
|
||||
arr.append(item);
|
||||
}
|
||||
|
||||
cb(JsonOk(arr));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void MeController::upsertWrongBookNote(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t problem_id) {
|
||||
try {
|
||||
const auto user_id = RequireAuth(req, cb);
|
||||
if (!user_id.has_value()) return;
|
||||
|
||||
const auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string note = (*json).get("note", "").asString();
|
||||
if (note.size() > 4000) {
|
||||
cb(JsonError(drogon::k400BadRequest, "note too long"));
|
||||
return;
|
||||
}
|
||||
|
||||
services::WrongBookService wrong_book(csp::AppState::Instance().db());
|
||||
wrong_book.UpsertNote(*user_id, problem_id, note);
|
||||
|
||||
Json::Value data;
|
||||
data["user_id"] = Json::Int64(*user_id);
|
||||
data["problem_id"] = Json::Int64(problem_id);
|
||||
data["note"] = note;
|
||||
cb(JsonOk(data));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void MeController::deleteWrongBookItem(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t problem_id) {
|
||||
try {
|
||||
const auto user_id = RequireAuth(req, cb);
|
||||
if (!user_id.has_value()) return;
|
||||
|
||||
services::WrongBookService wrong_book(csp::AppState::Instance().db());
|
||||
wrong_book.Remove(*user_id, problem_id);
|
||||
|
||||
Json::Value data;
|
||||
data["user_id"] = Json::Int64(*user_id);
|
||||
data["problem_id"] = Json::Int64(problem_id);
|
||||
data["deleted"] = true;
|
||||
cb(JsonOk(data));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -0,0 +1,273 @@
|
||||
#include "csp/controllers/meta_controller.h"
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/domain/enum_strings.h"
|
||||
#include "csp/domain/json.h"
|
||||
#include "csp/services/problem_gen_runner.h"
|
||||
#include "csp/services/problem_service.h"
|
||||
#include "csp/services/submission_service.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
|
||||
const std::string& msg) {
|
||||
Json::Value j;
|
||||
j["ok"] = false;
|
||||
j["error"] = msg;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(code);
|
||||
return resp;
|
||||
}
|
||||
|
||||
Json::Value BuildOpenApiSpec() {
|
||||
Json::Value root;
|
||||
root["openapi"] = "3.1.0";
|
||||
root["info"]["title"] = "CSP Platform API";
|
||||
root["info"]["version"] = "1.0.0";
|
||||
root["info"]["description"] =
|
||||
"CSP 训练平台接口文档,含认证、题库、评测、导入、MCP。";
|
||||
|
||||
root["servers"][0]["url"] = "/";
|
||||
|
||||
auto& paths = root["paths"];
|
||||
paths["/api/health"]["get"]["summary"] = "健康检查";
|
||||
paths["/api/health"]["get"]["responses"]["200"]["description"] = "OK";
|
||||
|
||||
paths["/api/v1/auth/login"]["post"]["summary"] = "登录";
|
||||
paths["/api/v1/auth/register"]["post"]["summary"] = "注册";
|
||||
|
||||
paths["/api/v1/problems"]["get"]["summary"] = "题库列表";
|
||||
paths["/api/v1/problems/{id}"]["get"]["summary"] = "题目详情";
|
||||
paths["/api/v1/problems/{id}/submit"]["post"]["summary"] = "提交评测";
|
||||
paths["/api/v1/problems/{id}/draft"]["get"]["summary"] = "读取代码草稿";
|
||||
paths["/api/v1/problems/{id}/draft"]["put"]["summary"] = "保存代码草稿";
|
||||
paths["/api/v1/problems/{id}/solutions"]["get"]["summary"] = "题解列表/任务状态";
|
||||
paths["/api/v1/problems/{id}/solutions/generate"]["post"]["summary"] =
|
||||
"异步生成题解";
|
||||
|
||||
paths["/api/v1/run/cpp"]["post"]["summary"] = "C++ 试运行";
|
||||
paths["/api/v1/submissions"]["get"]["summary"] = "提交记录";
|
||||
paths["/api/v1/submissions/{id}"]["get"]["summary"] = "提交详情";
|
||||
|
||||
paths["/api/v1/import/jobs/latest"]["get"]["summary"] = "最新导入任务";
|
||||
paths["/api/v1/import/jobs/run"]["post"]["summary"] = "触发导入任务";
|
||||
paths["/api/v1/problem-gen/status"]["get"]["summary"] = "CSP-J 生成任务状态";
|
||||
paths["/api/v1/problem-gen/run"]["post"]["summary"] = "触发生成新题(RAG+去重)";
|
||||
|
||||
paths["/api/v1/mcp"]["post"]["summary"] = "MCP JSON-RPC 入口";
|
||||
|
||||
auto& components = root["components"];
|
||||
components["securitySchemes"]["bearerAuth"]["type"] = "http";
|
||||
components["securitySchemes"]["bearerAuth"]["scheme"] = "bearer";
|
||||
components["securitySchemes"]["bearerAuth"]["bearerFormat"] = "JWT";
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
Json::Value BuildMcpError(const Json::Value& id,
|
||||
int code,
|
||||
const std::string& message) {
|
||||
Json::Value out;
|
||||
out["jsonrpc"] = "2.0";
|
||||
out["id"] = id;
|
||||
out["error"]["code"] = code;
|
||||
out["error"]["message"] = message;
|
||||
return out;
|
||||
}
|
||||
|
||||
Json::Value BuildMcpOk(const Json::Value& id, const Json::Value& result) {
|
||||
Json::Value out;
|
||||
out["jsonrpc"] = "2.0";
|
||||
out["id"] = id;
|
||||
out["result"] = result;
|
||||
return out;
|
||||
}
|
||||
|
||||
Json::Value McpToolsList() {
|
||||
Json::Value tools(Json::arrayValue);
|
||||
|
||||
Json::Value t1;
|
||||
t1["name"] = "health";
|
||||
t1["description"] = "Get backend health";
|
||||
tools.append(t1);
|
||||
|
||||
Json::Value t2;
|
||||
t2["name"] = "list_problems";
|
||||
t2["description"] = "List problems with filters";
|
||||
tools.append(t2);
|
||||
|
||||
Json::Value t3;
|
||||
t3["name"] = "get_problem";
|
||||
t3["description"] = "Get problem by id";
|
||||
tools.append(t3);
|
||||
|
||||
Json::Value t4;
|
||||
t4["name"] = "run_cpp";
|
||||
t4["description"] = "Run C++ code with input";
|
||||
tools.append(t4);
|
||||
|
||||
Json::Value t5;
|
||||
t5["name"] = "generate_cspj_problem";
|
||||
t5["description"] = "Trigger RAG-based CSP-J problem generation";
|
||||
tools.append(t5);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MetaController::openapi(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildOpenApiSpec());
|
||||
resp->setStatusCode(drogon::k200OK);
|
||||
cb(resp);
|
||||
}
|
||||
|
||||
void MetaController::mcp(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
const auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(
|
||||
BuildMcpError(Json::nullValue, -32700, "body must be json"));
|
||||
resp->setStatusCode(drogon::k400BadRequest);
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
const Json::Value id = (*json).isMember("id") ? (*json)["id"] : Json::nullValue;
|
||||
const std::string method = (*json).get("method", "").asString();
|
||||
const Json::Value params = (*json).isMember("params") ? (*json)["params"] : Json::Value();
|
||||
|
||||
if (method == "initialize") {
|
||||
Json::Value result;
|
||||
result["server_name"] = "csp-platform-mcp";
|
||||
result["server_version"] = "1.0.0";
|
||||
result["capabilities"]["tools"] = true;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (method == "tools/list") {
|
||||
Json::Value result;
|
||||
result["tools"] = McpToolsList();
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (method != "tools/call") {
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(
|
||||
BuildMcpError(id, -32601, "method not found"));
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string tool_name = params.get("name", "").asString();
|
||||
const Json::Value args = params.isMember("arguments") ? params["arguments"] : Json::Value();
|
||||
|
||||
if (tool_name == "health") {
|
||||
Json::Value result;
|
||||
result["ok"] = true;
|
||||
result["service"] = "csp-backend";
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tool_name == "list_problems") {
|
||||
services::ProblemQuery q;
|
||||
q.q = args.get("q", "").asString();
|
||||
q.page = std::max(1, args.get("page", 1).asInt());
|
||||
q.page_size = std::max(1, std::min(100, args.get("page_size", 20).asInt()));
|
||||
q.source_prefix = args.get("source_prefix", "").asString();
|
||||
q.difficulty = std::max(0, std::min(10, args.get("difficulty", 0).asInt()));
|
||||
|
||||
services::ProblemService svc(csp::AppState::Instance().db());
|
||||
const auto rows = svc.List(q);
|
||||
|
||||
Json::Value items(Json::arrayValue);
|
||||
for (const auto& row : rows.items) {
|
||||
items.append(domain::ToJson(row));
|
||||
}
|
||||
|
||||
Json::Value result;
|
||||
result["items"] = items;
|
||||
result["total_count"] = rows.total_count;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tool_name == "get_problem") {
|
||||
const int64_t problem_id = args.get("problem_id", 0).asInt64();
|
||||
services::ProblemService svc(csp::AppState::Instance().db());
|
||||
const auto p = svc.GetById(problem_id);
|
||||
if (!p.has_value()) {
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(
|
||||
BuildMcpError(id, -32004, "problem not found"));
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
Json::Value result = domain::ToJson(*p);
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tool_name == "run_cpp") {
|
||||
const std::string code = args.get("code", "").asString();
|
||||
const std::string input = args.get("input", "").asString();
|
||||
services::SubmissionService svc(csp::AppState::Instance().db());
|
||||
const auto r = svc.RunOnlyCpp(code, input);
|
||||
|
||||
Json::Value result;
|
||||
result["status"] = domain::ToString(r.status);
|
||||
result["time_ms"] = r.time_ms;
|
||||
result["stdout"] = r.stdout_text;
|
||||
result["stderr"] = r.stderr_text;
|
||||
result["compile_log"] = r.compile_log;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tool_name == "generate_cspj_problem") {
|
||||
const int count = std::max(1, std::min(5, args.get("count", 1).asInt()));
|
||||
const bool started =
|
||||
services::ProblemGenRunner::Instance().TriggerAsync("mcp", count);
|
||||
Json::Value result;
|
||||
result["started"] = started;
|
||||
result["count"] = count;
|
||||
result["running"] = services::ProblemGenRunner::Instance().IsRunning();
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(BuildMcpOk(id, result));
|
||||
cb(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(
|
||||
BuildMcpError(id, -32602, "unknown tool"));
|
||||
cb(resp);
|
||||
} catch (const std::runtime_error& e) {
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(
|
||||
BuildMcpError(Json::nullValue, -32000, e.what()));
|
||||
resp->setStatusCode(drogon::k400BadRequest);
|
||||
cb(resp);
|
||||
} catch (const std::exception& e) {
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(
|
||||
BuildMcpError(Json::nullValue, -32000, e.what()));
|
||||
resp->setStatusCode(drogon::k500InternalServerError);
|
||||
cb(resp);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -0,0 +1,299 @@
|
||||
#include "csp/controllers/problem_controller.h"
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/domain/json.h"
|
||||
#include "csp/services/problem_service.h"
|
||||
#include "csp/services/problem_solution_runner.h"
|
||||
#include "csp/services/problem_workspace_service.h"
|
||||
#include "http_auth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <exception>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
|
||||
const std::string& msg) {
|
||||
Json::Value j;
|
||||
j["ok"] = false;
|
||||
j["error"] = msg;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(code);
|
||||
return resp;
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
|
||||
Json::Value j;
|
||||
j["ok"] = true;
|
||||
j["data"] = data;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(drogon::k200OK);
|
||||
return resp;
|
||||
}
|
||||
|
||||
int ParsePositiveInt(const std::string& s,
|
||||
int default_value,
|
||||
int min_value,
|
||||
int max_value) {
|
||||
if (s.empty()) return default_value;
|
||||
const int v = std::stoi(s);
|
||||
return std::max(min_value, std::min(max_value, v));
|
||||
}
|
||||
|
||||
std::vector<std::string> ParseCsv(const std::string& raw) {
|
||||
auto trim = [](std::string s) {
|
||||
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) {
|
||||
s.erase(s.begin());
|
||||
}
|
||||
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) {
|
||||
s.pop_back();
|
||||
}
|
||||
return s;
|
||||
};
|
||||
std::vector<std::string> out;
|
||||
std::stringstream ss(raw);
|
||||
std::string item;
|
||||
while (std::getline(ss, item, ',')) {
|
||||
item = trim(item);
|
||||
if (item.empty()) continue;
|
||||
out.push_back(item);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ProblemController::list(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
services::ProblemQuery q;
|
||||
q.q = req->getParameter("q");
|
||||
q.tag = req->getParameter("tag");
|
||||
q.tags = ParseCsv(req->getParameter("tags"));
|
||||
q.source_prefix = req->getParameter("source_prefix");
|
||||
q.difficulty = ParsePositiveInt(req->getParameter("difficulty"), 0, 0, 10);
|
||||
q.page = ParsePositiveInt(req->getParameter("page"), 1, 1, 100000);
|
||||
q.page_size = ParsePositiveInt(req->getParameter("page_size"), 20, 1, 200);
|
||||
q.order_by = req->getParameter("order_by");
|
||||
q.order = req->getParameter("order");
|
||||
|
||||
services::ProblemService svc(csp::AppState::Instance().db());
|
||||
const auto result = svc.List(q);
|
||||
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& p : result.items) arr.append(domain::ToJson(p));
|
||||
|
||||
Json::Value payload;
|
||||
payload["items"] = arr;
|
||||
payload["total_count"] = result.total_count;
|
||||
payload["page"] = q.page;
|
||||
payload["page_size"] = q.page_size;
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::invalid_argument&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "invalid query parameter"));
|
||||
} catch (const std::out_of_range&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "query parameter out of range"));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ProblemController::getById(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t problem_id) {
|
||||
try {
|
||||
services::ProblemService svc(csp::AppState::Instance().db());
|
||||
const auto p = svc.GetById(problem_id);
|
||||
if (!p.has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "problem not found"));
|
||||
return;
|
||||
}
|
||||
cb(JsonOk(domain::ToJson(*p)));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ProblemController::getDraft(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t problem_id) {
|
||||
try {
|
||||
std::string auth_error;
|
||||
const auto user_id = GetAuthedUserId(req, auth_error);
|
||||
if (!user_id.has_value()) {
|
||||
cb(JsonError(drogon::k401Unauthorized, auth_error));
|
||||
return;
|
||||
}
|
||||
|
||||
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
|
||||
const auto draft = svc.GetDraft(*user_id, problem_id);
|
||||
if (!draft.has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "draft not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
Json::Value payload;
|
||||
payload["language"] = draft->language;
|
||||
payload["code"] = draft->code;
|
||||
payload["stdin"] = draft->stdin_text;
|
||||
payload["updated_at"] = Json::Int64(draft->updated_at);
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ProblemController::saveDraft(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t problem_id) {
|
||||
try {
|
||||
std::string auth_error;
|
||||
const auto user_id = GetAuthedUserId(req, auth_error);
|
||||
if (!user_id.has_value()) {
|
||||
cb(JsonError(drogon::k401Unauthorized, auth_error));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string language = (*json).get("language", "cpp").asString();
|
||||
const std::string code = (*json).get("code", "").asString();
|
||||
const std::string stdin_text = (*json).get("stdin", "").asString();
|
||||
|
||||
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
|
||||
if (!svc.ProblemExists(problem_id)) {
|
||||
cb(JsonError(drogon::k404NotFound, "problem not found"));
|
||||
return;
|
||||
}
|
||||
svc.SaveDraft(*user_id, problem_id, language, code, stdin_text);
|
||||
|
||||
Json::Value payload;
|
||||
payload["saved"] = true;
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::runtime_error& e) {
|
||||
cb(JsonError(drogon::k400BadRequest, e.what()));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ProblemController::listSolutions(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t problem_id) {
|
||||
try {
|
||||
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
|
||||
if (!svc.ProblemExists(problem_id)) {
|
||||
cb(JsonError(drogon::k404NotFound, "problem not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto rows = svc.ListSolutions(problem_id);
|
||||
const auto latest_job = svc.GetLatestSolutionJob(problem_id);
|
||||
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& item : rows) {
|
||||
Json::Value j;
|
||||
j["id"] = Json::Int64(item.id);
|
||||
j["problem_id"] = Json::Int64(item.problem_id);
|
||||
j["variant"] = item.variant;
|
||||
j["title"] = item.title;
|
||||
j["idea_md"] = item.idea_md;
|
||||
j["explanation_md"] = item.explanation_md;
|
||||
j["code_cpp"] = item.code_cpp;
|
||||
j["complexity"] = item.complexity;
|
||||
j["tags_json"] = item.tags_json;
|
||||
j["source"] = item.source;
|
||||
j["created_at"] = Json::Int64(item.created_at);
|
||||
j["updated_at"] = Json::Int64(item.updated_at);
|
||||
arr.append(j);
|
||||
}
|
||||
|
||||
Json::Value payload;
|
||||
payload["items"] = arr;
|
||||
payload["runner_running"] =
|
||||
services::ProblemSolutionRunner::Instance().IsRunning(problem_id);
|
||||
if (latest_job.has_value()) {
|
||||
Json::Value j;
|
||||
j["id"] = Json::Int64(latest_job->id);
|
||||
j["problem_id"] = Json::Int64(latest_job->problem_id);
|
||||
j["status"] = latest_job->status;
|
||||
j["progress"] = latest_job->progress;
|
||||
j["message"] = latest_job->message;
|
||||
j["created_at"] = Json::Int64(latest_job->created_at);
|
||||
if (latest_job->started_at.has_value()) {
|
||||
j["started_at"] = Json::Int64(*latest_job->started_at);
|
||||
} else {
|
||||
j["started_at"] = Json::nullValue;
|
||||
}
|
||||
if (latest_job->finished_at.has_value()) {
|
||||
j["finished_at"] = Json::Int64(*latest_job->finished_at);
|
||||
} else {
|
||||
j["finished_at"] = Json::nullValue;
|
||||
}
|
||||
payload["latest_job"] = j;
|
||||
} else {
|
||||
payload["latest_job"] = Json::nullValue;
|
||||
}
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ProblemController::generateSolutions(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t problem_id) {
|
||||
try {
|
||||
std::string auth_error;
|
||||
const auto user_id = GetAuthedUserId(req, auth_error);
|
||||
if (!user_id.has_value()) {
|
||||
cb(JsonError(drogon::k401Unauthorized, auth_error));
|
||||
return;
|
||||
}
|
||||
|
||||
int max_solutions = 3;
|
||||
const auto json = req->getJsonObject();
|
||||
if (json && (*json).isMember("max_solutions")) {
|
||||
max_solutions = std::max(1, std::min(5, (*json)["max_solutions"].asInt()));
|
||||
}
|
||||
|
||||
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
|
||||
if (!svc.ProblemExists(problem_id)) {
|
||||
cb(JsonError(drogon::k404NotFound, "problem not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
const int64_t job_id = svc.CreateSolutionJob(problem_id, *user_id, max_solutions);
|
||||
const bool started = services::ProblemSolutionRunner::Instance().TriggerAsync(
|
||||
problem_id, job_id, max_solutions);
|
||||
if (!started) {
|
||||
cb(JsonError(drogon::k409Conflict, "solution generation is already running"));
|
||||
return;
|
||||
}
|
||||
|
||||
Json::Value payload;
|
||||
payload["started"] = true;
|
||||
payload["job_id"] = Json::Int64(job_id);
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "csp/controllers/problem_gen_controller.h"
|
||||
|
||||
#include "csp/services/problem_gen_runner.h"
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
|
||||
const std::string& msg) {
|
||||
Json::Value j;
|
||||
j["ok"] = false;
|
||||
j["error"] = msg;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(code);
|
||||
return resp;
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
|
||||
Json::Value j;
|
||||
j["ok"] = true;
|
||||
j["data"] = data;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(drogon::k200OK);
|
||||
return resp;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ProblemGenController::status(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
auto& runner = services::ProblemGenRunner::Instance();
|
||||
Json::Value payload;
|
||||
payload["running"] = runner.IsRunning();
|
||||
payload["last_command"] = runner.LastCommand();
|
||||
if (runner.LastExitCode().has_value()) {
|
||||
payload["last_exit_code"] = *runner.LastExitCode();
|
||||
} else {
|
||||
payload["last_exit_code"] = Json::nullValue;
|
||||
}
|
||||
payload["last_started_at"] = Json::Int64(runner.LastStartedAt());
|
||||
payload["last_finished_at"] = Json::Int64(runner.LastFinishedAt());
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void ProblemGenController::run(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
int count = 1;
|
||||
const auto json = req->getJsonObject();
|
||||
if (json && (*json).isMember("count")) {
|
||||
count = (*json)["count"].asInt();
|
||||
}
|
||||
|
||||
auto& runner = services::ProblemGenRunner::Instance();
|
||||
const bool started = runner.TriggerAsync("manual", count);
|
||||
if (!started) {
|
||||
cb(JsonError(drogon::k409Conflict, "problem generation job already running"));
|
||||
return;
|
||||
}
|
||||
|
||||
Json::Value payload;
|
||||
payload["started"] = true;
|
||||
payload["count"] = count;
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -0,0 +1,192 @@
|
||||
#include "csp/controllers/submission_controller.h"
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/domain/enum_strings.h"
|
||||
#include "csp/domain/json.h"
|
||||
#include "csp/services/contest_service.h"
|
||||
#include "csp/services/submission_service.h"
|
||||
#include "http_auth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
|
||||
const std::string& msg) {
|
||||
Json::Value j;
|
||||
j["ok"] = false;
|
||||
j["error"] = msg;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(code);
|
||||
return resp;
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
|
||||
Json::Value j;
|
||||
j["ok"] = true;
|
||||
j["data"] = data;
|
||||
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||
resp->setStatusCode(drogon::k200OK);
|
||||
return resp;
|
||||
}
|
||||
|
||||
int ParseClampedInt(const std::string& s,
|
||||
int default_value,
|
||||
int min_value,
|
||||
int max_value) {
|
||||
if (s.empty()) return default_value;
|
||||
const int v = std::stoi(s);
|
||||
return std::max(min_value, std::min(max_value, v));
|
||||
}
|
||||
|
||||
std::optional<int64_t> ParseOptionalInt64(const std::string& s) {
|
||||
if (s.empty()) return std::nullopt;
|
||||
return std::stoll(s);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SubmissionController::submitProblem(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t problem_id) {
|
||||
try {
|
||||
std::string auth_error;
|
||||
const auto user_id = GetAuthedUserId(req, auth_error);
|
||||
if (!user_id.has_value()) {
|
||||
cb(JsonError(drogon::k401Unauthorized, auth_error));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
||||
return;
|
||||
}
|
||||
|
||||
services::SubmissionCreateRequest create;
|
||||
create.user_id = *user_id;
|
||||
create.problem_id = problem_id;
|
||||
create.language = (*json).get("language", "cpp").asString();
|
||||
create.code = (*json).get("code", "").asString();
|
||||
|
||||
if ((*json).isMember("contest_id") && !(*json)["contest_id"].isNull()) {
|
||||
create.contest_id = (*json)["contest_id"].asInt64();
|
||||
services::ContestService contest(csp::AppState::Instance().db());
|
||||
|
||||
if (!contest.GetContest(*create.contest_id).has_value()) {
|
||||
cb(JsonError(drogon::k400BadRequest, "contest not found"));
|
||||
return;
|
||||
}
|
||||
if (!contest.ContainsProblem(*create.contest_id, problem_id)) {
|
||||
cb(JsonError(drogon::k400BadRequest, "problem not in contest"));
|
||||
return;
|
||||
}
|
||||
if (!contest.IsRegistered(*create.contest_id, *user_id)) {
|
||||
cb(JsonError(drogon::k403Forbidden, "user is not registered for contest"));
|
||||
return;
|
||||
}
|
||||
if (!contest.IsRunning(*create.contest_id)) {
|
||||
cb(JsonError(drogon::k403Forbidden, "contest is not running"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
services::SubmissionService svc(csp::AppState::Instance().db());
|
||||
auto s = svc.CreateAndJudge(create);
|
||||
cb(JsonOk(domain::ToJson(s)));
|
||||
} catch (const std::invalid_argument&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "invalid numeric field"));
|
||||
} catch (const std::out_of_range&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "numeric field out of range"));
|
||||
} catch (const std::runtime_error& e) {
|
||||
cb(JsonError(drogon::k400BadRequest, e.what()));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void SubmissionController::listSubmissions(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
const auto user_id = ParseOptionalInt64(req->getParameter("user_id"));
|
||||
const auto problem_id = ParseOptionalInt64(req->getParameter("problem_id"));
|
||||
const auto contest_id = ParseOptionalInt64(req->getParameter("contest_id"));
|
||||
const int page = ParseClampedInt(req->getParameter("page"), 1, 1, 100000);
|
||||
const int page_size =
|
||||
ParseClampedInt(req->getParameter("page_size"), 20, 1, 200);
|
||||
|
||||
services::SubmissionService svc(csp::AppState::Instance().db());
|
||||
const auto rows = svc.List(user_id, problem_id, contest_id, page, page_size);
|
||||
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& s : rows) arr.append(domain::ToJson(s));
|
||||
|
||||
Json::Value payload;
|
||||
payload["items"] = arr;
|
||||
payload["page"] = page;
|
||||
payload["page_size"] = page_size;
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::invalid_argument&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "invalid query parameter"));
|
||||
} catch (const std::out_of_range&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "query parameter out of range"));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void SubmissionController::getSubmission(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t submission_id) {
|
||||
try {
|
||||
services::SubmissionService svc(csp::AppState::Instance().db());
|
||||
const auto s = svc.GetById(submission_id);
|
||||
if (!s.has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "submission not found"));
|
||||
return;
|
||||
}
|
||||
cb(JsonOk(domain::ToJson(*s)));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void SubmissionController::runCpp(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
const auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string code = (*json).get("code", "").asString();
|
||||
const std::string input = (*json).get("input", "").asString();
|
||||
|
||||
services::SubmissionService svc(csp::AppState::Instance().db());
|
||||
const auto r = svc.RunOnlyCpp(code, input);
|
||||
|
||||
Json::Value payload;
|
||||
payload["status"] = domain::ToString(r.status);
|
||||
payload["time_ms"] = r.time_ms;
|
||||
payload["stdout"] = r.stdout_text;
|
||||
payload["stderr"] = r.stderr_text;
|
||||
payload["compile_log"] = r.compile_log;
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::runtime_error& e) {
|
||||
cb(JsonError(drogon::k400BadRequest, e.what()));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "csp/db/sqlite_db.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace csp::db {
|
||||
@@ -8,11 +11,203 @@ namespace csp::db {
|
||||
namespace {
|
||||
|
||||
void ThrowSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK) return;
|
||||
if (rc == SQLITE_OK || rc == SQLITE_DONE || rc == SQLITE_ROW) return;
|
||||
const char* msg = db ? sqlite3_errmsg(db) : "";
|
||||
throw std::runtime_error(std::string(what) + ": " + msg);
|
||||
}
|
||||
|
||||
int64_t NowSec() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
bool ColumnExists(sqlite3* db, const char* table, const char* col) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const std::string sql = std::string("PRAGMA table_info(") + table + ")";
|
||||
const int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
if (stmt) sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const unsigned char* name = sqlite3_column_text(stmt, 1);
|
||||
if (name && std::string(reinterpret_cast<const char*>(name)) == col) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return found;
|
||||
}
|
||||
|
||||
void EnsureColumn(SqliteDb& db,
|
||||
const char* table,
|
||||
const char* col_name,
|
||||
const char* col_def) {
|
||||
if (ColumnExists(db.raw(), table, col_name)) return;
|
||||
db.Exec(std::string("ALTER TABLE ") + table + " ADD COLUMN " + col_def + ";");
|
||||
}
|
||||
|
||||
int CountRows(sqlite3* db, const char* table) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const std::string sql = std::string("SELECT COUNT(1) FROM ") + table;
|
||||
int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
if (stmt) sqlite3_finalize(stmt);
|
||||
return 0;
|
||||
}
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
int count = 0;
|
||||
if (rc == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return count;
|
||||
}
|
||||
|
||||
std::optional<int64_t> QueryOneId(sqlite3* db, const std::string& sql) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
if (stmt) sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto id = sqlite3_column_int64(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return id;
|
||||
}
|
||||
|
||||
void InsertProblem(sqlite3* db,
|
||||
const std::string& slug,
|
||||
const std::string& title,
|
||||
const std::string& statement,
|
||||
int difficulty,
|
||||
const std::string& source,
|
||||
const std::string& sample_in,
|
||||
const std::string& sample_out,
|
||||
int64_t created_at) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT INTO problems(slug,title,statement_md,difficulty,source,sample_input,sample_output,created_at) "
|
||||
"VALUES(?,?,?,?,?,?,?,?)";
|
||||
ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare insert problem");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind problem.slug");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 2, title.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind problem.title");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 3, statement.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind problem.statement");
|
||||
ThrowSqlite(sqlite3_bind_int(stmt, 4, difficulty), db,
|
||||
"bind problem.difficulty");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 5, source.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind problem.source");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 6, sample_in.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind problem.sample_input");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 7, sample_out.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind problem.sample_output");
|
||||
ThrowSqlite(sqlite3_bind_int64(stmt, 8, created_at), db,
|
||||
"bind problem.created_at");
|
||||
ThrowSqlite(sqlite3_step(stmt), db, "insert problem");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void InsertProblemTag(sqlite3* db, int64_t problem_id, const std::string& tag) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT OR IGNORE INTO problem_tags(problem_id,tag) VALUES(?,?)";
|
||||
ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare insert problem_tag");
|
||||
ThrowSqlite(sqlite3_bind_int64(stmt, 1, problem_id), db,
|
||||
"bind problem_tag.problem_id");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 2, tag.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind problem_tag.tag");
|
||||
ThrowSqlite(sqlite3_step(stmt), db, "insert problem_tag");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void InsertKbArticle(sqlite3* db,
|
||||
const std::string& slug,
|
||||
const std::string& title,
|
||||
const std::string& content_md,
|
||||
int64_t created_at) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT INTO kb_articles(slug,title,content_md,created_at) VALUES(?,?,?,?)";
|
||||
ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare insert kb_article");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind kb_article.slug");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 2, title.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind kb_article.title");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 3, content_md.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind kb_article.content");
|
||||
ThrowSqlite(sqlite3_bind_int64(stmt, 4, created_at), db,
|
||||
"bind kb_article.created_at");
|
||||
ThrowSqlite(sqlite3_step(stmt), db, "insert kb_article");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void InsertKbLink(sqlite3* db, int64_t article_id, int64_t problem_id) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT OR IGNORE INTO kb_article_links(article_id,problem_id) VALUES(?,?)";
|
||||
ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare insert kb_article_link");
|
||||
ThrowSqlite(sqlite3_bind_int64(stmt, 1, article_id), db,
|
||||
"bind kb_article_link.article_id");
|
||||
ThrowSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db,
|
||||
"bind kb_article_link.problem_id");
|
||||
ThrowSqlite(sqlite3_step(stmt), db, "insert kb_article_link");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void InsertContest(sqlite3* db,
|
||||
const std::string& title,
|
||||
int64_t starts_at,
|
||||
int64_t ends_at,
|
||||
const std::string& rule_json) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT INTO contests(title,starts_at,ends_at,rule_json) VALUES(?,?,?,?)";
|
||||
ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare insert contest");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 1, title.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind contest.title");
|
||||
ThrowSqlite(sqlite3_bind_int64(stmt, 2, starts_at), db,
|
||||
"bind contest.starts_at");
|
||||
ThrowSqlite(sqlite3_bind_int64(stmt, 3, ends_at), db,
|
||||
"bind contest.ends_at");
|
||||
ThrowSqlite(sqlite3_bind_text(stmt, 4, rule_json.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind contest.rule_json");
|
||||
ThrowSqlite(sqlite3_step(stmt), db, "insert contest");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void InsertContestProblem(sqlite3* db,
|
||||
int64_t contest_id,
|
||||
int64_t problem_id,
|
||||
int idx) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT OR IGNORE INTO contest_problems(contest_id,problem_id,idx) VALUES(?,?,?)";
|
||||
ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare insert contest_problem");
|
||||
ThrowSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db,
|
||||
"bind contest_problem.contest_id");
|
||||
ThrowSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db,
|
||||
"bind contest_problem.problem_id");
|
||||
ThrowSqlite(sqlite3_bind_int(stmt, 3, idx), db,
|
||||
"bind contest_problem.idx");
|
||||
ThrowSqlite(sqlite3_step(stmt), db, "insert contest_problem");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SqliteDb SqliteDb::OpenFile(const std::string& path) {
|
||||
@@ -60,11 +255,9 @@ void SqliteDb::Exec(const std::string& sql) {
|
||||
}
|
||||
|
||||
void ApplyMigrations(SqliteDb& db) {
|
||||
// Keep it simple for MVP: apply the bundled init SQL.
|
||||
// In later iterations we'll add a migrations table + incremental runner.
|
||||
// Keep it simple for MVP: create missing tables, then patch missing columns.
|
||||
db.Exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
// 001_init.sql (embedded)
|
||||
db.Exec(R"SQL(
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -90,6 +283,10 @@ CREATE TABLE IF NOT EXISTS problems (
|
||||
statement_md TEXT NOT NULL,
|
||||
difficulty INTEGER NOT NULL DEFAULT 1,
|
||||
source TEXT NOT NULL DEFAULT "",
|
||||
statement_url TEXT NOT NULL DEFAULT "",
|
||||
llm_profile_json TEXT NOT NULL DEFAULT "{}",
|
||||
sample_input TEXT NOT NULL DEFAULT "",
|
||||
sample_output TEXT NOT NULL DEFAULT "",
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
@@ -104,15 +301,19 @@ CREATE TABLE IF NOT EXISTS submissions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
problem_id INTEGER NOT NULL,
|
||||
contest_id INTEGER,
|
||||
language TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
score INTEGER NOT NULL DEFAULT 0,
|
||||
time_ms INTEGER NOT NULL DEFAULT 0,
|
||||
memory_kb INTEGER NOT NULL DEFAULT 0,
|
||||
compile_log TEXT NOT NULL DEFAULT "",
|
||||
runtime_log TEXT NOT NULL DEFAULT "",
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wrong_book (
|
||||
@@ -127,9 +328,280 @@ CREATE TABLE IF NOT EXISTS wrong_book (
|
||||
FOREIGN KEY(last_submission_id) REFERENCES submissions(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
starts_at INTEGER NOT NULL,
|
||||
ends_at INTEGER NOT NULL,
|
||||
rule_json TEXT NOT NULL DEFAULT "{}"
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contest_problems (
|
||||
contest_id INTEGER NOT NULL,
|
||||
problem_id INTEGER NOT NULL,
|
||||
idx INTEGER NOT NULL,
|
||||
PRIMARY KEY(contest_id, problem_id),
|
||||
FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contest_registrations (
|
||||
contest_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
registered_at INTEGER NOT NULL,
|
||||
PRIMARY KEY(contest_id, user_id),
|
||||
FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_articles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
content_md TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_article_links (
|
||||
article_id INTEGER NOT NULL,
|
||||
problem_id INTEGER NOT NULL,
|
||||
PRIMARY KEY(article_id, problem_id),
|
||||
FOREIGN KEY(article_id) REFERENCES kb_articles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS import_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
status TEXT NOT NULL,
|
||||
trigger TEXT NOT NULL DEFAULT "manual",
|
||||
total_count INTEGER NOT NULL DEFAULT 0,
|
||||
processed_count INTEGER NOT NULL DEFAULT 0,
|
||||
success_count INTEGER NOT NULL DEFAULT 0,
|
||||
failed_count INTEGER NOT NULL DEFAULT 0,
|
||||
options_json TEXT NOT NULL DEFAULT "{}",
|
||||
last_error TEXT NOT NULL DEFAULT "",
|
||||
started_at INTEGER NOT NULL,
|
||||
finished_at INTEGER,
|
||||
updated_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS import_job_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id INTEGER NOT NULL,
|
||||
source_path TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT "queued",
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
difficulty INTEGER NOT NULL DEFAULT 0,
|
||||
problem_id INTEGER,
|
||||
error_text TEXT NOT NULL DEFAULT "",
|
||||
started_at INTEGER,
|
||||
finished_at INTEGER,
|
||||
updated_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(job_id) REFERENCES import_jobs(id) ON DELETE CASCADE,
|
||||
UNIQUE(job_id, source_path)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problem_drafts (
|
||||
user_id INTEGER NOT NULL,
|
||||
problem_id INTEGER NOT NULL,
|
||||
language TEXT NOT NULL DEFAULT "cpp",
|
||||
code TEXT NOT NULL DEFAULT "",
|
||||
stdin TEXT NOT NULL DEFAULT "",
|
||||
updated_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY(user_id, problem_id),
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problem_solution_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
problem_id INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT "queued",
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
message TEXT NOT NULL DEFAULT "",
|
||||
created_by INTEGER NOT NULL DEFAULT 0,
|
||||
max_solutions INTEGER NOT NULL DEFAULT 3,
|
||||
created_at INTEGER NOT NULL,
|
||||
started_at INTEGER,
|
||||
finished_at INTEGER,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problem_solutions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
problem_id INTEGER NOT NULL,
|
||||
variant INTEGER NOT NULL DEFAULT 1,
|
||||
title TEXT NOT NULL DEFAULT "",
|
||||
idea_md TEXT NOT NULL DEFAULT "",
|
||||
explanation_md TEXT NOT NULL DEFAULT "",
|
||||
code_cpp TEXT NOT NULL DEFAULT "",
|
||||
complexity TEXT NOT NULL DEFAULT "",
|
||||
tags_json TEXT NOT NULL DEFAULT "[]",
|
||||
source TEXT NOT NULL DEFAULT "llm",
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
)SQL");
|
||||
|
||||
// Backward-compatible schema upgrades for existing deployments.
|
||||
EnsureColumn(db, "problems", "sample_input",
|
||||
"sample_input TEXT NOT NULL DEFAULT ''");
|
||||
EnsureColumn(db, "problems", "sample_output",
|
||||
"sample_output TEXT NOT NULL DEFAULT ''");
|
||||
EnsureColumn(db, "problems", "statement_url",
|
||||
"statement_url TEXT NOT NULL DEFAULT ''");
|
||||
EnsureColumn(db, "problems", "llm_profile_json",
|
||||
"llm_profile_json TEXT NOT NULL DEFAULT '{}'");
|
||||
EnsureColumn(db, "import_jobs", "trigger",
|
||||
"trigger TEXT NOT NULL DEFAULT 'manual'");
|
||||
EnsureColumn(db, "import_jobs", "options_json",
|
||||
"options_json TEXT NOT NULL DEFAULT '{}'");
|
||||
EnsureColumn(db, "import_jobs", "last_error",
|
||||
"last_error TEXT NOT NULL DEFAULT ''");
|
||||
EnsureColumn(db, "submissions", "contest_id", "contest_id INTEGER");
|
||||
EnsureColumn(db, "submissions", "compile_log",
|
||||
"compile_log TEXT NOT NULL DEFAULT ''");
|
||||
EnsureColumn(db, "submissions", "runtime_log",
|
||||
"runtime_log TEXT NOT NULL DEFAULT ''");
|
||||
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, "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",
|
||||
"explanation_md TEXT NOT NULL DEFAULT ''");
|
||||
EnsureColumn(db, "problem_solutions", "code_cpp", "code_cpp TEXT NOT NULL DEFAULT ''");
|
||||
EnsureColumn(db, "problem_solutions", "complexity",
|
||||
"complexity TEXT NOT NULL DEFAULT ''");
|
||||
EnsureColumn(db, "problem_solutions", "tags_json", "tags_json TEXT NOT NULL DEFAULT '[]'");
|
||||
|
||||
// Build indexes after compatibility ALTERs so old schemas won't fail on
|
||||
// missing columns (e.g. legacy submissions table without contest_id).
|
||||
db.Exec(R"SQL(
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_user_created_at ON submissions(user_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_problem_created_at ON submissions(problem_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_submissions_contest_user_created_at ON submissions(contest_id, user_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_problem_tags_tag ON problem_tags(tag);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_article_links_problem_id ON kb_article_links(problem_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_import_jobs_created_at ON import_jobs(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_import_jobs_status ON import_jobs(status, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_import_job_items_job_status ON import_job_items(job_id, status, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_problem_drafts_updated ON problem_drafts(updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_problem_solution_jobs_problem ON problem_solution_jobs(problem_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_problem_solutions_problem ON problem_solutions(problem_id, variant, id);
|
||||
)SQL");
|
||||
}
|
||||
|
||||
void SeedDemoData(SqliteDb& db) {
|
||||
sqlite3* raw = db.raw();
|
||||
const int64_t now = NowSec();
|
||||
|
||||
if (CountRows(raw, "problems") == 0) {
|
||||
InsertProblem(
|
||||
raw,
|
||||
"a-plus-b",
|
||||
"A + B",
|
||||
"给定两个整数 A 与 B,输出它们的和。",
|
||||
1,
|
||||
"CSP-入门",
|
||||
"1 2\n",
|
||||
"3\n",
|
||||
now);
|
||||
|
||||
InsertProblem(
|
||||
raw,
|
||||
"fibonacci-n",
|
||||
"Fibonacci 第 n 项",
|
||||
"输入 n (0<=n<=40),输出第 n 项 Fibonacci 数。",
|
||||
2,
|
||||
"CSP-基础",
|
||||
"10\n",
|
||||
"55\n",
|
||||
now);
|
||||
|
||||
InsertProblem(
|
||||
raw,
|
||||
"sort-numbers",
|
||||
"整数排序",
|
||||
"输入 n 和 n 个整数,按升序输出。",
|
||||
2,
|
||||
"CSP-基础",
|
||||
"5\n5 1 4 2 3\n",
|
||||
"1 2 3 4 5\n",
|
||||
now);
|
||||
}
|
||||
|
||||
if (CountRows(raw, "problem_tags") == 0) {
|
||||
const auto p1 =
|
||||
QueryOneId(raw, "SELECT id FROM problems WHERE slug='a-plus-b'");
|
||||
const auto p2 =
|
||||
QueryOneId(raw, "SELECT id FROM problems WHERE slug='fibonacci-n'");
|
||||
const auto p3 =
|
||||
QueryOneId(raw, "SELECT id FROM problems WHERE slug='sort-numbers'");
|
||||
if (p1) {
|
||||
InsertProblemTag(raw, *p1, "math");
|
||||
InsertProblemTag(raw, *p1, "implementation");
|
||||
}
|
||||
if (p2) {
|
||||
InsertProblemTag(raw, *p2, "dp");
|
||||
InsertProblemTag(raw, *p2, "recursion");
|
||||
}
|
||||
if (p3) {
|
||||
InsertProblemTag(raw, *p3, "sort");
|
||||
InsertProblemTag(raw, *p3, "array");
|
||||
}
|
||||
}
|
||||
|
||||
if (CountRows(raw, "kb_articles") == 0) {
|
||||
InsertKbArticle(
|
||||
raw,
|
||||
"cpp-fast-io",
|
||||
"C++ 快速输入输出",
|
||||
"# C++ 快速输入输出\n\n在 OI/CSP 中,建议关闭同步并解绑 cin/cout:\n\n```cpp\nstd::ios::sync_with_stdio(false);\nstd::cin.tie(nullptr);\n```\n",
|
||||
now);
|
||||
|
||||
InsertKbArticle(
|
||||
raw,
|
||||
"intro-dp",
|
||||
"动态规划入门",
|
||||
"# 动态规划入门\n\n动态规划的核心是:**状态定义**、**状态转移**、**边界条件**。\n",
|
||||
now);
|
||||
}
|
||||
|
||||
if (CountRows(raw, "kb_article_links") == 0) {
|
||||
const auto p1 =
|
||||
QueryOneId(raw, "SELECT id FROM problems WHERE slug='a-plus-b'");
|
||||
const auto p2 =
|
||||
QueryOneId(raw, "SELECT id FROM problems WHERE slug='fibonacci-n'");
|
||||
const auto a1 =
|
||||
QueryOneId(raw, "SELECT id FROM kb_articles WHERE slug='cpp-fast-io'");
|
||||
const auto a2 =
|
||||
QueryOneId(raw, "SELECT id FROM kb_articles WHERE slug='intro-dp'");
|
||||
if (a1 && p1) InsertKbLink(raw, *a1, *p1);
|
||||
if (a2 && p2) InsertKbLink(raw, *a2, *p2);
|
||||
}
|
||||
|
||||
if (CountRows(raw, "contests") == 0) {
|
||||
InsertContest(
|
||||
raw,
|
||||
"CSP 模拟赛(示例)",
|
||||
now - 3600,
|
||||
now + 7 * 24 * 3600,
|
||||
R"({"type":"acm","desc":"按通过题数与罚时排名"})");
|
||||
}
|
||||
|
||||
if (CountRows(raw, "contest_problems") == 0) {
|
||||
const auto contest_id = QueryOneId(raw, "SELECT id FROM contests ORDER BY id LIMIT 1");
|
||||
const auto p1 = QueryOneId(raw, "SELECT id FROM problems ORDER BY id LIMIT 1");
|
||||
const auto p2 = QueryOneId(raw, "SELECT id FROM problems ORDER BY id LIMIT 1 OFFSET 1");
|
||||
if (contest_id && p1) InsertContestProblem(raw, *contest_id, *p1, 1);
|
||||
if (contest_id && p2) InsertContestProblem(raw, *contest_id, *p2, 2);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::db
|
||||
|
||||
@@ -19,6 +19,10 @@ Json::Value ToJson(const Problem& p) {
|
||||
j["statement_md"] = p.statement_md;
|
||||
j["difficulty"] = p.difficulty;
|
||||
j["source"] = p.source;
|
||||
j["statement_url"] = p.statement_url;
|
||||
j["llm_profile_json"] = p.llm_profile_json;
|
||||
j["sample_input"] = p.sample_input;
|
||||
j["sample_output"] = p.sample_output;
|
||||
j["created_at"] = Json::Int64(p.created_at);
|
||||
return j;
|
||||
}
|
||||
@@ -28,11 +32,18 @@ Json::Value ToJson(const Submission& s) {
|
||||
j["id"] = Json::Int64(s.id);
|
||||
j["user_id"] = Json::Int64(s.user_id);
|
||||
j["problem_id"] = Json::Int64(s.problem_id);
|
||||
if (s.contest_id.has_value()) {
|
||||
j["contest_id"] = Json::Int64(*s.contest_id);
|
||||
} else {
|
||||
j["contest_id"] = Json::nullValue;
|
||||
}
|
||||
j["language"] = ToString(s.language);
|
||||
j["status"] = ToString(s.status);
|
||||
j["score"] = s.score;
|
||||
j["time_ms"] = s.time_ms;
|
||||
j["memory_kb"] = s.memory_kb;
|
||||
j["compile_log"] = s.compile_log;
|
||||
j["runtime_log"] = s.runtime_log;
|
||||
j["created_at"] = Json::Int64(s.created_at);
|
||||
return j;
|
||||
}
|
||||
@@ -51,4 +62,42 @@ Json::Value ToJson(const WrongBookItem& w) {
|
||||
return j;
|
||||
}
|
||||
|
||||
Json::Value ToJson(const Contest& c) {
|
||||
Json::Value j;
|
||||
j["id"] = Json::Int64(c.id);
|
||||
j["title"] = c.title;
|
||||
j["starts_at"] = Json::Int64(c.starts_at);
|
||||
j["ends_at"] = Json::Int64(c.ends_at);
|
||||
j["rule_json"] = c.rule_json;
|
||||
return j;
|
||||
}
|
||||
|
||||
Json::Value ToJson(const KbArticle& a) {
|
||||
Json::Value j;
|
||||
j["id"] = Json::Int64(a.id);
|
||||
j["slug"] = a.slug;
|
||||
j["title"] = a.title;
|
||||
j["content_md"] = a.content_md;
|
||||
j["created_at"] = Json::Int64(a.created_at);
|
||||
return j;
|
||||
}
|
||||
|
||||
Json::Value ToJson(const GlobalLeaderboardEntry& e) {
|
||||
Json::Value j;
|
||||
j["user_id"] = Json::Int64(e.user_id);
|
||||
j["username"] = e.username;
|
||||
j["rating"] = e.rating;
|
||||
j["created_at"] = Json::Int64(e.created_at);
|
||||
return j;
|
||||
}
|
||||
|
||||
Json::Value ToJson(const ContestLeaderboardEntry& e) {
|
||||
Json::Value j;
|
||||
j["user_id"] = Json::Int64(e.user_id);
|
||||
j["username"] = e.username;
|
||||
j["solved"] = e.solved;
|
||||
j["penalty_sec"] = Json::Int64(e.penalty_sec);
|
||||
return j;
|
||||
}
|
||||
|
||||
} // namespace csp::domain
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/services/auth_service.h"
|
||||
#include "csp/services/import_runner.h"
|
||||
#include "csp/services/problem_gen_runner.h"
|
||||
#include "csp/services/problem_solution_runner.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
@@ -12,6 +15,9 @@ int main(int argc, char** argv) {
|
||||
if (!parent.empty()) std::filesystem::create_directories(parent);
|
||||
|
||||
csp::AppState::Instance().Init(db_path);
|
||||
csp::services::ImportRunner::Instance().Configure(db_path);
|
||||
csp::services::ProblemSolutionRunner::Instance().Configure(db_path);
|
||||
csp::services::ProblemGenRunner::Instance().Configure(db_path);
|
||||
|
||||
// Optional seed admin user for dev/test.
|
||||
{
|
||||
@@ -29,6 +35,11 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run PDF -> LLM import workflow on startup unless explicitly disabled.
|
||||
csp::services::ImportRunner::Instance().AutoStartIfEnabled();
|
||||
// Auto-generate one CSP-J new problem (RAG + dedupe) on startup by default.
|
||||
csp::services::ProblemGenRunner::Instance().AutoStartIfEnabled();
|
||||
|
||||
// CORS (dev-friendly). In production, prefer reverse proxy same-origin.
|
||||
drogon::app().registerPreRoutingAdvice([](const drogon::HttpRequestPtr& req,
|
||||
drogon::AdviceCallback&& cb,
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
#include "csp/services/contest_service.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
void CheckSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
|
||||
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
int64_t NowSec() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
std::string ColText(sqlite3_stmt* stmt, int col) {
|
||||
const unsigned char* txt = sqlite3_column_text(stmt, col);
|
||||
return txt ? reinterpret_cast<const char*>(txt) : std::string();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<domain::Contest> ContestService::ListContests() {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,title,starts_at,ends_at,rule_json FROM contests ORDER BY id ASC";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare list contests");
|
||||
|
||||
std::vector<domain::Contest> out;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
domain::Contest c;
|
||||
c.id = sqlite3_column_int64(stmt, 0);
|
||||
c.title = ColText(stmt, 1);
|
||||
c.starts_at = sqlite3_column_int64(stmt, 2);
|
||||
c.ends_at = sqlite3_column_int64(stmt, 3);
|
||||
c.rule_json = ColText(stmt, 4);
|
||||
out.push_back(std::move(c));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<domain::Contest> ContestService::GetContest(int64_t contest_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,title,starts_at,ends_at,rule_json FROM contests WHERE id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare get contest");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db,
|
||||
"bind contest_id");
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
domain::Contest c;
|
||||
c.id = sqlite3_column_int64(stmt, 0);
|
||||
c.title = ColText(stmt, 1);
|
||||
c.starts_at = sqlite3_column_int64(stmt, 2);
|
||||
c.ends_at = sqlite3_column_int64(stmt, 3);
|
||||
c.rule_json = ColText(stmt, 4);
|
||||
sqlite3_finalize(stmt);
|
||||
return c;
|
||||
}
|
||||
|
||||
std::vector<domain::Problem> ContestService::ListContestProblems(int64_t contest_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT p.id,p.slug,p.title,p.statement_md,p.difficulty,p.source,p.statement_url,p.llm_profile_json,"
|
||||
"p.sample_input,p.sample_output,p.created_at "
|
||||
"FROM contest_problems cp "
|
||||
"JOIN problems p ON p.id=cp.problem_id "
|
||||
"WHERE cp.contest_id=? ORDER BY cp.idx ASC";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare list contest problems");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db,
|
||||
"bind contest_id");
|
||||
|
||||
std::vector<domain::Problem> out;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
domain::Problem p;
|
||||
p.id = sqlite3_column_int64(stmt, 0);
|
||||
p.slug = ColText(stmt, 1);
|
||||
p.title = ColText(stmt, 2);
|
||||
p.statement_md = ColText(stmt, 3);
|
||||
p.difficulty = sqlite3_column_int(stmt, 4);
|
||||
p.source = ColText(stmt, 5);
|
||||
p.statement_url = ColText(stmt, 6);
|
||||
p.llm_profile_json = ColText(stmt, 7);
|
||||
p.sample_input = ColText(stmt, 8);
|
||||
p.sample_output = ColText(stmt, 9);
|
||||
p.created_at = sqlite3_column_int64(stmt, 10);
|
||||
out.push_back(std::move(p));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
void ContestService::Register(int64_t contest_id, int64_t user_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT OR IGNORE INTO contest_registrations(contest_id,user_id,registered_at) VALUES(?,?,?)";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare contest register");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db,
|
||||
"bind contest_id");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 2, user_id), db,
|
||||
"bind user_id");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 3, NowSec()), db,
|
||||
"bind registered_at");
|
||||
CheckSqlite(sqlite3_step(stmt), db, "contest register");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
bool ContestService::IsRegistered(int64_t contest_id, int64_t user_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT 1 FROM contest_registrations WHERE contest_id=? AND user_id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare contest is_registered");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db,
|
||||
"bind contest_id");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 2, user_id), db, "bind user_id");
|
||||
const bool exists = sqlite3_step(stmt) == SQLITE_ROW;
|
||||
sqlite3_finalize(stmt);
|
||||
return exists;
|
||||
}
|
||||
|
||||
bool ContestService::ContainsProblem(int64_t contest_id, int64_t problem_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT 1 FROM contest_problems WHERE contest_id=? AND problem_id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare contest contains_problem");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db,
|
||||
"bind contest_id");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db,
|
||||
"bind problem_id");
|
||||
const bool exists = sqlite3_step(stmt) == SQLITE_ROW;
|
||||
sqlite3_finalize(stmt);
|
||||
return exists;
|
||||
}
|
||||
|
||||
bool ContestService::IsRunning(int64_t contest_id) {
|
||||
const auto c = GetContest(contest_id);
|
||||
if (!c.has_value()) return false;
|
||||
const auto now = NowSec();
|
||||
return now >= c->starts_at && now <= c->ends_at;
|
||||
}
|
||||
|
||||
std::vector<domain::ContestLeaderboardEntry> ContestService::Leaderboard(int64_t contest_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
|
||||
const char* sql = R"SQL(
|
||||
SELECT
|
||||
r.user_id,
|
||||
u.username,
|
||||
COUNT(a.problem_id) AS solved,
|
||||
COALESCE(SUM(a.first_ac - c.starts_at), 0) AS penalty_sec
|
||||
FROM contest_registrations r
|
||||
JOIN users u ON u.id = r.user_id
|
||||
JOIN contests c ON c.id = r.contest_id
|
||||
LEFT JOIN (
|
||||
SELECT user_id, problem_id, MIN(created_at) AS first_ac
|
||||
FROM submissions
|
||||
WHERE contest_id = ? AND status = 'AC'
|
||||
GROUP BY user_id, problem_id
|
||||
) a ON a.user_id = r.user_id
|
||||
WHERE r.contest_id = ?
|
||||
GROUP BY r.user_id, u.username, c.starts_at
|
||||
ORDER BY solved DESC, penalty_sec ASC, r.user_id ASC
|
||||
)SQL";
|
||||
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare contest leaderboard");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db,
|
||||
"bind contest_id ac");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 2, contest_id), db,
|
||||
"bind contest_id reg");
|
||||
|
||||
std::vector<domain::ContestLeaderboardEntry> out;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
domain::ContestLeaderboardEntry e;
|
||||
e.user_id = sqlite3_column_int64(stmt, 0);
|
||||
e.username = ColText(stmt, 1);
|
||||
e.solved = sqlite3_column_int(stmt, 2);
|
||||
e.penalty_sec = sqlite3_column_int64(stmt, 3);
|
||||
out.push_back(std::move(e));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,183 @@
|
||||
#include "csp/services/import_runner.h"
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
int64_t NowSec() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
bool EnvBool(const char* key, bool default_value) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw) return default_value;
|
||||
std::string val(raw);
|
||||
for (auto& c : val) c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
|
||||
if (val == "1" || val == "true" || val == "yes" || val == "on") return true;
|
||||
if (val == "0" || val == "false" || val == "no" || val == "off") return false;
|
||||
return default_value;
|
||||
}
|
||||
|
||||
int EnvInt(const char* key, int default_value) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw) return default_value;
|
||||
try {
|
||||
return std::stoi(raw);
|
||||
} catch (...) {
|
||||
return default_value;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ShellQuote(const std::string& text) {
|
||||
std::string out = "'";
|
||||
for (char c : text) {
|
||||
if (c == '\'') {
|
||||
out += "'\"'\"'";
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string ResolveScriptPath() {
|
||||
const char* env_path = std::getenv("OI_IMPORT_SCRIPT_PATH");
|
||||
if (env_path && std::filesystem::exists(env_path)) return env_path;
|
||||
|
||||
const std::vector<std::string> candidates = {
|
||||
"/app/scripts/import_luogu_csp.py",
|
||||
"scripts/import_luogu_csp.py",
|
||||
"../scripts/import_luogu_csp.py",
|
||||
"../../scripts/import_luogu_csp.py",
|
||||
"/app/scripts/import_winterant_oi.py",
|
||||
"scripts/import_winterant_oi.py",
|
||||
"../scripts/import_winterant_oi.py",
|
||||
"../../scripts/import_winterant_oi.py",
|
||||
};
|
||||
for (const auto& p : candidates) {
|
||||
if (std::filesystem::exists(p)) return p;
|
||||
}
|
||||
return "/app/scripts/import_luogu_csp.py";
|
||||
}
|
||||
|
||||
std::string BuildCommand(const std::string& db_path,
|
||||
const std::string& trigger,
|
||||
const ImportRunOptions& options) {
|
||||
const std::string script_path = ResolveScriptPath();
|
||||
const int workers = std::max(1, EnvInt("OI_IMPORT_WORKERS", 3));
|
||||
const int llm_limit = EnvInt("OI_IMPORT_LLM_LIMIT", 0);
|
||||
const int max_problems = EnvInt("OI_IMPORT_MAX_PROBLEMS", 0);
|
||||
const bool skip_llm = EnvBool("OI_IMPORT_SKIP_LLM", false);
|
||||
const bool clear_existing = EnvBool("OI_IMPORT_CLEAR_EXISTING", true);
|
||||
const bool clear_all_default = EnvBool("OI_IMPORT_CLEAR_ALL_PROBLEMS", false);
|
||||
const std::string clear_source_prefix =
|
||||
std::getenv("OI_IMPORT_CLEAR_SOURCE_PREFIX")
|
||||
? std::string(std::getenv("OI_IMPORT_CLEAR_SOURCE_PREFIX"))
|
||||
: std::string("winterant/oi");
|
||||
|
||||
std::string cmd = "python3 " + ShellQuote(script_path) + " --db-path " +
|
||||
ShellQuote(db_path) + " --workers " +
|
||||
std::to_string(workers) + " --job-trigger " +
|
||||
ShellQuote(trigger);
|
||||
|
||||
if (max_problems > 0) cmd += " --max-problems " + std::to_string(max_problems);
|
||||
if (skip_llm) cmd += " --skip-llm";
|
||||
if (llm_limit > 0) cmd += " --llm-limit " + std::to_string(llm_limit);
|
||||
|
||||
const bool clear_all = options.clear_all_problems || clear_all_default;
|
||||
if (clear_all) {
|
||||
cmd += " --clear-all-problems";
|
||||
} else if (clear_existing) {
|
||||
cmd += " --clear-existing --clear-existing-source-prefix " +
|
||||
ShellQuote(clear_source_prefix);
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ImportRunner& ImportRunner::Instance() {
|
||||
static ImportRunner inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void ImportRunner::Configure(std::string db_path) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
db_path_ = std::move(db_path);
|
||||
}
|
||||
|
||||
bool ImportRunner::TriggerAsync(const std::string& trigger,
|
||||
const ImportRunOptions& options) {
|
||||
std::string cmd;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (running_) return false;
|
||||
if (db_path_.empty()) return false;
|
||||
|
||||
cmd = BuildCommand(db_path_, trigger, options);
|
||||
running_ = true;
|
||||
last_started_at_ = NowSec();
|
||||
last_command_ = cmd;
|
||||
last_exit_code_.reset();
|
||||
}
|
||||
|
||||
std::thread([this, command = std::move(cmd)]() {
|
||||
const int rc = std::system(command.c_str());
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
last_exit_code_ = rc;
|
||||
last_finished_at_ = NowSec();
|
||||
running_ = false;
|
||||
}).detach();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImportRunner::AutoStartIfEnabled() {
|
||||
if (!EnvBool("OI_IMPORT_AUTO_RUN", true)) return;
|
||||
const bool started = TriggerAsync("auto", ImportRunOptions{});
|
||||
if (started) {
|
||||
LOG_INFO << "import runner auto-started";
|
||||
} else {
|
||||
LOG_INFO << "import runner auto-start skipped";
|
||||
}
|
||||
}
|
||||
|
||||
bool ImportRunner::IsRunning() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return running_;
|
||||
}
|
||||
|
||||
std::string ImportRunner::LastCommand() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return last_command_;
|
||||
}
|
||||
|
||||
std::optional<int> ImportRunner::LastExitCode() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return last_exit_code_;
|
||||
}
|
||||
|
||||
int64_t ImportRunner::LastStartedAt() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return last_started_at_;
|
||||
}
|
||||
|
||||
int64_t ImportRunner::LastFinishedAt() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return last_finished_at_;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,138 @@
|
||||
#include "csp/services/import_service.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
void CheckSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
|
||||
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
std::string ColText(sqlite3_stmt* stmt, int col) {
|
||||
const unsigned char* txt = sqlite3_column_text(stmt, col);
|
||||
return txt ? reinterpret_cast<const char*>(txt) : std::string();
|
||||
}
|
||||
|
||||
std::optional<int64_t> ColNullableInt64(sqlite3_stmt* stmt, int col) {
|
||||
if (sqlite3_column_type(stmt, col) == SQLITE_NULL) return std::nullopt;
|
||||
return sqlite3_column_int64(stmt, col);
|
||||
}
|
||||
|
||||
ImportJob ReadJob(sqlite3_stmt* stmt) {
|
||||
ImportJob j;
|
||||
j.id = sqlite3_column_int64(stmt, 0);
|
||||
j.status = ColText(stmt, 1);
|
||||
j.trigger = ColText(stmt, 2);
|
||||
j.total_count = sqlite3_column_int(stmt, 3);
|
||||
j.processed_count = sqlite3_column_int(stmt, 4);
|
||||
j.success_count = sqlite3_column_int(stmt, 5);
|
||||
j.failed_count = sqlite3_column_int(stmt, 6);
|
||||
j.options_json = ColText(stmt, 7);
|
||||
j.last_error = ColText(stmt, 8);
|
||||
j.started_at = sqlite3_column_int64(stmt, 9);
|
||||
j.finished_at = ColNullableInt64(stmt, 10);
|
||||
j.updated_at = sqlite3_column_int64(stmt, 11);
|
||||
j.created_at = sqlite3_column_int64(stmt, 12);
|
||||
return j;
|
||||
}
|
||||
|
||||
ImportJobItem ReadItem(sqlite3_stmt* stmt) {
|
||||
ImportJobItem item;
|
||||
item.id = sqlite3_column_int64(stmt, 0);
|
||||
item.job_id = sqlite3_column_int64(stmt, 1);
|
||||
item.source_path = ColText(stmt, 2);
|
||||
item.status = ColText(stmt, 3);
|
||||
item.title = ColText(stmt, 4);
|
||||
item.difficulty = sqlite3_column_int(stmt, 5);
|
||||
item.problem_id = ColNullableInt64(stmt, 6);
|
||||
item.error_text = ColText(stmt, 7);
|
||||
item.started_at = ColNullableInt64(stmt, 8);
|
||||
item.finished_at = ColNullableInt64(stmt, 9);
|
||||
item.updated_at = sqlite3_column_int64(stmt, 10);
|
||||
item.created_at = sqlite3_column_int64(stmt, 11);
|
||||
return item;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<ImportJob> ImportService::GetLatestJob() {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,status,trigger,total_count,processed_count,success_count,failed_count,"
|
||||
"options_json,last_error,started_at,finished_at,updated_at,created_at "
|
||||
"FROM import_jobs ORDER BY id DESC LIMIT 1";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare get latest import job");
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
auto row = ReadJob(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return row;
|
||||
}
|
||||
|
||||
std::optional<ImportJob> ImportService::GetById(int64_t job_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,status,trigger,total_count,processed_count,success_count,failed_count,"
|
||||
"options_json,last_error,started_at,finished_at,updated_at,created_at "
|
||||
"FROM import_jobs WHERE id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare get import job by id");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, job_id), db, "bind job_id");
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
auto row = ReadJob(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return row;
|
||||
}
|
||||
|
||||
std::vector<ImportJobItem> ImportService::ListItems(int64_t job_id,
|
||||
const ImportJobItemQuery& query) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
std::string sql =
|
||||
"SELECT id,job_id,source_path,status,title,difficulty,problem_id,error_text,"
|
||||
"started_at,finished_at,updated_at,created_at "
|
||||
"FROM import_job_items WHERE job_id=?";
|
||||
if (!query.status.empty()) {
|
||||
sql += " AND status=?";
|
||||
}
|
||||
sql += " ORDER BY id ASC LIMIT ? OFFSET ?";
|
||||
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr), db,
|
||||
"prepare list import job items");
|
||||
int bind_index = 1;
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, bind_index++, job_id), db, "bind job_id");
|
||||
if (!query.status.empty()) {
|
||||
CheckSqlite(
|
||||
sqlite3_bind_text(stmt, bind_index++, query.status.c_str(), -1, SQLITE_TRANSIENT),
|
||||
db, "bind status");
|
||||
}
|
||||
const int page = query.page <= 0 ? 1 : query.page;
|
||||
const int page_size = std::max(1, std::min(500, query.page_size <= 0 ? 50 : query.page_size));
|
||||
const int offset = (page - 1) * page_size;
|
||||
CheckSqlite(sqlite3_bind_int(stmt, bind_index++, page_size), db, "bind page_size");
|
||||
CheckSqlite(sqlite3_bind_int(stmt, bind_index++, offset), db, "bind offset");
|
||||
|
||||
std::vector<ImportJobItem> rows;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
rows.push_back(ReadItem(stmt));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return rows;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "csp/services/kb_service.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
void CheckSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
|
||||
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
std::string ColText(sqlite3_stmt* stmt, int col) {
|
||||
const unsigned char* txt = sqlite3_column_text(stmt, col);
|
||||
return txt ? reinterpret_cast<const char*>(txt) : std::string();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<domain::KbArticle> KbService::ListArticles() {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,slug,title,content_md,created_at FROM kb_articles ORDER BY id ASC";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare kb list");
|
||||
|
||||
std::vector<domain::KbArticle> out;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
domain::KbArticle a;
|
||||
a.id = sqlite3_column_int64(stmt, 0);
|
||||
a.slug = ColText(stmt, 1);
|
||||
a.title = ColText(stmt, 2);
|
||||
a.content_md = ColText(stmt, 3);
|
||||
a.created_at = sqlite3_column_int64(stmt, 4);
|
||||
out.push_back(std::move(a));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<KbArticleDetail> KbService::GetBySlug(const std::string& slug) {
|
||||
sqlite3* db = db_.raw();
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,slug,title,content_md,created_at FROM kb_articles WHERE slug=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare kb get");
|
||||
CheckSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind kb slug");
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
KbArticleDetail detail;
|
||||
detail.article.id = sqlite3_column_int64(stmt, 0);
|
||||
detail.article.slug = ColText(stmt, 1);
|
||||
detail.article.title = ColText(stmt, 2);
|
||||
detail.article.content_md = ColText(stmt, 3);
|
||||
detail.article.created_at = sqlite3_column_int64(stmt, 4);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sqlite3_stmt* link_stmt = nullptr;
|
||||
const char* link_sql =
|
||||
"SELECT p.id,p.title FROM kb_article_links l "
|
||||
"JOIN problems p ON p.id=l.problem_id "
|
||||
"WHERE l.article_id=? ORDER BY p.id ASC";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, link_sql, -1, &link_stmt, nullptr), db,
|
||||
"prepare kb links");
|
||||
CheckSqlite(sqlite3_bind_int64(link_stmt, 1, detail.article.id), db,
|
||||
"bind article_id");
|
||||
while (sqlite3_step(link_stmt) == SQLITE_ROW) {
|
||||
detail.related_problems.emplace_back(sqlite3_column_int64(link_stmt, 0),
|
||||
ColText(link_stmt, 1));
|
||||
}
|
||||
sqlite3_finalize(link_stmt);
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "csp/services/problem_gen_runner.h"
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
#include "csp/services/import_runner.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
int64_t NowSec() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
bool EnvBool(const char* key, bool default_value) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw) return default_value;
|
||||
std::string val(raw);
|
||||
for (auto& c : val) c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
|
||||
if (val == "1" || val == "true" || val == "yes" || val == "on") return true;
|
||||
if (val == "0" || val == "false" || val == "no" || val == "off") return false;
|
||||
return default_value;
|
||||
}
|
||||
|
||||
int EnvInt(const char* key, int default_value) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw) return default_value;
|
||||
try {
|
||||
return std::stoi(raw);
|
||||
} catch (...) {
|
||||
return default_value;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ShellQuote(const std::string& text) {
|
||||
std::string out = "'";
|
||||
for (char c : text) {
|
||||
if (c == '\'') {
|
||||
out += "'\"'\"'";
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string ResolveScriptPath() {
|
||||
const char* env_path = std::getenv("CSP_GEN_SCRIPT_PATH");
|
||||
if (env_path && std::filesystem::exists(env_path)) return env_path;
|
||||
|
||||
const std::vector<std::string> candidates = {
|
||||
"/app/scripts/generate_cspj_problem_rag.py",
|
||||
"scripts/generate_cspj_problem_rag.py",
|
||||
"../scripts/generate_cspj_problem_rag.py",
|
||||
"../../scripts/generate_cspj_problem_rag.py",
|
||||
};
|
||||
for (const auto& p : candidates) {
|
||||
if (std::filesystem::exists(p)) return p;
|
||||
}
|
||||
return "/app/scripts/generate_cspj_problem_rag.py";
|
||||
}
|
||||
|
||||
std::string BuildCommand(const std::string& db_path,
|
||||
const std::string& trigger,
|
||||
int count) {
|
||||
const std::string script_path = ResolveScriptPath();
|
||||
const int final_count = std::max(1, std::min(5, count));
|
||||
return "python3 " + ShellQuote(script_path) + " --db-path " +
|
||||
ShellQuote(db_path) + " --count " + std::to_string(final_count);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProblemGenRunner& ProblemGenRunner::Instance() {
|
||||
static ProblemGenRunner inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void ProblemGenRunner::Configure(std::string db_path) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
db_path_ = std::move(db_path);
|
||||
}
|
||||
|
||||
bool ProblemGenRunner::TriggerAsync(const std::string& /*trigger*/, int count) {
|
||||
std::string cmd;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (running_) return false;
|
||||
if (db_path_.empty()) return false;
|
||||
|
||||
cmd = BuildCommand(db_path_, "manual", count);
|
||||
running_ = true;
|
||||
last_started_at_ = NowSec();
|
||||
last_command_ = cmd;
|
||||
last_exit_code_.reset();
|
||||
}
|
||||
|
||||
std::thread([this, command = std::move(cmd)]() {
|
||||
const int rc = std::system(command.c_str());
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
last_exit_code_ = rc;
|
||||
last_finished_at_ = NowSec();
|
||||
running_ = false;
|
||||
}).detach();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProblemGenRunner::AutoStartIfEnabled() {
|
||||
if (!EnvBool("CSP_GEN_AUTO_RUN", true)) return;
|
||||
const int count = EnvInt("CSP_GEN_COUNT", 1);
|
||||
|
||||
if (EnvBool("CSP_GEN_WAIT_FOR_IMPORT", true) &&
|
||||
ImportRunner::Instance().IsRunning()) {
|
||||
std::thread([count]() {
|
||||
using namespace std::chrono_literals;
|
||||
while (ImportRunner::Instance().IsRunning()) {
|
||||
std::this_thread::sleep_for(2s);
|
||||
}
|
||||
const bool started = ProblemGenRunner::Instance().TriggerAsync("auto", count);
|
||||
if (started) {
|
||||
LOG_INFO << "problem generator auto-started after import";
|
||||
} else {
|
||||
LOG_INFO << "problem generator delayed auto-start skipped";
|
||||
}
|
||||
}).detach();
|
||||
LOG_INFO << "problem generator waiting for import completion";
|
||||
return;
|
||||
}
|
||||
|
||||
const bool started = TriggerAsync("auto", count);
|
||||
if (started) {
|
||||
LOG_INFO << "problem generator auto-started";
|
||||
} else {
|
||||
LOG_INFO << "problem generator auto-start skipped";
|
||||
}
|
||||
}
|
||||
|
||||
bool ProblemGenRunner::IsRunning() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return running_;
|
||||
}
|
||||
|
||||
std::string ProblemGenRunner::LastCommand() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return last_command_;
|
||||
}
|
||||
|
||||
std::optional<int> ProblemGenRunner::LastExitCode() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return last_exit_code_;
|
||||
}
|
||||
|
||||
int64_t ProblemGenRunner::LastStartedAt() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return last_started_at_;
|
||||
}
|
||||
|
||||
int64_t ProblemGenRunner::LastFinishedAt() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return last_finished_at_;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,201 @@
|
||||
#include "csp/services/problem_service.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
void CheckSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
|
||||
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
std::string ColText(sqlite3_stmt* stmt, int col) {
|
||||
const unsigned char* txt = sqlite3_column_text(stmt, col);
|
||||
return txt ? reinterpret_cast<const char*>(txt) : std::string();
|
||||
}
|
||||
|
||||
domain::Problem ReadProblem(sqlite3_stmt* stmt) {
|
||||
domain::Problem p;
|
||||
p.id = sqlite3_column_int64(stmt, 0);
|
||||
p.slug = ColText(stmt, 1);
|
||||
p.title = ColText(stmt, 2);
|
||||
p.statement_md = ColText(stmt, 3);
|
||||
p.difficulty = sqlite3_column_int(stmt, 4);
|
||||
p.source = ColText(stmt, 5);
|
||||
p.statement_url = ColText(stmt, 6);
|
||||
p.llm_profile_json = ColText(stmt, 7);
|
||||
p.sample_input = ColText(stmt, 8);
|
||||
p.sample_output = ColText(stmt, 9);
|
||||
p.created_at = sqlite3_column_int64(stmt, 10);
|
||||
return p;
|
||||
}
|
||||
|
||||
std::string LowerCopy(const std::string& input) {
|
||||
std::string out = input;
|
||||
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string JoinWithAnd(const std::vector<std::string>& clauses) {
|
||||
if (clauses.empty()) return "1=1";
|
||||
std::string out;
|
||||
for (size_t i = 0; i < clauses.size(); ++i) {
|
||||
if (i) out += " AND ";
|
||||
out += clauses[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
struct BindValue {
|
||||
enum class Type { Text, Int } type = Type::Text;
|
||||
std::string text;
|
||||
int value = 0;
|
||||
};
|
||||
|
||||
void BindStmt(sqlite3* db, sqlite3_stmt* stmt, const std::vector<BindValue>& binds) {
|
||||
int bind_index = 1;
|
||||
for (const auto& bind : binds) {
|
||||
if (bind.type == BindValue::Type::Text) {
|
||||
CheckSqlite(
|
||||
sqlite3_bind_text(stmt, bind_index++, bind.text.c_str(), -1, SQLITE_TRANSIENT),
|
||||
db, "bind text");
|
||||
} else {
|
||||
CheckSqlite(sqlite3_bind_int(stmt, bind_index++, bind.value), db, "bind int");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string> ResolveOrderBy(const ProblemQuery& query) {
|
||||
const std::string key = LowerCopy(query.order_by);
|
||||
const std::string dir = LowerCopy(query.order);
|
||||
|
||||
std::string order_col = "p.id";
|
||||
if (key == "difficulty") {
|
||||
order_col = "p.difficulty";
|
||||
} else if (key == "created_at") {
|
||||
order_col = "p.created_at";
|
||||
} else if (key == "title") {
|
||||
order_col = "p.title";
|
||||
}
|
||||
|
||||
std::string order_dir = "ASC";
|
||||
if (dir == "desc") order_dir = "DESC";
|
||||
return {order_col, order_dir};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProblemListResult ProblemService::List(const ProblemQuery& query) {
|
||||
sqlite3* db = db_.raw();
|
||||
std::vector<std::string> clauses;
|
||||
std::vector<BindValue> binds;
|
||||
|
||||
if (!query.q.empty()) {
|
||||
clauses.push_back("(p.title LIKE ? OR p.statement_md LIKE ?)");
|
||||
const std::string pat = "%" + query.q + "%";
|
||||
binds.push_back(BindValue{BindValue::Type::Text, pat, 0});
|
||||
binds.push_back(BindValue{BindValue::Type::Text, pat, 0});
|
||||
}
|
||||
if (query.difficulty > 0) {
|
||||
clauses.push_back("p.difficulty=?");
|
||||
binds.push_back(BindValue{BindValue::Type::Int, "", query.difficulty});
|
||||
}
|
||||
if (!query.source_prefix.empty()) {
|
||||
clauses.push_back("p.source LIKE ?");
|
||||
binds.push_back(
|
||||
BindValue{BindValue::Type::Text, query.source_prefix + "%", 0});
|
||||
}
|
||||
if (!query.tag.empty()) {
|
||||
clauses.push_back(
|
||||
"EXISTS(SELECT 1 FROM problem_tags pt1 WHERE pt1.problem_id=p.id AND pt1.tag=?)");
|
||||
binds.push_back(BindValue{BindValue::Type::Text, query.tag, 0});
|
||||
}
|
||||
if (!query.tags.empty()) {
|
||||
std::string in_clause;
|
||||
for (size_t i = 0; i < query.tags.size(); ++i) {
|
||||
if (i) in_clause += ",";
|
||||
in_clause += "?";
|
||||
binds.push_back(BindValue{BindValue::Type::Text, query.tags[i], 0});
|
||||
}
|
||||
clauses.push_back(
|
||||
"EXISTS(SELECT 1 FROM problem_tags ptm WHERE ptm.problem_id=p.id AND ptm.tag IN (" +
|
||||
in_clause + "))");
|
||||
}
|
||||
|
||||
const std::string where_sql = JoinWithAnd(clauses);
|
||||
const auto [order_col, order_dir] = ResolveOrderBy(query);
|
||||
const int page = query.page <= 0 ? 1 : query.page;
|
||||
const int page_size = std::max(1, std::min(200, query.page_size <= 0 ? 20 : query.page_size));
|
||||
const int offset = (page - 1) * page_size;
|
||||
|
||||
ProblemListResult result;
|
||||
|
||||
{
|
||||
sqlite3_stmt* count_stmt = nullptr;
|
||||
const std::string count_sql = "SELECT COUNT(1) FROM problems p WHERE " + where_sql;
|
||||
CheckSqlite(sqlite3_prepare_v2(db, count_sql.c_str(), -1, &count_stmt, nullptr),
|
||||
db, "prepare count problems");
|
||||
BindStmt(db, count_stmt, binds);
|
||||
if (sqlite3_step(count_stmt) == SQLITE_ROW) {
|
||||
result.total_count = sqlite3_column_int(count_stmt, 0);
|
||||
}
|
||||
sqlite3_finalize(count_stmt);
|
||||
}
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const std::string sql =
|
||||
"SELECT "
|
||||
"p.id,p.slug,p.title,p.statement_md,p.difficulty,p.source,p.statement_url,p.llm_profile_json,"
|
||||
"p.sample_input,p.sample_output,p.created_at "
|
||||
"FROM problems p "
|
||||
"WHERE " + where_sql + " "
|
||||
"ORDER BY " + order_col + " " + order_dir + " "
|
||||
"LIMIT ? OFFSET ?";
|
||||
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr), db,
|
||||
"prepare list problems");
|
||||
|
||||
std::vector<BindValue> list_binds = binds;
|
||||
list_binds.push_back(BindValue{BindValue::Type::Int, "", page_size});
|
||||
list_binds.push_back(BindValue{BindValue::Type::Int, "", offset});
|
||||
BindStmt(db, stmt, list_binds);
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
result.items.push_back(ReadProblem(stmt));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<domain::Problem> ProblemService::GetById(int64_t id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,slug,title,statement_md,difficulty,source,statement_url,llm_profile_json,"
|
||||
"sample_input,sample_output,created_at "
|
||||
"FROM problems WHERE id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare get problem");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, id), db, "bind problem_id");
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto p = ReadProblem(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,86 @@
|
||||
#include "csp/services/problem_solution_runner.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ShellQuote(const std::string& text) {
|
||||
std::string out = "'";
|
||||
for (char c : text) {
|
||||
if (c == '\'') {
|
||||
out += "'\"'\"'";
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string ResolveScriptPath() {
|
||||
const char* env_path = std::getenv("CSP_SOLUTION_SCRIPT_PATH");
|
||||
if (env_path && std::filesystem::exists(env_path)) return env_path;
|
||||
|
||||
const std::vector<std::string> candidates = {
|
||||
"/app/scripts/generate_problem_solutions.py",
|
||||
"scripts/generate_problem_solutions.py",
|
||||
"../scripts/generate_problem_solutions.py",
|
||||
"../../scripts/generate_problem_solutions.py",
|
||||
};
|
||||
for (const auto& p : candidates) {
|
||||
if (std::filesystem::exists(p)) return p;
|
||||
}
|
||||
return "/app/scripts/generate_problem_solutions.py";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProblemSolutionRunner& ProblemSolutionRunner::Instance() {
|
||||
static ProblemSolutionRunner inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void ProblemSolutionRunner::Configure(std::string db_path) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
db_path_ = std::move(db_path);
|
||||
}
|
||||
|
||||
bool ProblemSolutionRunner::TriggerAsync(int64_t problem_id,
|
||||
int64_t job_id,
|
||||
int max_solutions) {
|
||||
std::string cmd;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (db_path_.empty()) return false;
|
||||
if (running_problem_ids_.count(problem_id) > 0) return false;
|
||||
running_problem_ids_.insert(problem_id);
|
||||
|
||||
const std::string script_path = ResolveScriptPath();
|
||||
const int clamped = std::max(1, std::min(5, max_solutions));
|
||||
cmd = "python3 " + ShellQuote(script_path) + " --db-path " +
|
||||
ShellQuote(db_path_) + " --problem-id " + std::to_string(problem_id) +
|
||||
" --job-id " + std::to_string(job_id) + " --max-solutions " +
|
||||
std::to_string(clamped);
|
||||
}
|
||||
|
||||
std::thread([this, problem_id, command = std::move(cmd)]() {
|
||||
std::system(command.c_str());
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
running_problem_ids_.erase(problem_id);
|
||||
}).detach();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProblemSolutionRunner::IsRunning(int64_t problem_id) const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return running_problem_ids_.count(problem_id) > 0;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,212 @@
|
||||
#include "csp/services/problem_workspace_service.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
int64_t NowSec() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
void CheckSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
|
||||
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
std::string ColText(sqlite3_stmt* stmt, int col) {
|
||||
const unsigned char* txt = sqlite3_column_text(stmt, col);
|
||||
return txt ? reinterpret_cast<const char*>(txt) : std::string();
|
||||
}
|
||||
|
||||
std::optional<int64_t> ColNullableInt64(sqlite3_stmt* stmt, int col) {
|
||||
if (sqlite3_column_type(stmt, col) == SQLITE_NULL) return std::nullopt;
|
||||
return sqlite3_column_int64(stmt, col);
|
||||
}
|
||||
|
||||
ProblemSolution ReadSolution(sqlite3_stmt* stmt) {
|
||||
ProblemSolution s;
|
||||
s.id = sqlite3_column_int64(stmt, 0);
|
||||
s.problem_id = sqlite3_column_int64(stmt, 1);
|
||||
s.variant = sqlite3_column_int(stmt, 2);
|
||||
s.title = ColText(stmt, 3);
|
||||
s.idea_md = ColText(stmt, 4);
|
||||
s.explanation_md = ColText(stmt, 5);
|
||||
s.code_cpp = ColText(stmt, 6);
|
||||
s.complexity = ColText(stmt, 7);
|
||||
s.tags_json = ColText(stmt, 8);
|
||||
s.source = ColText(stmt, 9);
|
||||
s.created_at = sqlite3_column_int64(stmt, 10);
|
||||
s.updated_at = sqlite3_column_int64(stmt, 11);
|
||||
return s;
|
||||
}
|
||||
|
||||
ProblemSolutionJob ReadSolutionJob(sqlite3_stmt* stmt) {
|
||||
ProblemSolutionJob j;
|
||||
j.id = sqlite3_column_int64(stmt, 0);
|
||||
j.problem_id = sqlite3_column_int64(stmt, 1);
|
||||
j.status = ColText(stmt, 2);
|
||||
j.progress = sqlite3_column_int(stmt, 3);
|
||||
j.message = ColText(stmt, 4);
|
||||
j.created_by = sqlite3_column_int64(stmt, 5);
|
||||
j.max_solutions = sqlite3_column_int(stmt, 6);
|
||||
j.created_at = sqlite3_column_int64(stmt, 7);
|
||||
j.started_at = ColNullableInt64(stmt, 8);
|
||||
j.finished_at = ColNullableInt64(stmt, 9);
|
||||
j.updated_at = sqlite3_column_int64(stmt, 10);
|
||||
return j;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ProblemWorkspaceService::ProblemExists(int64_t problem_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql = "SELECT 1 FROM problems WHERE id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare exists problem");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, problem_id), db, "bind problem_id");
|
||||
const bool ok = sqlite3_step(stmt) == SQLITE_ROW;
|
||||
sqlite3_finalize(stmt);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void ProblemWorkspaceService::SaveDraft(int64_t user_id,
|
||||
int64_t problem_id,
|
||||
const std::string& language,
|
||||
const std::string& code,
|
||||
const std::string& stdin_text) {
|
||||
if (user_id <= 0 || problem_id <= 0) {
|
||||
throw std::runtime_error("invalid user_id/problem_id");
|
||||
}
|
||||
if (code.empty()) {
|
||||
throw std::runtime_error("code is empty");
|
||||
}
|
||||
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"INSERT INTO problem_drafts(user_id,problem_id,language,code,stdin,updated_at,created_at) "
|
||||
"VALUES(?,?,?,?,?,?,?) "
|
||||
"ON CONFLICT(user_id,problem_id) DO UPDATE SET "
|
||||
"language=excluded.language,code=excluded.code,stdin=excluded.stdin,updated_at=excluded.updated_at";
|
||||
const int64_t now = NowSec();
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare save draft");
|
||||
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_text(stmt, 3, language.c_str(), -1, SQLITE_TRANSIENT),
|
||||
db, "bind language");
|
||||
CheckSqlite(sqlite3_bind_text(stmt, 4, code.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind code");
|
||||
CheckSqlite(
|
||||
sqlite3_bind_text(stmt, 5, stdin_text.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind stdin");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 6, now), db, "bind updated_at");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 7, now), db, "bind created_at");
|
||||
CheckSqlite(sqlite3_step(stmt), db, "save draft");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
std::optional<ProblemDraft> ProblemWorkspaceService::GetDraft(int64_t user_id,
|
||||
int64_t problem_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT language,code,stdin,updated_at FROM problem_drafts WHERE user_id=? AND problem_id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare get draft");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id");
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ProblemDraft d;
|
||||
d.language = ColText(stmt, 0);
|
||||
d.code = ColText(stmt, 1);
|
||||
d.stdin_text = ColText(stmt, 2);
|
||||
d.updated_at = sqlite3_column_int64(stmt, 3);
|
||||
sqlite3_finalize(stmt);
|
||||
return d;
|
||||
}
|
||||
|
||||
int64_t ProblemWorkspaceService::CreateSolutionJob(int64_t problem_id,
|
||||
int64_t created_by,
|
||||
int max_solutions) {
|
||||
if (problem_id <= 0) throw std::runtime_error("invalid problem_id");
|
||||
const int clamped = std::max(1, std::min(5, max_solutions));
|
||||
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const int64_t now = NowSec();
|
||||
const char* sql =
|
||||
"INSERT INTO problem_solution_jobs(problem_id,status,progress,message,created_by,max_solutions,"
|
||||
"created_at,started_at,finished_at,updated_at) "
|
||||
"VALUES(?,?,?,?,?,?,?,?,?,?)";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare create solution job");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, problem_id), db, "bind problem_id");
|
||||
CheckSqlite(sqlite3_bind_text(stmt, 2, "queued", -1, SQLITE_STATIC), db,
|
||||
"bind status");
|
||||
CheckSqlite(sqlite3_bind_int(stmt, 3, 0), db, "bind progress");
|
||||
CheckSqlite(sqlite3_bind_text(stmt, 4, "", -1, SQLITE_STATIC), db,
|
||||
"bind message");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 5, created_by), db, "bind created_by");
|
||||
CheckSqlite(sqlite3_bind_int(stmt, 6, clamped), db, "bind max_solutions");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 7, now), db, "bind created_at");
|
||||
CheckSqlite(sqlite3_bind_null(stmt, 8), db, "bind started_at");
|
||||
CheckSqlite(sqlite3_bind_null(stmt, 9), db, "bind finished_at");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 10, now), db, "bind updated_at");
|
||||
CheckSqlite(sqlite3_step(stmt), db, "create solution job");
|
||||
sqlite3_finalize(stmt);
|
||||
return sqlite3_last_insert_rowid(db);
|
||||
}
|
||||
|
||||
std::optional<ProblemSolutionJob> ProblemWorkspaceService::GetLatestSolutionJob(
|
||||
int64_t problem_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,problem_id,status,progress,message,created_by,max_solutions,"
|
||||
"created_at,started_at,finished_at,updated_at "
|
||||
"FROM problem_solution_jobs WHERE problem_id=? ORDER BY id DESC LIMIT 1";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare latest solution job");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, problem_id), db, "bind problem_id");
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
auto row = ReadSolutionJob(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return row;
|
||||
}
|
||||
|
||||
std::vector<ProblemSolution> ProblemWorkspaceService::ListSolutions(int64_t problem_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,problem_id,variant,title,idea_md,explanation_md,code_cpp,complexity,tags_json,source,created_at,updated_at "
|
||||
"FROM problem_solutions WHERE problem_id=? ORDER BY variant ASC, id ASC";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare list solutions");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, problem_id), db, "bind problem_id");
|
||||
|
||||
std::vector<ProblemSolution> rows;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
rows.push_back(ReadSolution(stmt));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return rows;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,418 @@
|
||||
#include "csp/services/submission_service.h"
|
||||
|
||||
#include "csp/domain/enum_strings.h"
|
||||
#include "csp/services/crypto.h"
|
||||
#include "csp/services/wrong_book_service.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <sys/wait.h>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
struct JudgeOutcome {
|
||||
domain::SubmissionStatus status = domain::SubmissionStatus::Unknown;
|
||||
int time_ms = 0;
|
||||
std::string stdout_text;
|
||||
std::string stderr_text;
|
||||
std::string compile_log;
|
||||
};
|
||||
|
||||
void CheckSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
|
||||
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
int64_t NowSec() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
std::string ColText(sqlite3_stmt* stmt, int col) {
|
||||
const unsigned char* txt = sqlite3_column_text(stmt, col);
|
||||
return txt ? reinterpret_cast<const char*>(txt) : std::string();
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& p) {
|
||||
std::ifstream in(p, std::ios::in | std::ios::binary);
|
||||
if (!in) return "";
|
||||
return std::string((std::istreambuf_iterator<char>(in)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
void WriteFile(const std::filesystem::path& p, const std::string& s) {
|
||||
std::ofstream out(p, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
if (!out) throw std::runtime_error("cannot open file: " + p.string());
|
||||
out << s;
|
||||
}
|
||||
|
||||
std::string Trim(std::string s) {
|
||||
while (!s.empty() &&
|
||||
(s.back() == '\n' || s.back() == '\r' || s.back() == ' ' ||
|
||||
s.back() == '\t')) {
|
||||
s.pop_back();
|
||||
}
|
||||
size_t i = 0;
|
||||
while (i < s.size() &&
|
||||
(s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) {
|
||||
++i;
|
||||
}
|
||||
return s.substr(i);
|
||||
}
|
||||
|
||||
int ExitCodeFromSystem(int rc) {
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
if (WIFSIGNALED(rc)) return 128 + WTERMSIG(rc);
|
||||
return -1;
|
||||
}
|
||||
|
||||
JudgeOutcome JudgeCpp(const std::string& code,
|
||||
const std::string& input,
|
||||
const std::optional<std::string>& expected_output) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path workdir = fs::path("/tmp") / ("csp_judge_" + crypto::RandomHex(8));
|
||||
fs::create_directories(workdir);
|
||||
|
||||
const fs::path src = workdir / "main.cpp";
|
||||
const fs::path bin = workdir / "main.bin";
|
||||
const fs::path in = workdir / "input.txt";
|
||||
const fs::path out = workdir / "output.txt";
|
||||
const fs::path compile_log = workdir / "compile.log";
|
||||
const fs::path runtime_log = workdir / "runtime.log";
|
||||
|
||||
WriteFile(src, code);
|
||||
WriteFile(in, input);
|
||||
|
||||
JudgeOutcome outcome;
|
||||
try {
|
||||
const std::string compile_cmd =
|
||||
"g++ -std=c++20 -O2 \"" + src.string() + "\" -o \"" + bin.string() +
|
||||
"\" 2> \"" + compile_log.string() + "\"";
|
||||
const int compile_rc = std::system(compile_cmd.c_str());
|
||||
outcome.compile_log = ReadFile(compile_log);
|
||||
if (ExitCodeFromSystem(compile_rc) != 0) {
|
||||
outcome.status = domain::SubmissionStatus::CE;
|
||||
fs::remove_all(workdir);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const std::string run_cmd =
|
||||
"/usr/bin/timeout 2s \"" + bin.string() + "\" < \"" + in.string() +
|
||||
"\" > \"" + out.string() + "\" 2> \"" + runtime_log.string() + "\"";
|
||||
const int run_rc = std::system(run_cmd.c_str());
|
||||
const auto end = std::chrono::steady_clock::now();
|
||||
outcome.time_ms = static_cast<int>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count());
|
||||
|
||||
outcome.stdout_text = ReadFile(out);
|
||||
outcome.stderr_text = ReadFile(runtime_log);
|
||||
|
||||
const int code_rc = ExitCodeFromSystem(run_rc);
|
||||
if (code_rc == 124) {
|
||||
outcome.status = domain::SubmissionStatus::TLE;
|
||||
fs::remove_all(workdir);
|
||||
return outcome;
|
||||
}
|
||||
if (code_rc != 0) {
|
||||
outcome.status = domain::SubmissionStatus::RE;
|
||||
fs::remove_all(workdir);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
if (expected_output.has_value()) {
|
||||
outcome.status =
|
||||
(Trim(outcome.stdout_text) == Trim(*expected_output))
|
||||
? domain::SubmissionStatus::AC
|
||||
: domain::SubmissionStatus::WA;
|
||||
fs::remove_all(workdir);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
outcome.status = domain::SubmissionStatus::Running;
|
||||
fs::remove_all(workdir);
|
||||
return outcome;
|
||||
} catch (...) {
|
||||
fs::remove_all(workdir);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<domain::Problem> GetProblem(sqlite3* db, int64_t problem_id) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,slug,title,statement_md,difficulty,source,statement_url,llm_profile_json,"
|
||||
"sample_input,sample_output,created_at "
|
||||
"FROM problems WHERE id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare submission.get_problem");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, problem_id), db, "bind problem_id");
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
domain::Problem p;
|
||||
p.id = sqlite3_column_int64(stmt, 0);
|
||||
p.slug = ColText(stmt, 1);
|
||||
p.title = ColText(stmt, 2);
|
||||
p.statement_md = ColText(stmt, 3);
|
||||
p.difficulty = sqlite3_column_int(stmt, 4);
|
||||
p.source = ColText(stmt, 5);
|
||||
p.statement_url = ColText(stmt, 6);
|
||||
p.llm_profile_json = ColText(stmt, 7);
|
||||
p.sample_input = ColText(stmt, 8);
|
||||
p.sample_output = ColText(stmt, 9);
|
||||
p.created_at = sqlite3_column_int64(stmt, 10);
|
||||
sqlite3_finalize(stmt);
|
||||
return p;
|
||||
}
|
||||
|
||||
bool HasSolvedBefore(sqlite3* db,
|
||||
int64_t user_id,
|
||||
int64_t problem_id,
|
||||
int64_t submission_id) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT 1 FROM submissions WHERE user_id=? AND problem_id=? AND status='AC' AND id<? LIMIT 1";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare has_solved_before");
|
||||
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_int64(stmt, 3, submission_id), db,
|
||||
"bind submission_id");
|
||||
const bool solved = sqlite3_step(stmt) == SQLITE_ROW;
|
||||
sqlite3_finalize(stmt);
|
||||
return solved;
|
||||
}
|
||||
|
||||
void AddRating(sqlite3* db, int64_t user_id, int delta) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql = "UPDATE users SET rating = rating + ? WHERE id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare add_rating");
|
||||
CheckSqlite(sqlite3_bind_int(stmt, 1, delta), db, "bind delta");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 2, user_id), db, "bind user_id");
|
||||
CheckSqlite(sqlite3_step(stmt), db, "exec add_rating");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
std::string ToStatusText(domain::SubmissionStatus s) { return domain::ToString(s); }
|
||||
|
||||
} // namespace
|
||||
|
||||
domain::Submission SubmissionService::CreateAndJudge(const SubmissionCreateRequest& req) {
|
||||
if (req.user_id <= 0 || req.problem_id <= 0) {
|
||||
throw std::runtime_error("invalid user_id/problem_id");
|
||||
}
|
||||
if (req.language != "cpp" && req.language != "c++" && req.language != "C++") {
|
||||
throw std::runtime_error("only cpp language is supported");
|
||||
}
|
||||
if (req.code.empty()) {
|
||||
throw std::runtime_error("code is empty");
|
||||
}
|
||||
if (req.code.size() > 200000) {
|
||||
throw std::runtime_error("code is too large");
|
||||
}
|
||||
|
||||
sqlite3* db = db_.raw();
|
||||
const auto problem = GetProblem(db, req.problem_id);
|
||||
if (!problem.has_value()) {
|
||||
throw std::runtime_error("problem not found");
|
||||
}
|
||||
|
||||
sqlite3_stmt* ins = nullptr;
|
||||
const char* ins_sql =
|
||||
"INSERT INTO submissions(user_id,problem_id,contest_id,language,code,status,score,time_ms,memory_kb,compile_log,runtime_log,created_at) "
|
||||
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?)";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, ins_sql, -1, &ins, nullptr), db,
|
||||
"prepare insert submission");
|
||||
CheckSqlite(sqlite3_bind_int64(ins, 1, req.user_id), db, "bind user_id");
|
||||
CheckSqlite(sqlite3_bind_int64(ins, 2, req.problem_id), db, "bind problem_id");
|
||||
if (req.contest_id.has_value()) {
|
||||
CheckSqlite(sqlite3_bind_int64(ins, 3, *req.contest_id), db, "bind contest_id");
|
||||
} else {
|
||||
CheckSqlite(sqlite3_bind_null(ins, 3), db, "bind contest_id null");
|
||||
}
|
||||
CheckSqlite(sqlite3_bind_text(ins, 4, "cpp", -1, SQLITE_STATIC), db,
|
||||
"bind language");
|
||||
CheckSqlite(sqlite3_bind_text(ins, 5, req.code.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind code");
|
||||
CheckSqlite(sqlite3_bind_text(ins, 6, "Pending", -1, SQLITE_STATIC), db,
|
||||
"bind status");
|
||||
CheckSqlite(sqlite3_bind_int(ins, 7, 0), db, "bind score");
|
||||
CheckSqlite(sqlite3_bind_int(ins, 8, 0), db, "bind time_ms");
|
||||
CheckSqlite(sqlite3_bind_int(ins, 9, 0), db, "bind memory_kb");
|
||||
CheckSqlite(sqlite3_bind_text(ins, 10, "", -1, SQLITE_STATIC), db,
|
||||
"bind compile_log");
|
||||
CheckSqlite(sqlite3_bind_text(ins, 11, "", -1, SQLITE_STATIC), db,
|
||||
"bind runtime_log");
|
||||
CheckSqlite(sqlite3_bind_int64(ins, 12, NowSec()), db, "bind created_at");
|
||||
CheckSqlite(sqlite3_step(ins), db, "insert submission");
|
||||
sqlite3_finalize(ins);
|
||||
|
||||
const int64_t submission_id = sqlite3_last_insert_rowid(db);
|
||||
|
||||
JudgeOutcome outcome = JudgeCpp(req.code, problem->sample_input, problem->sample_output);
|
||||
const int score = outcome.status == domain::SubmissionStatus::AC ? 100 : 0;
|
||||
|
||||
sqlite3_stmt* upd = nullptr;
|
||||
const char* upd_sql =
|
||||
"UPDATE submissions SET status=?,score=?,time_ms=?,memory_kb=?,compile_log=?,runtime_log=? WHERE id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, upd_sql, -1, &upd, nullptr), db,
|
||||
"prepare update submission");
|
||||
const auto status_text = ToStatusText(outcome.status);
|
||||
CheckSqlite(sqlite3_bind_text(upd, 1, status_text.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind status");
|
||||
CheckSqlite(sqlite3_bind_int(upd, 2, score), db, "bind score");
|
||||
CheckSqlite(sqlite3_bind_int(upd, 3, outcome.time_ms), db, "bind time_ms");
|
||||
CheckSqlite(sqlite3_bind_int(upd, 4, 0), db, "bind memory_kb");
|
||||
CheckSqlite(sqlite3_bind_text(upd, 5, outcome.compile_log.c_str(), -1, SQLITE_TRANSIENT),
|
||||
db, "bind compile_log");
|
||||
CheckSqlite(sqlite3_bind_text(upd, 6, outcome.stderr_text.c_str(), -1, SQLITE_TRANSIENT),
|
||||
db, "bind runtime_log");
|
||||
CheckSqlite(sqlite3_bind_int64(upd, 7, submission_id), db, "bind submission_id");
|
||||
CheckSqlite(sqlite3_step(upd), db, "update submission");
|
||||
sqlite3_finalize(upd);
|
||||
|
||||
WrongBookService wb(db_);
|
||||
if (outcome.status == domain::SubmissionStatus::AC) {
|
||||
wb.Remove(req.user_id, req.problem_id);
|
||||
if (!HasSolvedBefore(db, req.user_id, req.problem_id, submission_id)) {
|
||||
AddRating(db, req.user_id, problem->difficulty * 10);
|
||||
}
|
||||
} else {
|
||||
wb.UpsertBySubmission(req.user_id, req.problem_id, submission_id,
|
||||
"最近一次提交未通过,请复盘题解和思路。");
|
||||
}
|
||||
|
||||
const auto saved = GetById(submission_id);
|
||||
if (!saved.has_value()) {
|
||||
throw std::runtime_error("submission saved but reload failed");
|
||||
}
|
||||
return *saved;
|
||||
}
|
||||
|
||||
std::vector<domain::Submission> SubmissionService::List(std::optional<int64_t> user_id,
|
||||
std::optional<int64_t> problem_id,
|
||||
std::optional<int64_t> contest_id,
|
||||
int page,
|
||||
int page_size) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
|
||||
std::string sql =
|
||||
"SELECT id,user_id,problem_id,contest_id,language,code,status,score,time_ms,memory_kb,compile_log,runtime_log,created_at "
|
||||
"FROM submissions WHERE 1=1 ";
|
||||
if (user_id.has_value()) sql += "AND user_id=? ";
|
||||
if (problem_id.has_value()) sql += "AND problem_id=? ";
|
||||
if (contest_id.has_value()) sql += "AND contest_id=? ";
|
||||
sql += "ORDER BY id DESC LIMIT ? OFFSET ?";
|
||||
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr), db,
|
||||
"prepare list submissions");
|
||||
|
||||
int idx = 1;
|
||||
if (user_id.has_value())
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, idx++, *user_id), db, "bind user_id");
|
||||
if (problem_id.has_value())
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, idx++, *problem_id), db, "bind problem_id");
|
||||
if (contest_id.has_value())
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, idx++, *contest_id), db, "bind contest_id");
|
||||
|
||||
if (page <= 0) page = 1;
|
||||
if (page_size <= 0) page_size = 20;
|
||||
const int offset = (page - 1) * page_size;
|
||||
CheckSqlite(sqlite3_bind_int(stmt, idx++, page_size), db, "bind limit");
|
||||
CheckSqlite(sqlite3_bind_int(stmt, idx++, offset), db, "bind offset");
|
||||
|
||||
std::vector<domain::Submission> out;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
domain::Submission s;
|
||||
s.id = sqlite3_column_int64(stmt, 0);
|
||||
s.user_id = sqlite3_column_int64(stmt, 1);
|
||||
s.problem_id = sqlite3_column_int64(stmt, 2);
|
||||
if (sqlite3_column_type(stmt, 3) == SQLITE_NULL) {
|
||||
s.contest_id = std::nullopt;
|
||||
} else {
|
||||
s.contest_id = sqlite3_column_int64(stmt, 3);
|
||||
}
|
||||
s.language = domain::LanguageFromString(ColText(stmt, 4));
|
||||
s.code = ColText(stmt, 5);
|
||||
s.status = domain::SubmissionStatusFromString(ColText(stmt, 6));
|
||||
s.score = sqlite3_column_int(stmt, 7);
|
||||
s.time_ms = sqlite3_column_int(stmt, 8);
|
||||
s.memory_kb = sqlite3_column_int(stmt, 9);
|
||||
s.compile_log = ColText(stmt, 10);
|
||||
s.runtime_log = ColText(stmt, 11);
|
||||
s.created_at = sqlite3_column_int64(stmt, 12);
|
||||
out.push_back(std::move(s));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<domain::Submission> SubmissionService::GetById(int64_t id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,user_id,problem_id,contest_id,language,code,status,score,time_ms,memory_kb,compile_log,runtime_log,created_at "
|
||||
"FROM submissions WHERE id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare get submission");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, id), db, "bind submission_id");
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
domain::Submission s;
|
||||
s.id = sqlite3_column_int64(stmt, 0);
|
||||
s.user_id = sqlite3_column_int64(stmt, 1);
|
||||
s.problem_id = sqlite3_column_int64(stmt, 2);
|
||||
if (sqlite3_column_type(stmt, 3) == SQLITE_NULL) {
|
||||
s.contest_id = std::nullopt;
|
||||
} else {
|
||||
s.contest_id = sqlite3_column_int64(stmt, 3);
|
||||
}
|
||||
s.language = domain::LanguageFromString(ColText(stmt, 4));
|
||||
s.code = ColText(stmt, 5);
|
||||
s.status = domain::SubmissionStatusFromString(ColText(stmt, 6));
|
||||
s.score = sqlite3_column_int(stmt, 7);
|
||||
s.time_ms = sqlite3_column_int(stmt, 8);
|
||||
s.memory_kb = sqlite3_column_int(stmt, 9);
|
||||
s.compile_log = ColText(stmt, 10);
|
||||
s.runtime_log = ColText(stmt, 11);
|
||||
s.created_at = sqlite3_column_int64(stmt, 12);
|
||||
sqlite3_finalize(stmt);
|
||||
return s;
|
||||
}
|
||||
|
||||
RunOnlyResult SubmissionService::RunOnlyCpp(const std::string& code,
|
||||
const std::string& input) {
|
||||
if (code.empty()) throw std::runtime_error("code is empty");
|
||||
if (code.size() > 200000) throw std::runtime_error("code is too large");
|
||||
|
||||
auto outcome = JudgeCpp(code, input, std::nullopt);
|
||||
RunOnlyResult r;
|
||||
r.status = outcome.status;
|
||||
r.time_ms = outcome.time_ms;
|
||||
r.stdout_text = outcome.stdout_text;
|
||||
r.stderr_text = outcome.stderr_text;
|
||||
r.compile_log = outcome.compile_log;
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "csp/services/user_service.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
void CheckSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
|
||||
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
std::string ColText(sqlite3_stmt* stmt, int col) {
|
||||
const unsigned char* txt = sqlite3_column_text(stmt, col);
|
||||
return txt ? reinterpret_cast<const char*>(txt) : std::string();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<domain::User> UserService::GetById(int64_t id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,username,password_salt,password_hash,rating,created_at FROM users WHERE id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare user get");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, id), db, "bind user_id");
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
domain::User u;
|
||||
u.id = sqlite3_column_int64(stmt, 0);
|
||||
u.username = ColText(stmt, 1);
|
||||
u.password_salt = ColText(stmt, 2);
|
||||
u.password_hash = ColText(stmt, 3);
|
||||
u.rating = sqlite3_column_int(stmt, 4);
|
||||
u.created_at = sqlite3_column_int64(stmt, 5);
|
||||
sqlite3_finalize(stmt);
|
||||
return u;
|
||||
}
|
||||
|
||||
std::vector<domain::GlobalLeaderboardEntry> UserService::GlobalLeaderboard(int limit) {
|
||||
if (limit <= 0) limit = 100;
|
||||
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql =
|
||||
"SELECT id,username,rating,created_at FROM users ORDER BY rating DESC,id ASC LIMIT ?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare leaderboard");
|
||||
CheckSqlite(sqlite3_bind_int(stmt, 1, limit), db, "bind limit");
|
||||
|
||||
std::vector<domain::GlobalLeaderboardEntry> out;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
domain::GlobalLeaderboardEntry e;
|
||||
e.user_id = sqlite3_column_int64(stmt, 0);
|
||||
e.username = ColText(stmt, 1);
|
||||
e.rating = sqlite3_column_int(stmt, 2);
|
||||
e.created_at = sqlite3_column_int64(stmt, 3);
|
||||
out.push_back(std::move(e));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "csp/services/wrong_book_service.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace csp::services {
|
||||
|
||||
namespace {
|
||||
|
||||
void CheckSqlite(int rc, sqlite3* db, const char* what) {
|
||||
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
|
||||
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
int64_t NowSec() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
std::string ColText(sqlite3_stmt* stmt, int col) {
|
||||
const unsigned char* txt = sqlite3_column_text(stmt, col);
|
||||
return txt ? reinterpret_cast<const char*>(txt) : std::string();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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 "
|
||||
"FROM wrong_book w "
|
||||
"JOIN problems p ON p.id=w.problem_id "
|
||||
"WHERE w.user_id=? ORDER BY w.updated_at DESC";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare wrong_book list");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
|
||||
|
||||
std::vector<WrongBookEntry> out;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
WrongBookEntry e;
|
||||
e.item.user_id = sqlite3_column_int64(stmt, 0);
|
||||
e.item.problem_id = sqlite3_column_int64(stmt, 1);
|
||||
if (sqlite3_column_type(stmt, 2) == SQLITE_NULL) {
|
||||
e.item.last_submission_id = std::nullopt;
|
||||
} else {
|
||||
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);
|
||||
out.push_back(std::move(e));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return out;
|
||||
}
|
||||
|
||||
void WrongBookService::UpsertNote(int64_t user_id,
|
||||
int64_t problem_id,
|
||||
const std::string& note) {
|
||||
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) "
|
||||
"VALUES(?,?,?,?,?) "
|
||||
"ON CONFLICT(user_id,problem_id) DO UPDATE SET note=excluded.note,updated_at=excluded.updated_at";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare wrong_book upsert note");
|
||||
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, note.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind note");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 5, NowSec()), db, "bind updated_at");
|
||||
CheckSqlite(sqlite3_step(stmt), db, "wrong_book upsert note");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void WrongBookService::UpsertBySubmission(int64_t user_id,
|
||||
int64_t problem_id,
|
||||
int64_t submission_id,
|
||||
const std::string& note) {
|
||||
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) "
|
||||
"VALUES(?,?,?,?,?) "
|
||||
"ON CONFLICT(user_id,problem_id) DO UPDATE SET "
|
||||
"last_submission_id=excluded.last_submission_id,"
|
||||
"updated_at=excluded.updated_at,"
|
||||
"note=CASE WHEN wrong_book.note='' THEN excluded.note ELSE wrong_book.note END";
|
||||
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare wrong_book upsert by submission");
|
||||
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_int64(stmt, 3, submission_id), db,
|
||||
"bind submission_id");
|
||||
CheckSqlite(sqlite3_bind_text(stmt, 4, note.c_str(), -1, SQLITE_TRANSIENT), db,
|
||||
"bind note");
|
||||
CheckSqlite(sqlite3_bind_int64(stmt, 5, NowSec()), db, "bind updated_at");
|
||||
CheckSqlite(sqlite3_step(stmt), db, "wrong_book upsert by submission");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void WrongBookService::Remove(int64_t user_id, int64_t problem_id) {
|
||||
sqlite3* db = db_.raw();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
const char* sql = "DELETE FROM wrong_book WHERE user_id=? AND problem_id=?";
|
||||
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||
"prepare wrong_book delete");
|
||||
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_step(stmt), db, "wrong_book delete");
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
} // namespace csp::services
|
||||
@@ -0,0 +1,94 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/controllers/contest_controller.h"
|
||||
#include "csp/services/auth_service.h"
|
||||
|
||||
#include <drogon/HttpRequest.h>
|
||||
|
||||
#include <future>
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr CallList(csp::controllers::ContestController& ctl) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.list(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr CallGet(csp::controllers::ContestController& ctl,
|
||||
int64_t contest_id,
|
||||
const std::string& token) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
req->addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.getById(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
|
||||
contest_id);
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr CallRegister(csp::controllers::ContestController& ctl,
|
||||
int64_t contest_id,
|
||||
const std::string& token) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Post);
|
||||
req->addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.registerForContest(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) {
|
||||
p.set_value(resp);
|
||||
},
|
||||
contest_id);
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr CallBoard(csp::controllers::ContestController& ctl,
|
||||
int64_t contest_id) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.leaderboard(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
|
||||
contest_id);
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("contest controller list/get/register/leaderboard") {
|
||||
csp::AppState::Instance().Init(":memory:");
|
||||
|
||||
csp::services::AuthService auth(csp::AppState::Instance().db());
|
||||
const auto user = auth.Register("contest_http_user", "password123");
|
||||
|
||||
csp::controllers::ContestController ctl;
|
||||
|
||||
auto list = CallList(ctl);
|
||||
REQUIRE(list->statusCode() == drogon::k200OK);
|
||||
auto list_json = list->jsonObject();
|
||||
REQUIRE(list_json != nullptr);
|
||||
REQUIRE((*list_json)["data"].isArray());
|
||||
REQUIRE((*list_json)["data"].size() >= 1);
|
||||
|
||||
const int64_t contest_id = (*list_json)["data"][0]["id"].asInt64();
|
||||
|
||||
auto get = CallGet(ctl, contest_id, user.token);
|
||||
REQUIRE(get->statusCode() == drogon::k200OK);
|
||||
|
||||
auto reg = CallRegister(ctl, contest_id, user.token);
|
||||
REQUIRE(reg->statusCode() == drogon::k200OK);
|
||||
|
||||
auto board = CallBoard(ctl, contest_id);
|
||||
REQUIRE(board->statusCode() == drogon::k200OK);
|
||||
auto board_json = board->jsonObject();
|
||||
REQUIRE(board_json != nullptr);
|
||||
REQUIRE((*board_json)["data"].isArray());
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/db/sqlite_db.h"
|
||||
#include "csp/services/auth_service.h"
|
||||
#include "csp/services/contest_service.h"
|
||||
#include "csp/services/submission_service.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kAcCode = R"CPP(#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
int main() {
|
||||
long long a, b;
|
||||
if (!(cin >> a >> b)) return 0;
|
||||
cout << (a + b) << "\n";
|
||||
return 0;
|
||||
}
|
||||
)CPP";
|
||||
|
||||
const char* kWaCode = R"CPP(#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
int main() {
|
||||
cout << 0 << "\n";
|
||||
return 0;
|
||||
}
|
||||
)CPP";
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("contest service leaderboard") {
|
||||
auto db = csp::db::SqliteDb::OpenMemory();
|
||||
csp::db::ApplyMigrations(db);
|
||||
csp::db::SeedDemoData(db);
|
||||
|
||||
csp::services::AuthService auth(db);
|
||||
const auto u1 = auth.Register("contest_user_1", "password123");
|
||||
const auto u2 = auth.Register("contest_user_2", "password123");
|
||||
|
||||
csp::services::ContestService contest(db);
|
||||
const auto contests = contest.ListContests();
|
||||
REQUIRE_FALSE(contests.empty());
|
||||
|
||||
const int64_t contest_id = contests.front().id;
|
||||
contest.Register(contest_id, u1.user_id);
|
||||
contest.Register(contest_id, u2.user_id);
|
||||
|
||||
const auto problems = contest.ListContestProblems(contest_id);
|
||||
REQUIRE_FALSE(problems.empty());
|
||||
const int64_t problem_id = problems.front().id;
|
||||
|
||||
csp::services::SubmissionService submissions(db);
|
||||
|
||||
csp::services::SubmissionCreateRequest r1;
|
||||
r1.user_id = u1.user_id;
|
||||
r1.problem_id = problem_id;
|
||||
r1.contest_id = contest_id;
|
||||
r1.language = "cpp";
|
||||
r1.code = kAcCode;
|
||||
const auto s1 = submissions.CreateAndJudge(r1);
|
||||
REQUIRE(s1.status == csp::domain::SubmissionStatus::AC);
|
||||
|
||||
csp::services::SubmissionCreateRequest r2;
|
||||
r2.user_id = u2.user_id;
|
||||
r2.problem_id = problem_id;
|
||||
r2.contest_id = contest_id;
|
||||
r2.language = "cpp";
|
||||
r2.code = kWaCode;
|
||||
const auto s2 = submissions.CreateAndJudge(r2);
|
||||
REQUIRE(s2.status == csp::domain::SubmissionStatus::WA);
|
||||
|
||||
const auto board = contest.Leaderboard(contest_id);
|
||||
REQUIRE(board.size() >= 2);
|
||||
REQUIRE(board.front().user_id == u1.user_id);
|
||||
REQUIRE(board.front().solved >= 1);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/controllers/import_controller.h"
|
||||
|
||||
#include <drogon/HttpRequest.h>
|
||||
|
||||
#include <future>
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr CallLatest(csp::controllers::ImportController& ctl) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.latestJob(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr CallItems(csp::controllers::ImportController& ctl, int64_t job_id) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
req->setParameter("page", "1");
|
||||
req->setParameter("page_size", "20");
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.jobItems(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); }, job_id);
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("import controller latest and items") {
|
||||
csp::AppState::Instance().Init(":memory:");
|
||||
auto& db = csp::AppState::Instance().db();
|
||||
db.Exec(
|
||||
"INSERT INTO import_jobs(status,trigger,total_count,processed_count,success_count,"
|
||||
"failed_count,options_json,last_error,started_at,finished_at,updated_at,created_at)"
|
||||
"VALUES('success','auto',2,2,2,0,'{\"workers\":3}','',100,120,120,90);");
|
||||
db.Exec(
|
||||
"INSERT INTO import_job_items(job_id,source_path,status,title,difficulty,problem_id,"
|
||||
"error_text,started_at,finished_at,updated_at,created_at)"
|
||||
"VALUES(1,'CSP-J/2024/Round2/B.pdf','success','B',3,7,'',101,110,120,100);");
|
||||
|
||||
csp::controllers::ImportController ctl;
|
||||
|
||||
auto latest = CallLatest(ctl);
|
||||
REQUIRE(latest->statusCode() == drogon::k200OK);
|
||||
auto latest_json = latest->jsonObject();
|
||||
REQUIRE(latest_json != nullptr);
|
||||
REQUIRE((*latest_json)["ok"].asBool());
|
||||
REQUIRE((*latest_json)["data"]["job"]["id"].asInt64() == 1);
|
||||
|
||||
auto items = CallItems(ctl, 1);
|
||||
REQUIRE(items->statusCode() == drogon::k200OK);
|
||||
auto items_json = items->jsonObject();
|
||||
REQUIRE(items_json != nullptr);
|
||||
REQUIRE((*items_json)["ok"].asBool());
|
||||
REQUIRE((*items_json)["data"]["items"].isArray());
|
||||
REQUIRE((*items_json)["data"]["items"].size() == 1);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/db/sqlite_db.h"
|
||||
#include "csp/services/import_service.h"
|
||||
|
||||
TEST_CASE("import service query latest job and items") {
|
||||
auto db = csp::db::SqliteDb::OpenMemory();
|
||||
csp::db::ApplyMigrations(db);
|
||||
|
||||
db.Exec(
|
||||
"INSERT INTO import_jobs(status,trigger,total_count,processed_count,success_count,"
|
||||
"failed_count,options_json,last_error,started_at,finished_at,updated_at,created_at)"
|
||||
"VALUES('running','manual',10,3,2,1,'{}','',100,null,120,90);");
|
||||
db.Exec(
|
||||
"INSERT INTO import_job_items(job_id,source_path,status,title,difficulty,problem_id,"
|
||||
"error_text,started_at,finished_at,updated_at,created_at)"
|
||||
"VALUES(1,'CSP-J/2024/Round1/A.pdf','success','A',2,11,'',101,102,120,100);");
|
||||
|
||||
csp::services::ImportService svc(db);
|
||||
const auto latest = svc.GetLatestJob();
|
||||
REQUIRE(latest.has_value());
|
||||
REQUIRE(latest->id == 1);
|
||||
REQUIRE(latest->status == "running");
|
||||
REQUIRE(latest->processed_count == 3);
|
||||
|
||||
csp::services::ImportJobItemQuery q;
|
||||
q.page = 1;
|
||||
q.page_size = 20;
|
||||
const auto items = svc.ListItems(1, q);
|
||||
REQUIRE(items.size() == 1);
|
||||
REQUIRE(items[0].source_path == "CSP-J/2024/Round1/A.pdf");
|
||||
REQUIRE(items[0].status == "success");
|
||||
REQUIRE(items[0].problem_id.has_value());
|
||||
}
|
||||
18
backend/tests/kb_service_test.cc
普通文件
18
backend/tests/kb_service_test.cc
普通文件
@@ -0,0 +1,18 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/db/sqlite_db.h"
|
||||
#include "csp/services/kb_service.h"
|
||||
|
||||
TEST_CASE("kb service list/detail") {
|
||||
auto db = csp::db::SqliteDb::OpenMemory();
|
||||
csp::db::ApplyMigrations(db);
|
||||
csp::db::SeedDemoData(db);
|
||||
|
||||
csp::services::KbService svc(db);
|
||||
const auto rows = svc.ListArticles();
|
||||
REQUIRE(rows.size() >= 2);
|
||||
|
||||
const auto detail = svc.GetBySlug(rows.front().slug);
|
||||
REQUIRE(detail.has_value());
|
||||
REQUIRE(detail->article.slug == rows.front().slug);
|
||||
}
|
||||
105
backend/tests/me_http_test.cc
普通文件
105
backend/tests/me_http_test.cc
普通文件
@@ -0,0 +1,105 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/controllers/me_controller.h"
|
||||
#include "csp/services/auth_service.h"
|
||||
#include "csp/services/problem_service.h"
|
||||
|
||||
#include <drogon/HttpRequest.h>
|
||||
|
||||
#include <future>
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr CallProfile(csp::controllers::MeController& ctl,
|
||||
const std::string& token) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
req->addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.profile(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr CallListWrongBook(csp::controllers::MeController& ctl,
|
||||
const std::string& token) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
req->addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.listWrongBook(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr CallPatchWrongBook(csp::controllers::MeController& ctl,
|
||||
const std::string& token,
|
||||
int64_t problem_id,
|
||||
const std::string& note) {
|
||||
Json::Value body;
|
||||
body["note"] = note;
|
||||
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
|
||||
req->setMethod(drogon::Patch);
|
||||
req->addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.upsertWrongBookNote(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) {
|
||||
p.set_value(resp);
|
||||
},
|
||||
problem_id);
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr CallDeleteWrongBook(csp::controllers::MeController& ctl,
|
||||
const std::string& token,
|
||||
int64_t problem_id) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Delete);
|
||||
req->addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.deleteWrongBookItem(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) {
|
||||
p.set_value(resp);
|
||||
},
|
||||
problem_id);
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("me controller profile and wrong-book") {
|
||||
csp::AppState::Instance().Init(":memory:");
|
||||
|
||||
csp::services::AuthService auth(csp::AppState::Instance().db());
|
||||
const auto login = auth.Register("me_http_user", "password123");
|
||||
|
||||
csp::services::ProblemService problems(csp::AppState::Instance().db());
|
||||
const auto list = problems.List(csp::services::ProblemQuery{});
|
||||
REQUIRE_FALSE(list.items.empty());
|
||||
const int64_t problem_id = list.items.front().id;
|
||||
|
||||
csp::controllers::MeController ctl;
|
||||
|
||||
auto profile = CallProfile(ctl, login.token);
|
||||
REQUIRE(profile->statusCode() == drogon::k200OK);
|
||||
auto profile_json = profile->jsonObject();
|
||||
REQUIRE(profile_json != nullptr);
|
||||
REQUIRE((*profile_json)["ok"].asBool());
|
||||
|
||||
auto patch = CallPatchWrongBook(ctl, login.token, problem_id, "复盘记录");
|
||||
REQUIRE(patch->statusCode() == drogon::k200OK);
|
||||
|
||||
auto list_resp = CallListWrongBook(ctl, login.token);
|
||||
REQUIRE(list_resp->statusCode() == drogon::k200OK);
|
||||
auto list_json = list_resp->jsonObject();
|
||||
REQUIRE(list_json != nullptr);
|
||||
REQUIRE((*list_json)["data"].isArray());
|
||||
REQUIRE((*list_json)["data"].size() >= 1);
|
||||
|
||||
auto del = CallDeleteWrongBook(ctl, login.token, problem_id);
|
||||
REQUIRE(del->statusCode() == drogon::k200OK);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/controllers/problem_controller.h"
|
||||
|
||||
#include <drogon/HttpRequest.h>
|
||||
|
||||
#include <future>
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr CallList(csp::controllers::ProblemController& ctl,
|
||||
const std::string& page_size = "",
|
||||
const std::string& tags = "") {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
if (!page_size.empty()) req->setParameter("page_size", page_size);
|
||||
if (!tags.empty()) req->setParameter("tags", tags);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.list(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr CallGet(csp::controllers::ProblemController& ctl,
|
||||
int64_t problem_id) {
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.getById(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
|
||||
problem_id);
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("problem controller list/get") {
|
||||
csp::AppState::Instance().Init(":memory:");
|
||||
|
||||
csp::controllers::ProblemController ctl;
|
||||
|
||||
auto list_resp = CallList(ctl, "1");
|
||||
REQUIRE(list_resp->statusCode() == drogon::k200OK);
|
||||
|
||||
auto list_json = list_resp->jsonObject();
|
||||
REQUIRE(list_json != nullptr);
|
||||
REQUIRE((*list_json)["ok"].asBool());
|
||||
REQUIRE((*list_json)["data"]["items"].isArray());
|
||||
REQUIRE((*list_json)["data"]["items"].size() == 1);
|
||||
REQUIRE((*list_json)["data"]["total_count"].asInt() >= 1);
|
||||
|
||||
const int64_t problem_id = (*list_json)["data"]["items"][0]["id"].asInt64();
|
||||
auto get_resp = CallGet(ctl, problem_id);
|
||||
REQUIRE(get_resp->statusCode() == drogon::k200OK);
|
||||
|
||||
auto get_json = get_resp->jsonObject();
|
||||
REQUIRE(get_json != nullptr);
|
||||
REQUIRE((*get_json)["data"]["id"].asInt64() == problem_id);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/db/sqlite_db.h"
|
||||
#include "csp/services/problem_service.h"
|
||||
|
||||
TEST_CASE("problem service list/get") {
|
||||
auto db = csp::db::SqliteDb::OpenMemory();
|
||||
csp::db::ApplyMigrations(db);
|
||||
csp::db::SeedDemoData(db);
|
||||
|
||||
db.Exec(
|
||||
"INSERT INTO problems(slug,title,statement_md,difficulty,source,statement_url,llm_profile_json,"
|
||||
"sample_input,sample_output,created_at)"
|
||||
"VALUES('luogu-p1000','P1000','x',2,'luogu:P1000','https://www.luogu.com.cn/problem/P1000','{}','','',100);");
|
||||
db.Exec(
|
||||
"INSERT INTO problems(slug,title,statement_md,difficulty,source,statement_url,llm_profile_json,"
|
||||
"sample_input,sample_output,created_at)"
|
||||
"VALUES('luogu-p1001','P1001','x',3,'luogu:P1001','https://www.luogu.com.cn/problem/P1001','{}','','',100);");
|
||||
db.Exec("INSERT INTO problem_tags(problem_id,tag) VALUES((SELECT id FROM problems WHERE slug='luogu-p1000'),'csp-j');");
|
||||
db.Exec("INSERT INTO problem_tags(problem_id,tag) VALUES((SELECT id FROM problems WHERE slug='luogu-p1001'),'csp-s');");
|
||||
|
||||
csp::services::ProblemService svc(db);
|
||||
|
||||
csp::services::ProblemQuery q_all;
|
||||
const auto all = svc.List(q_all);
|
||||
REQUIRE(all.total_count >= 5);
|
||||
REQUIRE(all.items.size() >= 5);
|
||||
|
||||
csp::services::ProblemQuery q_dp;
|
||||
q_dp.tag = "dp";
|
||||
const auto dp = svc.List(q_dp);
|
||||
REQUIRE(dp.total_count >= 1);
|
||||
REQUIRE(dp.items.size() >= 1);
|
||||
|
||||
csp::services::ProblemQuery q_source;
|
||||
q_source.source_prefix = "luogu:";
|
||||
const auto luogu = svc.List(q_source);
|
||||
REQUIRE(luogu.total_count == 2);
|
||||
REQUIRE(luogu.items.size() == 2);
|
||||
|
||||
csp::services::ProblemQuery q_tags;
|
||||
q_tags.tags = {"csp-j", "csp-s"};
|
||||
const auto csp = svc.List(q_tags);
|
||||
REQUIRE(csp.total_count == 2);
|
||||
REQUIRE(csp.items.size() == 2);
|
||||
|
||||
const auto one = svc.GetById(all.items.front().id);
|
||||
REQUIRE(one.has_value());
|
||||
REQUIRE(one->id == all.items.front().id);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/controllers/problem_controller.h"
|
||||
#include "csp/services/auth_service.h"
|
||||
|
||||
#include <drogon/HttpRequest.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <future>
|
||||
|
||||
namespace {
|
||||
|
||||
int64_t FirstProblemId() {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
auto* db = csp::AppState::Instance().db().raw();
|
||||
REQUIRE(sqlite3_prepare_v2(db, "SELECT id FROM problems ORDER BY id LIMIT 1", -1,
|
||||
&stmt, nullptr) == SQLITE_OK);
|
||||
REQUIRE(sqlite3_step(stmt) == SQLITE_ROW);
|
||||
const auto id = sqlite3_column_int64(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return id;
|
||||
}
|
||||
|
||||
std::string NewToken() {
|
||||
csp::services::AuthService auth(csp::AppState::Instance().db());
|
||||
const auto r = auth.Register("workspace_tester", "password123");
|
||||
return r.token;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("problem controller draft and solution list") {
|
||||
csp::AppState::Instance().Init(":memory:");
|
||||
csp::controllers::ProblemController ctl;
|
||||
|
||||
const auto problem_id = FirstProblemId();
|
||||
const auto token = NewToken();
|
||||
|
||||
{
|
||||
Json::Value body;
|
||||
body["language"] = "cpp";
|
||||
body["code"] = "int main(){return 0;}";
|
||||
body["stdin"] = "1 2\n";
|
||||
|
||||
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
|
||||
req->setMethod(drogon::Put);
|
||||
req->addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.saveDraft(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
|
||||
problem_id);
|
||||
auto resp = p.get_future().get();
|
||||
REQUIRE(resp->statusCode() == drogon::k200OK);
|
||||
}
|
||||
|
||||
{
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
req->addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.getDraft(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
|
||||
problem_id);
|
||||
auto resp = p.get_future().get();
|
||||
REQUIRE(resp->statusCode() == drogon::k200OK);
|
||||
auto json = resp->jsonObject();
|
||||
REQUIRE(json != nullptr);
|
||||
REQUIRE((*json)["data"]["code"].asString().find("main") != std::string::npos);
|
||||
}
|
||||
|
||||
{
|
||||
auto req = drogon::HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.listSolutions(
|
||||
req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
|
||||
problem_id);
|
||||
auto resp = p.get_future().get();
|
||||
REQUIRE(resp->statusCode() == drogon::k200OK);
|
||||
auto json = resp->jsonObject();
|
||||
REQUIRE(json != nullptr);
|
||||
REQUIRE((*json)["data"]["items"].isArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/db/sqlite_db.h"
|
||||
#include "csp/services/problem_workspace_service.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
TEST_CASE("problem workspace service drafts and solution jobs") {
|
||||
auto db = csp::db::SqliteDb::OpenMemory();
|
||||
csp::db::ApplyMigrations(db);
|
||||
csp::db::SeedDemoData(db);
|
||||
REQUIRE(sqlite3_exec(
|
||||
db.raw(),
|
||||
"INSERT INTO users(username,password_salt,password_hash,created_at)"
|
||||
" VALUES('tester','salt','hash',0)",
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr) == SQLITE_OK);
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
REQUIRE(sqlite3_prepare_v2(db.raw(), "SELECT id FROM problems ORDER BY id LIMIT 1",
|
||||
-1, &stmt, nullptr) == SQLITE_OK);
|
||||
REQUIRE(sqlite3_step(stmt) == SQLITE_ROW);
|
||||
const int64_t pid = sqlite3_column_int64(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
REQUIRE(pid > 0);
|
||||
|
||||
csp::services::ProblemWorkspaceService svc(db);
|
||||
REQUIRE(svc.ProblemExists(pid));
|
||||
|
||||
svc.SaveDraft(1, pid, "cpp", "int main(){return 0;}", "1 2\n");
|
||||
const auto draft = svc.GetDraft(1, pid);
|
||||
REQUIRE(draft.has_value());
|
||||
REQUIRE(draft->language == "cpp");
|
||||
REQUIRE(draft->code.find("main") != std::string::npos);
|
||||
|
||||
const auto job_id = svc.CreateSolutionJob(pid, 1, 3);
|
||||
REQUIRE(job_id > 0);
|
||||
|
||||
const auto latest = svc.GetLatestSolutionJob(pid);
|
||||
REQUIRE(latest.has_value());
|
||||
REQUIRE(latest->id == job_id);
|
||||
REQUIRE(latest->status == "queued");
|
||||
REQUIRE(latest->max_solutions == 3);
|
||||
|
||||
const auto solutions = svc.ListSolutions(pid);
|
||||
REQUIRE(solutions.empty());
|
||||
}
|
||||
@@ -28,6 +28,17 @@ TEST_CASE("migrations create core tables") {
|
||||
REQUIRE(CountTable(db.raw(), "users") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "sessions") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "problems") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "problem_tags") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "submissions") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "wrong_book") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "contests") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "contest_problems") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "contest_registrations") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "kb_articles") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "kb_article_links") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "import_jobs") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "import_job_items") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "problem_drafts") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "problem_solution_jobs") == 1);
|
||||
REQUIRE(CountTable(db.raw(), "problem_solutions") == 1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/controllers/submission_controller.h"
|
||||
#include "csp/services/auth_service.h"
|
||||
#include "csp/services/problem_service.h"
|
||||
|
||||
#include <drogon/HttpRequest.h>
|
||||
|
||||
#include <future>
|
||||
|
||||
namespace {
|
||||
|
||||
drogon::HttpResponsePtr CallRun(csp::controllers::SubmissionController& ctl,
|
||||
const std::string& code,
|
||||
const std::string& input) {
|
||||
Json::Value body;
|
||||
body["code"] = code;
|
||||
body["input"] = input;
|
||||
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
|
||||
req->setMethod(drogon::Post);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.runCpp(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr CallSubmit(csp::controllers::SubmissionController& ctl,
|
||||
int64_t problem_id,
|
||||
const std::string& token,
|
||||
const std::string& code) {
|
||||
Json::Value body;
|
||||
body["language"] = "cpp";
|
||||
body["code"] = code;
|
||||
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
|
||||
req->setMethod(drogon::Post);
|
||||
req->addHeader("Authorization", "Bearer " + token);
|
||||
|
||||
std::promise<drogon::HttpResponsePtr> p;
|
||||
ctl.submitProblem(req,
|
||||
[&p](const drogon::HttpResponsePtr& resp) {
|
||||
p.set_value(resp);
|
||||
},
|
||||
problem_id);
|
||||
return p.get_future().get();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("submission controller run and submit") {
|
||||
csp::AppState::Instance().Init(":memory:");
|
||||
|
||||
csp::services::AuthService auth(csp::AppState::Instance().db());
|
||||
const auto user = auth.Register("submission_http_user", "password123");
|
||||
|
||||
csp::services::ProblemService problems(csp::AppState::Instance().db());
|
||||
const auto list = problems.List(csp::services::ProblemQuery{});
|
||||
REQUIRE_FALSE(list.items.empty());
|
||||
|
||||
csp::controllers::SubmissionController ctl;
|
||||
|
||||
auto run = CallRun(ctl,
|
||||
"#include <iostream>\nint main(){std::cout<<\"ok\\n\";}",
|
||||
"");
|
||||
REQUIRE(run->statusCode() == drogon::k200OK);
|
||||
|
||||
auto submit = CallSubmit(
|
||||
ctl,
|
||||
list.items.front().id,
|
||||
user.token,
|
||||
R"CPP(#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
int main(){ long long a,b; if(!(cin>>a>>b)) return 0; cout << (a+b) << "\n"; }
|
||||
)CPP");
|
||||
|
||||
REQUIRE(submit->statusCode() == drogon::k200OK);
|
||||
auto submit_json = submit->jsonObject();
|
||||
REQUIRE(submit_json != nullptr);
|
||||
REQUIRE((*submit_json)["data"]["id"].asInt64() > 0);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "csp/db/sqlite_db.h"
|
||||
#include "csp/domain/enum_strings.h"
|
||||
#include "csp/services/auth_service.h"
|
||||
#include "csp/services/problem_service.h"
|
||||
#include "csp/services/submission_service.h"
|
||||
#include "csp/services/wrong_book_service.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kAcCode = R"CPP(#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
int main() {
|
||||
long long a, b;
|
||||
if (!(cin >> a >> b)) return 0;
|
||||
cout << (a + b) << "\n";
|
||||
return 0;
|
||||
}
|
||||
)CPP";
|
||||
|
||||
const char* kWaCode = R"CPP(#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
int main() {
|
||||
cout << 0 << "\n";
|
||||
return 0;
|
||||
}
|
||||
)CPP";
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("submission service judge and wrong-book flow") {
|
||||
auto db = csp::db::SqliteDb::OpenMemory();
|
||||
csp::db::ApplyMigrations(db);
|
||||
csp::db::SeedDemoData(db);
|
||||
|
||||
csp::services::AuthService auth(db);
|
||||
const auto user = auth.Register("submit_user", "password123");
|
||||
|
||||
csp::services::ProblemService problems(db);
|
||||
csp::services::ProblemQuery q;
|
||||
const auto list = problems.List(q);
|
||||
REQUIRE_FALSE(list.items.empty());
|
||||
const int64_t problem_id = list.items.front().id;
|
||||
|
||||
csp::services::SubmissionService submissions(db);
|
||||
csp::services::WrongBookService wrong_book(db);
|
||||
|
||||
csp::services::SubmissionCreateRequest bad;
|
||||
bad.user_id = user.user_id;
|
||||
bad.problem_id = problem_id;
|
||||
bad.language = "cpp";
|
||||
bad.code = kWaCode;
|
||||
|
||||
const auto bad_result = submissions.CreateAndJudge(bad);
|
||||
REQUIRE(bad_result.status == csp::domain::SubmissionStatus::WA);
|
||||
|
||||
const auto wb_after_bad = wrong_book.ListByUser(user.user_id);
|
||||
REQUIRE_FALSE(wb_after_bad.empty());
|
||||
|
||||
csp::services::SubmissionCreateRequest good;
|
||||
good.user_id = user.user_id;
|
||||
good.problem_id = problem_id;
|
||||
good.language = "cpp";
|
||||
good.code = kAcCode;
|
||||
|
||||
const auto good_result = submissions.CreateAndJudge(good);
|
||||
REQUIRE(good_result.status == csp::domain::SubmissionStatus::AC);
|
||||
|
||||
bool still_in_wrong_book = false;
|
||||
for (const auto& row : wrong_book.ListByUser(user.user_id)) {
|
||||
if (row.item.problem_id == problem_id) {
|
||||
still_in_wrong_book = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE_FALSE(still_in_wrong_book);
|
||||
|
||||
const auto run_only =
|
||||
submissions.RunOnlyCpp(R"CPP(#include <iostream>
|
||||
int main(){std::cout<<42<<"\n";}
|
||||
)CPP",
|
||||
"");
|
||||
REQUIRE(run_only.status == csp::domain::SubmissionStatus::Running);
|
||||
REQUIRE(run_only.stdout_text == "42\n");
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户