#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 "csp/services/solution_access_service.h" #include "http_auth.h" #include #include #include #include #include #include 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 ParseCsv(const std::string& raw) { auto trim = [](std::string s) { while (!s.empty() && std::isspace(static_cast(s.front()))) { s.erase(s.begin()); } while (!s.empty() && std::isspace(static_cast(s.back()))) { s.pop_back(); } return s; }; std::vector 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&& 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&& 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&& 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&& 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&& 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 bool has_solutions = !rows.empty(); const auto latest_job = svc.GetLatestSolutionJob(problem_id); const std::string mode = req->getParameter("mode"); const bool need_full = mode == "full"; Json::Value arr(Json::arrayValue); Json::Value access(Json::objectValue); access["required"] = true; access["daily_free_quota"] = 1; access["cost_after_free"] = 2; if (need_full && has_solutions) { 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::SolutionAccessService access_svc(csp::AppState::Instance().db()); const auto charge = access_svc.ConsumeSolutionView(*user_id, problem_id); if (!charge.granted) { cb(JsonError(drogon::k402PaymentRequired, "rating 不足:首次免费后每次查看答案需 2 分")); return; } access["mode"] = "full"; access["charged"] = charge.charged; access["daily_free"] = charge.daily_free; access["cost"] = charge.cost; access["day_key"] = charge.day_key; access["daily_used_count"] = charge.daily_used_count; access["rating_before"] = charge.rating_before; access["rating_after"] = charge.rating_after; access["viewed_at"] = Json::Int64(charge.viewed_at); 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); } } else { access["mode"] = "preview"; access["charged"] = false; access["daily_free"] = false; access["cost"] = 0; access["daily_used_count"] = 0; } Json::Value payload; payload["items"] = arr; payload["has_solutions"] = has_solutions; payload["answer_status"] = has_solutions ? "已有" : "待生成"; payload["access"] = access; 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&& 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); auto& runner = services::ProblemSolutionRunner::Instance(); const bool queued = runner.TriggerAsync( problem_id, job_id, max_solutions); if (!queued) { cb(JsonError(drogon::k500InternalServerError, "solution generation queue is unavailable")); return; } Json::Value payload; payload["queued"] = true; payload["started"] = true; payload["job_id"] = Json::Int64(job_id); payload["pending_jobs"] = Json::UInt64(runner.PendingCount()); cb(JsonOk(payload)); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } } // namespace csp::controllers