feat: 完成源晶权限与经验系统并优化 me/admin 交互
这个提交包含在:
@@ -1,6 +1,7 @@
|
||||
#include "csp/db/sqlite_db.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -21,6 +22,40 @@ int64_t NowSec() {
|
||||
return duration_cast<seconds>(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 + ")";
|
||||
@@ -153,6 +188,53 @@ void InsertKbArticle(sqlite3* db,
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
std::optional<int64_t> 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 =
|
||||
@@ -208,6 +290,105 @@ void InsertContestProblem(sqlite3* db,
|
||||
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,
|
||||
@@ -259,6 +440,12 @@ SqliteDb SqliteDb::OpenFile(const std::string& path) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -266,6 +453,12 @@ 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);
|
||||
}
|
||||
|
||||
@@ -317,6 +510,25 @@ CREATE TABLE IF NOT EXISTS sessions (
|
||||
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,
|
||||
@@ -399,6 +611,81 @@ CREATE TABLE IF NOT EXISTS contest_registrations (
|
||||
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,
|
||||
@@ -415,6 +702,49 @@ CREATE TABLE IF NOT EXISTS kb_article_links (
|
||||
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,
|
||||
@@ -448,6 +778,34 @@ CREATE TABLE IF NOT EXISTS import_job_items (
|
||||
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,
|
||||
@@ -547,6 +905,31 @@ CREATE TABLE IF NOT EXISTS redeem_records (
|
||||
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 daily_task_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
@@ -557,6 +940,42 @@ CREATE TABLE IF NOT EXISTS daily_task_logs (
|
||||
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.
|
||||
@@ -605,10 +1024,22 @@ CREATE INDEX IF NOT EXISTS idx_submissions_user_created_at ON submissions(user_i
|
||||
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);
|
||||
@@ -616,8 +1047,18 @@ CREATE INDEX IF NOT EXISTS idx_solution_view_logs_user_problem ON problem_soluti
|
||||
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_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) {
|
||||
@@ -709,6 +1150,357 @@ void SeedDemoData(SqliteDb& db) {
|
||||
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 <service>` 排查启动失败。
|
||||
- 开机自启与配置变更后的重载策略。
|
||||
|
||||
### 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,
|
||||
@@ -726,6 +1518,63 @@ void SeedDemoData(SqliteDb& db) {
|
||||
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,
|
||||
|
||||
在新工单中引用
屏蔽一个用户