Files
steel_prices_service/scripts/import-data.js
2026-01-06 18:00:43 +08:00

388 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.

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
};