366 lines
13 KiB
TypeScript
366 lines
13 KiB
TypeScript
// =====================================================
|
|
// 修復的資料庫雙寫同步機制
|
|
// 確保主機和備機使用各自的 ID 序列
|
|
// =====================================================
|
|
|
|
import mysql from 'mysql2/promise';
|
|
import { DatabaseFailover } from './database-failover';
|
|
|
|
export interface WriteResult {
|
|
success: boolean;
|
|
masterSuccess: boolean;
|
|
slaveSuccess: boolean;
|
|
masterError?: string;
|
|
slaveError?: string;
|
|
masterId?: string;
|
|
slaveId?: string;
|
|
}
|
|
|
|
export class DatabaseSyncFixed {
|
|
private masterPool: mysql.Pool | null = null;
|
|
private slavePool: mysql.Pool | null = null;
|
|
private dbFailover: DatabaseFailover;
|
|
|
|
constructor() {
|
|
this.dbFailover = new DatabaseFailover();
|
|
this.initializePools();
|
|
}
|
|
|
|
// 初始化連接池
|
|
private initializePools(): void {
|
|
try {
|
|
// 主機連接池
|
|
this.masterPool = mysql.createPool({
|
|
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',
|
|
connectionLimit: 10,
|
|
acquireTimeout: 60000,
|
|
timeout: 60000,
|
|
reconnect: true
|
|
});
|
|
|
|
// 備機連接池
|
|
this.slavePool = mysql.createPool({
|
|
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',
|
|
charset: 'utf8mb4',
|
|
connectionLimit: 10,
|
|
acquireTimeout: 60000,
|
|
timeout: 60000,
|
|
reconnect: true
|
|
});
|
|
|
|
console.log('✅ 修復的雙寫連接池初始化成功');
|
|
} catch (error) {
|
|
console.error('❌ 修復的雙寫連接池初始化失敗:', error);
|
|
}
|
|
}
|
|
|
|
// 智能雙寫插入 - 每個資料庫使用自己的 ID
|
|
async smartDualInsert(tableName: string, data: Record<string, any>): Promise<WriteResult> {
|
|
const result: WriteResult = {
|
|
success: false,
|
|
masterSuccess: false,
|
|
slaveSuccess: false
|
|
};
|
|
|
|
try {
|
|
// 同時寫入主機和備機,各自生成 ID
|
|
const masterPromise = this.insertToMaster(tableName, data);
|
|
const slavePromise = this.insertToSlave(tableName, data);
|
|
|
|
const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]);
|
|
|
|
result.masterSuccess = masterResult.status === 'fulfilled';
|
|
result.slaveSuccess = slaveResult.status === 'fulfilled';
|
|
result.success = result.masterSuccess || result.slaveSuccess;
|
|
|
|
if (masterResult.status === 'fulfilled') {
|
|
result.masterId = masterResult.value;
|
|
} else {
|
|
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機寫入失敗';
|
|
}
|
|
|
|
if (slaveResult.status === 'fulfilled') {
|
|
result.slaveId = slaveResult.value;
|
|
} else {
|
|
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機寫入失敗';
|
|
}
|
|
|
|
console.log(`📝 智能雙寫結果: 主機${result.masterSuccess ? '✅' : '❌'} 備機${result.slaveSuccess ? '✅' : '❌'}`);
|
|
|
|
} catch (error) {
|
|
result.masterError = error instanceof Error ? error.message : '智能雙寫執行失敗';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// 寫入主機 - 使用主機的 ID 生成
|
|
private async insertToMaster(tableName: string, data: Record<string, any>): Promise<string> {
|
|
if (!this.masterPool) {
|
|
throw new Error('主機連接池不可用');
|
|
}
|
|
|
|
const connection = await this.masterPool.getConnection();
|
|
try {
|
|
// 生成主機的 UUID
|
|
const [uuidResult] = await connection.execute('SELECT UUID() as id');
|
|
const masterId = (uuidResult as any)[0].id;
|
|
|
|
// 構建插入 SQL
|
|
const columns = Object.keys(data).join(', ');
|
|
const placeholders = Object.keys(data).map(() => '?').join(', ');
|
|
const values = Object.values(data);
|
|
|
|
const sql = `INSERT INTO ${tableName} (id, ${columns}) VALUES (?, ${placeholders})`;
|
|
await connection.execute(sql, [masterId, ...values]);
|
|
|
|
console.log(`✅ 主機寫入成功: ${masterId}`);
|
|
return masterId;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
// 寫入備機 - 使用備機的 ID 生成
|
|
private async insertToSlave(tableName: string, data: Record<string, any>): Promise<string> {
|
|
if (!this.slavePool) {
|
|
throw new Error('備機連接池不可用');
|
|
}
|
|
|
|
const connection = await this.slavePool.getConnection();
|
|
try {
|
|
// 生成備機的 UUID
|
|
const [uuidResult] = await connection.execute('SELECT UUID() as id');
|
|
const slaveId = (uuidResult as any)[0].id;
|
|
|
|
// 構建插入 SQL
|
|
const columns = Object.keys(data).join(', ');
|
|
const placeholders = Object.keys(data).map(() => '?').join(', ');
|
|
const values = Object.values(data);
|
|
|
|
const sql = `INSERT INTO ${tableName} (id, ${columns}) VALUES (?, ${placeholders})`;
|
|
await connection.execute(sql, [slaveId, ...values]);
|
|
|
|
console.log(`✅ 備機寫入成功: ${slaveId}`);
|
|
return slaveId;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
// 智能雙寫關聯表 - 使用對應的競賽 ID
|
|
async smartDualInsertRelation(
|
|
relationTable: string,
|
|
competitionId: string,
|
|
slaveCompetitionId: string,
|
|
relationData: any[],
|
|
relationIdField: string
|
|
): Promise<WriteResult> {
|
|
const result: WriteResult = {
|
|
success: false,
|
|
masterSuccess: false,
|
|
slaveSuccess: false
|
|
};
|
|
|
|
try {
|
|
console.log(`🔍 smartDualInsertRelation 開始執行`);
|
|
console.log(` 表名: ${relationTable}`);
|
|
console.log(` 主機競賽 ID: ${competitionId}`);
|
|
console.log(` 備機競賽 ID: ${slaveCompetitionId}`);
|
|
console.log(` 關聯數據:`, relationData);
|
|
console.log(` 關聯字段: ${relationIdField}`);
|
|
|
|
// 先獲取主機的競賽 ID
|
|
const masterCompetitionId = await this.getMasterCompetitionId(competitionId);
|
|
|
|
if (!masterCompetitionId || !slaveCompetitionId) {
|
|
throw new Error('找不到對應的競賽 ID');
|
|
}
|
|
|
|
console.log(`🔍 關聯雙寫開始: ${relationTable}`);
|
|
console.log(` 主機競賽 ID: ${masterCompetitionId}`);
|
|
console.log(` 備機競賽 ID: ${slaveCompetitionId}`);
|
|
console.log(` 關聯數據數量: ${relationData.length}`);
|
|
console.log(` 主機競賽存在: ${!!masterCompetitionId}`);
|
|
console.log(` 備機競賽存在: ${!!slaveCompetitionId}`);
|
|
|
|
// 同時寫入關聯數據
|
|
const masterPromise = this.insertRelationsToMaster(relationTable, masterCompetitionId, relationData, relationIdField);
|
|
const slavePromise = this.insertRelationsToSlave(relationTable, slaveCompetitionId, relationData, relationIdField);
|
|
|
|
const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]);
|
|
|
|
result.masterSuccess = masterResult.status === 'fulfilled';
|
|
result.slaveSuccess = slaveResult.status === 'fulfilled';
|
|
result.success = result.masterSuccess || result.slaveSuccess;
|
|
|
|
if (masterResult.status === 'rejected') {
|
|
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機關聯寫入失敗';
|
|
console.error(`❌ 主機關聯寫入失敗:`, masterResult.reason);
|
|
}
|
|
if (slaveResult.status === 'rejected') {
|
|
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機關聯寫入失敗';
|
|
console.error(`❌ 備機關聯寫入失敗:`, slaveResult.reason);
|
|
}
|
|
|
|
console.log(`📝 關聯雙寫結果: 主機${result.masterSuccess ? '✅' : '❌'} 備機${result.slaveSuccess ? '✅' : '❌'}`);
|
|
|
|
} catch (error) {
|
|
result.masterError = error instanceof Error ? error.message : '關聯雙寫執行失敗';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// 獲取主機競賽 ID
|
|
private async getMasterCompetitionId(competitionId: string): Promise<string | null> {
|
|
if (!this.masterPool) return null;
|
|
|
|
const connection = await this.masterPool.getConnection();
|
|
try {
|
|
const [rows] = await connection.execute('SELECT id FROM competitions WHERE id = ?', [competitionId]);
|
|
return (rows as any[])[0]?.id || null;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
// 獲取備機競賽 ID
|
|
private async getSlaveCompetitionId(competitionId: string): Promise<string | null> {
|
|
if (!this.slavePool) return null;
|
|
|
|
const connection = await this.slavePool.getConnection();
|
|
try {
|
|
const [rows] = await connection.execute('SELECT id FROM competitions WHERE id = ?', [competitionId]);
|
|
return (rows as any[])[0]?.id || null;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
// 獲取競賽信息
|
|
private async getCompetitionById(competitionId: string): Promise<any> {
|
|
if (!this.masterPool) return null;
|
|
|
|
const connection = await this.masterPool.getConnection();
|
|
try {
|
|
const [rows] = await connection.execute('SELECT * FROM competitions WHERE id = ?', [competitionId]);
|
|
return (rows as any[])[0] || null;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
// 根據名稱獲取備機競賽 ID
|
|
private async getSlaveCompetitionIdByName(name: string): Promise<string | null> {
|
|
if (!this.slavePool) return null;
|
|
|
|
console.log(`🔍 getSlaveCompetitionIdByName 調用,名稱:`, name);
|
|
|
|
const connection = await this.slavePool.getConnection();
|
|
try {
|
|
const [rows] = await connection.execute('SELECT id FROM competitions WHERE name = ? ORDER BY created_at DESC LIMIT 1', [name]);
|
|
console.log(`🔍 備機查詢結果:`, rows);
|
|
const result = (rows as any[])[0]?.id || null;
|
|
console.log(`🔍 備機競賽 ID 結果:`, result, typeof result);
|
|
|
|
// 確保返回的是字符串
|
|
if (result && typeof result !== 'string') {
|
|
console.log(`⚠️ 備機競賽 ID 不是字符串,轉換為字符串:`, String(result));
|
|
return String(result);
|
|
}
|
|
|
|
return result;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
// 寫入主機關聯表
|
|
private async insertRelationsToMaster(
|
|
relationTable: string,
|
|
competitionId: string,
|
|
relationData: any[],
|
|
relationIdField: string
|
|
): Promise<void> {
|
|
if (!this.masterPool) return;
|
|
|
|
const connection = await this.masterPool.getConnection();
|
|
try {
|
|
for (const data of relationData) {
|
|
const [uuidResult] = await connection.execute('SELECT UUID() as id');
|
|
const relationId = (uuidResult as any)[0].id;
|
|
|
|
const sql = `INSERT INTO ${relationTable} (id, competition_id, ${relationIdField}) VALUES (?, ?, ?)`;
|
|
await connection.execute(sql, [relationId, competitionId, data[relationIdField]]);
|
|
}
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
// 寫入備機關聯表
|
|
private async insertRelationsToSlave(
|
|
relationTable: string,
|
|
competitionId: string,
|
|
relationData: any[],
|
|
relationIdField: string
|
|
): Promise<void> {
|
|
if (!this.slavePool) return;
|
|
|
|
console.log(`🔍 備機關聯寫入開始: ${relationTable}`);
|
|
console.log(` 備機競賽 ID: ${competitionId}`);
|
|
console.log(` 關聯數據:`, relationData);
|
|
console.log(` 關聯字段: ${relationIdField}`);
|
|
|
|
const connection = await this.slavePool.getConnection();
|
|
try {
|
|
for (const data of relationData) {
|
|
const [uuidResult] = await connection.execute('SELECT UUID() as id');
|
|
const relationId = (uuidResult as any)[0].id;
|
|
|
|
console.log(`🔍 準備插入關聯數據:`, {
|
|
relationId,
|
|
competitionId,
|
|
relationField: relationIdField,
|
|
relationValue: data[relationIdField]
|
|
});
|
|
|
|
const sql = `INSERT INTO ${relationTable} (id, competition_id, ${relationIdField}) VALUES (?, ?, ?)`;
|
|
console.log(`🔍 執行 SQL:`, sql);
|
|
console.log(`🔍 參數:`, [relationId, competitionId, data[relationIdField]]);
|
|
|
|
await connection.execute(sql, [relationId, competitionId, data[relationIdField]]);
|
|
console.log(`✅ 備機關聯數據插入成功`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ 備機關聯寫入失敗:`, error);
|
|
throw error;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
// 清理資源
|
|
async close(): Promise<void> {
|
|
if (this.masterPool) {
|
|
await this.masterPool.end();
|
|
}
|
|
if (this.slavePool) {
|
|
await this.slavePool.end();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 導出實例
|
|
export const dbSyncFixed = new DatabaseSyncFixed();
|