require('dotenv').config(); const fs = require('fs'); const path = require('path'); const Price = require('../src/models/Price'); const SteelPriceCollector = require('../commonApi'); /** * 数据导入脚本 * 支持两种导入模式: * 1. 从本地 JSON 文件导入 * 2. 从 commonApi.js 接口实时获取并导入 */ // 数据源映射配置 const DATA_SOURCES = { // 本地文件模式 LOCAL: { 刚协指导价: { file: '刚协指导价.json', source: '云南钢协', sourceCode: 'YUNNAN_STEEL_ASSOC', priceField: 'PR_PRICESET_HANGPRICE', description: '云南钢协指导价', colorTag: '#FF6B6B' }, 钢材网架: { file: '钢材网架.json', source: '我的钢铁', sourceCode: 'MY_STEEL', priceField: 'PR_PRICESET_HANGPRICE', description: '我的钢铁网价格', colorTag: '#4ECDC4' }, 钢厂指导价: { file: '钢厂指导价.json', source: '德钢指导价', sourceCode: 'DE_STEEL_FACTORY', priceField: 'PR_PRICESET_HANGPRICE', description: '德钢钢厂指导价', colorTag: '#95E1D3' } }, // API 接口模式 API: { DEFAULT: { source: '云南钢协', sourceCode: 'YUNNAN_STEEL_ASSOC', priceField: 'PR_PRICESET_HANGPRICE', endpoint: 'DEFAULT', description: '云南钢协指导价(API)', colorTag: '#FF6B6B' }, BACKUP: { source: '我的钢铁', sourceCode: 'MY_STEEL', priceField: 'PR_PRICESET_HANGPRICE', endpoint: 'BACKUP', description: '我的钢铁网价格(API)', colorTag: '#4ECDC4' }, EXTENDED: { source: '德钢指导价', sourceCode: 'DE_STEEL_FACTORY', priceField: 'PR_PRICESET_HANGPRICE', endpoint: 'EXTENDED', description: '德钢钢厂指导价(API)', colorTag: '#95E1D3' } } }; /** * 转换数据格式 * @param {Object} rawData - 原始数据 * @param {Object} config - 数据源配置对象 * @returns {Array} 转换后的数据数组 */ function transformData(rawData, config) { if (!rawData || !rawData.data || !rawData.data.page || !rawData.data.page.result) { return []; } return rawData.data.page.result.map(item => { return { price_id: item.PRICE_ID || null, goods_material: item.GOODS_MATERIAL || '未知', goods_spec: item.GOODS_SPEC || '未知', partsname_name: item.PARTSNAME_NAME || '未知', productarea_name: item.PRODUCTAREA_NAME || '未知', // 数据源标识 - 使用 sourceCode 作为主要标识 price_source: item.PR_PRICE_SOURCE || config.source, // 新增:数据源代码(明确的英文标识) price_source_code: config.sourceCode, // 新增:数据源描述 price_source_desc: config.description, // 新增:数据来源标识(本地文件或 API) data_origin: config.endpoint ? `API:${config.endpoint}` : 'LOCAL_FILE', price_region: item.PR_PRICE_REGION || '未知', pntree_name: item.PNTREE_NAME || '钢筋', price_date: item.PIRCE_DATE || null, make_price: item.PR_PRICESET_MAKEPRICE || null, hang_price: item[config.priceField] || 0, last_make_price: item.PR_LAST_PRICESET_MAKEPRICE || null, last_hang_price: item.PR_LAST_PRICESET_HANGPRICE || 0, make_price_updw: item.PR_MAKEPRICE_UPDW || null, hang_price_updw: item.PR_HANGPRICE_UPDW || '0', operator_code: item.OPERATOR_CODE || null, operator_name: item.OPERATOR_NAME || null }; }).filter(item => item.price_date && item.hang_price > 0); // 过滤无效数据 } /** * 导入单个本地数据文件 * @param {string} filePath - 文件路径 * @param {Object} config - 数据源配置 */ async function importLocalFile(filePath, config) { try { console.log(`\n📄 正在读取本地文件: ${path.basename(filePath)}`); console.log(` 📊 数据源: ${config.description}`); console.log(` 🏷️ 标识码: ${config.sourceCode}`); console.log(` 🎨 标签: ${config.colorTag}`); // 读取 JSON 文件 const rawData = JSON.parse(fs.readFileSync(filePath, 'utf8')); // 转换数据格式 const prices = transformData(rawData, config); console.log(`✅ 解析到 ${prices.length} 条有效数据`); if (prices.length === 0) { console.log('⚠️ 没有有效数据可导入'); return 0; } // 批量插入数据库(每批 1000 条) const batchSize = 1000; let totalImported = 0; for (let i = 0; i < prices.length; i += batchSize) { const batch = prices.slice(i, i + batchSize); const imported = await Price.batchInsert(batch); totalImported += imported; console.log(` 进度: ${Math.min(i + batchSize, prices.length)}/${prices.length} 条`); } console.log(`✅ 成功导入 ${totalImported} 条数据`); return totalImported; } catch (error) { console.error(`❌ 导入本地文件失败 ${path.basename(filePath)}:`, error.message); return 0; } } /** * 从 API 接口导入数据 * @param {string} endpointKey - 接口键名 ('DEFAULT' | 'BACKUP' | 'EXTENDED') * @param {Object} params - API 查询参数 * @param {string} params.startDate - 开始日期 * @param {string} params.endDate - 结束日期 * @param {number} params.pageSize - 每页数量 */ async function importFromAPI(endpointKey, params = {}) { try { const config = DATA_SOURCES.API[endpointKey]; if (!config) { throw new Error(`未知的 API 端点: ${endpointKey}`); } console.log(`\n🌐 正在从 API 接口获取数据: ${endpointKey}`); console.log(` 📊 数据源: ${config.description}`); console.log(` 🏷️ 标识码: ${config.sourceCode}`); console.log(` 🎨 标签: ${config.colorTag}`); console.log(` 📅 查询参数:`, JSON.stringify(params, null, 2)); // 创建采集器实例并切换到指定端点 const collector = new SteelPriceCollector(); collector.useEndpoint(endpointKey); // 获取数据 const result = await collector.fetchPrices(params); if (!result.success) { throw new Error(`API 请求失败: ${result.error}`); } // 转换数据格式 const prices = transformData(result.data, config); console.log(`✅ 解析到 ${prices.length} 条有效数据`); if (prices.length === 0) { console.log('⚠️ 没有有效数据可导入'); return 0; } // 批量插入数据库 const batchSize = 1000; let totalImported = 0; for (let i = 0; i < prices.length; i += batchSize) { const batch = prices.slice(i, i + batchSize); const imported = await Price.batchInsert(batch); totalImported += imported; console.log(` 进度: ${Math.min(i + batchSize, prices.length)}/${prices.length} 条`); } console.log(`✅ 成功导入 ${totalImported} 条数据`); return totalImported; } catch (error) { console.error(`❌ 从 API 导入数据失败 (${endpointKey}):`, error.message); return 0; } } /** * 从所有本地文件导入数据 */ async function importAllLocalFiles() { console.log('🚀 开始从本地文件导入钢材价格数据...\n'); let totalImported = 0; const dataDir = path.join(__dirname, '../data'); for (const [, config] of Object.entries(DATA_SOURCES.LOCAL)) { const filePath = path.join(dataDir, config.file); // 检查文件是否存在 if (!fs.existsSync(filePath)) { console.log(`⚠️ 文件不存在,跳过: ${config.file}`); continue; } const count = await importLocalFile(filePath, config); totalImported += count; } console.log('\n' + '='.repeat(50)); console.log(`🎉 本地文件导入完成!总计导入 ${totalImported} 条数据`); console.log('='.repeat(50)); return totalImported; } /** * 从所有 API 接口导入数据 * @param {Object} params - API 查询参数(可选) */ async function importAllFromAPI(params = {}) { console.log('🚀 开始从 API 接口导入钢材价格数据...\n'); let totalImported = 0; // 默认参数 const defaultParams = { startDate: params.startDate || new Date().toISOString().split('T')[0], endDate: params.endDate || new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], page: 1, pageSize: params.pageSize || 100000 }; for (const [endpointKey] of Object.entries(DATA_SOURCES.API)) { const count = await importFromAPI(endpointKey, defaultParams); totalImported += count; } console.log('\n' + '='.repeat(50)); console.log(`🎉 API 接口导入完成!总计导入 ${totalImported} 条数据`); console.log('='.repeat(50)); return totalImported; } /** * 主导入函数(保持向后兼容) */ async function importAllData() { return importAllLocalFiles(); } /** * 查看导入统计 */ async function showStats() { try { const total = await Price.count(); const stats = await Price.getStats({}); console.log('\n📊 数据库统计信息:'); console.log(` 总记录数: ${total}`); // 处理 avgPrice 可能是字符串或 null 的情况 if (stats.avgPrice) { const avgPrice = typeof stats.avgPrice === 'number' ? stats.avgPrice.toFixed(2) : parseFloat(stats.avgPrice).toFixed(2); console.log(` 平均价格: ${avgPrice} 元/吨`); } else { console.log(` 平均价格: N/A`); } console.log(` 最低价格: ${stats.minPrice || 'N/A'} 元/吨`); console.log(` 最高价格: ${stats.maxPrice || 'N/A'} 元/吨`); } catch (error) { console.error('❌ 获取统计信息失败:', error.message); } } // 如果直接运行此脚本 if (require.main === module) { const args = process.argv.slice(2); const mode = args[0] || 'local'; // 默认使用本地文件模式 // 解析命令行参数 const params = {}; for (let i = 1; i < args.length; i++) { if (args[i].startsWith('--')) { const key = args[i].substring(2); const value = args[i + 1]; if (value && !value.startsWith('--')) { params[key] = value; i++; } } } // 根据模式执行导入 let importPromise; if (mode === 'api') { console.log('📡 模式: API 接口导入'); console.log('📅 查询参数:', JSON.stringify(params, null, 2)); importPromise = importAllFromAPI(params); } else if (mode === 'local') { console.log('📁 模式: 本地文件导入'); importPromise = importAllLocalFiles(); } else if (mode === 'single-api') { // 单个 API 接口导入:node scripts/import-data.js single-api DEFAULT const endpointKey = args[1]; if (!endpointKey || !DATA_SOURCES.API[endpointKey]) { console.error(`❌ 无效的端点: ${endpointKey}`); console.error(`可用端点: ${Object.keys(DATA_SOURCES.API).join(', ')}`); process.exit(1); } importPromise = importFromAPI(endpointKey, params); } else { console.error('❌ 无效的模式:', mode); console.error('使用方法:'); console.error(' node scripts/import-data.js [local|api|single-api] [options]'); console.error(''); console.error('示例:'); console.error(' node scripts/import-data.js local # 从本地文件导入'); console.error(' node scripts/import-data.js api # 从所有 API 接口导入'); console.error(' node scripts/import-data.js single-api DEFAULT # 从单个 API 接口导入'); console.error(''); console.error('API 参数:'); console.error(' --startDate 2025-01-01 # 开始日期'); console.error(' --endDate 2026-01-01 # 结束日期'); console.error(' --pageSize 100000 # 每页数量'); process.exit(1); } importPromise .then(() => showStats()) .then(() => { console.log('\n✅ 脚本执行完成'); process.exit(0); }) .catch(err => { console.error('\n❌ 脚本执行失败:', err); process.exit(1); }); } module.exports = { // 向后兼容的导出 importAllData, importFile: importLocalFile, transformData, // 新增的 API 导入功能 importFromAPI, importAllFromAPI, importAllLocalFiles };