Initial commit of akmon project
This commit is contained in:
427
push-receiver-service/lib/database.js
Normal file
427
push-receiver-service/lib/database.js
Normal file
@@ -0,0 +1,427 @@
|
||||
/**
|
||||
* 数据库连接和操作类
|
||||
* 提供 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;
|
||||
Reference in New Issue
Block a user