/** * 广告数据迁移脚本 - Node.js * Feature: database-migration, Property 2: 数据记录数一致性 * Validates: Requirements 9.4 * * 源表: MySQL advert (16 条记录) * 目标表: SQL Server adverts * * 字段映射: * - id -> id * - imgurl -> img_url * - url -> url * - sort -> sort * - type -> type * - addtime (Unix时间戳) -> created_at (DATETIME2) * - update_time (Unix时间戳) -> updated_at (DATETIME2) * - ttype -> ttype * - coupon_id -> coupon_id * - goods_id -> goods_id */ 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 adverts'); return new Set(result.recordset.map(r => r.id)); } // 批量插入广告数据 async function insertAdvertsBatch(pool, adverts) { if (adverts.length === 0) return 0; let insertedCount = 0; // 构建批量插入SQL let sqlBatch = 'SET IDENTITY_INSERT adverts ON;\n'; for (const advert of adverts) { const ttypeValue = advert.ttype !== null && advert.ttype !== undefined ? advert.ttype : 'NULL'; const couponIdValue = advert.coupon_id !== null && advert.coupon_id !== undefined ? advert.coupon_id : 'NULL'; const goodsIdValue = advert.goods_id !== null && advert.goods_id !== undefined ? advert.goods_id : 'NULL'; sqlBatch += ` INSERT INTO adverts (id, img_url, url, sort, type, created_at, updated_at, ttype, coupon_id, goods_id) VALUES (${advert.id}, ${escapeString(advert.imgurl)}, ${escapeString(advert.url)}, ${advert.sort}, ${advert.type}, ${unixToDatetime(advert.addtime)}, ${unixToDatetime(advert.update_time)}, ${ttypeValue}, ${couponIdValue}, ${goodsIdValue}); `; } sqlBatch += 'SET IDENTITY_INSERT adverts OFF;'; try { await pool.request().batch(sqlBatch); insertedCount = adverts.length; console.log(` 批量插入成功: ${adverts.length} 条记录`); } catch (err) { console.error('批量插入失败:', err.message); // 如果批量失败,尝试逐条插入 for (const advert of adverts) { try { const ttypeValue = advert.ttype !== null && advert.ttype !== undefined ? advert.ttype : 'NULL'; const couponIdValue = advert.coupon_id !== null && advert.coupon_id !== undefined ? advert.coupon_id : 'NULL'; const goodsIdValue = advert.goods_id !== null && advert.goods_id !== undefined ? advert.goods_id : 'NULL'; const singleSql = ` SET IDENTITY_INSERT adverts ON; INSERT INTO adverts (id, img_url, url, sort, type, created_at, updated_at, ttype, coupon_id, goods_id) VALUES (${advert.id}, ${escapeString(advert.imgurl)}, ${escapeString(advert.url)}, ${advert.sort}, ${advert.type}, ${unixToDatetime(advert.addtime)}, ${unixToDatetime(advert.update_time)}, ${ttypeValue}, ${couponIdValue}, ${goodsIdValue}); SET IDENTITY_INSERT adverts OFF;`; await pool.request().batch(singleSql); insertedCount++; console.log(` ✓ 插入广告 ${advert.id}`); } catch (singleErr) { console.error(` ✗ 插入广告 ${advert.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, url, sort, type, addtime, update_time, ttype, coupon_id, goods_id FROM advert ORDER BY id `); console.log(`MySQL 广告总数: ${rows.length}\n`); // 显示广告列表 console.log('广告列表:'); for (const row of rows) { const status = migratedIds.has(row.id) ? '已迁移' : '待迁移'; console.log(` ${row.id}. type=${row.type}, sort=${row.sort} [${status}]`); } console.log(''); // 过滤出未迁移的广告 const advertsToMigrate = rows.filter(advert => !migratedIds.has(advert.id)); console.log(`待迁移广告数: ${advertsToMigrate.length}\n`); if (advertsToMigrate.length === 0) { console.log('所有广告数据已迁移完成!'); } else { // 批量迁移 console.log('开始迁移广告数据...'); const inserted = await insertAdvertsBatch(sqlPool, advertsToMigrate); console.log(`\n迁移完成!共插入 ${inserted} 条记录`); } // 验证迁移结果 console.log('\n========================================'); console.log('迁移结果验证'); console.log('========================================'); const [mysqlCount] = await mysqlConn.execute('SELECT COUNT(*) as count FROM advert'); const sqlResult = await sqlPool.request().query('SELECT COUNT(*) as count FROM adverts'); console.log(`MySQL advert 表记录数: ${mysqlCount[0].count}`); console.log(`SQL Server adverts 表记录数: ${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(DISTINCT type) as type_count, COUNT(CASE WHEN goods_id > 0 THEN 1 END) as with_goods, COUNT(CASE WHEN coupon_id > 0 THEN 1 END) as with_coupon FROM adverts `); console.log(` 总记录数: ${stats.recordset[0].total}`); console.log(` 广告类型数: ${stats.recordset[0].type_count}`); console.log(` 关联商品: ${stats.recordset[0].with_goods}`); console.log(` 关联优惠券: ${stats.recordset[0].with_coupon}`); } catch (err) { console.error('迁移过程中发生错误:', err); process.exit(1); } finally { // 关闭连接 if (mysqlConn) await mysqlConn.end(); if (sqlPool) await sqlPool.close(); } } main();