finance-dashboard/lib/db.js

82 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ═══════════════════════════════════════════════════════════
// 本機資料庫SQLite使用 Node 內建的 node:sqlite免安裝套件
// 存三種東西:
// 1. cache — 整包 /api/macro 結果,重啟伺服器可即時載入
// 2. series — 每個指標的完整歷史序列,供「走勢大圖」使用
// 3. score_history — 每天記一筆健康分數,累積成「分數走勢」
// 資料庫檔data.db已在 .gitignore 忽略)
// ═══════════════════════════════════════════════════════════
import { DatabaseSync } from 'node:sqlite';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DB_PATH = path.join(__dirname, '..', 'data.db');
const db = new DatabaseSync(DB_PATH);
db.exec(`
CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
payload TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS series (
series_key TEXT NOT NULL,
date TEXT NOT NULL,
val REAL NOT NULL,
PRIMARY KEY (series_key, date)
);
CREATE TABLE IF NOT EXISTS score_history (
date TEXT PRIMARY KEY,
score INTEGER NOT NULL,
regime TEXT
);
`);
// ─── 整包結果的持久化快取 ───
export function savePayload(payload) {
db.prepare('INSERT OR REPLACE INTO cache (key, payload, updated_at) VALUES (?, ?, ?)')
.run('macro', JSON.stringify(payload), Date.now());
}
export function loadPayload() {
const row = db.prepare('SELECT payload, updated_at FROM cache WHERE key = ?').get('macro');
if (!row) return null;
try {
return { payload: JSON.parse(row.payload), updatedAt: row.updated_at };
} catch {
return null;
}
}
// ─── 指標歷史序列 ───
const insertPoint = db.prepare('INSERT OR REPLACE INTO series (series_key, date, val) VALUES (?, ?, ?)');
export function saveSeries(key, points) {
if (!points || points.length === 0) return;
db.exec('BEGIN');
try {
for (const p of points) insertPoint.run(key, p.date, p.val);
db.exec('COMMIT');
} catch (e) {
db.exec('ROLLBACK');
throw e;
}
}
export function getSeries(key, sinceISO) {
if (sinceISO) {
return db.prepare('SELECT date, val FROM series WHERE series_key = ? AND date >= ? ORDER BY date ASC')
.all(key, sinceISO);
}
return db.prepare('SELECT date, val FROM series WHERE series_key = ? ORDER BY date ASC').all(key);
}
// ─── 每日健康分數快照(一天一筆,最新覆蓋)───
export function saveScoreSnapshot(score, regimeLabel) {
const today = new Date().toISOString().slice(0, 10);
db.prepare('INSERT OR REPLACE INTO score_history (date, score, regime) VALUES (?, ?, ?)')
.run(today, score, regimeLabel || null);
}
export function getScoreHistory() {
return db.prepare('SELECT date, score, regime FROM score_history ORDER BY date ASC').all();
}