feat: rebuild CSP practice workflow, UX and automation
这个提交包含在:
@@ -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
|
||||
在新工单中引用
屏蔽一个用户