279 lines
9.2 KiB
JavaScript
279 lines
9.2 KiB
JavaScript
/**
|
||
* SQLite 封装:使用 sql.js(纯 JS/WebAssembly,无需 node-gyp)
|
||
* 对外接口与 better-sqlite3 兼容:db.prepare().get/all/run、db.exec
|
||
*/
|
||
const path = require('path')
|
||
const fs = require('fs')
|
||
|
||
const dbPath = process.env.DB_PATH || path.join(__dirname, 'data.db')
|
||
let _db = null
|
||
|
||
function getDb() {
|
||
if (!_db) throw new Error('DB not initialized. Call initDb() first.')
|
||
return _db
|
||
}
|
||
|
||
function wrapDatabase(nativeDb, persist) {
|
||
return {
|
||
prepare(sql) {
|
||
return {
|
||
get(...args) {
|
||
const stmt = nativeDb.prepare(sql)
|
||
stmt.bind(args.length ? args : null)
|
||
const row = stmt.step() ? stmt.getAsObject() : undefined
|
||
stmt.free()
|
||
return row
|
||
},
|
||
all(...args) {
|
||
const stmt = nativeDb.prepare(sql)
|
||
stmt.bind(args.length ? args : null)
|
||
const rows = []
|
||
while (stmt.step()) rows.push(stmt.getAsObject())
|
||
stmt.free()
|
||
return rows
|
||
},
|
||
run(...args) {
|
||
const stmt = nativeDb.prepare(sql)
|
||
stmt.bind(args.length ? args : null)
|
||
while (stmt.step());
|
||
stmt.free()
|
||
persist()
|
||
},
|
||
}
|
||
},
|
||
exec(sql) {
|
||
const statements = sql.split(';').map((s) => s.trim()).filter(Boolean)
|
||
statements.forEach((s) => nativeDb.run(s))
|
||
persist()
|
||
},
|
||
pragma(str) {
|
||
nativeDb.run('PRAGMA ' + str)
|
||
},
|
||
}
|
||
}
|
||
|
||
function runMigrations(db) {
|
||
const exec = (sql) => db.exec(sql)
|
||
const prepare = (sql) => db.prepare(sql)
|
||
|
||
exec(`
|
||
CREATE TABLE IF NOT EXISTS situation (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
data TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS force_summary (
|
||
side TEXT PRIMARY KEY CHECK (side IN ('us', 'iran')),
|
||
total_assets INTEGER NOT NULL,
|
||
personnel INTEGER NOT NULL,
|
||
naval_ships INTEGER NOT NULL,
|
||
aircraft INTEGER NOT NULL,
|
||
ground_units INTEGER NOT NULL,
|
||
uav INTEGER NOT NULL,
|
||
missile_consumed INTEGER NOT NULL,
|
||
missile_stock INTEGER NOT NULL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS power_index (
|
||
side TEXT PRIMARY KEY CHECK (side IN ('us', 'iran')),
|
||
overall INTEGER NOT NULL,
|
||
military_strength INTEGER NOT NULL,
|
||
economic_power INTEGER NOT NULL,
|
||
geopolitical_influence INTEGER NOT NULL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS force_asset (
|
||
id TEXT PRIMARY KEY,
|
||
side TEXT NOT NULL CHECK (side IN ('us', 'iran')),
|
||
name TEXT NOT NULL,
|
||
type TEXT NOT NULL,
|
||
count INTEGER NOT NULL,
|
||
status TEXT NOT NULL CHECK (status IN ('active', 'standby', 'alert')),
|
||
lat REAL,
|
||
lng REAL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS key_location (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
side TEXT NOT NULL CHECK (side IN ('us', 'iran')),
|
||
name TEXT NOT NULL,
|
||
lat REAL NOT NULL,
|
||
lng REAL NOT NULL,
|
||
type TEXT,
|
||
region TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS combat_losses (
|
||
side TEXT PRIMARY KEY CHECK (side IN ('us', 'iran')),
|
||
bases_destroyed INTEGER NOT NULL,
|
||
bases_damaged INTEGER NOT NULL,
|
||
personnel_killed INTEGER NOT NULL,
|
||
personnel_wounded INTEGER NOT NULL,
|
||
aircraft INTEGER NOT NULL,
|
||
warships INTEGER NOT NULL,
|
||
armor INTEGER NOT NULL,
|
||
vehicles INTEGER NOT NULL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS wall_street_trend (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
time TEXT NOT NULL,
|
||
value INTEGER NOT NULL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS retaliation_current (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
value INTEGER NOT NULL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS retaliation_history (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
time TEXT NOT NULL,
|
||
value INTEGER NOT NULL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS situation_update (
|
||
id TEXT PRIMARY KEY,
|
||
timestamp TEXT NOT NULL,
|
||
category TEXT NOT NULL,
|
||
summary TEXT NOT NULL,
|
||
severity TEXT NOT NULL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS gdelt_events (
|
||
event_id TEXT PRIMARY KEY,
|
||
event_time TEXT NOT NULL,
|
||
title TEXT NOT NULL,
|
||
lat REAL NOT NULL,
|
||
lng REAL NOT NULL,
|
||
impact_score INTEGER NOT NULL,
|
||
url TEXT,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS conflict_stats (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
total_events INTEGER NOT NULL DEFAULT 0,
|
||
high_impact_events INTEGER NOT NULL DEFAULT 0,
|
||
estimated_casualties INTEGER NOT NULL DEFAULT 0,
|
||
estimated_strike_count INTEGER NOT NULL DEFAULT 0,
|
||
updated_at TEXT NOT NULL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS news_content (
|
||
id TEXT PRIMARY KEY,
|
||
content_hash TEXT NOT NULL UNIQUE,
|
||
title TEXT NOT NULL,
|
||
summary TEXT NOT NULL,
|
||
url TEXT NOT NULL DEFAULT '',
|
||
source TEXT NOT NULL DEFAULT '',
|
||
published_at TEXT NOT NULL,
|
||
category TEXT NOT NULL DEFAULT 'other',
|
||
severity TEXT NOT NULL DEFAULT 'medium',
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
`)
|
||
try { exec('CREATE INDEX IF NOT EXISTS idx_news_content_hash ON news_content(content_hash)') } catch (_) {}
|
||
try { exec('CREATE INDEX IF NOT EXISTS idx_news_content_published ON news_content(published_at DESC)') } catch (_) {}
|
||
|
||
try {
|
||
const cols = prepare('PRAGMA table_info(key_location)').all()
|
||
const names = cols.map((c) => c.name)
|
||
if (!names.includes('type')) exec('ALTER TABLE key_location ADD COLUMN type TEXT')
|
||
if (!names.includes('region')) exec('ALTER TABLE key_location ADD COLUMN region TEXT')
|
||
if (!names.includes('status')) exec('ALTER TABLE key_location ADD COLUMN status TEXT DEFAULT "operational"')
|
||
if (!names.includes('damage_level')) exec('ALTER TABLE key_location ADD COLUMN damage_level INTEGER')
|
||
} catch (_) {}
|
||
try {
|
||
const lossCols = prepare('PRAGMA table_info(combat_losses)').all()
|
||
const lossNames = lossCols.map((c) => c.name)
|
||
if (!lossNames.includes('civilian_killed')) exec('ALTER TABLE combat_losses ADD COLUMN civilian_killed INTEGER NOT NULL DEFAULT 0')
|
||
if (!lossNames.includes('civilian_wounded')) exec('ALTER TABLE combat_losses ADD COLUMN civilian_wounded INTEGER NOT NULL DEFAULT 0')
|
||
if (!lossNames.includes('updated_at')) exec('ALTER TABLE combat_losses ADD COLUMN updated_at TEXT DEFAULT (datetime("now"))')
|
||
if (!lossNames.includes('drones')) exec('ALTER TABLE combat_losses ADD COLUMN drones INTEGER NOT NULL DEFAULT 0')
|
||
if (!lossNames.includes('missiles')) exec('ALTER TABLE combat_losses ADD COLUMN missiles INTEGER NOT NULL DEFAULT 0')
|
||
if (!lossNames.includes('helicopters')) exec('ALTER TABLE combat_losses ADD COLUMN helicopters INTEGER NOT NULL DEFAULT 0')
|
||
if (!lossNames.includes('submarines')) exec('ALTER TABLE combat_losses ADD COLUMN submarines INTEGER NOT NULL DEFAULT 0')
|
||
if (!lossNames.includes('tanks')) exec('ALTER TABLE combat_losses ADD COLUMN tanks INTEGER NOT NULL DEFAULT 0')
|
||
if (!lossNames.includes('carriers')) {
|
||
exec('ALTER TABLE combat_losses ADD COLUMN carriers INTEGER NOT NULL DEFAULT 0')
|
||
exec('UPDATE combat_losses SET carriers = tanks')
|
||
}
|
||
if (!lossNames.includes('civilian_ships')) exec('ALTER TABLE combat_losses ADD COLUMN civilian_ships INTEGER NOT NULL DEFAULT 0')
|
||
if (!lossNames.includes('airport_port')) exec('ALTER TABLE combat_losses ADD COLUMN airport_port INTEGER NOT NULL DEFAULT 0')
|
||
} catch (_) {}
|
||
|
||
const addUpdatedAt = (table) => {
|
||
try {
|
||
const cols = prepare(`PRAGMA table_info(${table})`).all()
|
||
if (!cols.some((c) => c.name === 'updated_at')) {
|
||
exec(`ALTER TABLE ${table} ADD COLUMN updated_at TEXT DEFAULT (datetime("now"))`)
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
;['force_summary', 'power_index', 'force_asset', 'key_location', 'retaliation_current'].forEach(addUpdatedAt)
|
||
|
||
try {
|
||
exec(`
|
||
CREATE TABLE IF NOT EXISTS visits (
|
||
ip TEXT PRIMARY KEY,
|
||
last_seen TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS visitor_count (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
total INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
INSERT OR IGNORE INTO visitor_count (id, total) VALUES (1, 0);
|
||
`)
|
||
} catch (_) {}
|
||
try {
|
||
exec(`
|
||
CREATE TABLE IF NOT EXISTS feedback (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
content TEXT NOT NULL,
|
||
ip TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
)
|
||
`)
|
||
} catch (_) {}
|
||
try {
|
||
exec(`
|
||
CREATE TABLE IF NOT EXISTS share_count (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
total INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
INSERT OR IGNORE INTO share_count (id, total) VALUES (1, 0);
|
||
`)
|
||
} catch (_) {}
|
||
}
|
||
|
||
async function initDb() {
|
||
const initSqlJs = require('sql.js')
|
||
const SQL = await initSqlJs()
|
||
let data = new Uint8Array(0)
|
||
if (fs.existsSync(dbPath)) {
|
||
data = new Uint8Array(fs.readFileSync(dbPath))
|
||
}
|
||
const nativeDb = new SQL.Database(data)
|
||
|
||
function persist() {
|
||
try {
|
||
const buf = nativeDb.export()
|
||
fs.writeFileSync(dbPath, Buffer.from(buf))
|
||
} catch (e) {
|
||
console.error('[db] persist error:', e.message)
|
||
}
|
||
}
|
||
|
||
nativeDb.run('PRAGMA journal_mode = WAL')
|
||
const wrapped = wrapDatabase(nativeDb, persist)
|
||
runMigrations(wrapped)
|
||
_db = wrapped
|
||
return _db
|
||
}
|
||
|
||
const proxy = {
|
||
prepare(sql) {
|
||
return getDb().prepare(sql)
|
||
},
|
||
exec(sql) {
|
||
return getDb().exec(sql)
|
||
},
|
||
pragma(str) {
|
||
getDb().pragma(str)
|
||
},
|
||
}
|
||
|
||
module.exports = proxy
|
||
module.exports.initDb = initDb
|
||
module.exports.getDb = getDb
|