ops-store.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. 'use strict';
  2. /**
  3. * ops 存储门面
  4. * ----------------------------------------------------------------------------
  5. * 读 config.storage 决定驱动(默认 SQLite),把驱动实例藏起来,对外只暴露业务接口。
  6. * 关键约束:初始化失败不能拖垮主服务 —— 所有失败都收敛成 health() 的 ok:false,
  7. * 只有「店铺运营」相关接口会返回 503。
  8. */
  9. const path = require('path');
  10. const ROOT_DIR = __dirname;
  11. function pickDriver(storage) {
  12. const raw = String((storage && storage.driver) || 'sqlite').toLowerCase();
  13. return raw === 'postgres' || raw === 'postgresql' || raw === 'pg' ? 'postgres' : 'sqlite';
  14. }
  15. function createOpsStore(config) {
  16. const storage = (config && config.storage) || {};
  17. const driver = pickDriver(storage);
  18. let impl = null;
  19. let initPromise = null;
  20. let lastFailure = null;
  21. function build() {
  22. if (driver === 'postgres') {
  23. const pgCfg = storage.postgres || {};
  24. let connectionString = process.env.OPS_DATABASE_URL || pgCfg.connectionString || '';
  25. // 也支持「主机/库/用户/密码」分开填(设置页用这种)
  26. if (!String(connectionString).trim() && pgCfg.host && pgCfg.database) {
  27. const user = encodeURIComponent(pgCfg.user || 'postgres');
  28. const pass = pgCfg.password ? ':' + encodeURIComponent(pgCfg.password) : '';
  29. const port = pgCfg.port || 5432;
  30. connectionString = `postgres://${user}${pass}@${pgCfg.host}:${port}/${pgCfg.database}`;
  31. if (pgCfg.ssl) connectionString += (connectionString.includes('?') ? '&' : '?') + 'sslmode=require';
  32. }
  33. if (!String(connectionString).trim()) {
  34. const err = new Error('未配置 PostgreSQL 连接信息');
  35. err.reason = 'storage-not-configured';
  36. throw err;
  37. }
  38. const { createPgStore } = require('./server/stores/ops-pg.js');
  39. return createPgStore({ ...pgCfg, connectionString });
  40. }
  41. const sqliteCfg = storage.sqlite || {};
  42. const file = sqliteCfg.file
  43. ? path.resolve(ROOT_DIR, String(sqliteCfg.file))
  44. : path.join(ROOT_DIR, 'data', 'ops.db');
  45. const { createSqliteStore } = require('./server/stores/ops-sqlite.js');
  46. return createSqliteStore({ file });
  47. }
  48. function get() {
  49. if (impl) return impl;
  50. try {
  51. impl = build();
  52. lastFailure = null;
  53. return impl;
  54. } catch (e) {
  55. lastFailure = e;
  56. throw e;
  57. }
  58. }
  59. function reset() {
  60. impl = null;
  61. initPromise = null;
  62. }
  63. async function init() {
  64. if (!initPromise) {
  65. initPromise = Promise.resolve()
  66. .then(() => get())
  67. .then((store) => store.init())
  68. .then(() => get())
  69. .catch((e) => {
  70. lastFailure = e;
  71. initPromise = null;
  72. throw e;
  73. });
  74. }
  75. return initPromise;
  76. }
  77. /** never throws:{ ok, kind, reason?, detail? } */
  78. async function health() {
  79. try {
  80. await init();
  81. const result = await get().health();
  82. return { ...result, kind: driver };
  83. } catch (e) {
  84. return {
  85. ok: false,
  86. kind: driver,
  87. reason: e.reason || (e.code === 'MODULE_NOT_FOUND' ? 'driver-missing' : 'storage-unreachable'),
  88. detail: e.message || String(e),
  89. stack: undefined,
  90. };
  91. }
  92. }
  93. /** 包装:统一先 ensure init,再调用驱动方法 */
  94. function call(name) {
  95. return async (...args) => {
  96. await init();
  97. return get()[name](...args);
  98. };
  99. }
  100. return {
  101. kind: driver,
  102. init,
  103. health,
  104. reset,
  105. close: async () => {
  106. if (impl && typeof impl.close === 'function') {
  107. try { await impl.close(); } catch { /* ignore */ }
  108. }
  109. reset();
  110. },
  111. withTransaction: async (fn) => {
  112. await init();
  113. return get().withTransaction(fn);
  114. },
  115. findUploadBySha: call('findUploadBySha'),
  116. listUploads: call('listUploads'),
  117. getUpload: call('getUpload'),
  118. getContent: call('getContent'),
  119. queryRows: call('queryRows'),
  120. queryPlatformRows: call('queryPlatformRows'),
  121. insertReport: call('insertReport'),
  122. getReportCache: call('getReportCache'),
  123. saveReportCache: call('saveReportCache'),
  124. listReports: call('listReports'),
  125. getReport: call('getReport'),
  126. deleteReport: call('deleteReport'),
  127. summary: call('summary'),
  128. get failure() {
  129. return lastFailure;
  130. },
  131. };
  132. }
  133. module.exports = { createOpsStore, pickDriver };