82 lines
3.1 KiB
JavaScript
82 lines
3.1 KiB
JavaScript
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// 本機資料庫(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();
|
|||
|
|
}
|