388 lines
12 KiB
JavaScript
388 lines
12 KiB
JavaScript
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
|
||
};
|