feat: expand platform management, admin controls, and learning workflows
这个提交包含在:
@@ -0,0 +1,320 @@
|
||||
#include "csp/controllers/admin_controller.h"
|
||||
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/services/redeem_service.h"
|
||||
#include "csp/services/user_service.h"
|
||||
#include "http_auth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#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));
|
||||
}
|
||||
|
||||
bool ParseBoolLike(const std::string& raw, bool default_value) {
|
||||
if (raw.empty()) return default_value;
|
||||
std::string v = raw;
|
||||
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;
|
||||
}
|
||||
|
||||
std::optional<int64_t> ParseOptionalInt64(const std::string& raw) {
|
||||
if (raw.empty()) return std::nullopt;
|
||||
return std::stoll(raw);
|
||||
}
|
||||
|
||||
services::RedeemItemWrite ParseRedeemItemWrite(const Json::Value& json) {
|
||||
services::RedeemItemWrite write;
|
||||
write.name = json.get("name", "").asString();
|
||||
write.description = json.get("description", "").asString();
|
||||
write.unit_label = json.get("unit_label", "小时").asString();
|
||||
write.holiday_cost = json.get("holiday_cost", 5).asInt();
|
||||
write.studyday_cost = json.get("studyday_cost", 25).asInt();
|
||||
write.is_active = json.get("is_active", true).asBool();
|
||||
write.is_global = json.get("is_global", true).asBool();
|
||||
return write;
|
||||
}
|
||||
|
||||
Json::Value ToJson(const services::RedeemItem& item) {
|
||||
Json::Value j;
|
||||
j["id"] = Json::Int64(item.id);
|
||||
j["name"] = item.name;
|
||||
j["description"] = item.description;
|
||||
j["unit_label"] = item.unit_label;
|
||||
j["holiday_cost"] = item.holiday_cost;
|
||||
j["studyday_cost"] = item.studyday_cost;
|
||||
j["is_active"] = item.is_active;
|
||||
j["is_global"] = item.is_global;
|
||||
j["created_by"] = Json::Int64(item.created_by);
|
||||
j["created_at"] = Json::Int64(item.created_at);
|
||||
j["updated_at"] = Json::Int64(item.updated_at);
|
||||
return j;
|
||||
}
|
||||
|
||||
std::optional<int64_t> RequireAdminUserId(
|
||||
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;
|
||||
}
|
||||
|
||||
services::UserService users(csp::AppState::Instance().db());
|
||||
const auto user = users.GetById(*user_id);
|
||||
if (!user.has_value() || user->username != "admin") {
|
||||
cb(JsonError(drogon::k403Forbidden, "admin only"));
|
||||
return std::nullopt;
|
||||
}
|
||||
return user_id;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AdminController::listUsers(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
const int page = ParseClampedInt(req->getParameter("page"), 1, 1, 100000);
|
||||
const int page_size =
|
||||
ParseClampedInt(req->getParameter("page_size"), 50, 1, 200);
|
||||
|
||||
services::UserService users(csp::AppState::Instance().db());
|
||||
const auto result = users.ListUsers(page, page_size);
|
||||
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& item : result.items) {
|
||||
Json::Value one;
|
||||
one["id"] = Json::Int64(item.user_id);
|
||||
one["username"] = item.username;
|
||||
one["rating"] = item.rating;
|
||||
one["created_at"] = Json::Int64(item.created_at);
|
||||
arr.append(one);
|
||||
}
|
||||
|
||||
Json::Value payload;
|
||||
payload["items"] = arr;
|
||||
payload["total_count"] = result.total_count;
|
||||
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 AdminController::updateUserRating(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t user_id) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
const auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(*json).isMember("rating")) {
|
||||
cb(JsonError(drogon::k400BadRequest, "rating is required"));
|
||||
return;
|
||||
}
|
||||
const int rating = (*json)["rating"].asInt();
|
||||
if (rating < 0) {
|
||||
cb(JsonError(drogon::k400BadRequest, "rating must be >= 0"));
|
||||
return;
|
||||
}
|
||||
|
||||
services::UserService users(csp::AppState::Instance().db());
|
||||
users.SetRating(user_id, rating);
|
||||
|
||||
const auto updated = users.GetById(user_id);
|
||||
if (!updated.has_value()) {
|
||||
cb(JsonError(drogon::k404NotFound, "user not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
Json::Value payload;
|
||||
payload["id"] = Json::Int64(updated->id);
|
||||
payload["username"] = updated->username;
|
||||
payload["rating"] = updated->rating;
|
||||
payload["updated"] = true;
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::invalid_argument&) {
|
||||
cb(JsonError(drogon::k400BadRequest, "invalid rating"));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void AdminController::listRedeemItems(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
const bool include_inactive =
|
||||
ParseBoolLike(req->getParameter("include_inactive"), true);
|
||||
services::RedeemService redeem(csp::AppState::Instance().db());
|
||||
const auto items = redeem.ListItems(include_inactive);
|
||||
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& item : items) arr.append(ToJson(item));
|
||||
cb(JsonOk(arr));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void AdminController::createRedeemItem(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
const auto admin_user_id = RequireAdminUserId(req, cb);
|
||||
if (!admin_user_id.has_value()) return;
|
||||
|
||||
const auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto input = ParseRedeemItemWrite(*json);
|
||||
services::RedeemService redeem(csp::AppState::Instance().db());
|
||||
const auto item = redeem.CreateItem(*admin_user_id, input);
|
||||
cb(JsonOk(ToJson(item)));
|
||||
} catch (const std::runtime_error& e) {
|
||||
cb(JsonError(drogon::k400BadRequest, e.what()));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void AdminController::updateRedeemItem(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t item_id) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
const auto json = req->getJsonObject();
|
||||
if (!json) {
|
||||
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto input = ParseRedeemItemWrite(*json);
|
||||
services::RedeemService redeem(csp::AppState::Instance().db());
|
||||
const auto item = redeem.UpdateItem(item_id, input);
|
||||
cb(JsonOk(ToJson(item)));
|
||||
} catch (const std::runtime_error& e) {
|
||||
cb(JsonError(drogon::k400BadRequest, e.what()));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void AdminController::deleteRedeemItem(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t item_id) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
services::RedeemService redeem(csp::AppState::Instance().db());
|
||||
redeem.DeactivateItem(item_id);
|
||||
|
||||
Json::Value payload;
|
||||
payload["id"] = Json::Int64(item_id);
|
||||
payload["deleted"] = 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 AdminController::listRedeemRecords(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
const auto user_id = ParseOptionalInt64(req->getParameter("user_id"));
|
||||
const int limit = ParseClampedInt(req->getParameter("limit"), 200, 1, 500);
|
||||
|
||||
services::RedeemService redeem(csp::AppState::Instance().db());
|
||||
const auto rows = redeem.ListRecordsAll(user_id, limit);
|
||||
|
||||
Json::Value arr(Json::arrayValue);
|
||||
for (const auto& row : rows) {
|
||||
Json::Value j;
|
||||
j["id"] = Json::Int64(row.id);
|
||||
j["user_id"] = Json::Int64(row.user_id);
|
||||
j["username"] = row.username;
|
||||
j["item_id"] = Json::Int64(row.item_id);
|
||||
j["item_name"] = row.item_name;
|
||||
j["quantity"] = row.quantity;
|
||||
j["day_type"] = row.day_type;
|
||||
j["unit_cost"] = row.unit_cost;
|
||||
j["total_cost"] = row.total_cost;
|
||||
j["note"] = row.note;
|
||||
j["created_at"] = Json::Int64(row.created_at);
|
||||
arr.append(j);
|
||||
}
|
||||
cb(JsonOk(arr));
|
||||
} 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()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace csp::controllers
|
||||
@@ -3,9 +3,12 @@
|
||||
#include "csp/app_state.h"
|
||||
#include "csp/services/import_runner.h"
|
||||
#include "csp/services/import_service.h"
|
||||
#include "csp/services/user_service.h"
|
||||
#include "http_auth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
@@ -40,6 +43,25 @@ int ParsePositiveInt(const std::string& s,
|
||||
return std::max(min_value, std::min(max_value, v));
|
||||
}
|
||||
|
||||
std::optional<int64_t> RequireAdminUserId(
|
||||
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;
|
||||
}
|
||||
|
||||
services::UserService users(csp::AppState::Instance().db());
|
||||
const auto user = users.GetById(*user_id);
|
||||
if (!user.has_value() || user->username != "admin") {
|
||||
cb(JsonError(drogon::k403Forbidden, "admin only"));
|
||||
return std::nullopt;
|
||||
}
|
||||
return user_id;
|
||||
}
|
||||
|
||||
Json::Value ToJson(const services::ImportJob& job) {
|
||||
Json::Value j;
|
||||
j["id"] = Json::Int64(job.id);
|
||||
@@ -94,9 +116,11 @@ Json::Value ToJson(const services::ImportJobItem& item) {
|
||||
} // namespace
|
||||
|
||||
void ImportController::latestJob(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
services::ImportService svc(csp::AppState::Instance().db());
|
||||
Json::Value payload;
|
||||
const auto job = svc.GetLatestJob();
|
||||
@@ -113,10 +137,12 @@ void ImportController::latestJob(
|
||||
}
|
||||
|
||||
void ImportController::jobById(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t job_id) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
services::ImportService svc(csp::AppState::Instance().db());
|
||||
const auto job = svc.GetById(job_id);
|
||||
if (!job.has_value()) {
|
||||
@@ -136,6 +162,8 @@ void ImportController::jobItems(
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t job_id) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
services::ImportJobItemQuery query;
|
||||
query.status = req->getParameter("status");
|
||||
query.page = ParsePositiveInt(req->getParameter("page"), 1, 1, 100000);
|
||||
@@ -164,12 +192,26 @@ void ImportController::runJob(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
services::ImportRunOptions opts;
|
||||
const auto json = req->getJsonObject();
|
||||
if (json) {
|
||||
if ((*json).isMember("mode")) {
|
||||
opts.mode = (*json)["mode"].asString();
|
||||
}
|
||||
opts.clear_all_problems =
|
||||
(*json).isMember("clear_all_problems") &&
|
||||
(*json)["clear_all_problems"].asBool();
|
||||
if ((*json).isMember("local_pdf_dir")) {
|
||||
opts.local_pdf_dir = (*json)["local_pdf_dir"].asString();
|
||||
}
|
||||
if ((*json).isMember("target_total")) {
|
||||
opts.target_total = std::max(1, std::min(50000, (*json)["target_total"].asInt()));
|
||||
}
|
||||
if ((*json).isMember("workers")) {
|
||||
opts.workers = std::max(1, std::min(16, (*json)["workers"].asInt()));
|
||||
}
|
||||
}
|
||||
const bool started =
|
||||
services::ImportRunner::Instance().TriggerAsync("manual", opts);
|
||||
@@ -181,6 +223,7 @@ void ImportController::runJob(
|
||||
Json::Value payload;
|
||||
payload["started"] = true;
|
||||
payload["running"] = true;
|
||||
payload["mode"] = opts.mode;
|
||||
cb(JsonOk(payload));
|
||||
} catch (const std::exception& e) {
|
||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
#include "csp/services/problem_solution_runner.h"
|
||||
#include "csp/services/problem_workspace_service.h"
|
||||
#include "csp/services/submission_service.h"
|
||||
#include "csp/services/user_service.h"
|
||||
#include "http_auth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace csp::controllers {
|
||||
@@ -47,6 +49,25 @@ int ParsePositiveInt(const std::string& s,
|
||||
return std::max(min_value, std::min(max_value, v));
|
||||
}
|
||||
|
||||
std::optional<int64_t> RequireAdminUserId(
|
||||
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;
|
||||
}
|
||||
|
||||
services::UserService users(csp::AppState::Instance().db());
|
||||
const auto user = users.GetById(*user_id);
|
||||
if (!user.has_value() || user->username != "admin") {
|
||||
cb(JsonError(drogon::k403Forbidden, "admin only"));
|
||||
return std::nullopt;
|
||||
}
|
||||
return user_id;
|
||||
}
|
||||
|
||||
Json::Value BuildOpenApiSpec() {
|
||||
Json::Value root;
|
||||
root["openapi"] = "3.1.0";
|
||||
@@ -173,6 +194,8 @@ void MetaController::backendLogs(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
const int limit =
|
||||
ParsePositiveInt(req->getParameter("limit"), 100, 1, 500);
|
||||
const int running_limit =
|
||||
@@ -274,9 +297,11 @@ void MetaController::backendLogs(
|
||||
}
|
||||
|
||||
void MetaController::kbRefreshStatus(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
try {
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
const auto& runner = services::KbImportRunner::Instance();
|
||||
Json::Value payload;
|
||||
payload["running"] = runner.IsRunning();
|
||||
@@ -299,12 +324,7 @@ void MetaController::triggerKbRefresh(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
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;
|
||||
}
|
||||
if (!RequireAdminUserId(req, cb).has_value()) return;
|
||||
|
||||
auto& runner = services::KbImportRunner::Instance();
|
||||
const bool started = runner.TriggerAsync("manual");
|
||||
@@ -332,12 +352,8 @@ void MetaController::triggerMissingSolutions(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||
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 user_id = RequireAdminUserId(req, cb);
|
||||
if (!user_id.has_value()) return;
|
||||
|
||||
int limit = 50000;
|
||||
int max_solutions = 3;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#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 <algorithm>
|
||||
@@ -192,7 +193,7 @@ void ProblemController::saveDraft(
|
||||
}
|
||||
|
||||
void ProblemController::listSolutions(
|
||||
const drogon::HttpRequestPtr& /*req*/,
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||
int64_t problem_id) {
|
||||
try {
|
||||
@@ -203,28 +204,72 @@ void ProblemController::listSolutions(
|
||||
}
|
||||
|
||||
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);
|
||||
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 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()) {
|
||||
@@ -280,16 +325,20 @@ void ProblemController::generateSolutions(
|
||||
}
|
||||
|
||||
const int64_t job_id = svc.CreateSolutionJob(problem_id, *user_id, max_solutions);
|
||||
const bool started = services::ProblemSolutionRunner::Instance().TriggerAsync(
|
||||
auto& runner = services::ProblemSolutionRunner::Instance();
|
||||
const bool queued = runner.TriggerAsync(
|
||||
problem_id, job_id, max_solutions);
|
||||
if (!started) {
|
||||
cb(JsonError(drogon::k409Conflict, "solution generation is already running"));
|
||||
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()));
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
#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_service.h"
|
||||
#include "csp/services/submission_service.h"
|
||||
#include "http_auth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
@@ -49,6 +54,32 @@ std::optional<int64_t> ParseOptionalInt64(const std::string& s) {
|
||||
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(
|
||||
@@ -153,7 +184,91 @@ void SubmissionController::getSubmission(
|
||||
cb(JsonError(drogon::k404NotFound, "submission not found"));
|
||||
return;
|
||||
}
|
||||
cb(JsonOk(domain::ToJson(*s)));
|
||||
Json::Value payload = domain::ToJson(*s);
|
||||
payload["code"] = s->code;
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户