// ═══════════════════════════════════════════════════════════ // 本機資料庫(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(); }