feat: add daily tasks and fix /admin139 admin entry

这个提交包含在:
Codex CLI
2026-02-15 12:51:42 +08:00
父节点 e2ab522b78
当前提交 ad29a9f62d
修改 13 个文件,包含 1200 行新增30 行删除

查看文件

@@ -3,9 +3,13 @@
#include "csp/app_state.h"
#include "csp/domain/enum_strings.h"
#include "csp/domain/json.h"
#include "csp/services/kb_import_runner.h"
#include "csp/services/problem_gen_runner.h"
#include "csp/services/problem_service.h"
#include "csp/services/problem_solution_runner.h"
#include "csp/services/problem_workspace_service.h"
#include "csp/services/submission_service.h"
#include "http_auth.h"
#include <algorithm>
#include <exception>
@@ -25,6 +29,24 @@ drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode 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));
}
Json::Value BuildOpenApiSpec() {
Json::Value root;
root["openapi"] = "3.1.0";
@@ -54,11 +76,28 @@ Json::Value BuildOpenApiSpec() {
paths["/api/v1/run/cpp"]["post"]["summary"] = "C++ 试运行";
paths["/api/v1/submissions"]["get"]["summary"] = "提交记录";
paths["/api/v1/submissions/{id}"]["get"]["summary"] = "提交详情";
paths["/api/v1/submissions/{id}/analysis"]["post"]["summary"] = "提交评测建议LLM";
paths["/api/v1/admin/users"]["get"]["summary"] = "管理员用户列表";
paths["/api/v1/admin/users/{id}/rating"]["patch"]["summary"] = "管理员修改用户积分";
paths["/api/v1/admin/redeem-items"]["get"]["summary"] = "管理员查看积分兑换物品";
paths["/api/v1/admin/redeem-items"]["post"]["summary"] = "管理员新增积分兑换物品";
paths["/api/v1/admin/redeem-items/{id}"]["patch"]["summary"] = "管理员修改积分兑换物品";
paths["/api/v1/admin/redeem-items/{id}"]["delete"]["summary"] = "管理员下架积分兑换物品";
paths["/api/v1/admin/redeem-records"]["get"]["summary"] = "管理员查看兑换记录";
paths["/api/v1/me/redeem/items"]["get"]["summary"] = "我的可兑换物品列表";
paths["/api/v1/me/redeem/records"]["get"]["summary"] = "我的兑换记录";
paths["/api/v1/me/redeem/records"]["post"]["summary"] = "创建兑换记录";
paths["/api/v1/me/daily-tasks"]["get"]["summary"] = "我的每日任务列表";
paths["/api/v1/import/jobs/latest"]["get"]["summary"] = "最新导入任务";
paths["/api/v1/import/jobs/run"]["post"]["summary"] = "触发导入任务";
paths["/api/v1/problem-gen/status"]["get"]["summary"] = "CSP-J 生成任务状态";
paths["/api/v1/problem-gen/run"]["post"]["summary"] = "触发生成新题RAG+去重)";
paths["/api/v1/backend/logs"]["get"]["summary"] = "后台日志(题解任务队列)";
paths["/api/v1/backend/kb/refresh"]["get"]["summary"] = "知识库资料更新状态";
paths["/api/v1/backend/kb/refresh"]["post"]["summary"] = "手动一键更新知识库资料";
paths["/api/v1/backend/solutions/generate-missing"]["post"]["summary"] =
"异步补全所有缺失题解";
paths["/api/v1/mcp"]["post"]["summary"] = "MCP JSON-RPC 入口";
@@ -130,6 +169,205 @@ void MetaController::openapi(
cb(resp);
}
void MetaController::backendLogs(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
const int limit =
ParsePositiveInt(req->getParameter("limit"), 100, 1, 500);
const int running_limit =
ParsePositiveInt(req->getParameter("running_limit"), 20, 1, 200);
const int queued_limit =
ParsePositiveInt(req->getParameter("queued_limit"), 100, 1, 1000);
services::ProblemWorkspaceService workspace(csp::AppState::Instance().db());
const auto jobs = workspace.ListRecentSolutionJobs(limit);
const auto running_jobs = workspace.ListSolutionJobsByStatus(
"running", running_limit);
const auto queued_jobs = workspace.ListSolutionJobsByStatus(
"queued", queued_limit);
auto& runner = services::ProblemSolutionRunner::Instance();
Json::Value items(Json::arrayValue);
for (const auto& job : jobs) {
Json::Value j;
j["id"] = Json::Int64(job.id);
j["problem_id"] = Json::Int64(job.problem_id);
j["problem_title"] = job.problem_title;
j["status"] = job.status;
j["progress"] = job.progress;
j["message"] = job.message;
j["created_by"] = Json::Int64(job.created_by);
j["max_solutions"] = job.max_solutions;
j["created_at"] = Json::Int64(job.created_at);
if (job.started_at.has_value()) {
j["started_at"] = Json::Int64(*job.started_at);
} else {
j["started_at"] = Json::nullValue;
}
if (job.finished_at.has_value()) {
j["finished_at"] = Json::Int64(*job.finished_at);
} else {
j["finished_at"] = Json::nullValue;
}
j["updated_at"] = Json::Int64(job.updated_at);
j["runner_pending"] = runner.IsRunning(job.problem_id);
items.append(j);
}
Json::Value running_items(Json::arrayValue);
Json::Value running_problem_ids(Json::arrayValue);
for (const auto& job : running_jobs) {
Json::Value j;
j["id"] = Json::Int64(job.id);
j["problem_id"] = Json::Int64(job.problem_id);
j["problem_title"] = job.problem_title;
j["status"] = job.status;
j["progress"] = job.progress;
j["message"] = job.message;
j["updated_at"] = Json::Int64(job.updated_at);
if (job.started_at.has_value()) {
j["started_at"] = Json::Int64(*job.started_at);
} else {
j["started_at"] = Json::nullValue;
}
running_items.append(j);
running_problem_ids.append(Json::Int64(job.problem_id));
}
Json::Value queued_items(Json::arrayValue);
Json::Value queued_problem_ids(Json::arrayValue);
for (const auto& job : queued_jobs) {
Json::Value j;
j["id"] = Json::Int64(job.id);
j["problem_id"] = Json::Int64(job.problem_id);
j["problem_title"] = job.problem_title;
j["status"] = job.status;
j["progress"] = job.progress;
j["message"] = job.message;
j["updated_at"] = Json::Int64(job.updated_at);
queued_items.append(j);
queued_problem_ids.append(Json::Int64(job.problem_id));
}
Json::Value payload;
payload["items"] = items;
payload["running_jobs"] = running_items;
payload["queued_jobs"] = queued_items;
payload["running_problem_ids"] = running_problem_ids;
payload["queued_problem_ids"] = queued_problem_ids;
payload["running_count"] = static_cast<int>(running_jobs.size());
payload["queued_count_preview"] = static_cast<int>(queued_jobs.size());
payload["pending_jobs"] = Json::UInt64(runner.PendingCount());
payload["missing_problems"] = workspace.CountProblemsWithoutSolutions();
payload["limit"] = limit;
payload["running_limit"] = running_limit;
payload["queued_limit"] = queued_limit;
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 MetaController::kbRefreshStatus(
const drogon::HttpRequestPtr& /*req*/,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
const auto& runner = services::KbImportRunner::Instance();
Json::Value payload;
payload["running"] = runner.IsRunning();
payload["last_command"] = runner.LastCommand();
payload["last_trigger"] = runner.LastTrigger();
if (const auto rc = runner.LastExitCode(); rc.has_value()) {
payload["last_exit_code"] = *rc;
} else {
payload["last_exit_code"] = Json::nullValue;
}
payload["last_started_at"] = Json::Int64(runner.LastStartedAt());
payload["last_finished_at"] = Json::Int64(runner.LastFinishedAt());
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
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;
}
auto& runner = services::KbImportRunner::Instance();
const bool started = runner.TriggerAsync("manual");
Json::Value payload;
payload["started"] = started;
payload["message"] = started ? "已触发异步资料更新" : "当前已有资料更新任务在运行中";
payload["running"] = runner.IsRunning();
payload["last_command"] = runner.LastCommand();
payload["last_trigger"] = runner.LastTrigger();
if (const auto rc = runner.LastExitCode(); rc.has_value()) {
payload["last_exit_code"] = *rc;
} else {
payload["last_exit_code"] = Json::nullValue;
}
payload["last_started_at"] = Json::Int64(runner.LastStartedAt());
payload["last_finished_at"] = Json::Int64(runner.LastFinishedAt());
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
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;
}
int limit = 50000;
int max_solutions = 3;
const auto json = req->getJsonObject();
if (json && (*json).isMember("limit")) {
limit = std::max(1, std::min(200000, (*json)["limit"].asInt()));
}
if (json && (*json).isMember("max_solutions")) {
max_solutions = std::max(1, std::min(5, (*json)["max_solutions"].asInt()));
}
auto& db = csp::AppState::Instance().db();
auto& runner = services::ProblemSolutionRunner::Instance();
const auto summary = runner.TriggerMissingAsync(
db, *user_id, max_solutions, limit);
Json::Value payload;
payload["started"] = true;
payload["missing_total"] = summary.missing_total;
payload["candidate_count"] = summary.candidate_count;
payload["queued_count"] = summary.queued_count;
payload["pending_jobs"] = Json::UInt64(runner.PendingCount());
payload["limit"] = limit;
payload["max_solutions"] = max_solutions;
cb(JsonOk(payload));
} catch (const std::exception& e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MetaController::mcp(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {