feat: 完成源晶权限与经验系统并优化 me/admin 交互
这个提交包含在:
@@ -18,6 +18,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,
|
||||
@@ -96,6 +115,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,
|
||||
@@ -112,6 +206,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,
|
||||
@@ -145,6 +282,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,
|
||||
@@ -189,14 +354,96 @@ CREATE TABLE IF NOT EXISTS problem_solutions (
|
||||
FOREIGN KEY(problem_id) REFERENCES problems(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO source_crystal_settings(id, monthly_interest_rate, updated_at)
|
||||
VALUES(1, 0.02, strftime('%s','now'));
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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_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);
|
||||
|
||||
在新工单中引用
屏蔽一个用户