feat: add streak reward and 100-achievement milestone system

这个提交包含在:
cryptocommuniums-afk
2026-02-23 20:29:14 +08:00
父节点 43cbd38bac
当前提交 0b53113a4b
修改 14 个文件,包含 1334 行新增57 行删除

查看文件

@@ -0,0 +1,108 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/db/sqlite_db.h"
#include "csp/services/achievement_service.h"
#include "csp/services/auth_service.h"
#include "csp/services/user_service.h"
#include <sqlite3.h>
#include <cstdint>
#include <stdexcept>
#include <string>
namespace {
void CheckSqlite(int rc, sqlite3* db, const char* what) {
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
}
int64_t InsertProblem(sqlite3* db, const std::string& slug, int64_t created_at) {
sqlite3_stmt* stmt = nullptr;
const char* sql =
"INSERT INTO problems(slug,title,statement_md,difficulty,source,created_at) "
"VALUES(?,?,?,?,?,?)";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare insert problem");
CheckSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db,
"bind slug");
CheckSqlite(sqlite3_bind_text(stmt, 2, slug.c_str(), -1, SQLITE_TRANSIENT), db,
"bind title");
CheckSqlite(sqlite3_bind_text(stmt, 3, "stub", -1, SQLITE_STATIC), db,
"bind statement");
CheckSqlite(sqlite3_bind_int(stmt, 4, 1), db, "bind difficulty");
CheckSqlite(sqlite3_bind_text(stmt, 5, "test", -1, SQLITE_STATIC), db,
"bind source");
CheckSqlite(sqlite3_bind_int64(stmt, 6, created_at), db, "bind created_at");
CheckSqlite(sqlite3_step(stmt), db, "insert problem");
sqlite3_finalize(stmt);
return sqlite3_last_insert_rowid(db);
}
void InsertSubmission(sqlite3* db, int64_t user_id, int64_t problem_id,
int64_t created_at) {
sqlite3_stmt* stmt = nullptr;
const char* sql =
"INSERT INTO submissions(user_id,problem_id,language,code,status,score,time_ms,memory_kb,created_at) "
"VALUES(?,?,?,?,?,?,?,?,?)";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare insert submission");
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id");
CheckSqlite(sqlite3_bind_text(stmt, 3, "cpp", -1, SQLITE_STATIC), db,
"bind language");
CheckSqlite(sqlite3_bind_text(stmt, 4, "int main(){return 0;}", -1, SQLITE_STATIC),
db, "bind code");
CheckSqlite(sqlite3_bind_text(stmt, 5, "AC", -1, SQLITE_STATIC), db,
"bind status");
CheckSqlite(sqlite3_bind_int(stmt, 6, 100), db, "bind score");
CheckSqlite(sqlite3_bind_int(stmt, 7, 1), db, "bind time");
CheckSqlite(sqlite3_bind_int(stmt, 8, 1), db, "bind memory");
CheckSqlite(sqlite3_bind_int64(stmt, 9, created_at), db, "bind created_at");
CheckSqlite(sqlite3_step(stmt), db, "insert submission");
sqlite3_finalize(stmt);
}
} // namespace
TEST_CASE("achievement snapshot grants milestone bonus once per milestone") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
csp::services::AuthService auth(db);
const auto login = auth.Register("achievement_user", "password123");
csp::services::UserService users(db);
csp::services::AchievementService achievements(db);
sqlite3* raw = db.raw();
int64_t now = 1700000000;
for (int i = 0; i < 40; ++i) {
const auto pid = InsertProblem(raw, "ach-test-" + std::to_string(i + 1), now + i);
InsertSubmission(raw, login.user_id, pid, now + i);
}
const auto before = users.GetById(login.user_id);
REQUIRE(before.has_value());
REQUIRE(before->rating == 1); // register triggers one login_checkin reward
const auto snap1 = achievements.GetSnapshot(login.user_id);
REQUIRE(snap1.items.size() == 100);
REQUIRE(snap1.summary.total_count == 100);
REQUIRE(snap1.summary.completed_count >= 10);
REQUIRE(snap1.summary.bonus_milestones_added_now >= 1);
REQUIRE(snap1.summary.bonus_rating_added_now ==
snap1.summary.bonus_milestones_added_now * 20);
const auto after_first = users.GetById(login.user_id);
REQUIRE(after_first.has_value());
REQUIRE(after_first->rating == before->rating + snap1.summary.bonus_rating_added_now);
const auto snap2 = achievements.GetSnapshot(login.user_id);
REQUIRE(snap2.summary.bonus_milestones_added_now == 0);
REQUIRE(snap2.summary.bonus_rating_added_now == 0);
const auto after_second = users.GetById(login.user_id);
REQUIRE(after_second.has_value());
REQUIRE(after_second->rating == after_first->rating);
}