/** * VIP等级奖品数据迁移脚本 - Node.js * Feature: database-migration, Property 2: 数据记录数一致性 * Validates: Requirements 8.2 * * 源表: MySQL quan_yi_level_jiang (225 条记录) * 目标表: SQL Server vip_level_rewards * * 字段映射: * - qy_level_id -> vip_level_id * - qy_level -> vip_level * - imgurl -> img_url * - shang_id -> prize_level_id * - addtime -> created_at (Unix时间戳转DATETIME2) * - updatetime -> updated_at (Unix时间戳转DATETIME2) * - deltime -> deleted_at (Unix时间戳转DATETIME2) */ 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', ' '); } // 格式化日期时间 function formatDatetime(dt) { if (!dt) return 'NULL'; return "'" + dt + "'"; } // 转义SQL字符串 function escapeString(str) { if (str === null || str === undefined) return 'NULL'; return "N'" + String(str).replace(/'/g, "''") + "'"; } // 格式化数值 function formatNumber(val, defaultVal = 0) { if (val === null || val === undefined) return defaultVal; return val; } // 格式化可空整数 function formatNullableInt(val) { if (val === null || val === undefined || val === 0) return 'NULL'; return val; } // 获取已迁移的ID列表 async function getMigratedIds(pool) { const result = await pool.request().query('SELECT id FROM vip_level_rewards'); return new Set(result.recordset.map(r => r.id)); } // 批量插入VIP等级奖品数据 async function insertVipLevelRewardsBatch(pool, rewards) { if (rewards.length === 0) return 0; let insertedCount = 0; // 构建批量插入SQL let sqlBatch = 'SET IDENTITY_INSERT vip_level_rewards ON;\n'; for (const reward of rewards) { const createdAt = unixToDatetime(reward.addtime) || new Date().toISOString().slice(0, 23).replace('T', ' '); const updatedAt = unixToDatetime(reward.updatetime) || createdAt; const deletedAt = unixToDatetime(reward.deltime); // 处理imgurl,如果是'0'则转为NULL const imgUrl = (reward.imgurl === '0' || reward.imgurl === 0 || !reward.imgurl) ? null : reward.imgurl; sqlBatch += ` INSERT INTO vip_level_rewards ( id, vip_level_id, vip_level, type, title, jian_price, coupon_id, man_price, probability, effective_day, z_num, img_url, prize_level_id, jiang_price, money, sc_money, prize_code, created_at, updated_at, deleted_at ) VALUES ( ${reward.id}, ${reward.qy_level_id}, ${formatNumber(reward.qy_level)}, ${reward.type || 0}, ${escapeString(reward.title)}, ${formatNumber(reward.jian_price, 0.00)}, ${formatNullableInt(reward.coupon_id)}, ${formatNumber(reward.man_price, 0.00)}, ${formatNumber(reward.probability, 0.00)}, ${formatNullableInt(reward.effective_day)}, ${formatNumber(reward.z_num, 0)}, ${escapeString(imgUrl)}, ${formatNullableInt(reward.shang_id)}, ${formatNumber(reward.jiang_price, 0.00)}, ${formatNumber(reward.money, 0.00)}, ${formatNumber(reward.sc_money, 0.00)}, ${escapeString(reward.prize_code)}, ${formatDatetime(createdAt)}, ${formatDatetime(updatedAt)}, ${formatDatetime(deletedAt)} ); `; } sqlBatch += 'SET IDENTITY_INSERT vip_level_rewards OFF;'; try { await pool.request().batch(sqlBatch); insertedCount = rewards.length; } catch (err) { console.error('批量插入失败:', err.message); // 如果批量失败,尝试逐条插入 for (const reward of rewards) { try { const createdAt = unixToDatetime(reward.addtime) || new Date().toISOString().slice(0, 23).replace('T', ' '); const updatedAt = unixToDatetime(reward.updatetime) || createdAt; const deletedAt = unixToDatetime(reward.deltime); const imgUrl = (reward.imgurl === '0' || reward.imgurl === 0 || !reward.imgurl) ? null : reward.imgurl; const singleSql = ` SET IDENTITY_INSERT vip_level_rewards ON; INSERT INTO vip_level_rewards ( id, vip_level_id, vip_level, type, title, jian_price, coupon_id, man_price, probability, effective_day, z_num, img_url, prize_level_id, jiang_price, money, sc_money, prize_code, created_at, updated_at, deleted_at ) VALUES ( ${reward.id}, ${reward.qy_level_id}, ${formatNumber(reward.qy_level)}, ${reward.type || 0}, ${escapeString(reward.title)}, ${formatNumber(reward.jian_price, 0.00)}, ${formatNullableInt(reward.coupon_id)}, ${formatNumber(reward.man_price, 0.00)}, ${formatNumber(reward.probability, 0.00)}, ${formatNullableInt(reward.effective_day)}, ${formatNumber(reward.z_num, 0)}, ${escapeString(imgUrl)}, ${formatNullableInt(reward.shang_id)}, ${formatNumber(reward.jiang_price, 0.00)}, ${formatNumber(reward.money, 0.00)}, ${formatNumber(reward.sc_money, 0.00)}, ${escapeString(reward.prize_code)}, ${formatDatetime(createdAt)}, ${formatDatetime(updatedAt)}, ${formatDatetime(deletedAt)} ); SET IDENTITY_INSERT vip_level_rewards OFF;`; await pool.request().batch(singleSql); insertedCount++; } catch (singleErr) { console.error(`插入VIP等级奖品 ${reward.id} 失败:`, singleErr.message); } } } return insertedCount; } async function main() { console.log('========================================'); console.log('VIP等级奖品数据迁移脚本 - 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('正在获取已迁移的VIP等级奖品ID...'); const migratedIds = await getMigratedIds(sqlPool); console.log(`已迁移VIP等级奖品数: ${migratedIds.size}\n`); // 从 MySQL 获取所有VIP等级奖品数据 console.log('正在从 MySQL 读取VIP等级奖品数据...'); const [rows] = await mysqlConn.execute(` SELECT id, qy_level_id, qy_level, type, title, jian_price, coupon_id, man_price, probability, effective_day, z_num, imgurl, shang_id, jiang_price, money, sc_money, prize_code, addtime, updatetime, deltime FROM quan_yi_level_jiang ORDER BY id `); console.log(`MySQL VIP等级奖品总数: ${rows.length}\n`); // 过滤出未迁移的记录 const rewardsToMigrate = rows.filter(reward => !migratedIds.has(reward.id)); console.log(`待迁移VIP等级奖品数: ${rewardsToMigrate.length}\n`); if (rewardsToMigrate.length === 0) { console.log('所有VIP等级奖品数据已迁移完成!'); } else { // 批量迁移(每批50条) const batchSize = 50; let totalInserted = 0; for (let i = 0; i < rewardsToMigrate.length; i += batchSize) { const batch = rewardsToMigrate.slice(i, i + batchSize); const inserted = await insertVipLevelRewardsBatch(sqlPool, batch); totalInserted += inserted; console.log(`进度: ${Math.min(i + batchSize, rewardsToMigrate.length)}/${rewardsToMigrate.length} (本批插入: ${inserted})`); } console.log(`\n迁移完成!共插入 ${totalInserted} 条记录`); } // 验证迁移结果 console.log('\n========================================'); console.log('迁移结果验证'); console.log('========================================'); const [mysqlCount] = await mysqlConn.execute('SELECT COUNT(*) as count FROM quan_yi_level_jiang'); const sqlResult = await sqlPool.request().query('SELECT COUNT(*) as count FROM vip_level_rewards'); console.log(`MySQL quan_yi_level_jiang 表记录数: ${mysqlCount[0].count}`); console.log(`SQL Server vip_level_rewards 表记录数: ${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}`); } } catch (err) { console.error('迁移过程中发生错误:', err); process.exit(1); } finally { // 关闭连接 if (mysqlConn) await mysqlConn.end(); if (sqlPool) await sqlPool.close(); } } main();