/** * 产品资料系统数据库连接模块(PostgreSQL) * * 数据来源:系统库 database_configs 表中 name = 'product_information_database' 的配置。 * 典型配置:postgres / 218.93.9.38:25432 / furniture3d_prod / external_full_reader / ssl=1 * * 该库用于 MKH-BOM 资料页面的商品资料匹配: * external.intake_sku_items_ready_v1 * —— 真实可用的 SKU 明细视图(约 4.6 万行) * —— source_sku_id 为聚水潭原始 SKU 编码(即业务库 BOM 的配件编码) * —— sku_code 为商品编码,barcode 为条码 * external.warehouse_locations_v1 —— 库位主数据(当前仅 1 行:A-01 成品区) * * 说明:external.product_bom_v1 / product_skus_v1 当前为空视图,不可用。 */ const { Pool } = require('pg'); const database = require('./database'); const CONFIG_NAME = 'product_information_database'; const READY_VIEW = 'external.intake_sku_items_ready_v1'; const LOCATION_VIEW = 'external.warehouse_locations_v1'; const parsePositiveInt = (value, fallback) => { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; }; const POOL_MAX = parsePositiveInt(process.env.PRODUCT_DB_POOL_MAX, 4); let pool = null; let cachedSignature = ''; const truthy = (value) => ( value === true || value === 1 || String(value || '').toLowerCase() === 'true' || String(value || '') === '1' ); async function loadStoredConfig() { const rows = await database.local.query( `SELECT id, host, port, database_name, username, password, \`ssl\`, driver, status FROM database_configs WHERE name = ? LIMIT 1`, [CONFIG_NAME] ); const config = rows[0]; if (!config) { const error = new Error('产品资料系统数据库尚未配置,请在「系统设置 - 数据库配置」中完成配置'); error.statusCode = 503; error.code = 'PRODUCT_DB_NOT_CONFIGURED'; throw error; } if (String(config.driver || '').toLowerCase() !== 'postgres') { const error = new Error('产品资料系统数据库需要选择 PostgreSQL 类型'); error.statusCode = 503; error.code = 'PRODUCT_DB_DRIVER_MISMATCH'; throw error; } return { id: config.id, host: String(config.host || '').trim(), port: Number(config.port) || 5432, database: String(config.database_name || '').trim(), user: String(config.username || '').trim(), password: config.password || '', ssl: truthy(config.ssl), }; } const buildSignature = (config) => [ config.id, config.host, config.port, config.database, config.user, config.ssl ? 'ssl' : 'nossl', ].join('::'); async function getPool() { const config = await loadStoredConfig(); const signature = buildSignature(config); if (pool && signature === cachedSignature) { return pool; } if (pool) { await pool.end().catch(() => {}); pool = null; } pool = new Pool({ host: config.host, port: config.port, database: config.database, user: config.user, password: config.password, ssl: config.ssl ? { rejectUnauthorized: false } : false, max: POOL_MAX, idleTimeoutMillis: 30000, connectionTimeoutMillis: 20000, query_timeout: 180000, statement_timeout: 180000, }); pool.on('error', (error) => { console.warn('[ProductDB] 连接池异常:', error.message); }); cachedSignature = signature; return pool; } /** * 执行查询,返回行数组。 * @param {string} sqlText PostgreSQL 语句(使用 $1/$2 占位) * @param {Array} params 参数数组 */ async function query(sqlText, params = []) { const maxAttempts = 2; let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { const activePool = await getPool(); const result = await activePool.query(sqlText, params); return result.rows; } catch (error) { lastError = error; if (error.code === 'PRODUCT_DB_NOT_CONFIGURED' || error.code === 'PRODUCT_DB_DRIVER_MISMATCH') { throw error; } if (attempt < maxAttempts) { console.warn(`[ProductDB] 查询失败,准备重连后重试 (${attempt}/${maxAttempts}): ${error.message}`); await reset(); } } } throw lastError; } async function reset() { if (pool) { await pool.end().catch(() => {}); } pool = null; cachedSignature = ''; } async function test() { try { const rows = await query('SELECT 1 AS test'); return { success: true, message: '产品资料系统数据库连接成功', data: rows }; } catch (error) { return { success: false, message: `连接失败: ${error.message}` }; } } module.exports = { query, test, reset, getPool, CONFIG_NAME, READY_VIEW, LOCATION_VIEW, };