'use strict'; /** * ops 存储门面 * ---------------------------------------------------------------------------- * 读 config.storage 决定驱动(默认 SQLite),把驱动实例藏起来,对外只暴露业务接口。 * 关键约束:初始化失败不能拖垮主服务 —— 所有失败都收敛成 health() 的 ok:false, * 只有「店铺运营」相关接口会返回 503。 */ const path = require('path'); const ROOT_DIR = __dirname; function pickDriver(storage) { const raw = String((storage && storage.driver) || 'sqlite').toLowerCase(); return raw === 'postgres' || raw === 'postgresql' || raw === 'pg' ? 'postgres' : 'sqlite'; } function createOpsStore(config) { const storage = (config && config.storage) || {}; const driver = pickDriver(storage); let impl = null; let initPromise = null; let lastFailure = null; function build() { if (driver === 'postgres') { const pgCfg = storage.postgres || {}; let connectionString = process.env.OPS_DATABASE_URL || pgCfg.connectionString || ''; // 也支持「主机/库/用户/密码」分开填(设置页用这种) if (!String(connectionString).trim() && pgCfg.host && pgCfg.database) { const user = encodeURIComponent(pgCfg.user || 'postgres'); const pass = pgCfg.password ? ':' + encodeURIComponent(pgCfg.password) : ''; const port = pgCfg.port || 5432; connectionString = `postgres://${user}${pass}@${pgCfg.host}:${port}/${pgCfg.database}`; if (pgCfg.ssl) connectionString += (connectionString.includes('?') ? '&' : '?') + 'sslmode=require'; } if (!String(connectionString).trim()) { const err = new Error('未配置 PostgreSQL 连接信息'); err.reason = 'storage-not-configured'; throw err; } const { createPgStore } = require('./server/stores/ops-pg.js'); return createPgStore({ ...pgCfg, connectionString }); } const sqliteCfg = storage.sqlite || {}; const file = sqliteCfg.file ? path.resolve(ROOT_DIR, String(sqliteCfg.file)) : path.join(ROOT_DIR, 'data', 'ops.db'); const { createSqliteStore } = require('./server/stores/ops-sqlite.js'); return createSqliteStore({ file }); } function get() { if (impl) return impl; try { impl = build(); lastFailure = null; return impl; } catch (e) { lastFailure = e; throw e; } } function reset() { impl = null; initPromise = null; } async function init() { if (!initPromise) { initPromise = Promise.resolve() .then(() => get()) .then((store) => store.init()) .then(() => get()) .catch((e) => { lastFailure = e; initPromise = null; throw e; }); } return initPromise; } /** never throws:{ ok, kind, reason?, detail? } */ async function health() { try { await init(); const result = await get().health(); return { ...result, kind: driver }; } catch (e) { return { ok: false, kind: driver, reason: e.reason || (e.code === 'MODULE_NOT_FOUND' ? 'driver-missing' : 'storage-unreachable'), detail: e.message || String(e), stack: undefined, }; } } /** 包装:统一先 ensure init,再调用驱动方法 */ function call(name) { return async (...args) => { await init(); return get()[name](...args); }; } return { kind: driver, init, health, reset, close: async () => { if (impl && typeof impl.close === 'function') { try { await impl.close(); } catch { /* ignore */ } } reset(); }, withTransaction: async (fn) => { await init(); return get().withTransaction(fn); }, findUploadBySha: call('findUploadBySha'), listUploads: call('listUploads'), getUpload: call('getUpload'), getContent: call('getContent'), queryRows: call('queryRows'), queryPlatformRows: call('queryPlatformRows'), insertReport: call('insertReport'), getReportCache: call('getReportCache'), saveReportCache: call('saveReportCache'), listReports: call('listReports'), getReport: call('getReport'), deleteReport: call('deleteReport'), summary: call('summary'), get failure() { return lastFailure; }, }; } module.exports = { createOpsStore, pickDriver };