331 行
12 KiB
C++
331 行
12 KiB
C++
#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/problem_service.h"
|
|
#include "csp/services/solution_access_service.h"
|
|
#include "csp/services/submission_feedback_runner.h"
|
|
#include "csp/services/submission_feedback_service.h"
|
|
#include "csp/services/submission_service.h"
|
|
#include "http_auth.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <exception>
|
|
#include <memory>
|
|
#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);
|
|
}
|
|
|
|
bool ParseBoolLike(const std::string& s, bool default_value) {
|
|
if (s.empty()) return default_value;
|
|
std::string v = s;
|
|
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
|
return static_cast<char>(std::tolower(c));
|
|
});
|
|
if (v == "1" || v == "true" || v == "yes" || v == "on") return true;
|
|
if (v == "0" || v == "false" || v == "no" || v == "off") return false;
|
|
return default_value;
|
|
}
|
|
|
|
Json::Value ParseLinksArray(const std::string& links_json) {
|
|
Json::CharReaderBuilder builder;
|
|
Json::Value parsed;
|
|
std::string errs;
|
|
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
|
|
if (!reader->parse(links_json.data(),
|
|
links_json.data() + links_json.size(),
|
|
&parsed,
|
|
&errs) ||
|
|
!parsed.isArray()) {
|
|
return Json::Value(Json::arrayValue);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
} // 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);
|
|
|
|
// Auto-enqueue LLM feedback generation in background.
|
|
services::SubmissionFeedbackRunner::Instance().Enqueue(s.id);
|
|
|
|
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 auto created_from = ParseOptionalInt64(req->getParameter("created_from"));
|
|
const auto created_to = ParseOptionalInt64(req->getParameter("created_to"));
|
|
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, created_from, created_to, 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;
|
|
}
|
|
Json::Value payload = domain::ToJson(*s);
|
|
payload["code"] = s->code;
|
|
const auto siblings = svc.GetSiblingIds(s->user_id, s->id);
|
|
payload["same_user_prev_submission_id"] =
|
|
siblings.prev_id.has_value() ? Json::Value(Json::Int64(*siblings.prev_id))
|
|
: Json::Value(Json::nullValue);
|
|
payload["same_user_next_submission_id"] =
|
|
siblings.next_id.has_value() ? Json::Value(Json::Int64(*siblings.next_id))
|
|
: Json::Value(Json::nullValue);
|
|
|
|
// Attach problem title for frontend linking.
|
|
{
|
|
services::ProblemService psvc(csp::AppState::Instance().db());
|
|
if (const auto p = psvc.GetById(s->problem_id); p.has_value()) {
|
|
payload["problem_title"] = p->title;
|
|
}
|
|
}
|
|
|
|
services::SolutionAccessService access_svc(csp::AppState::Instance().db());
|
|
const auto stats =
|
|
access_svc.QueryUserProblemViewStats(s->user_id, s->problem_id);
|
|
payload["has_viewed_answer"] = stats.has_viewed;
|
|
payload["answer_view_count"] = stats.total_views;
|
|
payload["answer_view_total_cost"] = stats.total_cost;
|
|
if (stats.last_viewed_at.has_value()) {
|
|
payload["last_answer_view_at"] = Json::Int64(*stats.last_viewed_at);
|
|
} else {
|
|
payload["last_answer_view_at"] = Json::nullValue;
|
|
}
|
|
|
|
services::SubmissionFeedbackService feedback_svc(csp::AppState::Instance().db());
|
|
if (const auto feedback = feedback_svc.GetBySubmissionId(s->id);
|
|
feedback.has_value()) {
|
|
Json::Value analysis;
|
|
analysis["feedback_md"] = feedback->feedback_md;
|
|
analysis["links"] = ParseLinksArray(feedback->links_json);
|
|
analysis["model_name"] = feedback->model_name;
|
|
analysis["status"] = feedback->status;
|
|
analysis["created_at"] = Json::Int64(feedback->created_at);
|
|
analysis["updated_at"] = Json::Int64(feedback->updated_at);
|
|
payload["analysis"] = analysis;
|
|
} else {
|
|
payload["analysis"] = Json::nullValue;
|
|
}
|
|
|
|
cb(JsonOk(payload));
|
|
} catch (const std::exception& e) {
|
|
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
|
}
|
|
}
|
|
|
|
void SubmissionController::analyzeSubmission(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
|
int64_t submission_id) {
|
|
try {
|
|
std::string auth_error;
|
|
if (!GetAuthedUserId(req, auth_error).has_value()) {
|
|
cb(JsonError(drogon::k401Unauthorized, auth_error));
|
|
return;
|
|
}
|
|
|
|
bool force_refresh = ParseBoolLike(req->getParameter("refresh"), false);
|
|
if (const auto json = req->getJsonObject();
|
|
json && (*json).isMember("refresh")) {
|
|
force_refresh = (*json)["refresh"].asBool();
|
|
}
|
|
|
|
services::SubmissionService submission_svc(csp::AppState::Instance().db());
|
|
const auto submission = submission_svc.GetById(submission_id);
|
|
if (!submission.has_value()) {
|
|
cb(JsonError(drogon::k404NotFound, "submission not found"));
|
|
return;
|
|
}
|
|
|
|
services::ProblemService problem_svc(csp::AppState::Instance().db());
|
|
const auto problem = problem_svc.GetById(submission->problem_id);
|
|
if (!problem.has_value()) {
|
|
cb(JsonError(drogon::k404NotFound, "problem not found"));
|
|
return;
|
|
}
|
|
|
|
services::SubmissionFeedbackService feedback_svc(csp::AppState::Instance().db());
|
|
const auto feedback =
|
|
feedback_svc.GenerateAndSave(*submission, *problem, force_refresh);
|
|
|
|
Json::Value payload;
|
|
payload["submission_id"] = Json::Int64(feedback.submission_id);
|
|
payload["feedback_md"] = feedback.feedback_md;
|
|
payload["links"] = ParseLinksArray(feedback.links_json);
|
|
payload["model_name"] = feedback.model_name;
|
|
payload["status"] = feedback.status;
|
|
payload["created_at"] = Json::Int64(feedback.created_at);
|
|
payload["updated_at"] = Json::Int64(feedback.updated_at);
|
|
payload["refresh"] = force_refresh;
|
|
cb(JsonOk(payload));
|
|
} catch (const std::invalid_argument&) {
|
|
cb(JsonError(drogon::k400BadRequest, "invalid request field"));
|
|
} 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::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
|