/** * 商品奖品数据迁移脚本 - Node.js * Feature: database-migration, Property 2: 数据记录数一致性 * Validates: Requirements 2.6 * * 源表: MySQL goods_list (1,852 条记录) * 目标表: SQL Server goods_items * * 字段映射: * - imgurl -> img_url * - imgurl_detail -> img_url_detail * - addtime -> created_at (Unix时间戳转DATETIME2) * - update_time -> updated_at (Unix时间戳转DATETIME2) * - sale_time -> sale_time (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', ' '); } // 转义SQL字符串 function escapeString(str) { if (str === null || str === undefined || str === 'None') 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 goods_items'); return new Set(result.recordset.map(r => r.id)); } // 批量插入商品奖品数据 async function insertGoodsItemsBatch(pool, items) { if (items.length === 0) return 0; let insertedCount = 0; for (const item of items) { try { // 时间戳转换 const createdAt = unixToDatetime(item.addtime) || new Date().toISOString().slice(0, 23).replace('T', ' '); const updatedAt = unixToDatetime(item.update_time) || createdAt; const saleTime = unixToDatetime(item.sale_time); const insertSql = ` SET IDENTITY_INSERT goods_items ON; INSERT INTO goods_items ( id, goods_id, num, title, img_url, stock, surplus_stock, price, money, sc_money, real_pro, goods_type, sale_time, sort, shang_id, reward_num, rank, give_money, special_stock, card_no, prize_code, created_at, updated_at, prize_num, type, lian_ji_type, reward_id, img_url_detail, doubling, goods_list_id, is_lingzhu ) VALUES ( ${item.id}, ${item.goods_id || 0}, ${item.num || 0}, ${escapeString(item.title)}, ${escapeString(item.imgurl)}, ${item.stock || 0}, ${item.surplus_stock || 0}, ${parseFloat(item.price) || 0}, ${parseFloat(item.money) || 0}, ${parseFloat(item.sc_money) || 0}, ${parseFloat(item.real_pro) || 0}, ${item.goods_type || 1}, ${formatDatetime(saleTime)}, ${item.sort || 0}, ${item.shang_id !== null && item.shang_id !== undefined ? item.shang_id : 'NULL'}, ${item.reward_num || 0}, ${item.rank || 0}, ${item.give_money || 0}, ${item.special_stock !== null && item.special_stock !== undefined ? item.special_stock : -100}, ${escapeString(item.card_no)}, ${escapeString(item.prize_code)}, ${formatDatetime(createdAt)}, ${formatDatetime(updatedAt)}, ${item.prize_num || 0}, ${item.type || 0}, ${item.lian_ji_type || 0}, ${escapeString(item.reward_id)}, ${escapeString(item.imgurl_detail)}, ${item.doubling || 0}, ${item.goods_list_id || 0}, ${item.is_lingzhu || 0} ); SET IDENTITY_INSERT goods_items OFF;`; await pool.request().batch(insertSql); insertedCount++; } catch (err) { console.error(`插入商品奖品 ${item.id} (${item.title}) 失败:`, err.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, goods_id, num, title, imgurl, stock, surplus_stock, price, money, sc_money, real_pro, goods_type, sale_time, sort, shang_id, reward_num, \`rank\`, give_money, special_stock, card_no, prize_code, addtime, update_time, prize_num, type, lian_ji_type, reward_id, imgurl_detail, doubling, goods_list_id, is_lingzhu FROM goods_list ORDER BY id `); console.log(`MySQL 商品奖品总数: ${rows.length}\n`); // 过滤出未迁移的商品奖品 const itemsToMigrate = rows.filter(item => !migratedIds.has(item.id)); console.log(`待迁移商品奖品数: ${itemsToMigrate.length}\n`); if (itemsToMigrate.length === 0) { console.log('所有商品奖品数据已迁移完成!'); } else { // 批量迁移(每批50条) const batchSize = 50; let totalInserted = 0; for (let i = 0; i < itemsToMigrate.length; i += batchSize) { const batch = itemsToMigrate.slice(i, i + batchSize); const inserted = await insertGoodsItemsBatch(sqlPool, batch); totalInserted += inserted; console.log(`进度: ${Math.min(i + batchSize, itemsToMigrate.length)}/${itemsToMigrate.length} (本批插入: ${inserted})`); } console.log(`\n迁移完成!共插入 ${totalInserted} 条记录`); } // 验证迁移结果 console.log('\n========================================'); console.log('迁移结果验证'); console.log('========================================'); const [mysqlCount] = await mysqlConn.execute('SELECT COUNT(*) as count FROM goods_list'); const sqlResult = await sqlPool.request().query('SELECT COUNT(*) as count FROM goods_items'); console.log(`MySQL goods_list 表记录数: ${mysqlCount[0].count}`); console.log(`SQL Server goods_items 表记录数: ${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========================================'); console.log('商品类型分布验证'); console.log('========================================'); const [mysqlTypes] = await mysqlConn.execute('SELECT goods_type, COUNT(*) as count FROM goods_list GROUP BY goods_type ORDER BY goods_type'); const sqlTypesResult = await sqlPool.request().query('SELECT goods_type, COUNT(*) as count FROM goods_items GROUP BY goods_type ORDER BY goods_type'); console.log('MySQL 商品类型分布:'); for (const row of mysqlTypes) { console.log(` 类型 ${row.goods_type}: ${row.count} 条`); } console.log('SQL Server 商品类型分布:'); for (const row of sqlTypesResult.recordset) { console.log(` 类型 ${row.goods_type}: ${row.count} 条`); } } catch (err) { console.error('迁移过程中发生错误:', err); process.exit(1); } finally { // 关闭连接 if (mysqlConn) await mysqlConn.end(); if (sqlPool) await sqlPool.close(); } } main();