HaniBlindBox/server/scripts/migrate_goods.js
2026-01-02 05:18:05 +08:00

315 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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