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

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

查看文件

@@ -0,0 +1,274 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/app_state.h"
#include "csp/controllers/admin_controller.h"
#include "csp/controllers/contest_controller.h"
#include "csp/controllers/me_controller.h"
#include "csp/controllers/season_controller.h"
#include "csp/services/auth_service.h"
#include <drogon/HttpRequest.h>
#include <future>
namespace {
drogon::HttpResponsePtr CallSeasonCurrent(csp::controllers::SeasonController& ctl) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
std::promise<drogon::HttpResponsePtr> p;
ctl.currentSeason(req, [&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
});
return p.get_future().get();
}
drogon::HttpResponsePtr CallSeasonMe(csp::controllers::SeasonController& ctl,
int64_t season_id,
const std::string& token) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.mySeasonProgress(req, [&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
}, season_id);
return p.get_future().get();
}
drogon::HttpResponsePtr CallSeasonClaim(csp::controllers::SeasonController& ctl,
int64_t season_id,
const std::string& token,
int tier_no,
const std::string& reward_type) {
Json::Value body;
body["tier_no"] = tier_no;
body["reward_type"] = reward_type;
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
req->setMethod(drogon::Post);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.claimSeasonReward(req, [&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
}, season_id);
return p.get_future().get();
}
drogon::HttpResponsePtr CallMeLoot(csp::controllers::MeController& ctl,
const std::string& token) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->addHeader("Authorization", "Bearer " + token);
req->setParameter("limit", "20");
std::promise<drogon::HttpResponsePtr> p;
ctl.listLootDrops(req, [&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
});
return p.get_future().get();
}
drogon::HttpResponsePtr CallContestList(csp::controllers::ContestController& ctl) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
std::promise<drogon::HttpResponsePtr> p;
ctl.list(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
return p.get_future().get();
}
drogon::HttpResponsePtr CallContestModifiers(csp::controllers::ContestController& ctl,
int64_t contest_id,
bool include_inactive) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
if (include_inactive) req->setParameter("include_inactive", "true");
std::promise<drogon::HttpResponsePtr> p;
ctl.modifiers(req, [&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
}, contest_id);
return p.get_future().get();
}
drogon::HttpResponsePtr CallAdminCreateSeason(csp::controllers::AdminController& ctl,
const std::string& token,
const Json::Value& body) {
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
req->setMethod(drogon::Post);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.createSeason(req, [&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
});
return p.get_future().get();
}
drogon::HttpResponsePtr CallAdminUpdateSeason(csp::controllers::AdminController& ctl,
const std::string& token,
int64_t season_id,
const Json::Value& body) {
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
req->setMethod(drogon::Patch);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.updateSeason(req, [&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
}, season_id);
return p.get_future().get();
}
drogon::HttpResponsePtr CallAdminCreateModifier(csp::controllers::AdminController& ctl,
const std::string& token,
int64_t contest_id,
const Json::Value& body) {
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
req->setMethod(drogon::Post);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.createContestModifier(req, [&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
}, contest_id);
return p.get_future().get();
}
drogon::HttpResponsePtr CallAdminUpdateModifier(csp::controllers::AdminController& ctl,
const std::string& token,
int64_t contest_id,
int64_t modifier_id,
const Json::Value& body) {
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
req->setMethod(drogon::Patch);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.updateContestModifier(req, [&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
}, contest_id, modifier_id);
return p.get_future().get();
}
} // namespace
TEST_CASE("season controller current/me/claim and loot endpoint") {
csp::AppState::Instance().Init(":memory:");
csp::services::AuthService auth(csp::AppState::Instance().db());
const auto user = auth.Register("season_http_user", "password123");
csp::controllers::SeasonController season_ctl;
csp::controllers::MeController me_ctl;
auto current_resp = CallSeasonCurrent(season_ctl);
REQUIRE(current_resp->statusCode() == drogon::k200OK);
auto current_json = current_resp->jsonObject();
REQUIRE(current_json != nullptr);
const int64_t season_id = (*current_json)["data"]["season"]["id"].asInt64();
REQUIRE(season_id > 0);
REQUIRE((*current_json)["data"]["reward_tracks"].isArray());
REQUIRE((*current_json)["data"]["reward_tracks"].size() >= 1);
auto me_resp = CallSeasonMe(season_ctl, season_id, user.token);
REQUIRE(me_resp->statusCode() == drogon::k200OK);
auto me_json = me_resp->jsonObject();
REQUIRE(me_json != nullptr);
REQUIRE((*me_json)["data"]["progress"].isObject());
REQUIRE((*me_json)["data"]["reward_tracks"].isArray());
const int tier_no =
(*me_json)["data"]["reward_tracks"][0]["tier_no"].asInt();
const std::string reward_type =
(*me_json)["data"]["reward_tracks"][0]["reward_type"].asString();
auto claim_resp = CallSeasonClaim(
season_ctl, season_id, user.token, tier_no, reward_type);
REQUIRE(claim_resp->statusCode() == drogon::k200OK);
auto claim_json = claim_resp->jsonObject();
REQUIRE(claim_json != nullptr);
REQUIRE((*claim_json)["data"]["track"]["tier_no"].asInt() == tier_no);
auto loot_resp = CallMeLoot(me_ctl, user.token);
REQUIRE(loot_resp->statusCode() == drogon::k200OK);
auto loot_json = loot_resp->jsonObject();
REQUIRE(loot_json != nullptr);
REQUIRE((*loot_json)["data"].isArray());
REQUIRE((*loot_json)["data"].size() >= 1);
}
TEST_CASE("admin season/modifier endpoints and contest modifier read endpoint") {
csp::AppState::Instance().Init(":memory:");
csp::services::AuthService auth(csp::AppState::Instance().db());
const auto admin = auth.Register("admin", "password123");
csp::controllers::AdminController admin_ctl;
csp::controllers::ContestController contest_ctl;
auto contests_resp = CallContestList(contest_ctl);
REQUIRE(contests_resp->statusCode() == drogon::k200OK);
auto contests_json = contests_resp->jsonObject();
REQUIRE(contests_json != nullptr);
REQUIRE((*contests_json)["data"].isArray());
REQUIRE((*contests_json)["data"].size() >= 1);
const int64_t contest_id = (*contests_json)["data"][0]["id"].asInt64();
Json::Value create_season_body;
create_season_body["key"] = "season-http-admin";
create_season_body["title"] = "HTTP 管理赛季";
create_season_body["starts_at"] = Json::Int64(1700000000);
create_season_body["ends_at"] = Json::Int64(1900000000);
create_season_body["status"] = "active";
Json::Value tracks(Json::arrayValue);
Json::Value t1;
t1["tier_no"] = 1;
t1["required_xp"] = 0;
t1["reward_type"] = "free";
t1["reward_value"] = 3;
tracks.append(t1);
create_season_body["reward_tracks"] = tracks;
auto create_season_resp =
CallAdminCreateSeason(admin_ctl, admin.token, create_season_body);
REQUIRE(create_season_resp->statusCode() == drogon::k200OK);
auto create_season_json = create_season_resp->jsonObject();
REQUIRE(create_season_json != nullptr);
const int64_t new_season_id =
(*create_season_json)["data"]["season"]["id"].asInt64();
REQUIRE(new_season_id > 0);
Json::Value update_season_body;
update_season_body["title"] = "HTTP 管理赛季(更新)";
auto update_season_resp =
CallAdminUpdateSeason(admin_ctl, admin.token, new_season_id, update_season_body);
REQUIRE(update_season_resp->statusCode() == drogon::k200OK);
auto update_season_json = update_season_resp->jsonObject();
REQUIRE(update_season_json != nullptr);
REQUIRE((*update_season_json)["data"]["season"]["title"].asString() ==
"HTTP 管理赛季(更新)");
Json::Value create_modifier_body;
create_modifier_body["code"] = "limit10";
create_modifier_body["title"] = "限时十分钟";
create_modifier_body["description"] = "每道题建议 10 分钟内完成。";
create_modifier_body["is_active"] = true;
create_modifier_body["rule_json"] = R"({"time_limit_min":10})";
auto create_modifier_resp = CallAdminCreateModifier(
admin_ctl, admin.token, contest_id, create_modifier_body);
REQUIRE(create_modifier_resp->statusCode() == drogon::k200OK);
auto create_modifier_json = create_modifier_resp->jsonObject();
REQUIRE(create_modifier_json != nullptr);
const int64_t modifier_id = (*create_modifier_json)["data"]["id"].asInt64();
REQUIRE(modifier_id > 0);
Json::Value update_modifier_body;
update_modifier_body["is_active"] = false;
update_modifier_body["title"] = "限时十分钟(更新)";
auto update_modifier_resp = CallAdminUpdateModifier(
admin_ctl, admin.token, contest_id, modifier_id, update_modifier_body);
REQUIRE(update_modifier_resp->statusCode() == drogon::k200OK);
auto update_modifier_json = update_modifier_resp->jsonObject();
REQUIRE(update_modifier_json != nullptr);
REQUIRE((*update_modifier_json)["data"]["is_active"].asBool() == false);
REQUIRE((*update_modifier_json)["data"]["title"].asString() == "限时十分钟(更新)");
auto modifiers_resp = CallContestModifiers(contest_ctl, contest_id, true);
REQUIRE(modifiers_resp->statusCode() == drogon::k200OK);
auto modifiers_json = modifiers_resp->jsonObject();
REQUIRE(modifiers_json != nullptr);
REQUIRE((*modifiers_json)["data"].isArray());
REQUIRE((*modifiers_json)["data"].size() >= 1);
}