HaniBlindBox/server/scripts/migrate_pictures.js
2026-01-02 05:18:05 +08:00

207 lines
7.3 KiB
JavaScript

/**
* 图片数据迁移脚本 - Node.js
* Feature: database-migration, Property 2: 数据记录数一致性
* Validates: Requirements 9.3
*
* 源表: MySQL picture (1,175 条记录)
* 目标表: SQL Server pictures
*
* 字段映射:
* - id -> id
* - imgurl -> img_url
* - token -> token
* - addtime (Unix时间戳) -> created_at (DATETIME2)
* - status -> status
* - type -> type
*/
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
}
};
// 转义SQL字符串
function escapeString(str) {
if (str === null || str === undefined) return 'NULL';
return "N'" + String(str).replace(/'/g, "''") + "'";
}
// Unix时间戳转换为SQL Server DATETIME2格式
function unixToDatetime(timestamp) {
if (!timestamp || timestamp === 0) {
return 'GETDATE()';
}
const date = new Date(timestamp * 1000);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `'${year}-${month}-${day} ${hours}:${minutes}:${seconds}'`;
}
// 获取已迁移的图片ID列表
async function getMigratedIds(pool) {
const result = await pool.request().query('SELECT id FROM pictures');
return new Set(result.recordset.map(r => r.id));
}
// 批量插入图片数据
async function insertPicturesBatch(pool, pictures, batchSize = 100) {
if (pictures.length === 0) return 0;
let insertedCount = 0;
// 分批处理
for (let i = 0; i < pictures.length; i += batchSize) {
const batch = pictures.slice(i, i + batchSize);
let sqlBatch = 'SET IDENTITY_INSERT pictures ON;\n';
for (const pic of batch) {
const typeValue = pic.type !== null && pic.type !== undefined ? pic.type : 'NULL';
sqlBatch += `
INSERT INTO pictures (id, img_url, token, created_at, status, type)
VALUES (${pic.id}, ${escapeString(pic.imgurl)}, ${escapeString(pic.token)}, ${unixToDatetime(pic.addtime)}, ${pic.status}, ${typeValue});
`;
}
sqlBatch += 'SET IDENTITY_INSERT pictures OFF;';
try {
await pool.request().batch(sqlBatch);
insertedCount += batch.length;
console.log(` 批次 ${Math.floor(i / batchSize) + 1}: 成功插入 ${batch.length} 条记录`);
} catch (err) {
console.error(` 批次 ${Math.floor(i / batchSize) + 1} 批量插入失败:`, err.message);
// 如果批量失败,尝试逐条插入
for (const pic of batch) {
try {
const typeValue = pic.type !== null && pic.type !== undefined ? pic.type : 'NULL';
const singleSql = `
SET IDENTITY_INSERT pictures ON;
INSERT INTO pictures (id, img_url, token, created_at, status, type)
VALUES (${pic.id}, ${escapeString(pic.imgurl)}, ${escapeString(pic.token)}, ${unixToDatetime(pic.addtime)}, ${pic.status}, ${typeValue});
SET IDENTITY_INSERT pictures OFF;`;
await pool.request().batch(singleSql);
insertedCount++;
} catch (singleErr) {
console.error(` ✗ 插入图片 ${pic.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, imgurl, token, addtime, status, type
FROM picture
ORDER BY id
`);
console.log(`MySQL 图片总数: ${rows.length}\n`);
// 过滤出未迁移的图片
const picturesToMigrate = rows.filter(pic => !migratedIds.has(pic.id));
console.log(`待迁移图片数: ${picturesToMigrate.length}\n`);
if (picturesToMigrate.length === 0) {
console.log('所有图片数据已迁移完成!');
} else {
// 批量迁移
console.log('开始迁移图片数据...');
const inserted = await insertPicturesBatch(sqlPool, picturesToMigrate);
console.log(`\n迁移完成!共插入 ${inserted} 条记录`);
}
// 验证迁移结果
console.log('\n========================================');
console.log('迁移结果验证');
console.log('========================================');
const [mysqlCount] = await mysqlConn.execute('SELECT COUNT(*) as count FROM picture');
const sqlResult = await sqlPool.request().query('SELECT COUNT(*) as count FROM pictures');
console.log(`MySQL picture 表记录数: ${mysqlCount[0].count}`);
console.log(`SQL Server pictures 表记录数: ${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}`);
}
// 显示迁移统计
console.log('\n迁移统计:');
const stats = await sqlPool.request().query(`
SELECT
COUNT(*) as total,
COUNT(CASE WHEN type IS NULL THEN 1 END) as null_type,
COUNT(CASE WHEN type IS NOT NULL THEN 1 END) as with_type
FROM pictures
`);
console.log(` 总记录数: ${stats.recordset[0].total}`);
console.log(` 有类型: ${stats.recordset[0].with_type}`);
console.log(` 无类型: ${stats.recordset[0].null_type}`);
} catch (err) {
console.error('迁移过程中发生错误:', err);
process.exit(1);
} finally {
// 关闭连接
if (mysqlConn) await mysqlConn.end();
if (sqlPool) await sqlPool.close();
}
}
main();