Files
Toolbox/src/server/db.js
T

219 lines
6.2 KiB
JavaScript

const Database = require('better-sqlite3')
const fs = require('fs')
const path = require('path')
const dataDir = path.join(process.cwd(), 'data')
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
const dbPath = path.join(dataDir, 'toolbox.db')
const db = new Database(dbPath)
db.exec(
`CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT NOT NULL,
mid TEXT NOT NULL UNIQUE,
screen_name TEXT,
created_at TEXT,
text TEXT,
text_plain TEXT,
url TEXT,
reposts_count INTEGER,
comments_count INTEGER,
attitudes_count INTEGER,
pics_json TEXT,
fetched_at TEXT
);`
)
// migrations: add retweeted_* columns if missing
const cols = db.prepare(`PRAGMA table_info(posts)`).all().map(r => r.name)
const addCol = name => db.exec(`ALTER TABLE posts ADD COLUMN ${name} TEXT`)
if (!cols.includes('retweeted_mid')) addCol('retweeted_mid')
if (!cols.includes('retweeted_screen_name')) addCol('retweeted_screen_name')
if (!cols.includes('retweeted_text')) addCol('retweeted_text')
if (!cols.includes('retweeted_text_plain')) addCol('retweeted_text_plain')
if (!cols.includes('retweeted_url')) addCol('retweeted_url')
if (!cols.includes('retweeted_pics_json')) addCol('retweeted_pics_json')
db.exec(
`CREATE TABLE IF NOT EXISTS keywords (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT NOT NULL UNIQUE,
created_at TEXT DEFAULT (datetime('now'))
);`
)
db.exec(
`CREATE TABLE IF NOT EXISTS tracked_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT NOT NULL UNIQUE,
screen_name TEXT,
added_at TEXT DEFAULT (datetime('now'))
);`
)
db.exec(
`CREATE TABLE IF NOT EXISTS settings_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool TEXT NOT NULL,
payload TEXT NOT NULL,
saved_at TEXT DEFAULT (datetime('now'))
);`
)
db.exec(
`CREATE TABLE IF NOT EXISTS archives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT NOT NULL,
week_start TEXT NOT NULL,
week_end TEXT NOT NULL,
html TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(uid, week_start)
);`
)
db.exec(
`CREATE TABLE IF NOT EXISTS market_archives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
day TEXT NOT NULL UNIQUE,
html TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);`
)
db.exec(
`CREATE TABLE IF NOT EXISTS alert_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
skill TEXT NOT NULL,
alert_id TEXT NOT NULL,
slot_key TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(skill, alert_id)
);`
)
db.exec(
`CREATE TABLE IF NOT EXISTS market_state (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT DEFAULT (datetime('now'))
);`
)
db.exec(
`CREATE TABLE IF NOT EXISTS alert_email_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
skill TEXT NOT NULL,
alert_id TEXT NOT NULL,
subject TEXT,
status TEXT NOT NULL DEFAULT 'pending',
error_message TEXT,
created_at TEXT DEFAULT (datetime('now'))
);`
)
// 唯一索引:同一个告警事件同一种状态不重复记录
try { db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_alert_email_log_uniq ON alert_email_log(skill, alert_id, status)`) } catch {}
const insertPost = db.prepare(
`INSERT OR IGNORE INTO posts (
uid, mid, screen_name, created_at, text, text_plain, url,
reposts_count, comments_count, attitudes_count, pics_json, fetched_at
) VALUES (
@uid, @mid, @screen_name, @created_at, @text, @text_plain, @url,
@reposts_count, @comments_count, @attitudes_count, @pics_json, @fetched_at
)`
)
const updateRetweeted = db.prepare(
`UPDATE posts SET
retweeted_mid = @retweeted_mid,
retweeted_screen_name = @retweeted_screen_name,
retweeted_text = @retweeted_text,
retweeted_text_plain = @retweeted_text_plain,
retweeted_url = @retweeted_url,
retweeted_pics_json = @retweeted_pics_json,
text_plain = @text_plain
WHERE mid = @mid`
)
const queryPostsAny = (uid, terms, limit) => {
const likeClauses = terms.map(() => 'text_plain LIKE ?').join(' OR ')
const args = terms.map(t => `%${t}%`)
const stmt = db.prepare(
`SELECT * FROM posts WHERE uid = ? AND (${likeClauses}) ORDER BY fetched_at DESC, id ASC LIMIT ?`
)
return stmt.all(uid, ...args, limit)
}
const queryPostsAll = (uid, terms, limit) => {
const likeClauses = terms.map(() => 'text_plain LIKE ?').join(' AND ')
const args = terms.map(t => `%${t}%`)
const stmt = db.prepare(
`SELECT * FROM posts WHERE uid = ? AND (${likeClauses}) ORDER BY fetched_at DESC, id ASC LIMIT ?`
)
return stmt.all(uid, ...args, limit)
}
const insertKeyword = db.prepare(
`INSERT OR IGNORE INTO keywords (keyword) VALUES (?)`
)
const insertTrackedUser = db.prepare(
`INSERT OR IGNORE INTO tracked_users (uid, screen_name) VALUES (?, ?)`
)
const getSettings = () => {
const keywords = db.prepare(`SELECT keyword FROM keywords ORDER BY id DESC`).all().map(r => r.keyword)
const users = db.prepare(`SELECT uid, screen_name FROM tracked_users ORDER BY id DESC`).all()
return { keywords, users }
}
const insertSettingHistory = db.prepare(
`INSERT INTO settings_history (tool, payload) VALUES (@tool, @payload)`
)
const upsertArchive = db.prepare(
`INSERT INTO archives (uid, week_start, week_end, html, created_at, updated_at)
VALUES (@uid, @week_start, @week_end, @html, datetime('now'), datetime('now'))
ON CONFLICT(uid, week_start) DO UPDATE SET
week_end = excluded.week_end,
html = excluded.html,
updated_at = datetime('now')`
)
const getArchive = db.prepare(
`SELECT * FROM archives WHERE uid = ? AND week_start = ?`
)
const upsertMarketArchive = db.prepare(
`INSERT INTO market_archives (day, html, created_at, updated_at)
VALUES (@day, @html, datetime('now'), datetime('now'))
ON CONFLICT(day) DO UPDATE SET
html = excluded.html,
updated_at = datetime('now')`
)
const getMarketArchive = db.prepare(
`SELECT * FROM market_archives WHERE day = ?`
)
module.exports = {
db,
insertPost,
updateRetweeted,
queryPostsAny,
queryPostsAll,
insertKeyword,
insertTrackedUser,
getSettings
, upsertArchive
, getArchive
, insertSettingHistory
, upsertMarketArchive
, getMarketArchive
}