feat: expand platform management, admin controls, and learning workflows

这个提交包含在:
Codex CLI
2026-02-15 15:41:56 +08:00
父节点 ad29a9f62d
当前提交 f209ae82da
修改 75 个文件,包含 9663 行新增794 行删除

查看文件

@@ -0,0 +1,54 @@
#pragma once
#include <drogon/HttpController.h>
#include <cstdint>
namespace csp::controllers {
class AdminController : public drogon::HttpController<AdminController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(AdminController::listUsers, "/api/v1/admin/users", drogon::Get);
ADD_METHOD_TO(AdminController::updateUserRating,
"/api/v1/admin/users/{1}/rating",
drogon::Patch);
ADD_METHOD_TO(AdminController::listRedeemItems, "/api/v1/admin/redeem-items", drogon::Get);
ADD_METHOD_TO(AdminController::createRedeemItem, "/api/v1/admin/redeem-items", drogon::Post);
ADD_METHOD_TO(AdminController::updateRedeemItem,
"/api/v1/admin/redeem-items/{1}",
drogon::Patch);
ADD_METHOD_TO(AdminController::deleteRedeemItem,
"/api/v1/admin/redeem-items/{1}",
drogon::Delete);
ADD_METHOD_TO(AdminController::listRedeemRecords,
"/api/v1/admin/redeem-records",
drogon::Get);
METHOD_LIST_END
void listUsers(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void updateUserRating(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t user_id);
void listRedeemItems(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void createRedeemItem(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void updateRedeemItem(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t item_id);
void deleteRedeemItem(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t item_id);
void listRedeemRecords(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};
} // namespace csp::controllers

查看文件

@@ -8,12 +8,32 @@ class MetaController : public drogon::HttpController<MetaController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(MetaController::openapi, "/api/openapi.json", drogon::Get);
ADD_METHOD_TO(MetaController::backendLogs, "/api/v1/backend/logs", drogon::Get);
ADD_METHOD_TO(MetaController::kbRefreshStatus, "/api/v1/backend/kb/refresh", drogon::Get);
ADD_METHOD_TO(MetaController::triggerKbRefresh, "/api/v1/backend/kb/refresh", drogon::Post);
ADD_METHOD_TO(MetaController::triggerMissingSolutions, "/api/v1/backend/solutions/generate-missing",
drogon::Post);
ADD_METHOD_TO(MetaController::mcp, "/api/v1/mcp", drogon::Post);
METHOD_LIST_END
void openapi(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void backendLogs(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void kbRefreshStatus(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void triggerKbRefresh(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void triggerMissingSolutions(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void mcp(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};

查看文件

@@ -12,6 +12,7 @@ class SubmissionController : public drogon::HttpController<SubmissionController>
ADD_METHOD_TO(SubmissionController::submitProblem, "/api/v1/problems/{1}/submit", drogon::Post);
ADD_METHOD_TO(SubmissionController::listSubmissions, "/api/v1/submissions", drogon::Get);
ADD_METHOD_TO(SubmissionController::getSubmission, "/api/v1/submissions/{1}", drogon::Get);
ADD_METHOD_TO(SubmissionController::analyzeSubmission, "/api/v1/submissions/{1}/analysis", drogon::Post);
ADD_METHOD_TO(SubmissionController::runCpp, "/api/v1/run/cpp", drogon::Post);
METHOD_LIST_END
@@ -26,6 +27,10 @@ class SubmissionController : public drogon::HttpController<SubmissionController>
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t submission_id);
void analyzeSubmission(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t submission_id);
void runCpp(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};

查看文件

@@ -73,6 +73,7 @@ struct Submission {
std::string code;
SubmissionStatus status = SubmissionStatus::Pending;
int32_t score = 0;
int32_t rating_delta = 0;
int32_t time_ms = 0;
int32_t memory_kb = 0;
std::string compile_log;

查看文件

@@ -20,6 +20,7 @@ class AuthService {
// Throws on error; for controller we will catch and convert to JSON.
AuthResult Register(const std::string& username, const std::string& password);
AuthResult Login(const std::string& username, const std::string& password);
void ResetPassword(const std::string& username, const std::string& new_password);
std::optional<int> VerifyToken(const std::string& token);

查看文件

@@ -8,7 +8,11 @@
namespace csp::services {
struct ImportRunOptions {
std::string mode = "luogu";
bool clear_all_problems = false;
std::string local_pdf_dir;
int target_total = 5000;
int workers = 3;
};
class ImportRunner {

查看文件

@@ -0,0 +1,37 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <optional>
#include <string>
namespace csp::services {
class KbImportRunner {
public:
static KbImportRunner& Instance();
void Configure(std::string db_path);
bool TriggerAsync(const std::string& trigger);
bool IsRunning() const;
std::string LastCommand() const;
std::optional<int> LastExitCode() const;
int64_t LastStartedAt() const;
int64_t LastFinishedAt() const;
std::string LastTrigger() const;
private:
KbImportRunner() = default;
mutable std::mutex mu_;
std::string db_path_;
bool running_ = false;
std::string last_command_;
std::optional<int> last_exit_code_;
int64_t last_started_at_ = 0;
int64_t last_finished_at_ = 0;
std::string last_trigger_;
};
} // namespace csp::services

查看文件

@@ -1,28 +1,59 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include <cstdint>
#include <cstddef>
#include <deque>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
namespace csp::services {
class ProblemSolutionRunner {
public:
struct TriggerMissingSummary {
int missing_total = 0;
int candidate_count = 0;
int queued_count = 0;
};
static ProblemSolutionRunner& Instance();
void Configure(std::string db_path);
bool TriggerAsync(int64_t problem_id, int64_t job_id, int max_solutions);
TriggerMissingSummary TriggerMissingAsync(db::SqliteDb& db,
int64_t created_by,
int max_solutions,
int limit);
void AutoStartMissingIfEnabled(db::SqliteDb& db);
bool IsRunning(int64_t problem_id) const;
size_t PendingCount() const;
private:
struct Task {
int64_t problem_id = 0;
int64_t job_id = 0;
int max_solutions = 3;
};
ProblemSolutionRunner() = default;
void StartWorkerIfNeededLocked();
void WorkerLoop();
void RecoverQueuedJobsLocked();
void StartAutoPumpIfNeeded(db::SqliteDb* db, int max_solutions, int limit, int interval_sec);
std::string db_path_;
mutable std::mutex mu_;
std::set<int64_t> running_problem_ids_;
std::deque<Task> queue_;
std::unordered_map<int64_t, size_t> pending_problem_counts_;
size_t pending_jobs_ = 0;
bool worker_running_ = false;
bool recovered_from_db_ = false;
bool auto_pump_started_ = false;
};
} // namespace csp::services

查看文件

@@ -34,6 +34,7 @@ struct ProblemSolution {
struct ProblemSolutionJob {
int64_t id = 0;
int64_t problem_id = 0;
std::string problem_title;
std::string status;
int progress = 0;
std::string message;
@@ -60,7 +61,13 @@ class ProblemWorkspaceService {
int64_t CreateSolutionJob(int64_t problem_id, int64_t created_by, int max_solutions);
std::optional<ProblemSolutionJob> GetLatestSolutionJob(int64_t problem_id);
std::vector<ProblemSolutionJob> ListRecentSolutionJobs(int limit);
std::vector<ProblemSolutionJob> ListSolutionJobsByStatus(const std::string& status,
int limit);
std::vector<ProblemSolution> ListSolutions(int64_t problem_id);
int CountProblemsWithoutSolutions();
std::vector<int64_t> ListProblemIdsWithoutSolutions(int limit,
bool exclude_queued_or_running_jobs);
private:
db::SqliteDb& db_;

查看文件

@@ -0,0 +1,77 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace csp::services {
struct RedeemItem {
int64_t id = 0;
std::string name;
std::string description;
std::string unit_label;
int holiday_cost = 0;
int studyday_cost = 0;
bool is_active = true;
bool is_global = true;
int64_t created_by = 0;
int64_t created_at = 0;
int64_t updated_at = 0;
};
struct RedeemItemWrite {
std::string name;
std::string description;
std::string unit_label = "小时";
int holiday_cost = 5;
int studyday_cost = 25;
bool is_active = true;
bool is_global = true;
};
struct RedeemRecord {
int64_t id = 0;
int64_t user_id = 0;
int64_t item_id = 0;
std::string item_name;
int quantity = 1;
std::string day_type;
int unit_cost = 0;
int total_cost = 0;
std::string note;
int64_t created_at = 0;
std::string username;
};
struct RedeemRequest {
int64_t user_id = 0;
int64_t item_id = 0;
int quantity = 1;
std::string day_type = "studyday";
std::string note;
};
class RedeemService {
public:
explicit RedeemService(db::SqliteDb& db) : db_(db) {}
std::vector<RedeemItem> ListItems(bool include_inactive);
std::optional<RedeemItem> GetItemById(int64_t item_id);
RedeemItem CreateItem(int64_t admin_user_id, const RedeemItemWrite& input);
RedeemItem UpdateItem(int64_t item_id, const RedeemItemWrite& input);
void DeactivateItem(int64_t item_id);
std::vector<RedeemRecord> ListRecordsByUser(int64_t user_id, int limit);
std::vector<RedeemRecord> ListRecordsAll(std::optional<int64_t> user_id, int limit);
RedeemRecord Redeem(const RedeemRequest& request);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,44 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include <cstdint>
#include <optional>
#include <string>
namespace csp::services {
struct SolutionViewChargeResult {
bool granted = true;
bool charged = false;
bool daily_free = false;
int cost = 0;
int rating_before = 0;
int rating_after = 0;
int daily_used_count = 0;
int64_t viewed_at = 0;
std::string day_key;
std::string deny_reason;
};
struct SolutionViewStats {
bool has_viewed = false;
int total_views = 0;
int total_cost = 0;
std::optional<int64_t> last_viewed_at;
};
class SolutionAccessService {
public:
explicit SolutionAccessService(db::SqliteDb& db) : db_(db) {}
// Daily policy: first answer view is free, then each full view costs 2 rating.
SolutionViewChargeResult ConsumeSolutionView(int64_t user_id, int64_t problem_id);
SolutionViewStats QueryUserProblemViewStats(int64_t user_id, int64_t problem_id);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,36 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include "csp/domain/entities.h"
#include <cstdint>
#include <optional>
#include <string>
namespace csp::services {
struct SubmissionFeedback {
int64_t submission_id = 0;
std::string feedback_md;
std::string links_json;
std::string model_name;
std::string status;
int64_t created_at = 0;
int64_t updated_at = 0;
};
class SubmissionFeedbackService {
public:
explicit SubmissionFeedbackService(db::SqliteDb& db) : db_(db) {}
std::optional<SubmissionFeedback> GetBySubmissionId(int64_t submission_id);
SubmissionFeedback GenerateAndSave(const domain::Submission& submission,
const domain::Problem& problem,
bool force_refresh);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -9,12 +9,19 @@
namespace csp::services {
struct UserListResult {
std::vector<domain::GlobalLeaderboardEntry> items;
int total_count = 0;
};
class UserService {
public:
explicit UserService(db::SqliteDb& db) : db_(db) {}
std::optional<domain::User> GetById(int64_t id);
std::vector<domain::GlobalLeaderboardEntry> GlobalLeaderboard(int limit = 100);
UserListResult ListUsers(int page, int page_size);
void SetRating(int64_t user_id, int rating);
private:
db::SqliteDb& db_;