feat(backend): add sqlite migrations + app state + tests

这个提交包含在:
anygen-build-bot
2026-02-12 08:58:53 +00:00
父节点 b6befe69f9
当前提交 76b512939d
修改 8 个文件,包含 334 行新增5 行删除

22
backend/src/app_state.cc 普通文件
查看文件

@@ -0,0 +1,22 @@
#include "csp/app_state.h"
#include <stdexcept>
namespace csp {
AppState& AppState::Instance() {
static AppState inst;
return inst;
}
void AppState::Init(const std::string& sqlite_path) {
db_ = std::make_unique<db::SqliteDb>(db::SqliteDb::OpenFile(sqlite_path));
csp::db::ApplyMigrations(*db_);
}
csp::db::SqliteDb& AppState::db() {
if (!db_) throw std::runtime_error("AppState not initialized");
return *db_;
}
} // namespace csp

135
backend/src/db/sqlite_db.cc 普通文件
查看文件

@@ -0,0 +1,135 @@
#include "csp/db/sqlite_db.h"
#include <stdexcept>
#include <utility>
namespace csp::db {
namespace {
void ThrowSqlite(int rc, sqlite3* db, const char* what) {
if (rc == SQLITE_OK) return;
const char* msg = db ? sqlite3_errmsg(db) : "";
throw std::runtime_error(std::string(what) + ": " + msg);
}
} // namespace
SqliteDb SqliteDb::OpenFile(const std::string& path) {
sqlite3* db = nullptr;
const int rc = sqlite3_open(path.c_str(), &db);
if (rc != SQLITE_OK) {
const char* msg = db ? sqlite3_errmsg(db) : "";
if (db) sqlite3_close(db);
throw std::runtime_error(std::string("sqlite3_open failed: ") + msg);
}
return SqliteDb(db);
}
SqliteDb SqliteDb::OpenMemory() {
sqlite3* db = nullptr;
const int rc = sqlite3_open(":memory:", &db);
ThrowSqlite(rc, db, "sqlite3_open(:memory:) failed");
return SqliteDb(db);
}
SqliteDb::~SqliteDb() {
if (db_) sqlite3_close(db_);
}
SqliteDb::SqliteDb(SqliteDb&& other) noexcept : db_(other.db_) {
other.db_ = nullptr;
}
SqliteDb& SqliteDb::operator=(SqliteDb&& other) noexcept {
if (this == &other) return *this;
if (db_) sqlite3_close(db_);
db_ = other.db_;
other.db_ = nullptr;
return *this;
}
void SqliteDb::Exec(const std::string& sql) {
char* err = nullptr;
const int rc = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &err);
if (rc != SQLITE_OK) {
std::string msg = err ? err : "";
sqlite3_free(err);
ThrowSqlite(rc, db_, msg.c_str());
}
}
void ApplyMigrations(SqliteDb& db) {
// Keep it simple for MVP: apply the bundled init SQL.
// In later iterations we'll add a migrations table + incremental runner.
db.Exec("PRAGMA foreign_keys = ON;");
// 001_init.sql (embedded)
db.Exec(R"SQL(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_salt TEXT NOT NULL,
password_hash TEXT NOT NULL,
rating INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS problems (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
statement_md TEXT NOT NULL,
difficulty INTEGER NOT NULL DEFAULT 1,
source TEXT NOT NULL DEFAULT "",
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS problem_tags (
problem_id INTEGER NOT NULL,
tag TEXT NOT NULL,
PRIMARY KEY(problem_id, tag),
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS submissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
problem_id INTEGER NOT NULL,
language TEXT NOT NULL,
code TEXT NOT NULL,
status TEXT NOT NULL,
score INTEGER NOT NULL DEFAULT 0,
time_ms INTEGER NOT NULL DEFAULT 0,
memory_kb INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS wrong_book (
user_id INTEGER NOT NULL,
problem_id INTEGER NOT NULL,
last_submission_id INTEGER,
note TEXT NOT NULL DEFAULT "",
updated_at INTEGER NOT NULL,
PRIMARY KEY(user_id, problem_id),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE,
FOREIGN KEY(last_submission_id) REFERENCES submissions(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_submissions_user_created_at ON submissions(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_submissions_problem_created_at ON submissions(problem_id, created_at DESC);
)SQL");
}
} // namespace csp::db

查看文件

@@ -1,15 +1,22 @@
#include <drogon/drogon.h>
int main(int argc, char** argv) {
(void)argc;
(void)argv;
#include "csp/app_state.h"
#include <filesystem>
int main(int argc, char** argv) {
const std::string db_path = (argc >= 2) ? argv[1] : std::string("data/csp.db");
std::filesystem::create_directories(std::filesystem::path(db_path).parent_path());
csp::AppState::Instance().Init(db_path);
// Basic defaults. Will be moved to config-driven later.
drogon::app()
.addListener("0.0.0.0", 8080)
.setThreadNum(4);
LOG_INFO << "csp_server starting at http://0.0.0.0:8080";
LOG_INFO << "sqlite db: " << db_path;
drogon::app().run();
return 0;
}