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

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

查看文件

@@ -0,0 +1,115 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/db/sqlite_db.h"
#include "csp/services/auth_service.h"
#include "csp/services/contest_service.h"
#include "csp/services/season_service.h"
#include "csp/services/user_service.h"
#include <string>
TEST_CASE("season reward claim is idempotent and writes loot log") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
csp::db::SeedDemoData(db);
csp::services::AuthService auth(db);
const auto login = auth.Register("season_user_1", "password123");
csp::services::SeasonService seasons(db);
const auto season = seasons.GetCurrentSeason();
REQUIRE(season.has_value());
const auto tracks = seasons.ListRewardTracks(season->id);
REQUIRE_FALSE(tracks.empty());
const auto target_track = tracks.back();
db.Exec("UPDATE users SET rating=200 WHERE id=" + std::to_string(login.user_id));
const auto before_progress =
seasons.GetOrSyncUserProgress(season->id, login.user_id);
REQUIRE(before_progress.xp >= target_track.required_xp);
const auto first_claim = seasons.ClaimReward(
season->id, login.user_id, target_track.tier_no, target_track.reward_type);
REQUIRE(first_claim.claimed);
REQUIRE(first_claim.claim.has_value());
REQUIRE(first_claim.rating_after >= 200 + target_track.reward_value);
const auto second_claim = seasons.ClaimReward(
season->id, login.user_id, target_track.tier_no, target_track.reward_type);
REQUIRE_FALSE(second_claim.claimed);
REQUIRE(second_claim.claim.has_value());
REQUIRE(second_claim.rating_after == first_claim.rating_after);
const auto loot = seasons.ListLootDropsByUser(login.user_id, 20);
REQUIRE_FALSE(loot.empty());
REQUIRE(loot.front().source_type == "season");
REQUIRE(loot.front().source_id == season->id);
csp::services::UserService users(db);
const auto user = users.GetById(login.user_id);
REQUIRE(user.has_value());
REQUIRE(user->rating == first_claim.rating_after);
}
TEST_CASE("contest modifiers create update and filtered list") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
csp::db::SeedDemoData(db);
csp::services::ContestService contests(db);
const auto contest_list = contests.ListContests();
REQUIRE_FALSE(contest_list.empty());
const int64_t contest_id = contest_list.front().id;
csp::services::SeasonService seasons(db);
csp::services::ContestModifierWrite create;
create.code = "no_recursion";
create.title = "禁用递归";
create.description = "仅允许循环写法。";
create.rule_json = R"({"forbid":["recursion"]})";
create.is_active = true;
const auto created = seasons.CreateContestModifier(contest_id, create);
REQUIRE(created.id > 0);
REQUIRE(created.contest_id == contest_id);
REQUIRE(created.is_active);
const auto active_list = seasons.ListContestModifiers(contest_id, false);
bool found_created = false;
for (const auto& one : active_list) {
if (one.id == created.id) {
found_created = true;
break;
}
}
REQUIRE(found_created);
csp::services::ContestModifierPatch patch;
patch.title = "禁用递归(更新)";
patch.is_active = false;
const auto updated =
seasons.UpdateContestModifier(contest_id, created.id, patch);
REQUIRE(updated.title == "禁用递归(更新)");
REQUIRE_FALSE(updated.is_active);
const auto active_after = seasons.ListContestModifiers(contest_id, false);
bool still_active = false;
for (const auto& one : active_after) {
if (one.id == created.id) {
still_active = true;
break;
}
}
REQUIRE_FALSE(still_active);
const auto all_after = seasons.ListContestModifiers(contest_id, true);
bool found_updated = false;
for (const auto& one : all_after) {
if (one.id == created.id && one.title == "禁用递归(更新)" &&
!one.is_active) {
found_updated = true;
break;
}
}
REQUIRE(found_updated);
}