feat: 完成源晶权限与经验系统并优化 me/admin 交互
这个提交包含在:
@@ -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,
|
||||
|
||||
在新工单中引用
屏蔽一个用户