feat: 完成源晶权限与经验系统并优化 me/admin 交互

这个提交包含在:
cryptocommuniums-afk
2026-02-23 20:02:46 +08:00
父节点 2b6def2560
当前提交 43cbd38bac
修改 104 个文件,包含 13348 行新增776 行删除

查看文件

@@ -13,6 +13,15 @@ class AdminController : public drogon::HttpController<AdminController> {
ADD_METHOD_TO(AdminController::updateUserRating,
"/api/v1/admin/users/{1}/rating",
drogon::Patch);
ADD_METHOD_TO(AdminController::getUserSourceCrystalSummary,
"/api/v1/admin/users/{1}/source-crystal",
drogon::Get);
ADD_METHOD_TO(AdminController::listUserSourceCrystalRecords,
"/api/v1/admin/users/{1}/source-crystal/records",
drogon::Get);
ADD_METHOD_TO(AdminController::depositUserSourceCrystal,
"/api/v1/admin/users/{1}/source-crystal/deposit",
drogon::Post);
ADD_METHOD_TO(AdminController::deleteUser,
"/api/v1/admin/users/{1}",
drogon::Delete);
@@ -27,6 +36,22 @@ class AdminController : public drogon::HttpController<AdminController> {
ADD_METHOD_TO(AdminController::listRedeemRecords,
"/api/v1/admin/redeem-records",
drogon::Get);
ADD_METHOD_TO(AdminController::getSourceCrystalSettings,
"/api/v1/admin/source-crystal/settings",
drogon::Get);
ADD_METHOD_TO(AdminController::updateSourceCrystalSettings,
"/api/v1/admin/source-crystal/settings",
drogon::Patch);
ADD_METHOD_TO(AdminController::createSeason, "/api/v1/admin/seasons", drogon::Post);
ADD_METHOD_TO(AdminController::updateSeason,
"/api/v1/admin/seasons/{1}",
drogon::Patch);
ADD_METHOD_TO(AdminController::createContestModifier,
"/api/v1/admin/contests/{1}/modifiers",
drogon::Post);
ADD_METHOD_TO(AdminController::updateContestModifier,
"/api/v1/admin/contests/{1}/modifiers/{2}",
drogon::Patch);
ADD_METHOD_TO(AdminController::userRatingHistory,
"/api/v1/admin/users/{1}/rating-history",
drogon::Get);
@@ -38,6 +63,18 @@ class AdminController : public drogon::HttpController<AdminController> {
void updateUserRating(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t user_id);
void getUserSourceCrystalSummary(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t user_id);
void listUserSourceCrystalRecords(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t user_id);
void depositUserSourceCrystal(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t user_id);
void deleteUser(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t user_id);
@@ -59,6 +96,30 @@ class AdminController : public drogon::HttpController<AdminController> {
void listRedeemRecords(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void getSourceCrystalSettings(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void updateSourceCrystalSettings(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void createSeason(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void updateSeason(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t season_id);
void createContestModifier(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id);
void updateContestModifier(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id,
int64_t modifier_id);
void userRatingHistory(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t user_id);

查看文件

@@ -13,6 +13,7 @@ class ContestController : public drogon::HttpController<ContestController> {
ADD_METHOD_TO(ContestController::getById, "/api/v1/contests/{1}", drogon::Get);
ADD_METHOD_TO(ContestController::registerForContest, "/api/v1/contests/{1}/register", drogon::Post);
ADD_METHOD_TO(ContestController::leaderboard, "/api/v1/contests/{1}/leaderboard", drogon::Get);
ADD_METHOD_TO(ContestController::modifiers, "/api/v1/contests/{1}/modifiers", drogon::Get);
METHOD_LIST_END
void list(const drogon::HttpRequestPtr& req,
@@ -29,6 +30,10 @@ class ContestController : public drogon::HttpController<ContestController> {
void leaderboard(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id);
void modifiers(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t contest_id);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,41 @@
#pragma once
#include <drogon/HttpController.h>
namespace csp::controllers {
class CrawlerController : public drogon::HttpController<CrawlerController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(CrawlerController::listTargets, "/api/v1/admin/crawlers", drogon::Get);
ADD_METHOD_TO(CrawlerController::createTarget, "/api/v1/admin/crawlers", drogon::Post);
ADD_METHOD_TO(CrawlerController::queueTarget,
"/api/v1/admin/crawlers/{1}/queue",
drogon::Post);
ADD_METHOD_TO(CrawlerController::listRuns,
"/api/v1/admin/crawlers/{1}/runs",
drogon::Get);
ADD_METHOD_TO(CrawlerController::status,
"/api/v1/backend/crawler-guard/status",
drogon::Get);
METHOD_LIST_END
void listTargets(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void createTarget(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void queueTarget(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t target_id);
void listRuns(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t target_id);
void status(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};
} // namespace csp::controllers

查看文件

@@ -11,6 +11,10 @@ class KbController : public drogon::HttpController<KbController> {
METHOD_LIST_BEGIN
ADD_METHOD_TO(KbController::listArticles, "/api/v1/kb/articles", drogon::Get);
ADD_METHOD_TO(KbController::getArticle, "/api/v1/kb/articles/{1}", drogon::Get);
ADD_METHOD_TO(KbController::listClaims, "/api/v1/kb/articles/{1}/claims", drogon::Get);
ADD_METHOD_TO(KbController::claimSkillPoint, "/api/v1/kb/articles/{1}/claim", drogon::Post);
ADD_METHOD_TO(KbController::weeklyPlan, "/api/v1/kb/weekly-plan", drogon::Get);
ADD_METHOD_TO(KbController::claimWeeklyBonus, "/api/v1/kb/weekly-bonus/claim", drogon::Post);
METHOD_LIST_END
void listArticles(const drogon::HttpRequestPtr& req,
@@ -19,6 +23,20 @@ class KbController : public drogon::HttpController<KbController> {
void getArticle(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
std::string slug);
void listClaims(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
std::string slug);
void claimSkillPoint(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
std::string slug);
void weeklyPlan(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void claimWeeklyBonus(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};
} // namespace csp::controllers

查看文件

@@ -0,0 +1,18 @@
#pragma once
#include <drogon/HttpController.h>
namespace csp::controllers {
class LarkController : public drogon::HttpController<LarkController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(LarkController::events, "/api/v1/lark/events", drogon::Post);
METHOD_LIST_END
void events(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
};
} // namespace csp::controllers

查看文件

@@ -12,10 +12,29 @@ public:
ADD_METHOD_TO(MeController::profile, "/api/v1/me", drogon::Get);
ADD_METHOD_TO(MeController::listRedeemItems, "/api/v1/me/redeem/items",
drogon::Get);
ADD_METHOD_TO(MeController::getRedeemDayType, "/api/v1/me/redeem/day-type",
drogon::Get);
ADD_METHOD_TO(MeController::listRedeemRecords, "/api/v1/me/redeem/records",
drogon::Get);
ADD_METHOD_TO(MeController::createRedeemRecord, "/api/v1/me/redeem/records",
drogon::Post);
ADD_METHOD_TO(MeController::sourceCrystalSummary, "/api/v1/me/source-crystal",
drogon::Get);
ADD_METHOD_TO(MeController::listSourceCrystalRecords,
"/api/v1/me/source-crystal/records",
drogon::Get);
ADD_METHOD_TO(MeController::sourceCrystalDeposit,
"/api/v1/me/source-crystal/deposit",
drogon::Post);
ADD_METHOD_TO(MeController::sourceCrystalWithdraw,
"/api/v1/me/source-crystal/withdraw",
drogon::Post);
ADD_METHOD_TO(MeController::experienceSummary, "/api/v1/me/experience",
drogon::Get);
ADD_METHOD_TO(MeController::experienceHistory, "/api/v1/me/experience/history",
drogon::Get);
ADD_METHOD_TO(MeController::listLootDrops, "/api/v1/me/loot-drops",
drogon::Get);
ADD_METHOD_TO(MeController::listDailyTasks, "/api/v1/me/daily-tasks",
drogon::Get);
ADD_METHOD_TO(MeController::listWrongBook, "/api/v1/me/wrong-book",
@@ -41,6 +60,10 @@ public:
listRedeemItems(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void
getRedeemDayType(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void
listRedeemRecords(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
@@ -49,6 +72,34 @@ public:
createRedeemRecord(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void sourceCrystalSummary(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void listSourceCrystalRecords(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void sourceCrystalDeposit(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void sourceCrystalWithdraw(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void experienceSummary(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void experienceHistory(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void
listLootDrops(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void
listDailyTasks(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);

查看文件

@@ -9,6 +9,8 @@ class MetaController : public drogon::HttpController<MetaController> {
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::dbLockGuardStatus, "/api/v1/backend/db-lock-guard/status",
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",
@@ -22,6 +24,10 @@ class MetaController : public drogon::HttpController<MetaController> {
void backendLogs(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void dbLockGuardStatus(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void kbRefreshStatus(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);

查看文件

@@ -0,0 +1,38 @@
#pragma once
#include <drogon/HttpController.h>
#include <cstdint>
namespace csp::controllers {
class SeasonController : public drogon::HttpController<SeasonController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(SeasonController::currentSeason,
"/api/v1/seasons/current",
drogon::Get);
ADD_METHOD_TO(SeasonController::mySeasonProgress,
"/api/v1/seasons/{1}/me",
drogon::Get);
ADD_METHOD_TO(SeasonController::claimSeasonReward,
"/api/v1/seasons/{1}/claim",
drogon::Post);
METHOD_LIST_END
void currentSeason(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void mySeasonProgress(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t season_id);
void claimSeasonReward(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t season_id);
};
} // namespace csp::controllers

查看文件

@@ -114,6 +114,70 @@ struct ContestRegistration {
int64_t registered_at = 0;
};
struct ContestModifier {
int64_t id = 0;
int64_t contest_id = 0;
std::string code;
std::string title;
std::string description;
std::string rule_json;
bool is_active = true;
int64_t created_at = 0;
int64_t updated_at = 0;
};
struct Season {
int64_t id = 0;
std::string key;
std::string title;
int64_t starts_at = 0;
int64_t ends_at = 0;
std::string status;
std::string pass_json;
int64_t created_at = 0;
int64_t updated_at = 0;
};
struct SeasonRewardTrack {
int64_t id = 0;
int64_t season_id = 0;
int32_t tier_no = 0;
int32_t required_xp = 0;
std::string reward_type;
int32_t reward_value = 0;
std::string reward_meta_json;
};
struct SeasonUserProgress {
int64_t season_id = 0;
int64_t user_id = 0;
int32_t xp = 0;
int32_t level = 0;
int64_t updated_at = 0;
};
struct SeasonRewardClaim {
int64_t id = 0;
int64_t season_id = 0;
int64_t user_id = 0;
int32_t tier_no = 0;
std::string reward_type;
int64_t claimed_at = 0;
};
struct LootDropLog {
int64_t id = 0;
int64_t user_id = 0;
std::string source_type;
int64_t source_id = 0;
std::string item_code;
std::string item_name;
std::string rarity;
int32_t amount = 0;
std::string meta_json;
int64_t created_at = 0;
};
struct KbArticle {
int64_t id = 0;
std::string slug;
@@ -132,6 +196,7 @@ struct GlobalLeaderboardEntry {
std::string username;
int32_t rating = 0;
int64_t created_at = 0;
int32_t period_score = 0;
int32_t total_submissions = 0;
int32_t total_ac = 0;
};

查看文件

@@ -15,6 +15,12 @@ Json::Value ToJson(const Problem& p);
Json::Value ToJson(const Submission& s);
Json::Value ToJson(const WrongBookItem& w);
Json::Value ToJson(const Contest& c);
Json::Value ToJson(const ContestModifier& c);
Json::Value ToJson(const Season& s);
Json::Value ToJson(const SeasonRewardTrack& t);
Json::Value ToJson(const SeasonUserProgress& p);
Json::Value ToJson(const SeasonRewardClaim& c);
Json::Value ToJson(const LootDropLog& l);
Json::Value ToJson(const KbArticle& a);
Json::Value ToJson(const GlobalLeaderboardEntry& e);
Json::Value ToJson(const ContestLeaderboardEntry& e);

查看文件

@@ -22,6 +22,9 @@ class AuthService {
AuthResult Login(const std::string& username, const std::string& password);
void ResetPassword(const std::string& username, const std::string& new_password);
// Verify username/password without creating session.
std::optional<int> VerifyCredentials(const std::string& username,
const std::string& password);
std::optional<int> VerifyToken(const std::string& token);
private:

查看文件

@@ -0,0 +1,70 @@
#pragma once
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <string>
namespace csp::services {
class CrawlerRunner {
public:
struct Status {
bool enabled = true;
bool started = false;
int interval_sec = 20;
int active_requeue_interval_sec = 43200;
bool running = false;
int64_t processed_count = 0;
int64_t success_count = 0;
int64_t failed_count = 0;
int64_t last_started_at = 0;
int64_t last_finished_at = 0;
int64_t last_success_at = 0;
int64_t last_failure_at = 0;
std::string last_error;
int64_t current_target_id = 0;
};
static CrawlerRunner& Instance();
void Configure(std::string db_path);
void StartIfEnabled();
Status GetStatus();
void WakeUp();
private:
CrawlerRunner() = default;
void WorkerLoop();
std::mutex mu_;
std::condition_variable cv_;
std::string db_path_;
bool enabled_ = true;
bool started_ = false;
int interval_sec_ = 20;
int active_requeue_interval_sec_ = 43200;
int fetch_timeout_sec_ = 20;
std::string script_dir_ = "data/crawlers";
std::string llm_api_url_;
std::string llm_api_key_;
std::string llm_model_ = "qwen3-max";
std::string llm_system_prompt_;
int llm_timeout_sec_ = 30;
bool running_ = false;
int64_t processed_count_ = 0;
int64_t success_count_ = 0;
int64_t failed_count_ = 0;
int64_t last_started_at_ = 0;
int64_t last_finished_at_ = 0;
int64_t last_success_at_ = 0;
int64_t last_failure_at_ = 0;
std::string last_error_;
int64_t current_target_id_ = 0;
bool wake_requested_ = false;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,79 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace csp::services {
struct CrawlerTarget {
int64_t id = 0;
std::string url;
std::string normalized_url;
std::string status;
std::string submit_source;
std::string submitter_id;
std::string submitter_name;
std::string rule_json;
std::string script_path;
std::string last_error;
std::optional<int64_t> last_test_at;
std::optional<int64_t> last_run_at;
int64_t created_at = 0;
int64_t updated_at = 0;
};
struct CrawlerRun {
int64_t id = 0;
int64_t target_id = 0;
std::string status;
int32_t http_status = 0;
std::string output_json;
std::string error_text;
int64_t created_at = 0;
};
struct UpsertCrawlerTargetResult {
CrawlerTarget target;
bool inserted = false;
};
class CrawlerService {
public:
explicit CrawlerService(db::SqliteDb& db) : db_(db) {}
UpsertCrawlerTargetResult UpsertTarget(const std::string& raw_url,
const std::string& submit_source,
const std::string& submitter_id,
const std::string& submitter_name);
std::optional<CrawlerTarget> GetTargetById(int64_t target_id);
std::vector<CrawlerTarget> ListTargets(const std::string& status, int limit);
std::vector<CrawlerRun> ListRuns(int64_t target_id, int limit);
bool EnqueueTarget(int64_t target_id);
bool EnqueueDueActiveTarget(int interval_sec, int64_t now_sec, CrawlerTarget& out);
bool ClaimNextTarget(CrawlerTarget& out);
void UpdateGenerated(int64_t target_id,
const std::string& rule_json,
const std::string& script_path);
void MarkTesting(int64_t target_id);
void MarkActive(int64_t target_id, int64_t run_at);
void MarkFailed(int64_t target_id, const std::string& error);
void InsertRun(int64_t target_id,
const std::string& status,
int http_status,
const std::string& output_json,
const std::string& error_text);
static std::string NormalizeUrl(const std::string& raw_url);
static std::vector<std::string> ExtractUrls(const std::string& text);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,54 @@
#pragma once
#include <mutex>
#include <cstdint>
#include <string>
namespace csp::services {
// Background guardian for SQLite lock contention.
// It periodically probes writer lock health and performs best-effort
// maintenance when lock contention appears to be stuck.
class DbLockGuard {
public:
struct Status {
bool enabled = true;
bool started = false;
int interval_sec = 20;
int probe_busy_timeout_ms = 2000;
int busy_streak_trigger = 3;
int busy_streak = 0;
int64_t last_probe_at = 0;
int last_probe_rc = 0;
std::string last_probe_error;
int64_t last_repair_at = 0;
int64_t repair_count = 0;
};
static DbLockGuard& Instance();
void Configure(std::string db_path);
void StartIfEnabled();
Status GetStatus();
private:
DbLockGuard() = default;
void WorkerLoop();
std::mutex mu_;
std::string db_path_;
bool enabled_ = true;
bool started_ = false;
int interval_sec_ = 20;
int probe_busy_timeout_ms_ = 2000;
int busy_streak_trigger_ = 3;
int busy_streak_ = 0;
int64_t last_probe_at_ = 0;
int last_probe_rc_ = 0;
std::string last_probe_error_;
int64_t last_repair_at_ = 0;
int64_t repair_count_ = 0;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,39 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include <cstdint>
#include <string>
#include <vector>
namespace csp::services {
struct ExperienceSummary {
int64_t user_id = 0;
int experience = 0;
int64_t updated_at = 0;
};
struct ExperienceHistoryItem {
int64_t id = 0;
int64_t user_id = 0;
int xp_delta = 0;
int rating_before = 0;
int rating_after = 0;
std::string source;
std::string note;
int64_t created_at = 0;
};
class ExperienceService {
public:
explicit ExperienceService(db::SqliteDb& db) : db_(db) {}
ExperienceSummary GetSummary(int64_t user_id);
std::vector<ExperienceHistoryItem> ListHistory(int64_t user_id, int limit);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -13,6 +13,62 @@ namespace csp::services {
struct KbArticleDetail {
domain::KbArticle article;
std::vector<std::pair<int64_t, std::string>> related_problems;
struct SkillPoint {
std::string key;
std::string title;
std::string description;
std::string difficulty;
int reward = 1;
std::vector<std::string> prerequisites;
};
std::vector<SkillPoint> skill_points;
};
struct KbClaimSummary {
std::vector<std::string> claimed_keys;
int total_reward = 0;
int total_count = 0;
};
struct KbClaimResult {
bool claimed = false;
int reward = 0;
int rating_after = 0;
int total_claimed = 0;
};
struct KbWeeklyTask {
int64_t id = 0;
std::string week_key;
int64_t article_id = 0;
std::string article_slug;
std::string article_title;
std::string knowledge_key;
std::string knowledge_title;
std::string knowledge_description;
std::string difficulty;
int reward = 0;
std::vector<std::string> prerequisites;
bool completed = false;
int64_t completed_at = 0;
};
struct KbWeeklyPlan {
std::string week_key;
std::vector<KbWeeklyTask> tasks;
int total_reward = 0;
int gained_reward = 0;
int bonus_reward = 100;
bool bonus_claimed = false;
int completion_percent = 0;
};
struct KbWeeklyBonusResult {
bool claimed = false;
int reward = 0;
int rating_after = 0;
int completion_percent = 0;
std::string week_key;
};
class KbService {
@@ -21,8 +77,21 @@ class KbService {
std::vector<domain::KbArticle> ListArticles();
std::optional<KbArticleDetail> GetBySlug(const std::string& slug);
KbClaimSummary ListClaims(int64_t user_id, int64_t article_id);
KbClaimResult ClaimSkillPoint(int64_t user_id,
int64_t article_id,
const std::string& slug,
const std::string& skill_key);
KbWeeklyPlan GetWeeklyPlan(int64_t user_id);
KbWeeklyBonusResult ClaimWeeklyBonus(int64_t user_id);
private:
std::vector<KbArticleDetail::SkillPoint> SkillPointsBySlug(const std::string& slug);
std::vector<std::string> ClaimedKeysByUser(int64_t user_id);
std::vector<std::string> ClaimedKeysByArticle(int64_t user_id, int64_t article_id);
std::string CurrentWeekKey() const;
void EnsureWeeklyTasksGenerated(int64_t user_id, const std::string& week_key);
db::SqliteDb& db_;
};

查看文件

@@ -0,0 +1,85 @@
#pragma once
#include <cstdint>
#include <deque>
#include <mutex>
#include <string>
#include <unordered_map>
namespace csp::services {
class LarkBotService {
public:
struct IncomingTextEvent {
std::string event_id;
std::string message_id;
std::string chat_id;
std::string sender_id;
std::string text;
};
static LarkBotService& Instance();
// Read runtime options from environment variables.
void ConfigureFromEnv();
bool Enabled() const;
bool VerifyToken(const std::string& token) const;
// Fire-and-forget message handling. Returns immediately.
void HandleEventAsync(IncomingTextEvent event);
// Fire-and-forget plain text reply without LLM roundtrip.
void ReplyTextAsync(const std::string& message_id, const std::string& text);
private:
LarkBotService() = default;
struct ChatTurn {
std::string role;
std::string content;
};
struct ParsedUrl {
std::string origin;
std::string path;
};
std::string BuildReplyWithLlm(const std::string& session_key,
const std::string& user_text,
std::string& err);
std::string FallbackReply() const;
bool SendReplyToLark(const std::string& message_id,
const std::string& text,
std::string& err);
bool ObtainTenantToken(std::string& token, std::string& err);
bool HttpPostJson(const ParsedUrl& endpoint,
const std::string& body,
const std::unordered_map<std::string, std::string>& headers,
double timeout_sec,
std::string& response_body,
std::string& err) const;
static std::string Trim(const std::string& s);
static std::string ClipUtf8(const std::string& s, size_t max_bytes);
static bool ParseUrl(const std::string& url, ParsedUrl& out);
mutable std::mutex mu_;
bool enabled_ = false;
std::string verification_token_;
std::string app_id_;
std::string app_secret_;
std::string open_base_url_ = "https://open.feishu.cn";
std::string llm_api_url_;
std::string llm_api_key_;
std::string llm_model_ = "qwen3-max";
std::string llm_system_prompt_;
int llm_timeout_sec_ = 30;
int lark_timeout_sec_ = 15;
int memory_turns_ = 6;
size_t max_reply_chars_ = 1200;
std::string tenant_access_token_;
int64_t tenant_access_token_expire_at_ = 0;
std::unordered_map<std::string, std::deque<ChatTurn>> conversations_;
};
} // namespace csp::services

查看文件

@@ -49,11 +49,20 @@ struct RedeemRecord {
std::string username;
};
struct RedeemDayTypeDecision {
std::string day_type; // holiday / studyday
bool is_holiday = false;
std::string reason;
std::string source;
std::string date_ymd;
int64_t checked_at = 0;
};
struct RedeemRequest {
int64_t user_id = 0;
int64_t item_id = 0;
int quantity = 1;
std::string day_type = "studyday";
std::string day_type = "studyday"; // kept for compatibility, ignored now.
std::string note;
};
@@ -70,6 +79,7 @@ class RedeemService {
std::vector<RedeemRecord> ListRecordsByUser(int64_t user_id, int limit);
std::vector<RedeemRecord> ListRecordsAll(std::optional<int64_t> user_id, int limit);
RedeemDayTypeDecision ResolveCurrentDayType();
RedeemRecord Redeem(const RedeemRequest& request);
private:

查看文件

@@ -0,0 +1,102 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include "csp/domain/entities.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace csp::services {
struct SeasonWrite {
std::string key;
std::string title;
int64_t starts_at = 0;
int64_t ends_at = 0;
std::string status = "draft";
std::string pass_json = "{}";
};
struct SeasonPatch {
std::optional<std::string> key;
std::optional<std::string> title;
std::optional<int64_t> starts_at;
std::optional<int64_t> ends_at;
std::optional<std::string> status;
std::optional<std::string> pass_json;
};
struct SeasonRewardTrackWrite {
int tier_no = 0;
int required_xp = 0;
std::string reward_type = "free";
int reward_value = 0;
std::string reward_meta_json = "{}";
};
struct SeasonClaimResult {
bool claimed = false;
domain::SeasonRewardTrack track;
std::optional<domain::SeasonRewardClaim> claim;
domain::SeasonUserProgress progress;
int rating_after = 0;
};
struct ContestModifierWrite {
std::string code;
std::string title;
std::string description;
std::string rule_json = "{}";
bool is_active = true;
};
struct ContestModifierPatch {
std::optional<std::string> code;
std::optional<std::string> title;
std::optional<std::string> description;
std::optional<std::string> rule_json;
std::optional<bool> is_active;
};
class SeasonService {
public:
explicit SeasonService(db::SqliteDb& db) : db_(db) {}
std::optional<domain::Season> GetCurrentSeason();
std::optional<domain::Season> GetSeasonById(int64_t season_id);
std::vector<domain::SeasonRewardTrack> ListRewardTracks(int64_t season_id);
std::vector<domain::SeasonRewardClaim> ListUserClaims(int64_t season_id,
int64_t user_id);
domain::SeasonUserProgress GetOrSyncUserProgress(int64_t season_id,
int64_t user_id);
SeasonClaimResult ClaimReward(int64_t season_id,
int64_t user_id,
int tier_no,
const std::string& reward_type);
std::vector<domain::LootDropLog> ListLootDropsByUser(int64_t user_id, int limit);
domain::Season CreateSeason(const SeasonWrite& input,
const std::vector<SeasonRewardTrackWrite>& tracks);
domain::Season UpdateSeason(
int64_t season_id,
const SeasonPatch& patch,
const std::optional<std::vector<SeasonRewardTrackWrite>>& replace_tracks);
std::vector<domain::ContestModifier> ListContestModifiers(
int64_t contest_id,
bool include_inactive);
domain::ContestModifier CreateContestModifier(int64_t contest_id,
const ContestModifierWrite& input);
domain::ContestModifier UpdateContestModifier(
int64_t contest_id,
int64_t modifier_id,
const ContestModifierPatch& patch);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,56 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include <cstdint>
#include <string>
#include <vector>
namespace csp::services {
struct SourceCrystalSettings {
double monthly_interest_rate = 0.02;
int64_t updated_at = 0;
};
struct SourceCrystalAccountSummary {
int64_t user_id = 0;
double balance = 0.0;
double monthly_interest_rate = 0.02;
int64_t last_interest_at = 0;
int64_t updated_at = 0;
};
struct SourceCrystalTransaction {
int64_t id = 0;
int64_t user_id = 0;
std::string tx_type; // deposit / withdraw / interest
double amount = 0.0;
double balance_after = 0.0;
std::string note;
int64_t created_at = 0;
};
class SourceCrystalService {
public:
explicit SourceCrystalService(db::SqliteDb& db) : db_(db) {}
SourceCrystalSettings GetSettings();
SourceCrystalSettings UpdateMonthlyInterestRate(double monthly_interest_rate);
SourceCrystalAccountSummary GetSummary(int64_t user_id);
std::vector<SourceCrystalTransaction> ListTransactions(int64_t user_id,
int limit);
SourceCrystalTransaction Deposit(int64_t user_id,
double amount,
const std::string& note);
SourceCrystalTransaction Withdraw(int64_t user_id,
double amount,
const std::string& note);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -26,6 +26,11 @@ struct RunOnlyResult {
std::string compile_log;
};
struct SubmissionSiblingIds {
std::optional<int64_t> prev_id;
std::optional<int64_t> next_id;
};
class SubmissionService {
public:
explicit SubmissionService(db::SqliteDb& db) : db_(db) {}
@@ -35,9 +40,12 @@ class SubmissionService {
std::vector<domain::Submission> List(std::optional<int64_t> user_id,
std::optional<int64_t> problem_id,
std::optional<int64_t> contest_id,
std::optional<int64_t> created_from,
std::optional<int64_t> created_to,
int page,
int page_size);
std::optional<domain::Submission> GetById(int64_t id);
SubmissionSiblingIds GetSiblingIds(int64_t user_id, int64_t submission_id);
RunOnlyResult RunOnlyCpp(const std::string& code, const std::string& input);

查看文件

@@ -5,6 +5,7 @@
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace csp::services {
@@ -19,7 +20,8 @@ class UserService {
explicit UserService(db::SqliteDb& db) : db_(db) {}
std::optional<domain::User> GetById(int64_t id);
std::vector<domain::GlobalLeaderboardEntry> GlobalLeaderboard(int limit = 100);
std::vector<domain::GlobalLeaderboardEntry> GlobalLeaderboard(int limit = 100,
const std::string& scope = "all");
UserListResult ListUsers(int page, int page_size);
void SetRating(int64_t user_id, int rating);
void DeleteUser(int64_t user_id);

查看文件

@@ -20,6 +20,7 @@ class WrongBookService {
std::vector<WrongBookEntry> ListByUser(int64_t user_id);
void UpsertNote(int64_t user_id, int64_t problem_id, const std::string& note);
std::string GetNote(int64_t user_id, int64_t problem_id);
std::string GetNoteImagesJson(int64_t user_id, int64_t problem_id);
void SetNoteImagesJson(int64_t user_id, int64_t problem_id, const std::string& note_images_json);
void UpsertNoteScore(int64_t user_id,