HaniBlindBox/server/scripts/migrate_float_ball.js
2026-01-03 21:06:26 +08:00

268 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 悬浮球配置数据迁移脚本 - Node.js
* Feature: float-ball-migration, Property 1: Data Migration Record Count Consistency
* Feature: float-ball-migration, Property 2: Data Migration ID Preservation
* Feature: float-ball-migration, Property 3: Timestamp Transformation Validity
* Feature: float-ball-migration, Property 8: Incremental Migration Idempotence
* Validates: Requirements 2.1, 2.2, 2.3, 2.5, 2.6
*
* 源表: MySQL float_ball_config
* 目标表: SQL Server float_ball_configs
*/
const mysql = require('mysql2/promise');
const sql = require('mssql');
// MySQL 配置
const mysqlConfig = {
host: '192.168.195.16',
port: 1887,
user: 'root',
password: 'Dbt@com@123',
database: 'youdas',
charset: 'utf8mb4'
};
// SQL Server 配置
const sqlServerConfig = {
server: '192.168.195.15',
port: 1433,
user: 'sa',
password: 'Dbt@com@123',
database: 'honey_box',
options: {
encrypt: false,
trustServerCertificate: true
}
};
// Unix时间戳转换为 SQL Server DATETIME2 格式
function unixToDatetime(timestamp) {
if (!timestamp || timestamp === 0) {
return null;
}
const date = new Date(timestamp * 1000);
return date.toISOString().slice(0, 23).replace('T', ' ');
}
// 转义SQL字符串
function escapeString(str) {
if (str === null || str === undefined) return 'NULL';
return "N'" + String(str).replace(/'/g, "''") + "'";
}
// 格式化日期时间
function formatDatetime(dt) {
if (!dt) return 'NULL';
return "'" + dt + "'";
}
// 获取已迁移的悬浮球配置ID列表
async function getMigratedIds(pool) {
const result = await pool.request().query('SELECT id FROM float_ball_configs');
return new Set(result.recordset.map(r => r.id));
}
// 批量插入悬浮球配置数据
async function insertFloatBallsBatch(pool, floatBalls) {
if (floatBalls.length === 0) return 0;
let insertedCount = 0;
// 构建批量插入SQL
let sqlBatch = 'SET IDENTITY_INSERT float_ball_configs ON;\n';
for (const fb of floatBalls) {
const createdAt = unixToDatetime(fb.create_time) || new Date().toISOString().slice(0, 23).replace('T', ' ');
const updatedAt = unixToDatetime(fb.update_time) || createdAt;
sqlBatch += `
INSERT INTO float_ball_configs (
id, status, type, image, link_url, position_x, position_y, width, height, effect,
title, image_details, image_bj, image_details_x, image_details_y, image_details_w, image_details_h,
created_at, updated_at
) VALUES (
${fb.id},
${fb.status || 0},
${fb.type || 1},
${escapeString(fb.image || '')},
${escapeString(fb.link_url || '')},
${escapeString(fb.position_x || '')},
${escapeString(fb.position_y || '')},
${escapeString(fb.width || '')},
${escapeString(fb.height || '')},
${fb.effect || 0},
${escapeString(fb.title)},
${escapeString(fb.image_details)},
${escapeString(fb.image_bj)},
${escapeString(fb.image_details_x)},
${escapeString(fb.image_details_y)},
${escapeString(fb.image_details_w)},
${escapeString(fb.image_details_h)},
${formatDatetime(createdAt)},
${formatDatetime(updatedAt)}
);
`;
}
sqlBatch += 'SET IDENTITY_INSERT float_ball_configs OFF;';
try {
await pool.request().batch(sqlBatch);
insertedCount = floatBalls.length;
} catch (err) {
console.error('批量插入失败:', err.message);
// 如果批量失败,尝试逐条插入
for (const fb of floatBalls) {
try {
const createdAt = unixToDatetime(fb.create_time) || new Date().toISOString().slice(0, 23).replace('T', ' ');
const updatedAt = unixToDatetime(fb.update_time) || createdAt;
const singleSql = `
SET IDENTITY_INSERT float_ball_configs ON;
INSERT INTO float_ball_configs (
id, status, type, image, link_url, position_x, position_y, width, height, effect,
title, image_details, image_bj, image_details_x, image_details_y, image_details_w, image_details_h,
created_at, updated_at
) VALUES (
${fb.id},
${fb.status || 0},
${fb.type || 1},
${escapeString(fb.image || '')},
${escapeString(fb.link_url || '')},
${escapeString(fb.position_x || '')},
${escapeString(fb.position_y || '')},
${escapeString(fb.width || '')},
${escapeString(fb.height || '')},
${fb.effect || 0},
${escapeString(fb.title)},
${escapeString(fb.image_details)},
${escapeString(fb.image_bj)},
${escapeString(fb.image_details_x)},
${escapeString(fb.image_details_y)},
${escapeString(fb.image_details_w)},
${escapeString(fb.image_details_h)},
${formatDatetime(createdAt)},
${formatDatetime(updatedAt)}
);
SET IDENTITY_INSERT float_ball_configs OFF;`;
await pool.request().batch(singleSql);
insertedCount++;
console.log(` ✓ 插入悬浮球配置 ${fb.id} 成功`);
} catch (singleErr) {
console.error(` ✗ 插入悬浮球配置 ${fb.id} 失败:`, singleErr.message);
}
}
}
return insertedCount;
}
async function main() {
console.log('========================================');
console.log('悬浮球配置数据迁移脚本 - Node.js');
console.log('========================================\n');
let mysqlConn = null;
let sqlPool = null;
try {
// 连接 MySQL
console.log('正在连接 MySQL...');
mysqlConn = await mysql.createConnection(mysqlConfig);
console.log('MySQL 连接成功\n');
// 连接 SQL Server
console.log('正在连接 SQL Server...');
sqlPool = await sql.connect(sqlServerConfig);
console.log('SQL Server 连接成功\n');
// 获取已迁移的ID支持增量迁移
console.log('正在获取已迁移的悬浮球配置ID...');
const migratedIds = await getMigratedIds(sqlPool);
console.log(`已迁移悬浮球配置数: ${migratedIds.size}\n`);
// 从 MySQL 获取所有悬浮球配置数据
console.log('正在从 MySQL 读取悬浮球配置数据...');
const [rows] = await mysqlConn.execute(`
SELECT id, status, type, image, link_url, position_x, position_y, width, height, effect,
create_time, update_time, title, image_details, image_bj,
image_details_x, image_details_y, image_details_w, image_details_h
FROM float_ball_config
ORDER BY id
`);
console.log(`MySQL 悬浮球配置总数: ${rows.length}\n`);
// 过滤出未迁移的配置(增量迁移)
const floatBallsToMigrate = rows.filter(fb => !migratedIds.has(fb.id));
console.log(`待迁移悬浮球配置数: ${floatBallsToMigrate.length}\n`);
if (floatBallsToMigrate.length === 0) {
console.log('所有悬浮球配置数据已迁移完成!');
} else {
// 批量迁移每批50条
const batchSize = 50;
let totalInserted = 0;
for (let i = 0; i < floatBallsToMigrate.length; i += batchSize) {
const batch = floatBallsToMigrate.slice(i, i + batchSize);
const inserted = await insertFloatBallsBatch(sqlPool, batch);
totalInserted += inserted;
console.log(`进度: ${Math.min(i + batchSize, floatBallsToMigrate.length)}/${floatBallsToMigrate.length} (本批插入: ${inserted})`);
}
console.log(`\n迁移完成!共插入 ${totalInserted} 条记录`);
}
// 验证迁移结果
console.log('\n========================================');
console.log('迁移结果验证');
console.log('========================================');
const [mysqlCount] = await mysqlConn.execute('SELECT COUNT(*) as count FROM float_ball_config');
const sqlResult = await sqlPool.request().query('SELECT COUNT(*) as count FROM float_ball_configs');
console.log(`MySQL float_ball_config 表记录数: ${mysqlCount[0].count}`);
console.log(`SQL Server float_ball_configs 表记录数: ${sqlResult.recordset[0].count}`);
if (mysqlCount[0].count === sqlResult.recordset[0].count) {
console.log('\n✅ 数据迁移完成,记录数一致!');
} else {
console.log(`\n⚠️ 记录数不一致,差异: ${mysqlCount[0].count - sqlResult.recordset[0].count}`);
}
// 验证ID保留
console.log('\n验证ID保留...');
const [mysqlIds] = await mysqlConn.execute('SELECT id FROM float_ball_config ORDER BY id');
const sqlIds = await sqlPool.request().query('SELECT id FROM float_ball_configs ORDER BY id');
const mysqlIdSet = new Set(mysqlIds.map(r => r.id));
const sqlIdSet = new Set(sqlIds.recordset.map(r => r.id));
let idMismatch = false;
for (const id of mysqlIdSet) {
if (!sqlIdSet.has(id)) {
console.log(` ⚠️ MySQL ID ${id} 未在 SQL Server 中找到`);
idMismatch = true;
}
}
if (!idMismatch) {
console.log('✅ 所有ID已正确保留');
}
} catch (err) {
console.error('迁移过程中发生错误:', err);
process.exit(1);
} finally {
// 关闭连接
if (mysqlConn) await mysqlConn.end();
if (sqlPool) await sqlPool.close();
}
}
main();