完成 APP 建立流程和使用分析、增加主機備機的備援機制、管理者後臺增加資料庫監控
This commit is contained in:
424
lib/database-failover.js
Normal file
424
lib/database-failover.js
Normal file
@@ -0,0 +1,424 @@
|
||||
"use strict";
|
||||
// =====================================================
|
||||
// 資料庫備援連線服務
|
||||
// =====================================================
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.dbFailover = exports.DatabaseFailover = void 0;
|
||||
const promise_1 = __importDefault(require("mysql2/promise"));
|
||||
// 主機資料庫配置
|
||||
const masterConfig = {
|
||||
host: process.env.DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.DB_PORT || '33306'),
|
||||
user: process.env.DB_USER || 'AI_Platform',
|
||||
password: process.env.DB_PASSWORD || 'Aa123456',
|
||||
database: process.env.DB_NAME || 'db_AI_Platform',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
idleTimeout: 300000,
|
||||
ssl: false,
|
||||
};
|
||||
// 備機資料庫配置
|
||||
const slaveConfig = {
|
||||
host: process.env.SLAVE_DB_HOST || '122.100.99.161',
|
||||
port: parseInt(process.env.SLAVE_DB_PORT || '43306'),
|
||||
user: process.env.SLAVE_DB_USER || 'A999',
|
||||
password: process.env.SLAVE_DB_PASSWORD || '1023',
|
||||
database: process.env.SLAVE_DB_NAME || 'db_AI_Platform', // 修正為 AI 平台資料庫
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
idleTimeout: 300000,
|
||||
ssl: false,
|
||||
};
|
||||
// 備援資料庫管理類
|
||||
class DatabaseFailover {
|
||||
constructor() {
|
||||
this.masterPool = null;
|
||||
this.slavePool = null;
|
||||
this.healthCheckInterval = null;
|
||||
this.status = {
|
||||
isEnabled: process.env.DB_FAILOVER_ENABLED === 'true',
|
||||
currentDatabase: 'master',
|
||||
masterHealthy: false,
|
||||
slaveHealthy: false,
|
||||
lastHealthCheck: 0,
|
||||
consecutiveFailures: 0,
|
||||
};
|
||||
// 同步初始化連接池
|
||||
this.initializePoolsSync();
|
||||
this.startHealthCheck();
|
||||
}
|
||||
static getInstance() {
|
||||
if (!DatabaseFailover.instance) {
|
||||
DatabaseFailover.instance = new DatabaseFailover();
|
||||
}
|
||||
return DatabaseFailover.instance;
|
||||
}
|
||||
// 同步初始化連接池
|
||||
initializePoolsSync() {
|
||||
try {
|
||||
console.log('🚀 開始同步初始化資料庫備援系統...');
|
||||
// 直接創建連接池,不等待測試
|
||||
this.masterPool = promise_1.default.createPool(masterConfig);
|
||||
console.log('✅ 主機資料庫連接池創建成功');
|
||||
this.slavePool = promise_1.default.createPool(slaveConfig);
|
||||
console.log('✅ 備機資料庫連接池創建成功');
|
||||
// 設置默認使用主機
|
||||
this.status.currentDatabase = 'master';
|
||||
this.status.masterHealthy = true; // 假設主機健康,後續健康檢查會驗證
|
||||
console.log('🎯 當前使用資料庫: 主機');
|
||||
// 異步測試連接
|
||||
this.testConnectionsAsync();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ 資料庫連接池同步初始化失敗:', error);
|
||||
}
|
||||
}
|
||||
// 異步測試連接
|
||||
async testConnectionsAsync() {
|
||||
try {
|
||||
await this.testConnections();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ 異步連接測試失敗:', error);
|
||||
}
|
||||
}
|
||||
// 初始化連接池
|
||||
async initializePools() {
|
||||
try {
|
||||
console.log('🚀 開始初始化資料庫備援系統...');
|
||||
// 先測試主機連接
|
||||
console.log('📡 測試主機資料庫連接...');
|
||||
const masterHealthy = await this.testMasterConnection();
|
||||
if (masterHealthy) {
|
||||
console.log('✅ 主機資料庫連接正常,使用主機');
|
||||
this.status.currentDatabase = 'master';
|
||||
this.status.masterHealthy = true;
|
||||
// 初始化主機連接池
|
||||
this.masterPool = promise_1.default.createPool(masterConfig);
|
||||
console.log('✅ 主機資料庫連接池初始化成功');
|
||||
}
|
||||
else {
|
||||
console.log('❌ 主機資料庫連接失敗,測試備機...');
|
||||
// 測試備機連接
|
||||
const slaveHealthy = await this.testSlaveConnection();
|
||||
if (slaveHealthy) {
|
||||
console.log('✅ 備機資料庫連接正常,切換到備機');
|
||||
this.status.currentDatabase = 'slave';
|
||||
this.status.slaveHealthy = true;
|
||||
// 初始化備機連接池
|
||||
this.slavePool = promise_1.default.createPool(slaveConfig);
|
||||
console.log('✅ 備機資料庫連接池初始化成功');
|
||||
}
|
||||
else {
|
||||
console.log('❌ 主機和備機都無法連接,嘗試初始化主機連接池');
|
||||
this.masterPool = promise_1.default.createPool(masterConfig);
|
||||
this.status.currentDatabase = 'master';
|
||||
}
|
||||
}
|
||||
// 初始化另一個連接池(用於健康檢查)
|
||||
if (this.status.currentDatabase === 'master' && !this.slavePool) {
|
||||
this.slavePool = promise_1.default.createPool(slaveConfig);
|
||||
console.log('✅ 備機資料庫連接池初始化成功(用於健康檢查)');
|
||||
}
|
||||
else if (this.status.currentDatabase === 'slave' && !this.masterPool) {
|
||||
this.masterPool = promise_1.default.createPool(masterConfig);
|
||||
console.log('✅ 主機資料庫連接池初始化成功(用於健康檢查)');
|
||||
}
|
||||
console.log(`🎯 當前使用資料庫: ${this.status.currentDatabase === 'master' ? '主機' : '備機'}`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ 資料庫連接池初始化失敗:', error);
|
||||
}
|
||||
}
|
||||
// 測試主機連接
|
||||
async testMasterConnection() {
|
||||
try {
|
||||
const connection = await promise_1.default.createConnection(masterConfig);
|
||||
await connection.ping();
|
||||
await connection.end();
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('主機資料庫連接失敗:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 測試備機連接
|
||||
async testSlaveConnection() {
|
||||
try {
|
||||
const connection = await promise_1.default.createConnection(slaveConfig);
|
||||
await connection.ping();
|
||||
await connection.end();
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('備機資料庫連接失敗:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 測試資料庫連接
|
||||
async testConnections() {
|
||||
// 測試主機
|
||||
try {
|
||||
if (this.masterPool) {
|
||||
const connection = await this.masterPool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
this.status.masterHealthy = true;
|
||||
console.log('主機資料庫連接正常');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.status.masterHealthy = false;
|
||||
console.error('主機資料庫連接失敗:', error);
|
||||
}
|
||||
// 測試備機
|
||||
try {
|
||||
if (this.slavePool) {
|
||||
const connection = await this.slavePool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
this.status.slaveHealthy = true;
|
||||
console.log('備機資料庫連接正常');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.status.slaveHealthy = false;
|
||||
console.error('備機資料庫連接失敗:', error);
|
||||
}
|
||||
}
|
||||
// 開始健康檢查
|
||||
startHealthCheck() {
|
||||
if (!this.status.isEnabled)
|
||||
return;
|
||||
const interval = parseInt(process.env.DB_HEALTH_CHECK_INTERVAL || '30000');
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
await this.performHealthCheck();
|
||||
}, interval);
|
||||
}
|
||||
// 執行健康檢查
|
||||
async performHealthCheck() {
|
||||
const now = Date.now();
|
||||
this.status.lastHealthCheck = now;
|
||||
// 檢查主機
|
||||
if (this.masterPool) {
|
||||
try {
|
||||
const connection = await this.masterPool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
this.status.masterHealthy = true;
|
||||
}
|
||||
catch (error) {
|
||||
this.status.masterHealthy = false;
|
||||
console.error('主機資料庫健康檢查失敗:', error);
|
||||
}
|
||||
}
|
||||
// 檢查備機
|
||||
if (this.slavePool) {
|
||||
try {
|
||||
const connection = await this.slavePool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
this.status.slaveHealthy = true;
|
||||
}
|
||||
catch (error) {
|
||||
this.status.slaveHealthy = false;
|
||||
console.error('備機資料庫健康檢查失敗:', error);
|
||||
}
|
||||
}
|
||||
// 決定當前使用的資料庫
|
||||
this.determineCurrentDatabase();
|
||||
}
|
||||
// 決定當前使用的資料庫
|
||||
determineCurrentDatabase() {
|
||||
const previousDatabase = this.status.currentDatabase;
|
||||
if (this.status.masterHealthy) {
|
||||
if (this.status.currentDatabase !== 'master') {
|
||||
console.log('🔄 主機資料庫恢復,切換回主機');
|
||||
this.status.currentDatabase = 'master';
|
||||
this.status.consecutiveFailures = 0;
|
||||
}
|
||||
}
|
||||
else if (this.status.slaveHealthy) {
|
||||
if (this.status.currentDatabase !== 'slave') {
|
||||
console.log('🔄 主機資料庫故障,切換到備機');
|
||||
this.status.currentDatabase = 'slave';
|
||||
this.status.consecutiveFailures++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.status.consecutiveFailures++;
|
||||
console.error('❌ 主機和備機資料庫都無法連接');
|
||||
}
|
||||
// 記錄狀態變化
|
||||
if (previousDatabase !== this.status.currentDatabase) {
|
||||
console.log(`📊 資料庫狀態變化: ${previousDatabase} → ${this.status.currentDatabase}`);
|
||||
}
|
||||
}
|
||||
// 獲取當前連接池
|
||||
getCurrentPool() {
|
||||
if (this.status.currentDatabase === 'master') {
|
||||
if (this.masterPool) {
|
||||
return this.masterPool;
|
||||
}
|
||||
else if (this.slavePool) {
|
||||
// 主機不可用,嘗試使用備機
|
||||
console.log('⚠️ 主機連接池不可用,嘗試使用備機');
|
||||
this.status.currentDatabase = 'slave';
|
||||
return this.slavePool;
|
||||
}
|
||||
}
|
||||
else if (this.status.currentDatabase === 'slave') {
|
||||
if (this.slavePool) {
|
||||
return this.slavePool;
|
||||
}
|
||||
else if (this.masterPool) {
|
||||
// 備機不可用,嘗試使用主機
|
||||
console.log('⚠️ 備機連接池不可用,嘗試使用主機');
|
||||
this.status.currentDatabase = 'master';
|
||||
return this.masterPool;
|
||||
}
|
||||
}
|
||||
console.error('❌ 沒有可用的資料庫連接池');
|
||||
return null;
|
||||
}
|
||||
// 獲取連接
|
||||
async getConnection() {
|
||||
const pool = this.getCurrentPool();
|
||||
if (!pool) {
|
||||
throw new Error('沒有可用的資料庫連接');
|
||||
}
|
||||
let retries = 0;
|
||||
const maxRetries = parseInt(process.env.DB_RETRY_ATTEMPTS || '3');
|
||||
const retryDelay = parseInt(process.env.DB_RETRY_DELAY || '2000');
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
return await pool.getConnection();
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`資料庫連接失敗 (嘗試 ${retries + 1}/${maxRetries}):`, error.message);
|
||||
if (error.code === 'ECONNRESET' || error.code === 'PROTOCOL_CONNECTION_LOST') {
|
||||
// 觸發健康檢查
|
||||
await this.performHealthCheck();
|
||||
retries++;
|
||||
if (retries < maxRetries) {
|
||||
console.log(`等待 ${retryDelay}ms 後重試...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw new Error('資料庫連接失敗,已達到最大重試次數');
|
||||
}
|
||||
// 執行查詢
|
||||
async query(sql, params) {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [rows] = await connection.execute(sql, params);
|
||||
return rows;
|
||||
}
|
||||
finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
// 執行單一查詢
|
||||
async queryOne(sql, params) {
|
||||
try {
|
||||
const results = await this.query(sql, params);
|
||||
return results.length > 0 ? results[0] : null;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('資料庫單一查詢錯誤:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// 執行插入
|
||||
async insert(sql, params) {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result;
|
||||
}
|
||||
finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
// 執行更新
|
||||
async update(sql, params) {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result;
|
||||
}
|
||||
finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
// 執行刪除
|
||||
async delete(sql, params) {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result;
|
||||
}
|
||||
finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
// 開始事務
|
||||
async beginTransaction() {
|
||||
const connection = await this.getConnection();
|
||||
await connection.beginTransaction();
|
||||
return connection;
|
||||
}
|
||||
// 提交事務
|
||||
async commit(connection) {
|
||||
await connection.commit();
|
||||
connection.release();
|
||||
}
|
||||
// 回滾事務
|
||||
async rollback(connection) {
|
||||
await connection.rollback();
|
||||
connection.release();
|
||||
}
|
||||
// 獲取備援狀態
|
||||
getStatus() {
|
||||
return { ...this.status };
|
||||
}
|
||||
// 強制切換到指定資料庫
|
||||
async switchToDatabase(database) {
|
||||
if (database === 'master' && this.status.masterHealthy) {
|
||||
this.status.currentDatabase = 'master';
|
||||
return true;
|
||||
}
|
||||
else if (database === 'slave' && this.status.slaveHealthy) {
|
||||
this.status.currentDatabase = 'slave';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// 關閉所有連接池
|
||||
async close() {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
}
|
||||
if (this.masterPool) {
|
||||
await this.masterPool.end();
|
||||
}
|
||||
if (this.slavePool) {
|
||||
await this.slavePool.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.DatabaseFailover = DatabaseFailover;
|
||||
// 導出單例實例
|
||||
exports.dbFailover = DatabaseFailover.getInstance();
|
499
lib/database-failover.ts
Normal file
499
lib/database-failover.ts
Normal file
@@ -0,0 +1,499 @@
|
||||
// =====================================================
|
||||
// 資料庫備援連線服務
|
||||
// =====================================================
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
// 資料庫配置介面
|
||||
interface DatabaseConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
charset: string;
|
||||
timezone: string;
|
||||
acquireTimeout: number;
|
||||
timeout: number;
|
||||
reconnect: boolean;
|
||||
connectionLimit: number;
|
||||
queueLimit: number;
|
||||
retryDelay: number;
|
||||
maxRetries: number;
|
||||
idleTimeout: number;
|
||||
maxIdle: number;
|
||||
ssl: boolean;
|
||||
}
|
||||
|
||||
// 主機資料庫配置
|
||||
const masterConfig = {
|
||||
host: process.env.DB_HOST || 'mysql.theaken.com',
|
||||
port: parseInt(process.env.DB_PORT || '33306'),
|
||||
user: process.env.DB_USER || 'AI_Platform',
|
||||
password: process.env.DB_PASSWORD || 'Aa123456',
|
||||
database: process.env.DB_NAME || 'db_AI_Platform',
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
idleTimeout: 300000,
|
||||
ssl: false as any,
|
||||
};
|
||||
|
||||
// 備機資料庫配置
|
||||
const slaveConfig = {
|
||||
host: process.env.SLAVE_DB_HOST || '122.100.99.161',
|
||||
port: parseInt(process.env.SLAVE_DB_PORT || '43306'),
|
||||
user: process.env.SLAVE_DB_USER || 'A999',
|
||||
password: process.env.SLAVE_DB_PASSWORD || '1023',
|
||||
database: process.env.SLAVE_DB_NAME || 'db_AI_Platform', // 修正為 AI 平台資料庫
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
idleTimeout: 300000,
|
||||
ssl: false as any,
|
||||
};
|
||||
|
||||
// 備援狀態
|
||||
interface FailoverStatus {
|
||||
isEnabled: boolean;
|
||||
currentDatabase: 'master' | 'slave';
|
||||
masterHealthy: boolean;
|
||||
slaveHealthy: boolean;
|
||||
lastHealthCheck: number;
|
||||
consecutiveFailures: number;
|
||||
}
|
||||
|
||||
// 備援資料庫管理類
|
||||
export class DatabaseFailover {
|
||||
private static instance: DatabaseFailover;
|
||||
private masterPool: mysql.Pool | null = null;
|
||||
private slavePool: mysql.Pool | null = null;
|
||||
private status: FailoverStatus;
|
||||
private healthCheckInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
private constructor() {
|
||||
this.status = {
|
||||
isEnabled: process.env.DB_FAILOVER_ENABLED === 'true',
|
||||
currentDatabase: 'master',
|
||||
masterHealthy: false,
|
||||
slaveHealthy: false,
|
||||
lastHealthCheck: Date.now(), // 使用當前時間作為初始值
|
||||
consecutiveFailures: 0,
|
||||
};
|
||||
|
||||
// 同步初始化連接池
|
||||
this.initializePoolsSync();
|
||||
this.startHealthCheck();
|
||||
}
|
||||
|
||||
public static getInstance(): DatabaseFailover {
|
||||
if (!DatabaseFailover.instance) {
|
||||
DatabaseFailover.instance = new DatabaseFailover();
|
||||
}
|
||||
return DatabaseFailover.instance;
|
||||
}
|
||||
|
||||
// 同步初始化連接池
|
||||
private initializePoolsSync(): void {
|
||||
try {
|
||||
console.log('🚀 開始同步初始化資料庫備援系統...');
|
||||
|
||||
// 直接創建連接池,不等待測試
|
||||
this.masterPool = mysql.createPool(masterConfig);
|
||||
console.log('✅ 主機資料庫連接池創建成功');
|
||||
|
||||
this.slavePool = mysql.createPool(slaveConfig);
|
||||
console.log('✅ 備機資料庫連接池創建成功');
|
||||
|
||||
// 設置默認使用主機
|
||||
this.status.currentDatabase = 'master';
|
||||
this.status.masterHealthy = true; // 假設主機健康,後續健康檢查會驗證
|
||||
|
||||
console.log('🎯 當前使用資料庫: 主機');
|
||||
|
||||
// 異步測試連接
|
||||
this.testConnectionsAsync();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫連接池同步初始化失敗:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 異步測試連接
|
||||
private async testConnectionsAsync(): Promise<void> {
|
||||
try {
|
||||
await this.testConnections();
|
||||
} catch (error) {
|
||||
console.error('❌ 異步連接測試失敗:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化連接池
|
||||
private async initializePools(): Promise<void> {
|
||||
try {
|
||||
console.log('🚀 開始初始化資料庫備援系統...');
|
||||
|
||||
// 先測試主機連接
|
||||
console.log('📡 測試主機資料庫連接...');
|
||||
const masterHealthy = await this.testMasterConnection();
|
||||
|
||||
if (masterHealthy) {
|
||||
console.log('✅ 主機資料庫連接正常,使用主機');
|
||||
this.status.currentDatabase = 'master';
|
||||
this.status.masterHealthy = true;
|
||||
|
||||
// 初始化主機連接池
|
||||
this.masterPool = mysql.createPool(masterConfig);
|
||||
console.log('✅ 主機資料庫連接池初始化成功');
|
||||
} else {
|
||||
console.log('❌ 主機資料庫連接失敗,測試備機...');
|
||||
|
||||
// 測試備機連接
|
||||
const slaveHealthy = await this.testSlaveConnection();
|
||||
|
||||
if (slaveHealthy) {
|
||||
console.log('✅ 備機資料庫連接正常,切換到備機');
|
||||
this.status.currentDatabase = 'slave';
|
||||
this.status.slaveHealthy = true;
|
||||
|
||||
// 初始化備機連接池
|
||||
this.slavePool = mysql.createPool(slaveConfig);
|
||||
console.log('✅ 備機資料庫連接池初始化成功');
|
||||
} else {
|
||||
console.log('❌ 主機和備機都無法連接,嘗試初始化主機連接池');
|
||||
this.masterPool = mysql.createPool(masterConfig);
|
||||
this.status.currentDatabase = 'master';
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化另一個連接池(用於健康檢查)
|
||||
if (this.status.currentDatabase === 'master' && !this.slavePool) {
|
||||
this.slavePool = mysql.createPool(slaveConfig);
|
||||
console.log('✅ 備機資料庫連接池初始化成功(用於健康檢查)');
|
||||
} else if (this.status.currentDatabase === 'slave' && !this.masterPool) {
|
||||
this.masterPool = mysql.createPool(masterConfig);
|
||||
console.log('✅ 主機資料庫連接池初始化成功(用於健康檢查)');
|
||||
}
|
||||
|
||||
console.log(`🎯 當前使用資料庫: ${this.status.currentDatabase === 'master' ? '主機' : '備機'}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 資料庫連接池初始化失敗:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 測試主機連接
|
||||
private async testMasterConnection(): Promise<boolean> {
|
||||
try {
|
||||
const connection = await mysql.createConnection(masterConfig);
|
||||
await connection.ping();
|
||||
await connection.end();
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error('主機資料庫連接失敗:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 測試備機連接
|
||||
private async testSlaveConnection(): Promise<boolean> {
|
||||
try {
|
||||
const connection = await mysql.createConnection(slaveConfig);
|
||||
await connection.ping();
|
||||
await connection.end();
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error('備機資料庫連接失敗:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 測試資料庫連接
|
||||
private async testConnections(): Promise<void> {
|
||||
// 測試主機
|
||||
try {
|
||||
if (this.masterPool) {
|
||||
const connection = await this.masterPool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
this.status.masterHealthy = true;
|
||||
console.log('主機資料庫連接正常');
|
||||
}
|
||||
} catch (error) {
|
||||
this.status.masterHealthy = false;
|
||||
console.error('主機資料庫連接失敗:', error);
|
||||
}
|
||||
|
||||
// 測試備機
|
||||
try {
|
||||
if (this.slavePool) {
|
||||
const connection = await this.slavePool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
this.status.slaveHealthy = true;
|
||||
console.log('備機資料庫連接正常');
|
||||
}
|
||||
} catch (error) {
|
||||
this.status.slaveHealthy = false;
|
||||
console.error('備機資料庫連接失敗:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 開始健康檢查
|
||||
private startHealthCheck(): void {
|
||||
if (!this.status.isEnabled) return;
|
||||
|
||||
const interval = parseInt(process.env.DB_HEALTH_CHECK_INTERVAL || '30000');
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
await this.performHealthCheck();
|
||||
}, interval);
|
||||
}
|
||||
|
||||
// 執行健康檢查
|
||||
private async performHealthCheck(): Promise<void> {
|
||||
const now = Date.now();
|
||||
this.status.lastHealthCheck = now;
|
||||
|
||||
// 檢查主機
|
||||
if (this.masterPool) {
|
||||
try {
|
||||
const connection = await this.masterPool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
this.status.masterHealthy = true;
|
||||
} catch (error) {
|
||||
this.status.masterHealthy = false;
|
||||
console.error('主機資料庫健康檢查失敗:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 檢查備機
|
||||
if (this.slavePool) {
|
||||
try {
|
||||
const connection = await this.slavePool.getConnection();
|
||||
await connection.ping();
|
||||
connection.release();
|
||||
this.status.slaveHealthy = true;
|
||||
} catch (error) {
|
||||
this.status.slaveHealthy = false;
|
||||
console.error('備機資料庫健康檢查失敗:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 決定當前使用的資料庫
|
||||
this.determineCurrentDatabase();
|
||||
}
|
||||
|
||||
// 決定當前使用的資料庫
|
||||
private determineCurrentDatabase(): void {
|
||||
const previousDatabase = this.status.currentDatabase;
|
||||
|
||||
if (this.status.masterHealthy) {
|
||||
if (this.status.currentDatabase !== 'master') {
|
||||
console.log('🔄 主機資料庫恢復,切換回主機');
|
||||
this.status.currentDatabase = 'master';
|
||||
this.status.consecutiveFailures = 0;
|
||||
}
|
||||
} else if (this.status.slaveHealthy) {
|
||||
if (this.status.currentDatabase !== 'slave') {
|
||||
console.log('🔄 主機資料庫故障,切換到備機');
|
||||
this.status.currentDatabase = 'slave';
|
||||
this.status.consecutiveFailures++;
|
||||
}
|
||||
} else {
|
||||
this.status.consecutiveFailures++;
|
||||
console.error('❌ 主機和備機資料庫都無法連接');
|
||||
}
|
||||
|
||||
// 記錄狀態變化
|
||||
if (previousDatabase !== this.status.currentDatabase) {
|
||||
console.log(`📊 資料庫狀態變化: ${previousDatabase} → ${this.status.currentDatabase}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取當前連接池
|
||||
private getCurrentPool(): mysql.Pool | null {
|
||||
if (this.status.currentDatabase === 'master') {
|
||||
if (this.masterPool) {
|
||||
return this.masterPool;
|
||||
} else if (this.slavePool) {
|
||||
// 主機不可用,嘗試使用備機
|
||||
console.log('⚠️ 主機連接池不可用,嘗試使用備機');
|
||||
this.status.currentDatabase = 'slave';
|
||||
return this.slavePool;
|
||||
}
|
||||
} else if (this.status.currentDatabase === 'slave') {
|
||||
if (this.slavePool) {
|
||||
return this.slavePool;
|
||||
} else if (this.masterPool) {
|
||||
// 備機不可用,嘗試使用主機
|
||||
console.log('⚠️ 備機連接池不可用,嘗試使用主機');
|
||||
this.status.currentDatabase = 'master';
|
||||
return this.masterPool;
|
||||
}
|
||||
}
|
||||
|
||||
console.error('❌ 沒有可用的資料庫連接池');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 獲取連接
|
||||
public async getConnection(): Promise<mysql.PoolConnection> {
|
||||
const pool = this.getCurrentPool();
|
||||
if (!pool) {
|
||||
throw new Error('沒有可用的資料庫連接');
|
||||
}
|
||||
|
||||
let retries = 0;
|
||||
const maxRetries = parseInt(process.env.DB_RETRY_ATTEMPTS || '3');
|
||||
const retryDelay = parseInt(process.env.DB_RETRY_DELAY || '2000');
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
return await pool.getConnection();
|
||||
} catch (error: any) {
|
||||
console.error(`資料庫連接失敗 (嘗試 ${retries + 1}/${maxRetries}):`, error.message);
|
||||
|
||||
if (error.code === 'ECONNRESET' || error.code === 'PROTOCOL_CONNECTION_LOST') {
|
||||
// 觸發健康檢查
|
||||
await this.performHealthCheck();
|
||||
|
||||
retries++;
|
||||
if (retries < maxRetries) {
|
||||
console.log(`等待 ${retryDelay}ms 後重試...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('資料庫連接失敗,已達到最大重試次數');
|
||||
}
|
||||
|
||||
// 執行查詢
|
||||
public async query<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [rows] = await connection.execute(sql, params);
|
||||
return rows as T[];
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 執行單一查詢
|
||||
public async queryOne<T = any>(sql: string, params?: any[]): Promise<T | null> {
|
||||
try {
|
||||
const results = await this.query<T>(sql, params);
|
||||
return results.length > 0 ? results[0] : null;
|
||||
} catch (error) {
|
||||
console.error('資料庫單一查詢錯誤:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 執行插入
|
||||
public async insert(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result as mysql.ResultSetHeader;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 執行更新
|
||||
public async update(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result as mysql.ResultSetHeader;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 執行刪除
|
||||
public async delete(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result as mysql.ResultSetHeader;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 開始事務
|
||||
public async beginTransaction(): Promise<mysql.PoolConnection> {
|
||||
const connection = await this.getConnection();
|
||||
await connection.beginTransaction();
|
||||
return connection;
|
||||
}
|
||||
|
||||
// 提交事務
|
||||
public async commit(connection: mysql.PoolConnection): Promise<void> {
|
||||
await connection.commit();
|
||||
connection.release();
|
||||
}
|
||||
|
||||
// 回滾事務
|
||||
public async rollback(connection: mysql.PoolConnection): Promise<void> {
|
||||
await connection.rollback();
|
||||
connection.release();
|
||||
}
|
||||
|
||||
// 獲取備援狀態
|
||||
public getStatus(): FailoverStatus {
|
||||
return { ...this.status };
|
||||
}
|
||||
|
||||
// 獲取主機連接池
|
||||
public getMasterPool(): mysql.Pool | null {
|
||||
return this.masterPool;
|
||||
}
|
||||
|
||||
// 獲取備機連接池
|
||||
public getSlavePool(): mysql.Pool | null {
|
||||
return this.slavePool;
|
||||
}
|
||||
|
||||
// 強制切換到指定資料庫
|
||||
public async switchToDatabase(database: 'master' | 'slave'): Promise<boolean> {
|
||||
if (database === 'master' && this.status.masterHealthy) {
|
||||
this.status.currentDatabase = 'master';
|
||||
return true;
|
||||
} else if (database === 'slave' && this.status.slaveHealthy) {
|
||||
this.status.currentDatabase = 'slave';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 關閉所有連接池
|
||||
public async close(): Promise<void> {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
}
|
||||
|
||||
if (this.masterPool) {
|
||||
await this.masterPool.end();
|
||||
}
|
||||
|
||||
if (this.slavePool) {
|
||||
await this.slavePool.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 導出單例實例
|
||||
export const dbFailover = DatabaseFailover.getInstance();
|
||||
|
||||
// 導出類型
|
||||
export type { PoolConnection } from 'mysql2/promise';
|
229
lib/database-sync.ts
Normal file
229
lib/database-sync.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
// =====================================================
|
||||
// 資料庫雙寫同步服務
|
||||
// =====================================================
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
import { dbFailover } from './database-failover';
|
||||
|
||||
// 雙寫配置
|
||||
interface DualWriteConfig {
|
||||
enabled: boolean;
|
||||
masterPriority: boolean; // 主機優先,如果主機失敗則只寫備機
|
||||
conflictResolution: 'master' | 'slave' | 'timestamp'; // 衝突解決策略
|
||||
retryAttempts: number;
|
||||
retryDelay: number;
|
||||
}
|
||||
|
||||
// 寫入結果
|
||||
interface WriteResult {
|
||||
success: boolean;
|
||||
masterSuccess: boolean;
|
||||
slaveSuccess: boolean;
|
||||
masterError?: string;
|
||||
slaveError?: string;
|
||||
conflictDetected?: boolean;
|
||||
}
|
||||
|
||||
export class DatabaseSync {
|
||||
private static instance: DatabaseSync;
|
||||
private config: DualWriteConfig;
|
||||
|
||||
private constructor() {
|
||||
this.config = {
|
||||
enabled: process.env.DB_DUAL_WRITE_ENABLED === 'true',
|
||||
masterPriority: process.env.DB_MASTER_PRIORITY !== 'false',
|
||||
conflictResolution: (process.env.DB_CONFLICT_RESOLUTION as any) || 'master',
|
||||
retryAttempts: parseInt(process.env.DB_RETRY_ATTEMPTS || '3'),
|
||||
retryDelay: parseInt(process.env.DB_RETRY_DELAY || '1000')
|
||||
};
|
||||
}
|
||||
|
||||
public static getInstance(): DatabaseSync {
|
||||
if (!DatabaseSync.instance) {
|
||||
DatabaseSync.instance = new DatabaseSync();
|
||||
}
|
||||
return DatabaseSync.instance;
|
||||
}
|
||||
|
||||
// 雙寫插入
|
||||
async dualInsert(sql: string, params?: any[]): Promise<WriteResult> {
|
||||
if (!this.config.enabled) {
|
||||
// 如果雙寫未啟用,使用備援系統選擇的資料庫
|
||||
try {
|
||||
await dbFailover.insert(sql, params);
|
||||
return {
|
||||
success: true,
|
||||
masterSuccess: true,
|
||||
slaveSuccess: false
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
masterSuccess: false,
|
||||
slaveSuccess: false,
|
||||
masterError: error instanceof Error ? error.message : '未知錯誤'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result: WriteResult = {
|
||||
success: false,
|
||||
masterSuccess: false,
|
||||
slaveSuccess: false
|
||||
};
|
||||
|
||||
// 獲取主機和備機連接
|
||||
const masterPool = dbFailover.getMasterPool();
|
||||
const slavePool = dbFailover.getSlavePool();
|
||||
|
||||
if (!masterPool || !slavePool) {
|
||||
result.masterError = '無法獲取資料庫連接池';
|
||||
return result;
|
||||
}
|
||||
|
||||
// 根據優先級決定寫入順序
|
||||
const writeOrder = this.config.masterPriority
|
||||
? [{ pool: masterPool, name: 'master' }, { pool: slavePool, name: 'slave' }]
|
||||
: [{ pool: slavePool, name: 'slave' }, { pool: masterPool, name: 'master' }];
|
||||
|
||||
// 執行雙寫
|
||||
for (const { pool, name } of writeOrder) {
|
||||
try {
|
||||
await this.executeWithRetry(pool, sql, params);
|
||||
result[`${name}Success`] = true;
|
||||
console.log(`✅ ${name} 資料庫寫入成功`);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '未知錯誤';
|
||||
result[`${name}Error`] = errorMsg;
|
||||
console.error(`❌ ${name} 資料庫寫入失敗:`, errorMsg);
|
||||
|
||||
// 如果主機優先且主機失敗,嘗試備機
|
||||
if (this.config.masterPriority && name === 'master') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判斷整體成功狀態
|
||||
result.success = result.masterSuccess || result.slaveSuccess;
|
||||
|
||||
// 檢查衝突
|
||||
if (result.masterSuccess && result.slaveSuccess) {
|
||||
result.conflictDetected = await this.checkForConflicts(sql, params);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 雙寫更新
|
||||
async dualUpdate(sql: string, params?: any[]): Promise<WriteResult> {
|
||||
return await this.dualInsert(sql, params);
|
||||
}
|
||||
|
||||
// 雙寫刪除
|
||||
async dualDelete(sql: string, params?: any[]): Promise<WriteResult> {
|
||||
return await this.dualInsert(sql, params);
|
||||
}
|
||||
|
||||
// 帶重試的執行
|
||||
private async executeWithRetry(pool: mysql.Pool, sql: string, params?: any[]): Promise<any> {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
return result;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error('未知錯誤');
|
||||
|
||||
if (attempt < this.config.retryAttempts) {
|
||||
console.log(`重試 ${attempt}/${this.config.retryAttempts}: ${lastError.message}`);
|
||||
await new Promise(resolve => setTimeout(resolve, this.config.retryDelay * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// 檢查衝突(簡化版本,實際應用中可能需要更複雜的邏輯)
|
||||
private async checkForConflicts(sql: string, params?: any[]): Promise<boolean> {
|
||||
// 這裡可以實現更複雜的衝突檢測邏輯
|
||||
// 例如:比較主機和備機的資料是否一致
|
||||
return false;
|
||||
}
|
||||
|
||||
// 同步資料(從主機到備機)
|
||||
async syncFromMasterToSlave(tableName: string, condition?: string): Promise<boolean> {
|
||||
try {
|
||||
const masterPool = dbFailover.getMasterPool();
|
||||
const slavePool = dbFailover.getSlavePool();
|
||||
|
||||
if (!masterPool || !slavePool) {
|
||||
throw new Error('無法獲取資料庫連接池');
|
||||
}
|
||||
|
||||
// 從主機讀取資料
|
||||
const masterConnection = await masterPool.getConnection();
|
||||
const slaveConnection = await slavePool.getConnection();
|
||||
|
||||
try {
|
||||
const selectSql = condition
|
||||
? `SELECT * FROM ${tableName} WHERE ${condition}`
|
||||
: `SELECT * FROM ${tableName}`;
|
||||
|
||||
const [rows] = await masterConnection.execute(selectSql);
|
||||
|
||||
if (Array.isArray(rows) && rows.length > 0) {
|
||||
// 清空備機表(可選)
|
||||
await slaveConnection.execute(`DELETE FROM ${tableName}${condition ? ` WHERE ${condition}` : ''}`);
|
||||
|
||||
// 插入資料到備機
|
||||
for (const row of rows as any[]) {
|
||||
const columns = Object.keys(row);
|
||||
const values = columns.map(() => '?').join(', ');
|
||||
const insertSql = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${values})`;
|
||||
const insertParams = columns.map(col => row[col]);
|
||||
|
||||
await slaveConnection.execute(insertSql, insertParams);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 成功同步 ${tableName} 表資料到備機`);
|
||||
return true;
|
||||
} finally {
|
||||
masterConnection.release();
|
||||
slaveConnection.release();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ 同步 ${tableName} 表資料失敗:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取同步狀態
|
||||
async getSyncStatus(): Promise<{
|
||||
enabled: boolean;
|
||||
masterHealthy: boolean;
|
||||
slaveHealthy: boolean;
|
||||
lastSyncTime?: string;
|
||||
}> {
|
||||
const masterPool = dbFailover.getMasterPool();
|
||||
const slavePool = dbFailover.getSlavePool();
|
||||
|
||||
return {
|
||||
enabled: this.config.enabled,
|
||||
masterHealthy: masterPool ? true : false,
|
||||
slaveHealthy: slavePool ? true : false,
|
||||
lastSyncTime: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 導出單例實例
|
||||
export const dbSync = DatabaseSync.getInstance();
|
118
lib/database.ts
118
lib/database.ts
@@ -1,8 +1,10 @@
|
||||
// =====================================================
|
||||
// 資料庫連接配置
|
||||
// 資料庫連接配置 (整合備援功能)
|
||||
// =====================================================
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
import { dbFailover } from './database-failover';
|
||||
import { dbSync } from './database-sync';
|
||||
|
||||
// 資料庫配置
|
||||
const dbConfig = {
|
||||
@@ -25,19 +27,21 @@ const dbConfig = {
|
||||
idleTimeout: 300000,
|
||||
maxIdle: 10,
|
||||
// 添加 SSL 配置(如果需要)
|
||||
ssl: false,
|
||||
ssl: false as any,
|
||||
};
|
||||
|
||||
// 創建連接池
|
||||
const pool = mysql.createPool(dbConfig);
|
||||
|
||||
// 資料庫連接類
|
||||
// 資料庫連接類 (整合備援功能)
|
||||
export class Database {
|
||||
private static instance: Database;
|
||||
private pool: mysql.Pool;
|
||||
private useFailover: boolean;
|
||||
|
||||
private constructor() {
|
||||
this.pool = pool;
|
||||
this.useFailover = process.env.DB_FAILOVER_ENABLED === 'true';
|
||||
}
|
||||
|
||||
public static getInstance(): Database {
|
||||
@@ -49,11 +53,18 @@ export class Database {
|
||||
|
||||
// 獲取連接
|
||||
public async getConnection(): Promise<mysql.PoolConnection> {
|
||||
if (this.useFailover) {
|
||||
return await dbFailover.getConnection();
|
||||
}
|
||||
return await this.pool.getConnection();
|
||||
}
|
||||
|
||||
// 執行查詢
|
||||
public async query<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
||||
if (this.useFailover) {
|
||||
return await dbFailover.query<T>(sql, params);
|
||||
}
|
||||
|
||||
let connection;
|
||||
let retries = 0;
|
||||
const maxRetries = 3;
|
||||
@@ -92,6 +103,10 @@ export class Database {
|
||||
|
||||
// 執行單一查詢
|
||||
public async queryOne<T = any>(sql: string, params?: any[]): Promise<T | null> {
|
||||
if (this.useFailover) {
|
||||
return await dbFailover.queryOne<T>(sql, params);
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await this.query<T>(sql, params);
|
||||
return results.length > 0 ? results[0] : null;
|
||||
@@ -101,8 +116,28 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
// 執行插入
|
||||
// 執行插入(支援雙寫)
|
||||
public async insert(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
if (this.useFailover) {
|
||||
// 檢查是否啟用雙寫
|
||||
const syncStatus = await dbSync.getSyncStatus();
|
||||
if (syncStatus.enabled) {
|
||||
const result = await dbSync.dualInsert(sql, params);
|
||||
if (result.success) {
|
||||
// 返回主機的結果(如果主機成功)
|
||||
if (result.masterSuccess) {
|
||||
return await dbFailover.insert(sql, params);
|
||||
} else if (result.slaveSuccess) {
|
||||
// 如果只有備機成功,返回備機結果
|
||||
return await dbFailover.insert(sql, params);
|
||||
}
|
||||
}
|
||||
throw new Error(`雙寫失敗: 主機${result.masterError || '成功'}, 備機${result.slaveError || '成功'}`);
|
||||
} else {
|
||||
return await dbFailover.insert(sql, params);
|
||||
}
|
||||
}
|
||||
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
@@ -112,8 +147,28 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
// 執行更新
|
||||
// 執行更新(支援雙寫)
|
||||
public async update(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
if (this.useFailover) {
|
||||
// 檢查是否啟用雙寫
|
||||
const syncStatus = await dbSync.getSyncStatus();
|
||||
if (syncStatus.enabled) {
|
||||
const result = await dbSync.dualUpdate(sql, params);
|
||||
if (result.success) {
|
||||
// 返回主機的結果(如果主機成功)
|
||||
if (result.masterSuccess) {
|
||||
return await dbFailover.update(sql, params);
|
||||
} else if (result.slaveSuccess) {
|
||||
// 如果只有備機成功,返回備機結果
|
||||
return await dbFailover.update(sql, params);
|
||||
}
|
||||
}
|
||||
throw new Error(`雙寫失敗: 主機${result.masterError || '成功'}, 備機${result.slaveError || '成功'}`);
|
||||
} else {
|
||||
return await dbFailover.update(sql, params);
|
||||
}
|
||||
}
|
||||
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
@@ -123,8 +178,28 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
// 執行刪除
|
||||
// 執行刪除(支援雙寫)
|
||||
public async delete(sql: string, params?: any[]): Promise<mysql.ResultSetHeader> {
|
||||
if (this.useFailover) {
|
||||
// 檢查是否啟用雙寫
|
||||
const syncStatus = await dbSync.getSyncStatus();
|
||||
if (syncStatus.enabled) {
|
||||
const result = await dbSync.dualDelete(sql, params);
|
||||
if (result.success) {
|
||||
// 返回主機的結果(如果主機成功)
|
||||
if (result.masterSuccess) {
|
||||
return await dbFailover.delete(sql, params);
|
||||
} else if (result.slaveSuccess) {
|
||||
// 如果只有備機成功,返回備機結果
|
||||
return await dbFailover.delete(sql, params);
|
||||
}
|
||||
}
|
||||
throw new Error(`雙寫失敗: 主機${result.masterError || '成功'}, 備機${result.slaveError || '成功'}`);
|
||||
} else {
|
||||
return await dbFailover.delete(sql, params);
|
||||
}
|
||||
}
|
||||
|
||||
const connection = await this.getConnection();
|
||||
try {
|
||||
const [result] = await connection.execute(sql, params);
|
||||
@@ -136,6 +211,10 @@ export class Database {
|
||||
|
||||
// 開始事務
|
||||
public async beginTransaction(): Promise<mysql.PoolConnection> {
|
||||
if (this.useFailover) {
|
||||
return await dbFailover.beginTransaction();
|
||||
}
|
||||
|
||||
const connection = await this.getConnection();
|
||||
await connection.beginTransaction();
|
||||
return connection;
|
||||
@@ -143,18 +222,45 @@ export class Database {
|
||||
|
||||
// 提交事務
|
||||
public async commit(connection: mysql.PoolConnection): Promise<void> {
|
||||
if (this.useFailover) {
|
||||
return await dbFailover.commit(connection);
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
connection.release();
|
||||
}
|
||||
|
||||
// 回滾事務
|
||||
public async rollback(connection: mysql.PoolConnection): Promise<void> {
|
||||
if (this.useFailover) {
|
||||
return await dbFailover.rollback(connection);
|
||||
}
|
||||
|
||||
await connection.rollback();
|
||||
connection.release();
|
||||
}
|
||||
|
||||
// 獲取備援狀態
|
||||
public getFailoverStatus() {
|
||||
if (this.useFailover) {
|
||||
return dbFailover.getStatus();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 切換資料庫
|
||||
public async switchDatabase(database: 'master' | 'slave'): Promise<boolean> {
|
||||
if (this.useFailover) {
|
||||
return await dbFailover.switchToDatabase(database);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 關閉連接池
|
||||
public async close(): Promise<void> {
|
||||
if (this.useFailover) {
|
||||
await dbFailover.close();
|
||||
}
|
||||
await this.pool.end();
|
||||
}
|
||||
}
|
||||
|
@@ -202,69 +202,6 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
// 獲取用戶的應用和評價統計
|
||||
async getUserAppAndReviewStats(userId: string): Promise<{
|
||||
appCount: number;
|
||||
reviewCount: number;
|
||||
}> {
|
||||
try {
|
||||
console.log('獲取用戶統計數據,userId:', userId);
|
||||
|
||||
// 先檢查資料庫中是否有數據
|
||||
const checkAppsSql = 'SELECT COUNT(*) as total FROM apps';
|
||||
const checkRatingsSql = 'SELECT COUNT(*) as total FROM user_ratings';
|
||||
const checkUsersSql = 'SELECT COUNT(*) as total FROM users';
|
||||
|
||||
const [totalApps, totalRatings, totalUsers] = await Promise.all([
|
||||
this.queryOne(checkAppsSql),
|
||||
this.queryOne(checkRatingsSql),
|
||||
this.queryOne(checkUsersSql)
|
||||
]);
|
||||
|
||||
console.log('資料庫總數據:', {
|
||||
totalApps: totalApps?.total || 0,
|
||||
totalRatings: totalRatings?.total || 0,
|
||||
totalUsers: totalUsers?.total || 0
|
||||
});
|
||||
|
||||
// 獲取用戶創建的應用數量
|
||||
const appCountSql = `
|
||||
SELECT COUNT(*) as app_count
|
||||
FROM apps
|
||||
WHERE creator_id = ?
|
||||
`;
|
||||
|
||||
// 獲取用戶撰寫的評價數量
|
||||
const reviewCountSql = `
|
||||
SELECT COUNT(*) as review_count
|
||||
FROM user_ratings
|
||||
WHERE user_id = ?
|
||||
`;
|
||||
|
||||
const [appResult, reviewResult] = await Promise.all([
|
||||
this.queryOne(appCountSql, [userId]),
|
||||
this.queryOne(reviewCountSql, [userId])
|
||||
]);
|
||||
|
||||
console.log('應用查詢結果:', appResult);
|
||||
console.log('評價查詢結果:', reviewResult);
|
||||
|
||||
const result = {
|
||||
appCount: appResult?.app_count || 0,
|
||||
reviewCount: reviewResult?.review_count || 0
|
||||
};
|
||||
|
||||
console.log('最終統計結果:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('獲取用戶應用和評價統計錯誤:', error);
|
||||
return {
|
||||
appCount: 0,
|
||||
reviewCount: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取用戶活動記錄
|
||||
async getUserActivities(
|
||||
userId: string,
|
||||
@@ -804,7 +741,7 @@ export class UserService {
|
||||
|
||||
// 獲取所有用戶
|
||||
async getAllUsers(limit = 50, offset = 0): Promise<User[]> {
|
||||
const sql = 'SELECT * FROM users WHERE is_active = TRUE ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||
const sql = 'SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||
return await db.query<User>(sql, [limit, offset]);
|
||||
}
|
||||
|
||||
@@ -839,6 +776,46 @@ export class UserService {
|
||||
const service = new UserService();
|
||||
return await service.getAllUsers(limit, offset);
|
||||
}
|
||||
|
||||
// 獲取用戶的應用和評價統計
|
||||
async getUserAppAndReviewStats(userId: string): Promise<{
|
||||
appCount: number;
|
||||
reviewCount: number;
|
||||
}> {
|
||||
try {
|
||||
// 獲取用戶創建的應用數量
|
||||
const appCountSql = `
|
||||
SELECT COUNT(*) as app_count
|
||||
FROM apps
|
||||
WHERE creator_id = ?
|
||||
`;
|
||||
|
||||
// 獲取用戶撰寫的評價數量
|
||||
const reviewCountSql = `
|
||||
SELECT COUNT(*) as review_count
|
||||
FROM user_ratings
|
||||
WHERE user_id = ?
|
||||
`;
|
||||
|
||||
const [appResult, reviewResult] = await Promise.all([
|
||||
this.queryOne(appCountSql, [userId]),
|
||||
this.queryOne(reviewCountSql, [userId])
|
||||
]);
|
||||
|
||||
const result = {
|
||||
appCount: appResult?.app_count || 0,
|
||||
reviewCount: reviewResult?.review_count || 0
|
||||
};
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('獲取用戶應用和評價統計錯誤:', error);
|
||||
return {
|
||||
appCount: 0,
|
||||
reviewCount: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
@@ -1358,7 +1335,7 @@ export class AppService {
|
||||
}
|
||||
|
||||
// 獲取應用使用統計
|
||||
async getAppUsageStats(appId: string, startDate?: string, endDate?: string): Promise<{
|
||||
async getAppUsageStats(appId: string, startDate?: string, endDate?: string, department?: string): Promise<{
|
||||
dailyUsers: number;
|
||||
weeklyUsers: number;
|
||||
monthlyUsers: number;
|
||||
@@ -1369,7 +1346,7 @@ export class AppService {
|
||||
try {
|
||||
// 今日使用者
|
||||
const dailySql = `
|
||||
SELECT COUNT(DISTINCT user_id) as daily_users
|
||||
SELECT COUNT(*) as daily_users
|
||||
FROM user_views
|
||||
WHERE app_id = ? AND DATE(viewed_at) = CURDATE()
|
||||
`;
|
||||
@@ -1377,7 +1354,7 @@ export class AppService {
|
||||
|
||||
// 本週使用者
|
||||
const weeklySql = `
|
||||
SELECT COUNT(DISTINCT user_id) as weekly_users
|
||||
SELECT COUNT(*) as weekly_users
|
||||
FROM user_views
|
||||
WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK)
|
||||
`;
|
||||
@@ -1385,7 +1362,7 @@ export class AppService {
|
||||
|
||||
// 本月使用者
|
||||
const monthlySql = `
|
||||
SELECT COUNT(DISTINCT user_id) as monthly_users
|
||||
SELECT COUNT(*) as monthly_users
|
||||
FROM user_views
|
||||
WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)
|
||||
`;
|
||||
@@ -1399,48 +1376,73 @@ export class AppService {
|
||||
`;
|
||||
const totalResult = await this.queryOne(totalSql, [appId]);
|
||||
|
||||
// 部門使用統計
|
||||
const deptSql = `
|
||||
SELECT
|
||||
u.department,
|
||||
COUNT(*) as count
|
||||
FROM user_views uv
|
||||
JOIN users u ON uv.user_id = u.id
|
||||
WHERE uv.app_id = ?
|
||||
GROUP BY u.department
|
||||
ORDER BY count DESC
|
||||
LIMIT 5
|
||||
`;
|
||||
const deptResult = await this.query(deptSql, [appId]);
|
||||
// 部門使用統計 - 支援日期範圍過濾
|
||||
let deptSql: string;
|
||||
let deptParams: any[];
|
||||
|
||||
// 使用趨勢 - 支援自定義日期範圍
|
||||
if (startDate && endDate) {
|
||||
deptSql = `
|
||||
SELECT
|
||||
u.department,
|
||||
COUNT(*) as count
|
||||
FROM user_views uv
|
||||
JOIN users u ON uv.user_id = u.id
|
||||
WHERE uv.app_id = ? AND DATE(uv.viewed_at) BETWEEN ? AND ?
|
||||
GROUP BY u.department
|
||||
ORDER BY count DESC
|
||||
LIMIT 5
|
||||
`;
|
||||
deptParams = [appId, startDate, endDate];
|
||||
} else {
|
||||
deptSql = `
|
||||
SELECT
|
||||
u.department,
|
||||
COUNT(*) as count
|
||||
FROM user_views uv
|
||||
JOIN users u ON uv.user_id = u.id
|
||||
WHERE uv.app_id = ?
|
||||
GROUP BY u.department
|
||||
ORDER BY count DESC
|
||||
LIMIT 5
|
||||
`;
|
||||
deptParams = [appId];
|
||||
}
|
||||
const deptResult = await this.query(deptSql, deptParams);
|
||||
|
||||
// 使用趨勢 - 支援自定義日期範圍和部門過濾
|
||||
let trendSql: string;
|
||||
let trendParams: any[];
|
||||
|
||||
// 構建部門過濾條件
|
||||
const departmentFilter = department ? 'AND u.department = ?' : '';
|
||||
const baseWhere = `uv.app_id = ? ${departmentFilter}`;
|
||||
|
||||
if (startDate && endDate) {
|
||||
// 使用自定義日期範圍
|
||||
trendSql = `
|
||||
SELECT
|
||||
DATE(viewed_at) as date,
|
||||
COUNT(DISTINCT user_id) as users
|
||||
FROM user_views
|
||||
WHERE app_id = ? AND DATE(viewed_at) BETWEEN ? AND ?
|
||||
GROUP BY DATE(viewed_at)
|
||||
DATE(uv.viewed_at) as date,
|
||||
COUNT(*) as users
|
||||
FROM user_views uv
|
||||
JOIN users u ON uv.user_id = u.id
|
||||
WHERE ${baseWhere} AND DATE(uv.viewed_at) BETWEEN ? AND ?
|
||||
GROUP BY DATE(uv.viewed_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
trendParams = [appId, startDate, endDate];
|
||||
trendParams = department ? [appId, department, startDate, endDate] : [appId, startDate, endDate];
|
||||
} else {
|
||||
// 預設過去7天
|
||||
trendSql = `
|
||||
SELECT
|
||||
DATE(viewed_at) as date,
|
||||
COUNT(DISTINCT user_id) as users
|
||||
FROM user_views
|
||||
WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
||||
GROUP BY DATE(viewed_at)
|
||||
DATE(uv.viewed_at) as date,
|
||||
COUNT(*) as users
|
||||
FROM user_views uv
|
||||
JOIN users u ON uv.user_id = u.id
|
||||
WHERE ${baseWhere} AND uv.viewed_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
||||
GROUP BY DATE(uv.viewed_at)
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
trendParams = [appId];
|
||||
trendParams = department ? [appId, department] : [appId];
|
||||
}
|
||||
|
||||
const trendResult = await this.query(trendSql, trendParams);
|
||||
|
Reference in New Issue
Block a user