/** * 数据库连接和操作类 * 提供 PostgreSQL 数据库的连接池和基础操作方法 */ const { Pool } = require('pg'); const winston = require('winston'); require('dotenv').config(); class DatabaseManager { constructor() { this.pool = null; this.logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'logs/database.log' }) ] }); this.initializePool(); } /** * 初始化数据库连接池 */ initializePool() { try { this.pool = new Pool({ host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 5432, database: process.env.DB_NAME || 'push_messages', user: process.env.DB_USER || 'push_service', password: process.env.DB_PASSWORD, ssl: process.env.DB_SSL === 'true', max: parseInt(process.env.DB_MAX_CONNECTIONS) || 20, idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT) || 30000, connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT) || 5000, }); // 监听连接池事件 this.pool.on('connect', (client) => { this.logger.info('数据库连接已建立', { totalCount: this.pool.totalCount, idleCount: this.pool.idleCount, waitingCount: this.pool.waitingCount }); }); this.pool.on('error', (err, client) => { this.logger.error('数据库连接池错误', { error: err.message, stack: err.stack }); }); this.logger.info('数据库连接池初始化完成'); } catch (error) { this.logger.error('数据库连接池初始化失败', { error: error.message, stack: error.stack }); throw error; } } /** * 测试数据库连接 */ async testConnection() { try { const client = await this.pool.connect(); const result = await client.query('SELECT NOW() as current_time, version() as version'); client.release(); this.logger.info('数据库连接测试成功', { currentTime: result.rows[0].current_time, version: result.rows[0].version.split(' ')[0] }); return { success: true, currentTime: result.rows[0].current_time, version: result.rows[0].version }; } catch (error) { this.logger.error('数据库连接测试失败', { error: error.message, stack: error.stack }); throw error; } } /** * 插入推送消息 */ async insertPushMessage(messageData) { const startTime = Date.now(); const client = await this.pool.connect(); try { await client.query('BEGIN'); // 生成消息校验和用于去重 const checksum = this.generateChecksum(messageData); // 检查是否存在重复消息 const duplicateCheck = await client.query( 'SELECT id FROM push_messages WHERE checksum = $1 AND is_deleted = FALSE LIMIT 1', [checksum] ); let messageId; let isDuplicate = false; if (duplicateCheck.rows.length > 0) { // 处理重复消息 isDuplicate = true; const originalMessageId = duplicateCheck.rows[0].id; const insertQuery = ` INSERT INTO push_messages ( message_id, push_type, user_id, device_id, source_ip, user_agent, raw_data, parsed_data, priority, category, tags, checksum, is_duplicate, original_message_id, latitude, longitude, location_accuracy, location_timestamp ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ) RETURNING id `; const result = await client.query(insertQuery, [ messageData.message_id || null, messageData.push_type || messageData.pushType, messageData.user_id || messageData.userId, messageData.device_id || messageData.deviceId, messageData.source_ip, messageData.user_agent, JSON.stringify(messageData.raw_data || messageData), JSON.stringify(messageData.parsed_data || messageData), messageData.priority || 5, messageData.category, messageData.tags, checksum, true, originalMessageId, messageData.latitude || messageData.lat, messageData.longitude || messageData.lng, messageData.location_accuracy || messageData.accuracy, messageData.location_timestamp || null ]); messageId = result.rows[0].id; } else { // 插入新消息 const insertQuery = ` INSERT INTO push_messages ( message_id, push_type, user_id, device_id, source_ip, user_agent, raw_data, parsed_data, priority, category, tags, checksum, is_duplicate, latitude, longitude, location_accuracy, location_timestamp ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 ) RETURNING id `; const result = await client.query(insertQuery, [ messageData.message_id || null, messageData.push_type || messageData.pushType, messageData.user_id || messageData.userId, messageData.device_id || messageData.deviceId, messageData.source_ip, messageData.user_agent, JSON.stringify(messageData.raw_data || messageData), JSON.stringify(messageData.parsed_data || messageData), messageData.priority || 5, messageData.category, messageData.tags, checksum, false, messageData.latitude || messageData.lat, messageData.longitude || messageData.lng, messageData.location_accuracy || messageData.accuracy, messageData.location_timestamp || null ]); messageId = result.rows[0].id; } // 记录处理日志 await client.query( `INSERT INTO message_processing_logs (message_id, processing_step, status, started_at, completed_at, duration_ms, details) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [ messageId, 'message_received', 'completed', new Date(startTime), new Date(), Date.now() - startTime, JSON.stringify({ isDuplicate, checksum }) ] ); // 更新设备信息 if (messageData.device_id || messageData.deviceId) { await this.upsertDevice(client, { device_id: messageData.device_id || messageData.deviceId, device_name: messageData.device_name, device_type: messageData.device_type, user_id: messageData.user_id || messageData.userId, metadata: messageData.device_metadata }); } // 更新用户信息 if (messageData.user_id || messageData.userId) { await this.upsertUser(client, { user_id: messageData.user_id || messageData.userId, user_name: messageData.user_name, user_type: messageData.user_type }); } await client.query('COMMIT'); this.logger.info('推送消息插入成功', { messageId, pushType: messageData.push_type || messageData.pushType, userId: messageData.user_id || messageData.userId, isDuplicate, processingTime: Date.now() - startTime }); return { success: true, messageId, isDuplicate, processingTime: Date.now() - startTime }; } catch (error) { await client.query('ROLLBACK'); this.logger.error('推送消息插入失败', { error: error.message, stack: error.stack }); throw error; } finally { client.release(); } } /** * 批量插入推送消息 */ async insertPushMessagesBatch(messagesData) { const startTime = Date.now(); const client = await this.pool.connect(); try { await client.query('BEGIN'); const results = []; for (const messageData of messagesData) { try { const result = await this.insertPushMessage(messageData); results.push(result); } catch (error) { results.push({ success: false, error: error.message, messageData: messageData }); } } await client.query('COMMIT'); const successCount = results.filter(r => r.success).length; const failureCount = results.filter(r => !r.success).length; this.logger.info('批量推送消息插入完成', { totalCount: messagesData.length, successCount, failureCount, processingTime: Date.now() - startTime }); return { success: true, totalCount: messagesData.length, successCount, failureCount, results, processingTime: Date.now() - startTime }; } catch (error) { await client.query('ROLLBACK'); this.logger.error('批量推送消息插入失败', { error: error.message, stack: error.stack }); throw error; } finally { client.release(); } } /** * 更新或插入设备信息 */ async upsertDevice(client, deviceData) { const query = ` INSERT INTO devices (device_id, device_name, device_type, user_id, last_seen_at, metadata) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, $5) ON CONFLICT (device_id) DO UPDATE SET device_name = COALESCE(EXCLUDED.device_name, devices.device_name), device_type = COALESCE(EXCLUDED.device_type, devices.device_type), user_id = COALESCE(EXCLUDED.user_id, devices.user_id), last_seen_at = CURRENT_TIMESTAMP, metadata = COALESCE(EXCLUDED.metadata, devices.metadata), updated_at = CURRENT_TIMESTAMP `; await client.query(query, [ deviceData.device_id, deviceData.device_name, deviceData.device_type, deviceData.user_id, JSON.stringify(deviceData.metadata || {}) ]); } /** * 更新或插入用户信息 */ async upsertUser(client, userData) { const query = ` INSERT INTO users (user_id, user_name, user_type) VALUES ($1, $2, $3) ON CONFLICT (user_id) DO UPDATE SET user_name = COALESCE(EXCLUDED.user_name, users.user_name), user_type = COALESCE(EXCLUDED.user_type, users.user_type), updated_at = CURRENT_TIMESTAMP `; await client.query(query, [ userData.user_id, userData.user_name, userData.user_type ]); } /** * 生成消息校验和 */ generateChecksum(data) { const crypto = require('crypto'); const normalizedData = JSON.stringify(data, Object.keys(data).sort()); return crypto.createHash('sha256').update(normalizedData).digest('hex'); } /** * 获取消息统计信息 */ async getMessageStats(hoursBack = 24) { try { const query = 'SELECT * FROM get_message_stats($1)'; const result = await this.pool.query(query, [hoursBack]); return result.rows; } catch (error) { this.logger.error('获取消息统计失败', { error: error.message, stack: error.stack }); throw error; } } /** * 获取系统健康状态 */ async getHealthStatus() { try { const poolStats = { totalCount: this.pool.totalCount, idleCount: this.pool.idleCount, waitingCount: this.pool.waitingCount }; const dbStats = await this.pool.query(` SELECT COUNT(*) as total_messages, COUNT(*) FILTER (WHERE received_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour') as messages_last_hour, COUNT(*) FILTER (WHERE processing_status = 'pending') as pending_messages, COUNT(*) FILTER (WHERE processing_status = 'failed') as failed_messages FROM push_messages WHERE is_deleted = FALSE `); return { database: { connected: true, pool: poolStats, stats: dbStats.rows[0] }, timestamp: new Date().toISOString() }; } catch (error) { this.logger.error('获取系统健康状态失败', { error: error.message, stack: error.stack }); return { database: { connected: false, error: error.message }, timestamp: new Date().toISOString() }; } } /** * 关闭数据库连接池 */ async close() { if (this.pool) { await this.pool.end(); this.logger.info('数据库连接池已关闭'); } } } module.exports = DatabaseManager;