/** * 商品数据迁移脚本 - Node.js * Feature: database-migration, Property 2: 数据记录数一致性 * Feature: database-migration, Property 5: 业务配置完整性 * Validates: Requirements 2.6, 2.7 * * 源表: MySQL goods (503 条记录) * 目标表: SQL Server goods * * 字段映射: * - imgurl -> img_url * - imgurl_detail -> img_url_detail * - prize_imgurl -> prize_img_url * - addtime -> created_at (Unix时间戳转DATETIME2) * - update_time -> updated_at (Unix时间戳转DATETIME2) * - delete_time -> deleted_at (Unix时间戳转DATETIME2) * - lock_time -> lock_time (Unix时间戳转DATETIME2) * - sale_time -> sale_time (Unix时间戳转DATETIME2) * - flw_start_time -> flw_start_time (Unix时间戳转DATETIME2) * - flw_end_time -> flw_end_time (Unix时间戳转DATETIME2) * - open_time -> open_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 + "'"; } // 格式化MySQL datetime字段 function formatMySqlDatetime(dt) { if (!dt || dt === 'None') return 'NULL'; if (dt instanceof Date) { return "'" + dt.toISOString().slice(0, 23).replace('T', ' ') + "'"; } return "'" + String(dt).slice(0, 23).replace('T', ' ') + "'"; } // 获取已迁移的商品ID列表 async function getMigratedIds(pool) { const result = await pool.request().query('SELECT id FROM goods'); return new Set(result.recordset.map(r => r.id)); } // 批量插入商品数据 async function insertGoodsBatch(pool, goodsList) { if (goodsList.length === 0) return 0; let insertedCount = 0; for (const goods of goodsList) { try { // 时间戳转换 const createdAt = unixToDatetime(goods.addtime) || new Date().toISOString().slice(0, 23).replace('T', ' '); const updatedAt = unixToDatetime(goods.update_time) || createdAt; const deletedAt = unixToDatetime(goods.delete_time); const lockTime = unixToDatetime(goods.lock_time); const saleTime = unixToDatetime(goods.sale_time); const flwStartTime = unixToDatetime(goods.flw_start_time); const flwEndTime = unixToDatetime(goods.flw_end_time); const openTime = unixToDatetime(goods.open_time); const asyncDate = formatMySqlDatetime(goods.async_date); const insertSql = ` SET IDENTITY_INSERT goods ON; INSERT INTO goods ( id, category_id, title, img_url, img_url_detail, price, stock, sale_stock, lock_is, lock_time, coupon_is, coupon_pro, integral_is, prize_num, status, sort, type, show_is, show_price, prize_img_url, card_banner, card_set, card_notice, card_num, sale_time, created_at, updated_at, deleted_at, rage_is, rage, item_card_id, lingzhu_is, lingzhu_fan, lingzhu_shang_id, king_user_id, lian_ji_num, lian_ji_shang_id, is_shou_zhe, new_is, goods_describe, quanju_xiangou, daily_xiangou, day_price, mouth_price, mouth_pay_price, day_pay_price, user_lv, is_flw, flw_start_time, flw_end_time, open_time, is_open, choujiang_xianzhi, async_code, async_date, is_auto_xiajia, xiajia_lirun, xiajia_auto_coushu, xiajia_jine, unlock_amount ) VALUES ( ${goods.id}, ${goods.category_id || 0}, ${escapeString(goods.title)}, ${escapeString(goods.imgurl)}, ${escapeString(goods.imgurl_detail)}, ${parseFloat(goods.price) || 0}, ${goods.stock || 0}, ${goods.sale_stock || 0}, ${goods.lock_is || 0}, ${formatDatetime(lockTime)}, ${goods.coupon_is || 0}, ${goods.coupon_pro || 0}, ${goods.integral_is || 0}, ${goods.prize_num || 0}, ${goods.status || 1}, ${goods.sort || 1}, ${goods.type || 0}, ${goods.show_is || 0}, ${escapeString(goods.show_price)}, ${escapeString(goods.prize_imgurl)}, ${escapeString(goods.card_banner)}, ${escapeString(goods.card_set)}, ${escapeString(goods.card_notice)}, ${goods.card_num || 1}, ${formatDatetime(saleTime)}, ${formatDatetime(createdAt)}, ${formatDatetime(updatedAt)}, ${formatDatetime(deletedAt)}, ${goods.rage_is || 0}, ${goods.rage || 0}, ${goods.item_card_id || 0}, ${goods.lingzhu_is || 0}, ${goods.lingzhu_fan || 0}, ${goods.lingzhu_shang_id || 0}, ${goods.king_user_id || 0}, ${goods.lian_ji_num || 0}, ${goods.lian_ji_shang_id || 0}, ${goods.is_shou_zhe || 0}, ${goods.new_is || 0}, ${escapeString(goods.goods_describe)}, ${goods.quanju_xiangou || 0}, ${goods.daily_xiangou || 0}, ${parseFloat(goods.day_price) || 0}, ${parseFloat(goods.mouth_price) || 0}, ${parseFloat(goods.mouth_pay_price) || 0}, ${parseFloat(goods.day_pay_price) || 0}, ${goods.user_lv !== null && goods.user_lv !== undefined ? goods.user_lv : -1}, ${goods.is_flw || 0}, ${formatDatetime(flwStartTime)}, ${formatDatetime(flwEndTime)}, ${formatDatetime(openTime)}, ${goods.is_open || 0}, ${goods.choujiang_xianzhi || 0}, ${escapeString(goods.async_code)}, ${asyncDate}, ${goods.is_auto_xiajia || 0}, ${goods.xiajia_lirun || 0}, ${goods.xiajia_auto_coushu || 0}, ${goods.xiajia_jine || 0}, ${parseFloat(goods.unlock_amount) || 0} ); SET IDENTITY_INSERT goods OFF;`; await pool.request().batch(insertSql); insertedCount++; } catch (err) { console.error(`插入商品 ${goods.id} (${goods.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, category_id, title, imgurl, imgurl_detail, price, stock, sale_stock, lock_is, lock_time, coupon_is, coupon_pro, integral_is, prize_num, status, sort, type, show_is, show_price, prize_imgurl, card_banner, card_set, card_notice, card_num, sale_time, addtime, update_time, delete_time, rage_is, rage, item_card_id, lingzhu_is, lingzhu_fan, lingzhu_shang_id, king_user_id, lian_ji_num, lian_ji_shang_id, is_shou_zhe, new_is, goods_describe, quanju_xiangou, daily_xiangou, day_price, mouth_price, mouth_pay_price, day_pay_price, user_lv, is_flw, flw_start_time, flw_end_time, open_time, is_open, choujiang_xianzhi, async_code, async_date, is_auto_xiajia, xiajia_lirun, xiajia_auto_coushu, xiajia_jine, unlock_amount FROM goods ORDER BY id `); console.log(`MySQL 商品总数: ${rows.length}\n`); // 过滤出未迁移的商品 const goodsToMigrate = rows.filter(goods => !migratedIds.has(goods.id)); console.log(`待迁移商品数: ${goodsToMigrate.length}\n`); if (goodsToMigrate.length === 0) { console.log('所有商品数据已迁移完成!'); } else { // 批量迁移(每批20条,避免SQL太长) const batchSize = 20; let totalInserted = 0; for (let i = 0; i < goodsToMigrate.length; i += batchSize) { const batch = goodsToMigrate.slice(i, i + batchSize); const inserted = await insertGoodsBatch(sqlPool, batch); totalInserted += inserted; console.log(`进度: ${Math.min(i + batchSize, goodsToMigrate.length)}/${goodsToMigrate.length} (本批插入: ${inserted})`); } console.log(`\n迁移完成!共插入 ${totalInserted} 条记录`); } // 验证迁移结果 console.log('\n========================================'); console.log('迁移结果验证'); console.log('========================================'); const [mysqlCount] = await mysqlConn.execute('SELECT COUNT(*) as count FROM goods'); const sqlResult = await sqlPool.request().query('SELECT COUNT(*) as count FROM goods'); console.log(`MySQL goods 表记录数: ${mysqlCount[0].count}`); console.log(`SQL Server goods 表记录数: ${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}`); } // 验证福利屋配置(is_flw, flw_start_time, flw_end_time) console.log('\n========================================'); console.log('福利屋配置验证'); console.log('========================================'); const [mysqlFlw] = await mysqlConn.execute('SELECT COUNT(*) as count FROM goods WHERE is_flw = 1'); const sqlFlwResult = await sqlPool.request().query('SELECT COUNT(*) as count FROM goods WHERE is_flw = 1'); console.log(`MySQL 福利屋商品数: ${mysqlFlw[0].count}`); console.log(`SQL Server 福利屋商品数: ${sqlFlwResult.recordset[0].count}`); if (mysqlFlw[0].count === sqlFlwResult.recordset[0].count) { console.log('✅ 福利屋配置迁移正确!'); } else { console.log('⚠️ 福利屋配置数量不一致!'); } // 验证限购配置(quanju_xiangou, daily_xiangou, choujiang_xianzhi) console.log('\n========================================'); console.log('限购配置验证'); console.log('========================================'); const [mysqlXiangou] = await mysqlConn.execute('SELECT COUNT(*) as count FROM goods WHERE quanju_xiangou > 0 OR daily_xiangou > 0 OR choujiang_xianzhi > 0'); const sqlXiangouResult = await sqlPool.request().query('SELECT COUNT(*) as count FROM goods WHERE quanju_xiangou > 0 OR daily_xiangou > 0 OR choujiang_xianzhi > 0'); console.log(`MySQL 有限购配置的商品数: ${mysqlXiangou[0].count}`); console.log(`SQL Server 有限购配置的商品数: ${sqlXiangouResult.recordset[0].count}`); if (mysqlXiangou[0].count === sqlXiangouResult.recordset[0].count) { console.log('✅ 限购配置迁移正确!'); } else { console.log('⚠️ 限购配置数量不一致!'); } } catch (err) { console.error('迁移过程中发生错误:', err); process.exit(1); } finally { // 关闭连接 if (mysqlConn) await mysqlConn.end(); if (sqlPool) await sqlPool.close(); } } main();