fix: 更改数据库包
This commit is contained in:
224
server/db.js
224
server/db.js
@@ -1,20 +1,67 @@
|
||||
const Database = require('better-sqlite3')
|
||||
/**
|
||||
* 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')
|
||||
const db = new Database(dbPath)
|
||||
let _db = null
|
||||
|
||||
// 启用外键
|
||||
db.pragma('journal_mode = WAL')
|
||||
function getDb() {
|
||||
if (!_db) throw new Error('DB not initialized. Call initDb() first.')
|
||||
return _db
|
||||
}
|
||||
|
||||
// 建表
|
||||
db.exec(`
|
||||
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,
|
||||
@@ -26,7 +73,6 @@ db.exec(`
|
||||
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,
|
||||
@@ -34,7 +80,6 @@ db.exec(`
|
||||
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')),
|
||||
@@ -45,7 +90,6 @@ db.exec(`
|
||||
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')),
|
||||
@@ -55,7 +99,6 @@ db.exec(`
|
||||
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,
|
||||
@@ -67,24 +110,20 @@ db.exec(`
|
||||
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,
|
||||
@@ -92,7 +131,6 @@ db.exec(`
|
||||
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,
|
||||
@@ -103,7 +141,6 @@ db.exec(`
|
||||
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,
|
||||
@@ -112,7 +149,6 @@ db.exec(`
|
||||
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,
|
||||
@@ -125,57 +161,49 @@ db.exec(`
|
||||
severity TEXT NOT NULL DEFAULT 'medium',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_news_content_hash ON news_content(content_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_news_content_published ON news_content(published_at DESC);
|
||||
`)
|
||||
`)
|
||||
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 (_) {}
|
||||
|
||||
// 迁移:为已有 key_location 表添加 type、region、status、damage_level 列
|
||||
try {
|
||||
const cols = db.prepare('PRAGMA table_info(key_location)').all()
|
||||
const names = cols.map((c) => c.name)
|
||||
if (!names.includes('type')) db.exec('ALTER TABLE key_location ADD COLUMN type TEXT')
|
||||
if (!names.includes('region')) db.exec('ALTER TABLE key_location ADD COLUMN region TEXT')
|
||||
if (!names.includes('status')) db.exec('ALTER TABLE key_location ADD COLUMN status TEXT DEFAULT "operational"')
|
||||
if (!names.includes('damage_level')) db.exec('ALTER TABLE key_location ADD COLUMN damage_level INTEGER')
|
||||
} catch (_) {}
|
||||
// 迁移:combat_losses 添加平民伤亡、updated_at
|
||||
try {
|
||||
const lossCols = db.prepare('PRAGMA table_info(combat_losses)').all()
|
||||
const lossNames = lossCols.map((c) => c.name)
|
||||
if (!lossNames.includes('civilian_killed')) db.exec('ALTER TABLE combat_losses ADD COLUMN civilian_killed INTEGER NOT NULL DEFAULT 0')
|
||||
if (!lossNames.includes('civilian_wounded')) db.exec('ALTER TABLE combat_losses ADD COLUMN civilian_wounded INTEGER NOT NULL DEFAULT 0')
|
||||
if (!lossNames.includes('updated_at')) db.exec('ALTER TABLE combat_losses ADD COLUMN updated_at TEXT DEFAULT (datetime("now"))')
|
||||
if (!lossNames.includes('drones')) db.exec('ALTER TABLE combat_losses ADD COLUMN drones INTEGER NOT NULL DEFAULT 0')
|
||||
if (!lossNames.includes('missiles')) db.exec('ALTER TABLE combat_losses ADD COLUMN missiles INTEGER NOT NULL DEFAULT 0')
|
||||
if (!lossNames.includes('helicopters')) db.exec('ALTER TABLE combat_losses ADD COLUMN helicopters INTEGER NOT NULL DEFAULT 0')
|
||||
if (!lossNames.includes('submarines')) db.exec('ALTER TABLE combat_losses ADD COLUMN submarines INTEGER NOT NULL DEFAULT 0')
|
||||
if (!lossNames.includes('tanks')) db.exec('ALTER TABLE combat_losses ADD COLUMN tanks INTEGER NOT NULL DEFAULT 0')
|
||||
if (!lossNames.includes('carriers')) {
|
||||
db.exec('ALTER TABLE combat_losses ADD COLUMN carriers INTEGER NOT NULL DEFAULT 0')
|
||||
db.exec('UPDATE combat_losses SET carriers = tanks')
|
||||
}
|
||||
if (!lossNames.includes('civilian_ships')) db.exec('ALTER TABLE combat_losses ADD COLUMN civilian_ships INTEGER NOT NULL DEFAULT 0')
|
||||
if (!lossNames.includes('airport_port')) db.exec('ALTER TABLE combat_losses ADD COLUMN airport_port INTEGER NOT NULL DEFAULT 0')
|
||||
} catch (_) {}
|
||||
|
||||
// 迁移:所有表添加 updated_at 用于数据回放
|
||||
const addUpdatedAt = (table) => {
|
||||
try {
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all()
|
||||
if (!cols.some((c) => c.name === 'updated_at')) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN updated_at TEXT DEFAULT (datetime("now"))`)
|
||||
}
|
||||
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 (_) {}
|
||||
}
|
||||
addUpdatedAt('force_summary')
|
||||
addUpdatedAt('power_index')
|
||||
addUpdatedAt('force_asset')
|
||||
addUpdatedAt('key_location')
|
||||
addUpdatedAt('retaliation_current')
|
||||
|
||||
// 来访统计:visits 用于在看(近期活跃 IP),visitor_count 用于累积人次(每次接入 +1)
|
||||
try {
|
||||
db.exec(`
|
||||
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'))
|
||||
@@ -185,30 +213,66 @@ try {
|
||||
total INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
INSERT OR IGNORE INTO visitor_count (id, total) VALUES (1, 0);
|
||||
`)
|
||||
} catch (_) {}
|
||||
|
||||
// 后台留言:供开发者收集用户反馈
|
||||
try {
|
||||
db.exec(`
|
||||
`)
|
||||
} 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 {
|
||||
db.exec(`
|
||||
`)
|
||||
} 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 (_) {}
|
||||
`)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
module.exports = db
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user