feat: rebuild CSP practice workflow, UX and automation

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

查看文件

@@ -0,0 +1,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