#include "csp/db/sqlite_db.h" #include #include #include #include #include #include namespace csp::db { namespace { void ThrowSqlite(int rc, sqlite3* db, const char* what) { if (rc == SQLITE_OK || rc == SQLITE_DONE || rc == SQLITE_ROW) return; const char* msg = db ? sqlite3_errmsg(db) : ""; throw std::runtime_error(std::string(what) + ": " + msg); } int64_t NowSec() { using namespace std::chrono; return duration_cast(system_clock::now().time_since_epoch()).count(); } void ExecSqlite(sqlite3* db, const char* sql, const char* what) { char* err = nullptr; const int rc = sqlite3_exec(db, sql, nullptr, nullptr, &err); if (rc != SQLITE_OK) { std::string msg = err ? err : ""; sqlite3_free(err); throw std::runtime_error(std::string(what) + ": " + msg); } } int ResolveBusyTimeoutMs() { constexpr int kDefaultTimeoutMs = 15000; const char* raw = std::getenv("CSP_SQLITE_BUSY_TIMEOUT_MS"); if (!raw || std::string(raw).empty()) return kDefaultTimeoutMs; try { const int parsed = std::stoi(raw); if (parsed < 100) return 100; if (parsed > 120000) return 120000; return parsed; } catch (...) { return kDefaultTimeoutMs; } } void ConfigureConnection(sqlite3* db, bool memory_db) { ThrowSqlite(sqlite3_busy_timeout(db, ResolveBusyTimeoutMs()), db, "sqlite3_busy_timeout"); ExecSqlite(db, "PRAGMA foreign_keys = ON;", "pragma foreign_keys"); if (!memory_db) { ExecSqlite(db, "PRAGMA journal_mode = WAL;", "pragma journal_mode"); ExecSqlite(db, "PRAGMA synchronous = NORMAL;", "pragma synchronous"); } } bool ColumnExists(sqlite3* db, const char* table, const char* col) { sqlite3_stmt* stmt = nullptr; const std::string sql = std::string("PRAGMA table_info(") + table + ")"; const int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr); if (rc != SQLITE_OK) { if (stmt) sqlite3_finalize(stmt); return false; } bool found = false; while (sqlite3_step(stmt) == SQLITE_ROW) { const unsigned char* name = sqlite3_column_text(stmt, 1); if (name && std::string(reinterpret_cast(name)) == col) { found = true; break; } } sqlite3_finalize(stmt); return found; } void EnsureColumn(SqliteDb& db, const char* table, const char* col_name, const char* col_def) { if (ColumnExists(db.raw(), table, col_name)) return; db.Exec(std::string("ALTER TABLE ") + table + " ADD COLUMN " + col_def + ";"); } int CountRows(sqlite3* db, const char* table) { sqlite3_stmt* stmt = nullptr; const std::string sql = std::string("SELECT COUNT(1) FROM ") + table; int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr); if (rc != SQLITE_OK) { if (stmt) sqlite3_finalize(stmt); return 0; } rc = sqlite3_step(stmt); int count = 0; if (rc == SQLITE_ROW) count = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return count; } std::optional QueryOneId(sqlite3* db, const std::string& sql) { sqlite3_stmt* stmt = nullptr; const int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr); if (rc != SQLITE_OK) { if (stmt) sqlite3_finalize(stmt); return std::nullopt; } if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return std::nullopt; } const auto id = sqlite3_column_int64(stmt, 0); sqlite3_finalize(stmt); return id; } void InsertProblem(sqlite3* db, const std::string& slug, const std::string& title, const std::string& statement, int difficulty, const std::string& source, const std::string& sample_in, const std::string& sample_out, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO problems(slug,title,statement_md,difficulty,source,sample_input,sample_output,created_at) " "VALUES(?,?,?,?,?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert problem"); ThrowSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.slug"); ThrowSqlite(sqlite3_bind_text(stmt, 2, title.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.title"); ThrowSqlite(sqlite3_bind_text(stmt, 3, statement.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.statement"); ThrowSqlite(sqlite3_bind_int(stmt, 4, difficulty), db, "bind problem.difficulty"); ThrowSqlite(sqlite3_bind_text(stmt, 5, source.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.source"); ThrowSqlite(sqlite3_bind_text(stmt, 6, sample_in.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.sample_input"); ThrowSqlite(sqlite3_bind_text(stmt, 7, sample_out.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem.sample_output"); ThrowSqlite(sqlite3_bind_int64(stmt, 8, created_at), db, "bind problem.created_at"); ThrowSqlite(sqlite3_step(stmt), db, "insert problem"); sqlite3_finalize(stmt); } void InsertProblemTag(sqlite3* db, int64_t problem_id, const std::string& tag) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT OR IGNORE INTO problem_tags(problem_id,tag) VALUES(?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert problem_tag"); ThrowSqlite(sqlite3_bind_int64(stmt, 1, problem_id), db, "bind problem_tag.problem_id"); ThrowSqlite(sqlite3_bind_text(stmt, 2, tag.c_str(), -1, SQLITE_TRANSIENT), db, "bind problem_tag.tag"); ThrowSqlite(sqlite3_step(stmt), db, "insert problem_tag"); sqlite3_finalize(stmt); } void InsertKbArticle(sqlite3* db, const std::string& slug, const std::string& title, const std::string& content_md, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO kb_articles(slug,title,content_md,created_at) VALUES(?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert kb_article"); ThrowSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db, "bind kb_article.slug"); ThrowSqlite(sqlite3_bind_text(stmt, 2, title.c_str(), -1, SQLITE_TRANSIENT), db, "bind kb_article.title"); ThrowSqlite(sqlite3_bind_text(stmt, 3, content_md.c_str(), -1, SQLITE_TRANSIENT), db, "bind kb_article.content"); ThrowSqlite(sqlite3_bind_int64(stmt, 4, created_at), db, "bind kb_article.created_at"); ThrowSqlite(sqlite3_step(stmt), db, "insert kb_article"); sqlite3_finalize(stmt); } std::optional FindKbArticleId(sqlite3* db, const std::string& slug) { sqlite3_stmt* stmt = nullptr; const char* sql = "SELECT id FROM kb_articles WHERE slug=? LIMIT 1"; const int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { if (stmt) sqlite3_finalize(stmt); return std::nullopt; } ThrowSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db, "bind find kb slug"); if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return std::nullopt; } const auto id = sqlite3_column_int64(stmt, 0); sqlite3_finalize(stmt); return id; } int64_t EnsureKbArticle(sqlite3* db, const std::string& slug, const std::string& title, const std::string& content_md, int64_t updated_at) { if (const auto existing = FindKbArticleId(db, slug); existing.has_value()) { sqlite3_stmt* stmt = nullptr; const char* sql = "UPDATE kb_articles SET title=?,content_md=?,created_at=? WHERE id=?"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare update kb article"); ThrowSqlite(sqlite3_bind_text(stmt, 1, title.c_str(), -1, SQLITE_TRANSIENT), db, "bind update kb title"); ThrowSqlite(sqlite3_bind_text(stmt, 2, content_md.c_str(), -1, SQLITE_TRANSIENT), db, "bind update kb content"); ThrowSqlite(sqlite3_bind_int64(stmt, 3, updated_at), db, "bind update kb time"); ThrowSqlite(sqlite3_bind_int64(stmt, 4, *existing), db, "bind update kb id"); ThrowSqlite(sqlite3_step(stmt), db, "update kb article"); sqlite3_finalize(stmt); return *existing; } InsertKbArticle(db, slug, title, content_md, updated_at); const auto inserted = FindKbArticleId(db, slug); if (!inserted.has_value()) { throw std::runtime_error("ensure kb article failed"); } return *inserted; } void InsertKbLink(sqlite3* db, int64_t article_id, int64_t problem_id) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT OR IGNORE INTO kb_article_links(article_id,problem_id) VALUES(?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert kb_article_link"); ThrowSqlite(sqlite3_bind_int64(stmt, 1, article_id), db, "bind kb_article_link.article_id"); ThrowSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind kb_article_link.problem_id"); ThrowSqlite(sqlite3_step(stmt), db, "insert kb_article_link"); sqlite3_finalize(stmt); } void InsertContest(sqlite3* db, const std::string& title, int64_t starts_at, int64_t ends_at, const std::string& rule_json) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO contests(title,starts_at,ends_at,rule_json) VALUES(?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert contest"); ThrowSqlite(sqlite3_bind_text(stmt, 1, title.c_str(), -1, SQLITE_TRANSIENT), db, "bind contest.title"); ThrowSqlite(sqlite3_bind_int64(stmt, 2, starts_at), db, "bind contest.starts_at"); ThrowSqlite(sqlite3_bind_int64(stmt, 3, ends_at), db, "bind contest.ends_at"); ThrowSqlite(sqlite3_bind_text(stmt, 4, rule_json.c_str(), -1, SQLITE_TRANSIENT), db, "bind contest.rule_json"); ThrowSqlite(sqlite3_step(stmt), db, "insert contest"); sqlite3_finalize(stmt); } void InsertContestProblem(sqlite3* db, int64_t contest_id, int64_t problem_id, int idx) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT OR IGNORE INTO contest_problems(contest_id,problem_id,idx) VALUES(?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert contest_problem"); ThrowSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db, "bind contest_problem.contest_id"); ThrowSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind contest_problem.problem_id"); ThrowSqlite(sqlite3_bind_int(stmt, 3, idx), db, "bind contest_problem.idx"); ThrowSqlite(sqlite3_step(stmt), db, "insert contest_problem"); sqlite3_finalize(stmt); } void InsertContestModifier(sqlite3* db, int64_t contest_id, const std::string& code, const std::string& title, const std::string& description, const std::string& rule_json, int is_active, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT OR IGNORE INTO contest_modifiers(" "contest_id,code,title,description,rule_json,is_active,created_at,updated_at" ") VALUES(?,?,?,?,?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert contest_modifier"); ThrowSqlite(sqlite3_bind_int64(stmt, 1, contest_id), db, "bind contest_modifier.contest_id"); ThrowSqlite(sqlite3_bind_text(stmt, 2, code.c_str(), -1, SQLITE_TRANSIENT), db, "bind contest_modifier.code"); ThrowSqlite(sqlite3_bind_text(stmt, 3, title.c_str(), -1, SQLITE_TRANSIENT), db, "bind contest_modifier.title"); ThrowSqlite(sqlite3_bind_text(stmt, 4, description.c_str(), -1, SQLITE_TRANSIENT), db, "bind contest_modifier.description"); ThrowSqlite(sqlite3_bind_text(stmt, 5, rule_json.c_str(), -1, SQLITE_TRANSIENT), db, "bind contest_modifier.rule_json"); ThrowSqlite(sqlite3_bind_int(stmt, 6, is_active), db, "bind contest_modifier.is_active"); ThrowSqlite(sqlite3_bind_int64(stmt, 7, created_at), db, "bind contest_modifier.created_at"); ThrowSqlite(sqlite3_bind_int64(stmt, 8, created_at), db, "bind contest_modifier.updated_at"); ThrowSqlite(sqlite3_step(stmt), db, "insert contest_modifier"); sqlite3_finalize(stmt); } void InsertSeason(sqlite3* db, const std::string& key, const std::string& title, int64_t starts_at, int64_t ends_at, const std::string& status, const std::string& pass_json, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO seasons(key,title,starts_at,ends_at,status,pass_json,created_at,updated_at) " "VALUES(?,?,?,?,?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert season"); ThrowSqlite(sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT), db, "bind season.key"); ThrowSqlite(sqlite3_bind_text(stmt, 2, title.c_str(), -1, SQLITE_TRANSIENT), db, "bind season.title"); ThrowSqlite(sqlite3_bind_int64(stmt, 3, starts_at), db, "bind season.starts_at"); ThrowSqlite(sqlite3_bind_int64(stmt, 4, ends_at), db, "bind season.ends_at"); ThrowSqlite(sqlite3_bind_text(stmt, 5, status.c_str(), -1, SQLITE_TRANSIENT), db, "bind season.status"); ThrowSqlite(sqlite3_bind_text(stmt, 6, pass_json.c_str(), -1, SQLITE_TRANSIENT), db, "bind season.pass_json"); ThrowSqlite(sqlite3_bind_int64(stmt, 7, created_at), db, "bind season.created_at"); ThrowSqlite(sqlite3_bind_int64(stmt, 8, created_at), db, "bind season.updated_at"); ThrowSqlite(sqlite3_step(stmt), db, "insert season"); sqlite3_finalize(stmt); } void InsertSeasonRewardTrack(sqlite3* db, int64_t season_id, int tier_no, int required_xp, const std::string& reward_type, int reward_value, const std::string& reward_meta_json) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT OR IGNORE INTO season_reward_tracks(" "season_id,tier_no,required_xp,reward_type,reward_value,reward_meta_json" ") VALUES(?,?,?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert season_reward_track"); ThrowSqlite(sqlite3_bind_int64(stmt, 1, season_id), db, "bind season_reward_track.season_id"); ThrowSqlite(sqlite3_bind_int(stmt, 2, tier_no), db, "bind season_reward_track.tier_no"); ThrowSqlite(sqlite3_bind_int(stmt, 3, required_xp), db, "bind season_reward_track.required_xp"); ThrowSqlite(sqlite3_bind_text(stmt, 4, reward_type.c_str(), -1, SQLITE_TRANSIENT), db, "bind season_reward_track.reward_type"); ThrowSqlite(sqlite3_bind_int(stmt, 5, reward_value), db, "bind season_reward_track.reward_value"); ThrowSqlite(sqlite3_bind_text(stmt, 6, reward_meta_json.c_str(), -1, SQLITE_TRANSIENT), db, "bind season_reward_track.reward_meta_json"); ThrowSqlite(sqlite3_step(stmt), db, "insert season_reward_track"); sqlite3_finalize(stmt); } void InsertRedeemItem(sqlite3* db, const std::string& name, const std::string& description, const std::string& unit_label, int holiday_cost, int studyday_cost, int is_active, int is_global, int64_t created_by, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO redeem_items(" "name,description,unit_label,holiday_cost,studyday_cost,is_active,is_global,created_by,created_at,updated_at" ") VALUES(?,?,?,?,?,?,?,?,?,?)"; ThrowSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert redeem_item"); ThrowSqlite(sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT), db, "bind redeem_item.name"); ThrowSqlite(sqlite3_bind_text(stmt, 2, description.c_str(), -1, SQLITE_TRANSIENT), db, "bind redeem_item.description"); ThrowSqlite(sqlite3_bind_text(stmt, 3, unit_label.c_str(), -1, SQLITE_TRANSIENT), db, "bind redeem_item.unit_label"); ThrowSqlite(sqlite3_bind_int(stmt, 4, holiday_cost), db, "bind redeem_item.holiday_cost"); ThrowSqlite(sqlite3_bind_int(stmt, 5, studyday_cost), db, "bind redeem_item.studyday_cost"); ThrowSqlite(sqlite3_bind_int(stmt, 6, is_active), db, "bind redeem_item.is_active"); ThrowSqlite(sqlite3_bind_int(stmt, 7, is_global), db, "bind redeem_item.is_global"); ThrowSqlite(sqlite3_bind_int64(stmt, 8, created_by), db, "bind redeem_item.created_by"); ThrowSqlite(sqlite3_bind_int64(stmt, 9, created_at), db, "bind redeem_item.created_at"); ThrowSqlite(sqlite3_bind_int64(stmt, 10, created_at), db, "bind redeem_item.updated_at"); ThrowSqlite(sqlite3_step(stmt), db, "insert redeem_item"); sqlite3_finalize(stmt); } } // 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); } try { ConfigureConnection(db, false); } catch (...) { sqlite3_close(db); throw; } return SqliteDb(db); } SqliteDb SqliteDb::OpenMemory() { sqlite3* db = nullptr; const int rc = sqlite3_open(":memory:", &db); ThrowSqlite(rc, db, "sqlite3_open(:memory:) failed"); try { ConfigureConnection(db, true); } catch (...) { sqlite3_close(db); throw; } 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: create missing tables, then patch missing columns. db.Exec("PRAGMA foreign_keys = ON;"); 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 user_experience ( user_id INTEGER PRIMARY KEY, xp INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS user_experience_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, xp_delta INTEGER NOT NULL, rating_before INTEGER NOT NULL DEFAULT 0, rating_after INTEGER NOT NULL DEFAULT 0, source TEXT NOT NULL DEFAULT "users.rating", note TEXT NOT NULL DEFAULT "", 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 "", statement_url TEXT NOT NULL DEFAULT "", llm_profile_json TEXT NOT NULL DEFAULT "{}", sample_input TEXT NOT NULL DEFAULT "", sample_output 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, contest_id INTEGER, 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, compile_log TEXT NOT NULL DEFAULT "", runtime_log TEXT NOT NULL DEFAULT "", 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, FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE SET NULL ); 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 "", note_score INTEGER NOT NULL DEFAULT 0, note_rating INTEGER NOT NULL DEFAULT 0, note_feedback_md TEXT NOT NULL DEFAULT "", note_scored_at INTEGER NOT NULL DEFAULT 0, 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 TABLE IF NOT EXISTS contests ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, starts_at INTEGER NOT NULL, ends_at INTEGER NOT NULL, rule_json TEXT NOT NULL DEFAULT "{}" ); CREATE TABLE IF NOT EXISTS contest_problems ( contest_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, idx INTEGER NOT NULL, PRIMARY KEY(contest_id, problem_id), FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE CASCADE, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS contest_registrations ( contest_id INTEGER NOT NULL, user_id INTEGER NOT NULL, registered_at INTEGER NOT NULL, PRIMARY KEY(contest_id, user_id), FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS contest_modifiers ( id INTEGER PRIMARY KEY AUTOINCREMENT, contest_id INTEGER NOT NULL, code TEXT NOT NULL, title TEXT NOT NULL, description TEXT NOT NULL DEFAULT "", rule_json TEXT NOT NULL DEFAULT "{}", is_active INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY(contest_id) REFERENCES contests(id) ON DELETE CASCADE, UNIQUE(contest_id, code) ); CREATE TABLE IF NOT EXISTS seasons ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL UNIQUE, title TEXT NOT NULL, starts_at INTEGER NOT NULL, ends_at INTEGER NOT NULL, status TEXT NOT NULL DEFAULT "draft", pass_json TEXT NOT NULL DEFAULT "{}", created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS season_reward_tracks ( id INTEGER PRIMARY KEY AUTOINCREMENT, season_id INTEGER NOT NULL, tier_no INTEGER NOT NULL, required_xp INTEGER NOT NULL DEFAULT 0, reward_type TEXT NOT NULL DEFAULT "free", reward_value INTEGER NOT NULL DEFAULT 0, reward_meta_json TEXT NOT NULL DEFAULT "{}", FOREIGN KEY(season_id) REFERENCES seasons(id) ON DELETE CASCADE, UNIQUE(season_id, tier_no, reward_type) ); CREATE TABLE IF NOT EXISTS season_user_progress ( season_id INTEGER NOT NULL, user_id INTEGER NOT NULL, xp INTEGER NOT NULL DEFAULT 0, level INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL, PRIMARY KEY(season_id, user_id), FOREIGN KEY(season_id) REFERENCES seasons(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS season_reward_claims ( id INTEGER PRIMARY KEY AUTOINCREMENT, season_id INTEGER NOT NULL, user_id INTEGER NOT NULL, tier_no INTEGER NOT NULL, reward_type TEXT NOT NULL DEFAULT "free", claimed_at INTEGER NOT NULL, FOREIGN KEY(season_id) REFERENCES seasons(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE(season_id, user_id, tier_no, reward_type) ); CREATE TABLE IF NOT EXISTS loot_drop_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, source_type TEXT NOT NULL, source_id INTEGER NOT NULL DEFAULT 0, item_code TEXT NOT NULL, item_name TEXT NOT NULL DEFAULT "", rarity TEXT NOT NULL DEFAULT "common", amount INTEGER NOT NULL DEFAULT 0, meta_json TEXT NOT NULL DEFAULT "{}", created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS kb_articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL UNIQUE, title TEXT NOT NULL, content_md TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS kb_article_links ( article_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, PRIMARY KEY(article_id, problem_id), FOREIGN KEY(article_id) REFERENCES kb_articles(id) ON DELETE CASCADE, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS kb_knowledge_claims ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, article_id INTEGER NOT NULL, knowledge_key TEXT NOT NULL, reward INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(article_id) REFERENCES kb_articles(id) ON DELETE CASCADE, UNIQUE(user_id, article_id, knowledge_key) ); CREATE TABLE IF NOT EXISTS kb_weekly_tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, week_key TEXT NOT NULL, article_id INTEGER NOT NULL, article_slug TEXT NOT NULL, article_title TEXT NOT NULL, knowledge_key TEXT NOT NULL, knowledge_title TEXT NOT NULL, knowledge_description TEXT NOT NULL DEFAULT "", difficulty TEXT NOT NULL DEFAULT "bronze", reward INTEGER NOT NULL DEFAULT 1, prerequisites TEXT NOT NULL DEFAULT "", order_no INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, completed_at INTEGER, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(article_id) REFERENCES kb_articles(id) ON DELETE CASCADE, UNIQUE(user_id, week_key, article_id, knowledge_key) ); CREATE TABLE IF NOT EXISTS kb_weekly_bonus_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, week_key TEXT NOT NULL, reward INTEGER NOT NULL DEFAULT 100, created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE(user_id, week_key) ); CREATE TABLE IF NOT EXISTS import_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, status TEXT NOT NULL, trigger TEXT NOT NULL DEFAULT "manual", total_count INTEGER NOT NULL DEFAULT 0, processed_count INTEGER NOT NULL DEFAULT 0, success_count INTEGER NOT NULL DEFAULT 0, failed_count INTEGER NOT NULL DEFAULT 0, options_json TEXT NOT NULL DEFAULT "{}", last_error TEXT NOT NULL DEFAULT "", started_at INTEGER NOT NULL, finished_at INTEGER, updated_at INTEGER NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS import_job_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_id INTEGER NOT NULL, source_path TEXT NOT NULL, status TEXT NOT NULL DEFAULT "queued", title TEXT NOT NULL DEFAULT "", difficulty INTEGER NOT NULL DEFAULT 0, problem_id INTEGER, error_text TEXT NOT NULL DEFAULT "", started_at INTEGER, finished_at INTEGER, updated_at INTEGER NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(job_id) REFERENCES import_jobs(id) ON DELETE CASCADE, UNIQUE(job_id, source_path) ); CREATE TABLE IF NOT EXISTS crawler_targets ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, normalized_url TEXT NOT NULL UNIQUE, status TEXT NOT NULL DEFAULT "queued", submit_source TEXT NOT NULL DEFAULT "manual", submitter_id TEXT NOT NULL DEFAULT "", submitter_name TEXT NOT NULL DEFAULT "", rule_json TEXT NOT NULL DEFAULT "{}", script_path TEXT NOT NULL DEFAULT "", last_error TEXT NOT NULL DEFAULT "", last_test_at INTEGER, last_run_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS crawler_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, target_id INTEGER NOT NULL, status TEXT NOT NULL DEFAULT "queued", http_status INTEGER NOT NULL DEFAULT 0, output_json TEXT NOT NULL DEFAULT "{}", error_text TEXT NOT NULL DEFAULT "", created_at INTEGER NOT NULL, FOREIGN KEY(target_id) REFERENCES crawler_targets(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS problem_drafts ( user_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, language TEXT NOT NULL DEFAULT "cpp", code TEXT NOT NULL DEFAULT "", stdin TEXT NOT NULL DEFAULT "", updated_at INTEGER NOT NULL, created_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 ); CREATE TABLE IF NOT EXISTS problem_solution_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, problem_id INTEGER NOT NULL, status TEXT NOT NULL DEFAULT "queued", progress INTEGER NOT NULL DEFAULT 0, message TEXT NOT NULL DEFAULT "", created_by INTEGER NOT NULL DEFAULT 0, max_solutions INTEGER NOT NULL DEFAULT 3, created_at INTEGER NOT NULL, started_at INTEGER, finished_at INTEGER, updated_at INTEGER NOT NULL, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS problem_solutions ( id INTEGER PRIMARY KEY AUTOINCREMENT, problem_id INTEGER NOT NULL, variant INTEGER NOT NULL DEFAULT 1, title TEXT NOT NULL DEFAULT "", idea_md TEXT NOT NULL DEFAULT "", explanation_md TEXT NOT NULL DEFAULT "", code_cpp TEXT NOT NULL DEFAULT "", complexity TEXT NOT NULL DEFAULT "", tags_json TEXT NOT NULL DEFAULT "[]", source TEXT NOT NULL DEFAULT "llm", created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS problem_solution_view_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, problem_id INTEGER NOT NULL, day_key TEXT NOT NULL, viewed_at INTEGER NOT NULL, charged INTEGER NOT NULL DEFAULT 0, cost 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 submission_feedback ( id INTEGER PRIMARY KEY AUTOINCREMENT, submission_id INTEGER NOT NULL UNIQUE, feedback_md TEXT NOT NULL DEFAULT "", links_json TEXT NOT NULL DEFAULT "[]", model_name TEXT NOT NULL DEFAULT "", status TEXT NOT NULL DEFAULT "ready", created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY(submission_id) REFERENCES submissions(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS redeem_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT "", unit_label TEXT NOT NULL DEFAULT "小时", holiday_cost INTEGER NOT NULL DEFAULT 5, studyday_cost INTEGER NOT NULL DEFAULT 25, duration_minutes INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, is_global INTEGER NOT NULL DEFAULT 1, created_by INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS redeem_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, item_id INTEGER NOT NULL, item_name TEXT NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, day_type TEXT NOT NULL DEFAULT "studyday", unit_cost INTEGER NOT NULL DEFAULT 0, total_cost INTEGER NOT NULL DEFAULT 0, note TEXT NOT NULL DEFAULT "", created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(item_id) REFERENCES redeem_items(id) ON DELETE RESTRICT ); CREATE TABLE IF NOT EXISTS source_crystal_settings ( id INTEGER PRIMARY KEY CHECK(id=1), monthly_interest_rate REAL NOT NULL DEFAULT 0.02, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS source_crystal_accounts ( user_id INTEGER PRIMARY KEY, balance REAL NOT NULL DEFAULT 0, last_interest_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS source_crystal_transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, tx_type TEXT NOT NULL, amount REAL NOT NULL, balance_after REAL NOT NULL, note TEXT NOT NULL DEFAULT "", created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS user_achievement_milestones ( user_id INTEGER NOT NULL, milestone_no INTEGER NOT NULL, completed_count_snapshot INTEGER NOT NULL DEFAULT 0, rating_bonus INTEGER NOT NULL DEFAULT 20, created_at INTEGER NOT NULL, PRIMARY KEY(user_id, milestone_no), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS daily_task_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, task_code TEXT NOT NULL, day_key TEXT NOT NULL, reward INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE(user_id, task_code, day_key) ); CREATE TRIGGER IF NOT EXISTS trg_users_init_experience AFTER INSERT ON users BEGIN INSERT OR IGNORE INTO user_experience(user_id, xp, updated_at) VALUES(NEW.id, CASE WHEN NEW.rating > 0 THEN NEW.rating ELSE 0 END, strftime('%s','now')); INSERT INTO user_experience_logs(user_id, xp_delta, rating_before, rating_after, source, note, created_at) SELECT NEW.id, NEW.rating, 0, NEW.rating, 'users.insert', 'initial rating gain', strftime('%s','now') WHERE NEW.rating > 0; END; CREATE TRIGGER IF NOT EXISTS trg_users_rating_gain_to_experience AFTER UPDATE OF rating ON users WHEN NEW.rating > OLD.rating BEGIN INSERT OR IGNORE INTO user_experience(user_id, xp, updated_at) VALUES(NEW.id, 0, strftime('%s','now')); UPDATE user_experience SET xp = xp + (NEW.rating - OLD.rating), updated_at = strftime('%s','now') WHERE user_id = NEW.id; INSERT INTO user_experience_logs(user_id, xp_delta, rating_before, rating_after, source, note, created_at) VALUES(NEW.id, (NEW.rating - OLD.rating), OLD.rating, NEW.rating, 'users.rating', 'rating gain', strftime('%s','now')); END; )SQL"); // Backward-compatible schema upgrades for existing deployments. EnsureColumn(db, "problems", "sample_input", "sample_input TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problems", "sample_output", "sample_output TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problems", "statement_url", "statement_url TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problems", "llm_profile_json", "llm_profile_json TEXT NOT NULL DEFAULT '{}'"); EnsureColumn(db, "import_jobs", "trigger", "trigger TEXT NOT NULL DEFAULT 'manual'"); EnsureColumn(db, "import_jobs", "options_json", "options_json TEXT NOT NULL DEFAULT '{}'"); EnsureColumn(db, "import_jobs", "last_error", "last_error TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "submissions", "contest_id", "contest_id INTEGER"); EnsureColumn(db, "submissions", "compile_log", "compile_log TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "submissions", "runtime_log", "runtime_log TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_drafts", "stdin", "stdin TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solution_jobs", "max_solutions", "max_solutions INTEGER NOT NULL DEFAULT 3"); EnsureColumn(db, "wrong_book", "note_score", "note_score INTEGER NOT NULL DEFAULT 0"); EnsureColumn(db, "wrong_book", "note_rating", "note_rating INTEGER NOT NULL DEFAULT 0"); EnsureColumn(db, "wrong_book", "note_feedback_md", "note_feedback_md TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "wrong_book", "note_scored_at", "note_scored_at INTEGER NOT NULL DEFAULT 0"); EnsureColumn(db, "wrong_book", "note_images_json", "note_images_json TEXT NOT NULL DEFAULT '[]'"); EnsureColumn(db, "problem_solutions", "variant", "variant INTEGER NOT NULL DEFAULT 1"); EnsureColumn(db, "problem_solutions", "idea_md", "idea_md TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solutions", "explanation_md", "explanation_md TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solutions", "code_cpp", "code_cpp TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solutions", "complexity", "complexity TEXT NOT NULL DEFAULT ''"); EnsureColumn(db, "problem_solutions", "tags_json", "tags_json TEXT NOT NULL DEFAULT '[]'"); EnsureColumn(db, "redeem_items", "duration_minutes", "duration_minutes INTEGER NOT NULL DEFAULT 0"); // Build indexes after compatibility ALTERs so old schemas won't fail on // missing columns (e.g. legacy submissions table without contest_id). db.Exec(R"SQL( 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); CREATE INDEX IF NOT EXISTS idx_submissions_contest_user_created_at ON submissions(contest_id, user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_problem_tags_tag ON problem_tags(tag); CREATE INDEX IF NOT EXISTS idx_contest_modifiers_contest_active ON contest_modifiers(contest_id, is_active, id); CREATE INDEX IF NOT EXISTS idx_seasons_status_range ON seasons(status, starts_at, ends_at); CREATE INDEX IF NOT EXISTS idx_season_reward_tracks_season_tier ON season_reward_tracks(season_id, tier_no, required_xp); CREATE INDEX IF NOT EXISTS idx_season_user_progress_user ON season_user_progress(user_id, season_id, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_season_reward_claims_user ON season_reward_claims(user_id, season_id, claimed_at DESC); CREATE INDEX IF NOT EXISTS idx_loot_drop_logs_user_created ON loot_drop_logs(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_kb_article_links_problem_id ON kb_article_links(problem_id); CREATE INDEX IF NOT EXISTS idx_kb_knowledge_claims_user_article ON kb_knowledge_claims(user_id, article_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_kb_weekly_tasks_user_week_order ON kb_weekly_tasks(user_id, week_key, order_no, id); CREATE INDEX IF NOT EXISTS idx_kb_weekly_tasks_user_week_completed ON kb_weekly_tasks(user_id, week_key, completed_at); CREATE INDEX IF NOT EXISTS idx_kb_weekly_bonus_logs_user_week ON kb_weekly_bonus_logs(user_id, week_key); CREATE INDEX IF NOT EXISTS idx_import_jobs_created_at ON import_jobs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_import_jobs_status ON import_jobs(status, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_import_job_items_job_status ON import_job_items(job_id, status, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_crawler_targets_status_updated ON crawler_targets(status, updated_at ASC); CREATE INDEX IF NOT EXISTS idx_crawler_runs_target_created ON crawler_runs(target_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_problem_drafts_updated ON problem_drafts(updated_at DESC); CREATE INDEX IF NOT EXISTS idx_problem_solution_jobs_problem ON problem_solution_jobs(problem_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_problem_solutions_problem ON problem_solutions(problem_id, variant, id); CREATE INDEX IF NOT EXISTS idx_solution_view_logs_user_problem ON problem_solution_view_logs(user_id, problem_id, viewed_at DESC); CREATE INDEX IF NOT EXISTS idx_solution_view_logs_user_day ON problem_solution_view_logs(user_id, day_key, viewed_at DESC); CREATE INDEX IF NOT EXISTS idx_redeem_items_active ON redeem_items(is_active, id); CREATE INDEX IF NOT EXISTS idx_redeem_records_user_created ON redeem_records(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_user_experience_logs_user_created ON user_experience_logs(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_source_crystal_tx_user_created ON source_crystal_transactions(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_user_achievement_milestones_user_created ON user_achievement_milestones(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_daily_task_logs_user_day ON daily_task_logs(user_id, day_key, created_at DESC); )SQL"); db.Exec( "INSERT OR IGNORE INTO user_experience(user_id,xp,updated_at) " "SELECT id, CASE WHEN rating>0 THEN rating ELSE 0 END, strftime('%s','now') FROM users;"); db.Exec( "INSERT OR IGNORE INTO source_crystal_settings(id,monthly_interest_rate,updated_at) " "VALUES(1,0.02,strftime('%s','now'));"); } void SeedDemoData(SqliteDb& db) { sqlite3* raw = db.raw(); const int64_t now = NowSec(); if (CountRows(raw, "problems") == 0) { InsertProblem( raw, "a-plus-b", "A + B", "给定两个整数 A 与 B,输出它们的和。", 1, "CSP-入门", "1 2\n", "3\n", now); InsertProblem( raw, "fibonacci-n", "Fibonacci 第 n 项", "输入 n (0<=n<=40),输出第 n 项 Fibonacci 数。", 2, "CSP-基础", "10\n", "55\n", now); InsertProblem( raw, "sort-numbers", "整数排序", "输入 n 和 n 个整数,按升序输出。", 2, "CSP-基础", "5\n5 1 4 2 3\n", "1 2 3 4 5\n", now); } if (CountRows(raw, "problem_tags") == 0) { const auto p1 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='a-plus-b'"); const auto p2 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='fibonacci-n'"); const auto p3 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='sort-numbers'"); if (p1) { InsertProblemTag(raw, *p1, "math"); InsertProblemTag(raw, *p1, "implementation"); } if (p2) { InsertProblemTag(raw, *p2, "dp"); InsertProblemTag(raw, *p2, "recursion"); } if (p3) { InsertProblemTag(raw, *p3, "sort"); InsertProblemTag(raw, *p3, "array"); } } if (CountRows(raw, "kb_articles") == 0) { InsertKbArticle( raw, "cpp-fast-io", "C++ 快速输入输出", "# C++ 快速输入输出\n\n在 OI/CSP 中,建议关闭同步并解绑 cin/cout:\n\n```cpp\nstd::ios::sync_with_stdio(false);\nstd::cin.tie(nullptr);\n```\n", now); InsertKbArticle( raw, "intro-dp", "动态规划入门", "# 动态规划入门\n\n动态规划的核心是:**状态定义**、**状态转移**、**边界条件**。\n", now); } if (CountRows(raw, "kb_article_links") == 0) { const auto p1 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='a-plus-b'"); const auto p2 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='fibonacci-n'"); const auto a1 = QueryOneId(raw, "SELECT id FROM kb_articles WHERE slug='cpp-fast-io'"); const auto a2 = QueryOneId(raw, "SELECT id FROM kb_articles WHERE slug='intro-dp'"); if (a1 && p1) InsertKbLink(raw, *a1, *p1); if (a2 && p2) InsertKbLink(raw, *a2, *p2); } // Curated skill-tree knowledge base (always upsert to keep latest structure). { const std::string cpp14_md = R"MD( # C++14 全栈技能树(面向 CSP) ## 目标与使用方式 - 目标:把 C++14 从“能写”升级到“稳、快、可评测”。 - 使用方式:每学完一个知识点,就在页面里领取对应知识点奖励。 - 评测规范:默认按 C++14 思维编码,避免 C++17 特性;`long long` 使用 `%lld`;`main` 返回 `int` 且 `return 0;`。 ## 知识图谱(从入门到进阶) ### 1. 语法与输入输出基础 - `cin/cout` 快速 IO、`scanf/printf` 基本格式。 - 字符串读取(`getline`)和空白字符处理。 - 常见格式坑:多组输入、行尾空格、输出格式严格匹配。 ### 2. 数据结构基础 - 数组、`vector`、`string`。 - `map/set/unordered_map` 适用场景。 - 栈队列(`stack/queue/deque/priority_queue`)模板化思维。 ### 3. 算法基础 - 排序、二分、贪心直觉。 - 前缀和、差分、双指针、滑窗。 - DFS/BFS 基础搜索框架与剪枝。 ### 4. 动态规划与复杂度控制 - 一维/二维 DP 的状态设计。 - 转移方程推导顺序。 - 内存压缩、滚动数组、复杂度上界估算。 ## 详细知识点清单(建议打卡顺序) 1. 输入输出与格式化:掌握 `%d/%lld`、读整行、格式严格匹配。 2. 类型系统与边界:掌握整型溢出与强转风险。 3. 函数与引用:掌握参数传递、函数拆分和复用。 4. 数组与字符串:掌握下标边界、初始化、防越界。 5. STL 容器与算法:掌握 `sort/lower_bound`、迭代器使用。 6. 排序与贪心:掌握比较器、贪心反例验证。 7. 前缀和与差分:掌握区间查询/更新模型。 8. 动态规划入门:掌握状态与转移设计。 9. DFS/BFS:掌握搜索顺序、剪枝、去重。 10. 调试与复杂度:掌握测试数据设计与超时定位。 ## 训练节奏建议(4 周) - 第 1 周:语法+IO+数组字符串,每天 2 题。 - 第 2 周:STL+排序+二分,每天 2~3 题。 - 第 3 周:前缀和+搜索,每天 2 题。 - 第 4 周:DP 入门+综合复盘,每天 1~2 题。 ## 失分点与避坑 - 边界漏判:`n=0/1`、负数、重复值。 - 复杂度超限:未提前估算 O(n^2) 是否可过。 - 提交错误:未清理调试输出、输入输出格式不一致。 ## 实战检查清单 - [ ] 代码符合 C++14(无 C++17 语法)。 - [ ] main 函数返回 int 且 return 0。 - [ ] long long 的输入输出格式正确。 - [ ] 关键循环复杂度可解释。 - [ ] 边界样例已自测。 )MD"; const std::string github_md = R"MD( # GitHub 仓库协作技能树(竞赛团队与项目实战) ## 目标 - 能独立完成:分支开发 → 提交 → PR → Review → 合并发布。 - 形成稳定协作规范,避免“代码在我机子上能跑”问题。 ## 分层知识 ### 1. 仓库结构与分支策略 - 主分支 `main`:可发布版本。 - 开发分支 `dev`:集成测试版本。 - 功能分支 `feature/*`:每个功能独立开发。 ### 2. 提交规范 - 一次提交只做一件事(原子提交)。 - 提交信息模板:`type(scope): summary`。 - 提交前自检:lint/test/build。 ### 3. PR 与 Review - PR 描述包含:背景、改动点、风险、验证结果。 - Review 关注:正确性、可维护性、回归风险。 - 评论处理:逐条回应并补充验证。 ### 4. 冲突处理与历史整理 - 学会 `rebase`,减少无意义 merge commit。 - 冲突处理后必须重新跑测试。 - 出问题可回滚到 tag 或稳定提交。 ## 详细技能点清单 1. 仓库与分支模型。 2. 提交规范与代码整洁。 3. PR 模板与审查习惯。 4. rebase 与冲突处理。 5. tag 与版本发布。 6. CI 流水线基础。 7. 密钥与权限安全。 ## 团队协作红线 - 不在 `main` 直接开发。 - 不提交大段无关重构与功能混改。 - 不上传密钥、token、服务器密码。 - 不跳过 review 直接合并高风险改动。 )MD"; const std::string linux_md = R"MD( # Linux 服务器基础技能树(开发与运维入门) ## 目标 - 会看日志、会查端口、会看进程、会重启服务、会排查故障。 - 形成“先观测、再操作、可回滚”的稳定运维习惯。 ## 核心模块 ### 1. Shell 与文件系统 - `pwd/ls/cd/cat/less/tail/head` - `grep/rg/find` 定位配置和错误日志。 - 目录权限与用户归属(`chmod/chown`)。 ### 2. 进程与资源 - `ps/top/htop` 看 CPU/内存占用。 - `free/df/du` 看内存和磁盘。 - OOM 与磁盘打满的典型处理流程。 ### 3. 网络与端口 - `ss -lntp` 看监听端口。 - `curl/wget` 验证服务连通性。 - 反向代理、内网服务、端口映射基本概念。 ### 4. 服务管理 - `systemctl start/stop/restart/status` - `journalctl -u ` 排查启动失败。 - 开机自启与配置变更后的重载策略。 ### 5. 备份恢复 - 数据目录与配置目录分离。 - 定时备份 + 恢复演练 + 校验。 - 事故时优先保证可恢复与数据一致性。 ## 详细技能点清单 1. Shell 基础命令。 2. 进程与资源监控。 3. 网络与端口排查。 4. systemd 服务管理。 5. 权限与用户组。 6. 日志与故障定位。 7. 备份与恢复演练。 ## 故障排查顺序建议 1. 服务是否在运行。 2. 端口是否监听。 3. 反代是否转发。 4. 日志是否出现错误栈。 5. 最近变更(配置/发布)是否引入问题。 )MD"; const std::string cs_md = R"MD( # 计算机基础技能树(算法学习必备底座) ## 目标 - 建立“算法 + 系统 + 工程”的统一认知,不只会刷题,也能解释程序为什么这样运行。 ## 模块一:数据表示 - 二进制、补码、位运算。 - 字符编码(ASCII/UTF-8)。 - 浮点误差与比较策略。 ## 模块二:程序执行模型 - 栈、堆、静态区。 - 函数调用、参数压栈、递归深度。 - 越界、悬垂引用、未定义行为风险。 ## 模块三:复杂度与性能 - 时间复杂度、空间复杂度。 - 输入规模与算法选择。 - 常数优化与缓存友好性直觉。 ## 模块四:操作系统与网络 - 进程/线程、上下文切换。 - 文件系统与 IO。 - TCP/UDP、HTTP 请求链路。 ## 模块五:数据库与安全基础 - 索引、事务、锁。 - 注入风险、鉴权边界、最小权限。 - 日志脱敏与凭据保护。 ## 详细技能点清单 1. 二进制与编码。 2. 内存模型与指针意识。 3. 复杂度与可扩展性。 4. 操作系统基本机制。 5. 网络协议基础。 6. 数据库基础。 7. 安全基本面。 ## 学习建议 - 每周固定 2 次“基础课”,每次 45 分钟。 - 每个知识点都配 1 个小实验(命令验证或小程序验证)。 - 和题目训练联动:每周总结“这周哪道题体现了哪个基础知识点”。 )MD"; const std::string web_cpp_md = R"MD( # C++ Web 开发技能树(从算法到可上线服务) ## 目标 - 用 C++ 做服务端开发,完成“接口设计 → 编码实现 → 本地联调 → 线上部署”闭环。 - 保持 CSP/OI 风格:重视性能、边界与可观测性。 ## C++ 视角的 Web 基础 ### 1) 协议与请求模型 - 理解 HTTP 请求/响应、状态码、Header、Body。 - 区分 GET/POST/PUT/PATCH/DELETE 的语义。 - 学会设计稳定 JSON 响应格式(`ok/data/error`)。 ### 2) 路由与控制器 - 学会把 URL 映射到 C++ 处理函数(Controller)。 - 参数校验:路径参数、query 参数、JSON Body。 - 错误返回统一化:400/401/403/404/500。 ### 3) 数据库与事务 - 用 SQLite/MySQL 做增删改查。 - 理解事务边界与并发写入冲突。 - 学会索引设计与慢查询定位。 ### 4) 鉴权与安全 - 账号密码 + token/session 模型。 - 最小权限:普通用户与管理员隔离。 - 输入校验、防注入、敏感信息脱敏。 ### 5) 工程化与上线 - 日志分级(info/warn/error)与错误追踪。 - Nginx 反代、HTTPS、证书续期。 - 健康检查、重启策略、自动化部署。 ## C++14 最小接口示例(伪代码) ```cpp void GetProfile(const HttpRequestPtr& req, Callback&& cb) { auto uid = RequireAuth(req); if (!uid) return cb(JsonError(401, "unauthorized")); auto user = user_service.GetById(*uid); if (!user) return cb(JsonError(404, "user not found")); Json::Value data; data["id"] = Json::Int64(user->id); data["username"] = user->username; return cb(JsonOk(data)); } ``` ## 学习路线(4 周) - 第 1 周:HTTP + 路由 + JSON。 - 第 2 周:数据库 CRUD + 参数校验。 - 第 3 周:鉴权 + 权限 + 日志。 - 第 4 周:部署 + 监控 + 故障演练。 ## 常见坑 - 把异常直接暴露给前端(不安全)。 - 接口返回结构不统一,前端处理复杂。 - 数据库锁冲突时缺少重试与告警。 ## 建议实践任务 - 实现一个“题目收藏”接口(增删查)。 - 实现一个“用户提交记录”分页接口(按时间筛选)。 - 加入统一错误码与请求日志追踪字段。 )MD"; const std::string game_cpp_md = R"MD( # C++ 游戏开发技能树(从算法训练到实时交互) ## 目标 - 用 C++ 构建可运行的小型 2D 游戏原型。 - 建立“数据结构 + 数学 + 渲染 + 性能”的整体认知。 ## C++ 视角的游戏核心 ### 1) 游戏循环(Game Loop) - 固定更新 + 渲染分离。 - 处理输入、更新状态、绘制画面。 - 使用 `deltaTime` 避免帧率依赖。 ### 2) 数学基础 - 向量与坐标系(位置、速度、加速度)。 - 碰撞检测(AABB、圆形碰撞)与简单响应。 - 插值、角度、方向单位向量。 ### 3) 资源与场景 - 贴图、音频、字体加载与生命周期管理。 - 场景切换(主菜单/关卡/结算)。 - 对象管理:玩家、敌人、子弹、道具。 ### 4) 架构与代码组织 - 从面向过程到组件化(ECS 思维入门)。 - 把“渲染、逻辑、输入”拆分为模块。 - 配置与常量表驱动,减少硬编码。 ### 5) 调试与优化 - 帧时间统计与瓶颈定位。 - 减少频繁内存分配,复用对象池。 - 用日志和调试可视化定位状态异常。 ## C++14 最小循环示例(伪代码) ```cpp while (running) { float dt = timer.Tick(); HandleInput(); UpdateWorld(dt); RenderFrame(); } ``` ## 学习路线(4 周) - 第 1 周:循环 + 输入 + 基础渲染。 - 第 2 周:角色移动 + 碰撞系统。 - 第 3 周:关卡与 UI + 存档。 - 第 4 周:性能优化 + 发布打包。 ## 常见坑 - 逻辑与渲染强耦合,后期难扩展。 - 帧率变化导致移动速度异常。 - 资源重复加载导致卡顿与内存上涨。 ## 建议实践任务 - 做一个“躲避方块”小游戏(计时 + 计分)。 - 做一个“像素迷宫”小游戏(碰撞 + 关卡)。 - 增加“暂停、继续、重开”状态机。 )MD"; const auto a_cpp14 = EnsureKbArticle(raw, "cpp14-skill-tree", "C++14 全栈技能树(CSP)", cpp14_md, now); const auto a_git = EnsureKbArticle(raw, "github-collaboration-basics", "GitHub 仓库协作基础", github_md, now); const auto a_linux = EnsureKbArticle(raw, "linux-server-basics", "Linux 服务器基础", linux_md, now); const auto a_cs = EnsureKbArticle(raw, "computer-fundamentals-for-oi", "计算机基础(OI 视角)", cs_md, now); const auto a_web_cpp = EnsureKbArticle(raw, "cpp-web-development-basics", "Web 开发(C++ 基础)", web_cpp_md, now); const auto a_game_cpp = EnsureKbArticle(raw, "cpp-game-development-basics", "游戏开发(C++ 基础)", game_cpp_md, now); const auto p1 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='a-plus-b'"); const auto p2 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='fibonacci-n'"); const auto p3 = QueryOneId(raw, "SELECT id FROM problems WHERE slug='sort-numbers'"); const auto p_any1 = QueryOneId(raw, "SELECT id FROM problems ORDER BY id LIMIT 1"); const auto p_any2 = QueryOneId(raw, "SELECT id FROM problems ORDER BY id LIMIT 1 OFFSET 1"); if (p1) InsertKbLink(raw, a_cpp14, *p1); if (p2) InsertKbLink(raw, a_cpp14, *p2); if (p3) InsertKbLink(raw, a_cpp14, *p3); if (p1) InsertKbLink(raw, a_cs, *p1); if (p2) InsertKbLink(raw, a_cs, *p2); if (p3) InsertKbLink(raw, a_web_cpp, *p3); else if (p_any1) InsertKbLink(raw, a_web_cpp, *p_any1); if (p2) InsertKbLink(raw, a_game_cpp, *p2); else if (p_any2) InsertKbLink(raw, a_game_cpp, *p_any2); (void)a_git; (void)a_linux; } if (CountRows(raw, "contests") == 0) { InsertContest( raw, "CSP 模拟赛(示例)", now - 3600, now + 7 * 24 * 3600, R"({"type":"acm","desc":"按通过题数与罚时排名"})"); } if (CountRows(raw, "contest_problems") == 0) { const auto contest_id = QueryOneId(raw, "SELECT id FROM contests ORDER BY id LIMIT 1"); const auto p1 = QueryOneId(raw, "SELECT id FROM problems ORDER BY id LIMIT 1"); const auto p2 = QueryOneId(raw, "SELECT id FROM problems ORDER BY id LIMIT 1 OFFSET 1"); if (contest_id && p1) InsertContestProblem(raw, *contest_id, *p1, 1); if (contest_id && p2) InsertContestProblem(raw, *contest_id, *p2, 2); } if (CountRows(raw, "contest_modifiers") == 0) { const auto contest_id = QueryOneId(raw, "SELECT id FROM contests ORDER BY id LIMIT 1"); if (contest_id.has_value()) { InsertContestModifier( raw, *contest_id, "cpp14_only", "远古工艺:仅 C++14", "副本中请使用 C++14 语法,不可依赖更高标准特性。", R"({"language":"cpp14","forbid":["concepts","ranges","coroutine"]})", 1, now); } } if (CountRows(raw, "seasons") == 0) { InsertSeason( raw, "season-2026-s1", "2026 第一赛季:像素远征", now - 7 * 24 * 3600, now + 60 * 24 * 3600, "active", R"({"name":"像素远征","theme":"minecraft","season_pass":true})", now); } if (CountRows(raw, "season_reward_tracks") == 0) { const auto season_id = QueryOneId(raw, "SELECT id FROM seasons WHERE status='active' ORDER BY id DESC LIMIT 1"); if (season_id.has_value()) { InsertSeasonRewardTrack( raw, *season_id, 1, 0, "free", 5, R"({"item_name":"绿宝石补给","rarity":"common"})"); InsertSeasonRewardTrack( raw, *season_id, 2, 30, "free", 15, R"({"item_name":"铁质宝箱","rarity":"rare"})"); InsertSeasonRewardTrack( raw, *season_id, 3, 80, "free", 30, R"({"item_name":"钻石宝箱","rarity":"epic"})"); } } if (CountRows(raw, "redeem_items") == 0) { InsertRedeemItem( raw, "私人玩游戏时间", "全局用户可兑换:假期 1 小时 5 Rating;学习日/非节假日 1 小时 25 Rating。", "小时", 5, 25, 1, 1, 0, now); } // Always seed C++基础课程任务(幂等:按 slug 检测存在)。 { const auto existing = QueryOneId(raw, "SELECT id FROM problems WHERE slug='cpp-basic-01-hello' LIMIT 1"); if (!existing.has_value()) { const int64_t created = now; struct CourseItem { const char* slug; const char* title; int diff; const char* source; const char* md; const char* tags[6]; }; const CourseItem items[] = { { "cpp-basic-01-hello", "C++基础01:环境配置与Hello World(VSCode)", 1, "course:cpp-basic:01", "# C++基础01:环境配置与Hello World(VSCode)\n\n## 学习目标\n- 安装并打开 VSCode,创建并运行第一个 C++14 程序\n- 学会新建文件、保存、运行、查看输出\n- 了解 `main()`、`#include `、`cout`\n\n## 推荐视频(观看后写笔记)\n- 保姆级:VSCode + mingw64 配置 C/C++(BV1tg411N7Fq)\n - https://www.bilibili.com/video/BV1tg411N7Fq/\n- 每天五分钟学C++:01 开发工具(BV1dK4y137bk,系列入口)\n - https://www.bilibili.com/video/BV1dK4y137bk/\n\n## 参考图文\n- LoongBa 极简配置:GCC/VSCode/HelloWorld\n - https://github.com/LoongBa/Cpp_Beginner_Guide\n\n## 练习(完成至少 2 题)\n- B2002 Hello, World! https://www.luogu.com.cn/problem/B2002\n- P1000 超级玛丽游戏 https://www.luogu.com.cn/problem/P1000\n\n## 提交要求\n- 在本题页面下方“学习笔记”区域写下:\n 1) 你安装了什么、遇到什么坑、怎么解决\n 2) 你的 HelloWorld 代码\n 3) 你学到的 3 个关键词\n", {"cpp-basic","vscode","io","", "", ""} }, { "cpp-basic-02-io", "C++基础02:输入输出与变量", 1, "course:cpp-basic:02", "# C++基础02:输入输出与变量\n\n## 学习目标\n- 会用 `cin` 读入、`cout` 输出\n- 理解变量:`int / long long / double / char / string`\n\n## 推荐视频\n- 每天五分钟学C++:02 输出、03 变量(系列入口见上)\n - https://www.bilibili.com/video/BV1dK4y137bk/\n\n## 练习\n- P1001 A+B Problem https://www.luogu.com.cn/problem/P1001\n- B2008 计算 (a+b)×c 的值 https://www.luogu.com.cn/problem/B2008\n- P5704 字母转换 https://www.luogu.com.cn/problem/P5704\n\n## 提交要求\n- 上传/填写学习笔记:写出 `cin/cout` 模板、常见错误(空格/换行)\n", {"cpp-basic","io","types","", "", ""} }, { "cpp-basic-03-branch", "C++基础03:分支结构(if / switch)", 2, "course:cpp-basic:03", "# C++基础03:分支结构(if / switch)\n\n## 学习目标\n- 会写 `if / else if / else` 与基本逻辑运算\n- 能处理边界与分类讨论\n\n## 练习\n- B2035 判断数正负 https://www.luogu.com.cn/problem/B2035\n- P5711 闰年判断 https://www.luogu.com.cn/problem/P5711\n- P1909 买铅笔 https://www.luogu.com.cn/problem/P1909\n\n## 提交要求\n- 笔记里写清:你如何找“边界”(例如 0、最小/最大、等于条件)\n", {"cpp-basic","branch","logic","", "", ""} }, { "cpp-basic-04-loop", "C++基础04:循环结构(for / while)", 2, "course:cpp-basic:04", "# C++基础04:循环结构(for / while)\n\n## 学习目标\n- 会用循环做:计数、累加、打印图形\n\n## 练习\n- B2083 画矩形 https://www.luogu.com.cn/problem/B2083\n- P1421 小玉买文具 https://www.luogu.com.cn/problem/P1421\n\n## 提交要求\n- 笔记里写:循环三要素(初始化/条件/更新)+ 你调试的方法\n", {"cpp-basic","loop","debug","", "", ""} }, { "cpp-basic-05-array", "C++基础05:数组入门(一维)", 3, "course:cpp-basic:05", "# C++基础05:数组入门(一维)\n\n## 学习目标\n- 会定义数组、遍历、统计\n\n## 练习\n- P1427 小鱼的数字游戏 https://www.luogu.com.cn/problem/P1427\n- P1428 小鱼比可爱 https://www.luogu.com.cn/problem/P1428\n\n## 提交要求\n- 笔记里写:数组下标从 0/1 的选择;如何避免越界\n", {"cpp-basic","array","", "", "", ""} }, // 06~20:新增课程 { "cpp-basic-06-char-string", "C++基础06:字符与字符串(char / string)", 3, "course:cpp-basic:06", "# C++基础06:字符与字符串(char / string)\n\n## 学习目标\n- 区分 `char`(一个字符)与 `string`(一串字符)\n- 会做:大小写转换、统计字母、简单加密\n\n## 重点\n- `char c='A'`,`string s=\"abc\"`\n- `s.size()` / `s[i]` / 遍历字符串\n\n## 练习(完成至少 2 题)\n- P5704 字母转换 https://www.luogu.com.cn/problem/P5704\n- P5733 自动修正 https://www.luogu.com.cn/problem/P5733\n- P1914 小书童——凯撒密码 https://www.luogu.com.cn/problem/P1914\n\n## 提交要求\n- 笔记写:`string` 下标访问与越界风险(最后一个下标是 size()-1)\n", {"cpp-basic","string","char","", "", ""} }, { "cpp-basic-07-operator-cast", "C++基础07:运算、取整与类型转换(/ %)", 3, "course:cpp-basic:07", "# C++基础07:运算、取整与类型转换(/ %)\n\n## 学习目标\n- 写对 `+ - * / %`\n- 理解:整数除法会向下取整;`%` 只能用于整数\n- 学会用 `long long` 防止溢出\n\n## 练习\n- B2008 计算 (a+b)×c 的值 https://www.luogu.com.cn/problem/B2008\n- P1421 小玉买文具 https://www.luogu.com.cn/problem/P1421\n- P5708 三角形面积 https://www.luogu.com.cn/problem/P5708\n\n## 提交要求\n- 笔记写:什么时候要用 `long long`(例如钱、人数、乘法很大)\n", {"cpp-basic","math","cast","", "", ""} }, { "cpp-basic-08-logic", "C++基础08:逻辑运算与边界(&& || !)", 3, "course:cpp-basic:08", "# C++基础08:逻辑运算与边界(&& || !)\n\n## 学习目标\n- 能把题目里的“并且/或者/不是”写成条件\n- 学会找边界:等于、最小/最大、0\n\n## 练习\n- P5710 数的性质 https://www.luogu.com.cn/problem/P5710\n- P5711 闰年判断 https://www.luogu.com.cn/problem/P5711\n- P1909 买铅笔 https://www.luogu.com.cn/problem/P1909\n\n## 提交要求\n- 笔记写:你列出的边界测试用例(至少 3 个)\n", {"cpp-basic","logic","boundary","", "", ""} }, { "cpp-basic-09-switch", "C++基础09:switch 与枚举列情况", 3, "course:cpp-basic:09", "# C++基础09:switch 与枚举列情况\n\n## 学习目标\n- 会写 `switch/case/break`\n- 面对“情况很少”的题,会列举所有情况\n\n## 练习\n- P2433 小学数学N合一 https://www.luogu.com.cn/problem/P2433\n\n## 提交要求\n- 笔记写:为什么每个 case 后要 `break`(避免贯穿执行)\n", {"cpp-basic","switch","enum","", "", ""} }, { "cpp-basic-10-for", "C++基础10:for循环(计数与累加)", 3, "course:cpp-basic:10", "# C++基础10:for循环(计数与累加)\n\n## 学习目标\n- 熟练掌握 `for(初始化; 条件; 更新)`\n- 会写:求和、求最大最小、统计个数\n\n## 练习\n- P1425 小鱼的游泳时间 https://www.luogu.com.cn/problem/P1425\n- P5718 找最小值 https://www.luogu.com.cn/problem/P5718\n\n## 提交要求\n- 笔记写:循环“三要素”分别是什么\n", {"cpp-basic","loop","for","", "", ""} }, { "cpp-basic-11-while", "C++基础11:while循环(未知次数)", 3, "course:cpp-basic:11", "# C++基础11:while循环(未知次数)\n\n## 学习目标\n- 会用 `while` 处理:读到结束、读到 0 停止\n- 避免死循环:每次循环必须改变状态\n\n## 练习\n- P1427 小鱼的数字游戏 https://www.luogu.com.cn/problem/P1427\n- P1047 校门外的树(选做)https://www.luogu.com.cn/problem/P1047\n\n## 提交要求\n- 笔记写:你是如何避免死循环的(更新变量写在哪里)\n", {"cpp-basic","loop","while","", "", ""} }, { "cpp-basic-12-nested", "C++基础12:嵌套循环与打印图形", 3, "course:cpp-basic:12", "# C++基础12:嵌套循环与打印图形\n\n## 学习目标\n- 外层循环=行,内层循环=列\n- 能打印:矩形、三角形、乘法表\n\n## 练习\n- B2083 画矩形 https://www.luogu.com.cn/problem/B2083\n- P5717 三角形分类(练分类讨论 + 输出格式)https://www.luogu.com.cn/problem/P5717\n\n## 提交要求\n- 笔记写:你画图时是怎么想“行/列”的\n", {"cpp-basic","nested-loop","print","", "", ""} }, { "cpp-basic-13-array-ops", "C++基础13:数组进阶(统计/查找/反转)", 4, "course:cpp-basic:13", "# C++基础13:数组进阶(统计/查找/反转)\n\n## 学习目标\n- 用数组做:统计、查找位置、反转、去重(了解)\n\n## 练习\n- P1428 小鱼比可爱 https://www.luogu.com.cn/problem/P1428\n- P5727 冰雹猜想 https://www.luogu.com.cn/problem/P5727\n\n## 提交要求\n- 笔记写:数组越界的 2 个典型原因 + 你的检查方法\n", {"cpp-basic","array","practice","", "", ""} }, { "cpp-basic-14-function", "C++基础14:函数入门(把重复代码变成工具)", 4, "course:cpp-basic:14", "# C++基础14:函数入门(参数/返回值/作用域)\n\n## 学习目标\n- 会定义并调用函数\n- 知道局部变量作用域({} 内有效)\n\n## 练习\n- P5735 距离函数 https://www.luogu.com.cn/problem/P5735\n- P5739 计算阶乘(也可用递归)https://www.luogu.com.cn/problem/P5739\n\n## 提交要求\n- 笔记写:你把哪段重复代码抽成了函数?为什么这样更清晰?\n", {"cpp-basic","function","scope","", "", ""} }, { "cpp-basic-15-recursion", "C++基础15:递归入门(选学)", 4, "course:cpp-basic:15", "# C++基础15:递归入门(选学)\n\n## 学习目标\n- 理解递归:函数调用自己\n- 必须有终止条件(否则会无限调用)\n\n## 练习\n- P5739 计算阶乘 https://www.luogu.com.cn/problem/P5739\n- P1028 数的计算(选做)https://www.luogu.com.cn/problem/P1028\n\n## 提交要求\n- 笔记写:你的递归“终止条件”是什么?\n", {"cpp-basic","recursion","", "", "", ""} }, { "cpp-basic-16-struct", "C++基础16:结构体 struct(选学)", 4, "course:cpp-basic:16", "# C++基础16:结构体 struct(选学)\n\n## 学习目标\n- 用 `struct` 把多个字段打包成一个“人物卡/记录”\n- 学会 `struct` 数组\n\n## 练习\n- P5740 最厉害的学生 https://www.luogu.com.cn/problem/P5740\n- P5744 培训 https://www.luogu.com.cn/problem/P5744\n\n## 提交要求\n- 笔记写:struct 适合用在什么场景(多个属性属于同一个对象)\n", {"cpp-basic","struct","", "", "", ""} }, { "cpp-basic-17-sort", "C++基础17:排序初步(sort)", 4, "course:cpp-basic:17", "# C++基础17:排序初步(sort)\n\n## 学习目标\n- 会用 `#include ` 和 `sort`\n- 排序后做统计/查找会更简单\n\n## 练习\n- P1059 明明的随机数 https://www.luogu.com.cn/problem/P1059\n- P1093 奖学金(选做)https://www.luogu.com.cn/problem/P1093\n\n## 提交要求\n- 笔记写:sort 的区间是 `[begin, end)`(end 不包含)\n", {"cpp-basic","sort","algorithm","", "", ""} }, { "cpp-basic-18-binary-search", "C++基础18:二分查找初步(lower_bound)", 4, "course:cpp-basic:18", "# C++基础18:二分查找初步(lower_bound)\n\n## 学习目标\n- 理解“在有序数组里快速找答案”\n- 会用 `lower_bound` 找第一个 >= x 的位置\n\n## 练习\n- P2249 查找 https://www.luogu.com.cn/problem/P2249\n\n## 提交要求\n- 笔记写:二分查找的前提:数组必须有序\n", {"cpp-basic","binary-search","", "", "", ""} }, { "cpp-basic-19-prefix-sum", "C++基础19:前缀和(区间求和神器)", 4, "course:cpp-basic:19", "# C++基础19:前缀和(区间求和神器)\n\n## 学习目标\n- 会构造前缀和 `s[i]=s[i-1]+a[i]`\n- 会算区间和 `sum(l,r)=s[r]-s[l-1]`\n\n## 练习\n- P8218 求区间和(前缀和基础)https://www.luogu.com.cn/problem/P8218\n\n## 提交要求\n- 笔记写:下标偏移是怎么处理的(从 1 开始更方便)\n", {"cpp-basic","prefix-sum","", "", "", ""} }, { "cpp-basic-20-simulation", "C++基础20:模拟题与综合复盘", 4, "course:cpp-basic:20", "# C++基础20:模拟题与综合复盘\n\n## 学习目标\n- 学会按题意一步步做(模拟)\n- 复盘:总结常见错误 + 常用模板\n\n## 练习(从这里选 3~5 题做小测)\n- P1957 口算练习题 https://www.luogu.com.cn/problem/P1957\n- P1055 ISBN号码 https://www.luogu.com.cn/problem/P1055\n- P2433 小学数学N合一(回顾)https://www.luogu.com.cn/problem/P2433\n\n## 提交要求\n- 笔记写:\n 1) 你最常犯的 3 类错误\n 2) 你最常用的 5 行代码模板\n 3) 下周你准备怎么练\n", {"cpp-basic","simulation","review","", "", ""} }, }; for (const auto& it : items) { InsertProblem(raw, it.slug, it.title, it.md, it.diff, it.source, "", "", created); const auto pid = QueryOneId(raw, std::string("SELECT id FROM problems WHERE slug='") + it.slug + "' LIMIT 1"); if (pid.has_value()) { for (const char* tag : it.tags) { if (!tag || !*tag) continue; InsertProblemTag(raw, *pid, tag); } } } } } } } // namespace csp::db