315 lines
12 KiB
JavaScript
315 lines
12 KiB
JavaScript
/**
|
||
* 商品数据迁移脚本 - 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();
|