modify:新增小程序
This commit is contained in:
307
scripts/README-IMPORT.md
Normal file
307
scripts/README-IMPORT.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 数据导入脚本使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
`scripts/import-data.js` 已升级,现在支持两种数据导入模式:
|
||||
|
||||
1. **本地文件模式** (`local`): 从 `data/` 目录的 JSON 文件导入
|
||||
2. **API 接口模式** (`api`): 从 `commonApi.js` 定义的接口实时获取数据
|
||||
|
||||
## 接口映射关系
|
||||
|
||||
| 接口端点 | 数据来源 | 对应文件 | 数据来源标识 |
|
||||
|---------|---------|---------|------------|
|
||||
| `DEFAULT` | 云南钢协 | 刚协指导价.json | 云南钢协 |
|
||||
| `BACKUP` | 我的钢铁 | 钢材网架.json | 我的钢铁 |
|
||||
| `EXTENDED` | 德钢指导价 | 钢厂指导价.json | 德钢指导价 |
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 本地文件导入(默认模式)
|
||||
|
||||
```bash
|
||||
# 方式 1: 显式指定模式
|
||||
node scripts/import-data.js local
|
||||
|
||||
# 方式 2: 使用默认模式(省略参数)
|
||||
npm run db:import
|
||||
# 或
|
||||
node scripts/import-data.js
|
||||
```
|
||||
|
||||
**功能说明:**
|
||||
- 从 `data/` 目录读取所有 JSON 文件
|
||||
- 依次导入:刚协指导价.json、钢材网架.json、钢厂指导价.json
|
||||
- 跳过不存在的文件
|
||||
|
||||
### 2. API 接口导入
|
||||
|
||||
#### 2.1 从所有 API 接口导入
|
||||
|
||||
```bash
|
||||
# 使用默认参数(今天到一年后,每页 10 万条)
|
||||
node scripts/import-data.js api
|
||||
|
||||
# 指定日期范围
|
||||
node scripts/import-data.js api --startDate 2025-01-01 --endDate 2025-12-31
|
||||
|
||||
# 指定每页数量
|
||||
node scripts/import-data.js api --pageSize 50000
|
||||
|
||||
# 组合参数
|
||||
node scripts/import-data.js api --startDate 2025-01-01 --endDate 2025-12-31 --pageSize 100000
|
||||
```
|
||||
|
||||
**执行流程:**
|
||||
1. 依次调用 DEFAULT、BACKUP、EXTENDED 三个接口
|
||||
2. 自动获取 Token
|
||||
3. 批量导入数据(每批 1000 条)
|
||||
4. 显示导入进度和统计信息
|
||||
|
||||
#### 2.2 从单个 API 接口导入
|
||||
|
||||
```bash
|
||||
# 仅导入云南钢协数据(DEFAULT 接口)
|
||||
node scripts/import-data.js single-api DEFAULT
|
||||
|
||||
# 仅导入我的钢铁数据(BACKUP 接口)
|
||||
node scripts/import-data.js single-api BACKUP
|
||||
|
||||
# 仅导入德钢指导价数据(EXTENDED 接口)
|
||||
node scripts/import-data.js single-api EXTENDED
|
||||
|
||||
# 带参数的单接口导入
|
||||
node scripts/import-data.js single-api DEFAULT --startDate 2025-01-01 --endDate 2025-12-31
|
||||
```
|
||||
|
||||
## 命令行参数
|
||||
|
||||
| 参数 | 说明 | 示例 | 默认值 |
|
||||
|-----|------|-----|-------|
|
||||
| `--startDate` | 查询开始日期(YYYY-MM-DD) | `--startDate 2025-01-01` | 今天 |
|
||||
| `--endDate` | 查询结束日期(YYYY-MM-DD) | `--endDate 2026-01-01` | 一年后 |
|
||||
| `--pageSize` | 每页数据条数 | `--pageSize 100000` | 100000 |
|
||||
|
||||
## 输出示例
|
||||
|
||||
### 本地文件导入
|
||||
|
||||
```
|
||||
🚀 开始从本地文件导入钢材价格数据...
|
||||
|
||||
📄 正在读取本地文件: 刚协指导价.json
|
||||
✅ 解析到 900 条有效数据
|
||||
进度: 900/900 条
|
||||
✅ 成功导入 900 条数据
|
||||
|
||||
📄 正在读取本地文件: 钢材网架.json
|
||||
✅ 解析到 211 条有效数据
|
||||
进度: 211/211 条
|
||||
✅ 成功导入 211 条数据
|
||||
|
||||
📄 正在读取本地文件: 钢厂指导价.json
|
||||
✅ 解析到 29987 条有效数据
|
||||
进度: 1000/29987 条
|
||||
进度: 2000/29987 条
|
||||
...
|
||||
进度: 29987/29987 条
|
||||
✅ 成功导入 29987 条数据
|
||||
|
||||
==================================================
|
||||
🎉 本地文件导入完成!总计导入 31098 条数据
|
||||
==================================================
|
||||
|
||||
📊 数据库统计信息:
|
||||
总记录数: 31098
|
||||
平均价格: 4500.25 元/吨
|
||||
最低价格: 3200 元/吨
|
||||
最高价格: 5800 元/吨
|
||||
|
||||
✅ 脚本执行完成
|
||||
```
|
||||
|
||||
### API 接口导入
|
||||
|
||||
```
|
||||
🚀 开始从 API 接口导入钢材价格数据...
|
||||
|
||||
🌐 正在从 API 接口获取数据: DEFAULT (云南钢协)
|
||||
查询参数: {
|
||||
"startDate": "2025-01-06",
|
||||
"endDate": "2026-01-06",
|
||||
"page": 1,
|
||||
"pageSize": 100000
|
||||
}
|
||||
🔐 正在获取 Token...
|
||||
✅ Token 获取成功: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
🔄 切换到接口: 默认钢材价格查询
|
||||
Page ID: PG-D615-D8E2-2FD84B8D
|
||||
Menu ID: MK-A8B8-109E-13D34116
|
||||
✅ 解析到 1250 条有效数据
|
||||
进度: 1000/1250 条
|
||||
进度: 1250/1250 条
|
||||
✅ 成功导入 1250 条数据
|
||||
|
||||
🌐 正在从 API 接口获取数据: BACKUP (我的钢铁)
|
||||
...
|
||||
|
||||
🌐 正在从 API 接口获取数据: EXTENDED (德钢指导价)
|
||||
...
|
||||
|
||||
==================================================
|
||||
🎉 API 接口导入完成!总计导入 15234 条数据
|
||||
==================================================
|
||||
|
||||
📊 数据库统计信息:
|
||||
总记录数: 46332
|
||||
平均价格: 4450.80 元/吨
|
||||
最低价格: 3100 元/吨
|
||||
最高价格: 5900 元/吨
|
||||
|
||||
✅ 脚本执行完成
|
||||
```
|
||||
|
||||
## 编程式调用
|
||||
|
||||
除了命令行调用,也可以在代码中直接使用:
|
||||
|
||||
```javascript
|
||||
const { importFromAPI, importAllFromAPI, importAllLocalFiles } = require('./scripts/import-data');
|
||||
|
||||
// 方式 1: 从单个 API 接口导入
|
||||
await importFromAPI('DEFAULT', {
|
||||
startDate: '2025-01-01',
|
||||
endDate: '2025-12-31',
|
||||
pageSize: 100000
|
||||
});
|
||||
|
||||
// 方式 2: 从所有 API 接口导入
|
||||
await importAllFromAPI({
|
||||
startDate: '2025-01-01',
|
||||
endDate: '2025-12-31'
|
||||
});
|
||||
|
||||
// 方式 3: 从本地文件导入
|
||||
await importAllLocalFiles();
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
脚本包含完善的错误处理机制:
|
||||
|
||||
1. **文件不存在**: 自动跳过,继续处理其他文件
|
||||
2. **API 请求失败**: 显示错误信息,继续处理下一个接口
|
||||
3. **数据解析失败**: 显示详细错误,不会中断整个流程
|
||||
4. **数据库插入失败**: 记录错误日志,支持批量插入失败重试
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **环境变量**: 确保 `.env` 文件配置正确,包含数据库连接信息
|
||||
2. **Token 管理**: API 模式会自动从 `loginApi.js` 获取 Token,确保登录凭证有效
|
||||
3. **网络连接**: API 模式需要稳定的网络连接
|
||||
4. **数据库性能**: 批量插入每批 1000 条,大数据量导入可能需要较长时间
|
||||
5. **重复数据**: 使用 `ON DUPLICATE KEY UPDATE` 策略,避免重复导入
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题 1: Token 获取失败
|
||||
|
||||
```
|
||||
❌ Token 获取失败: Invalid credentials
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
- 检查 `.env` 中的登录凭证
|
||||
- 确认 `loginApi.js` 中的 Token 存储路径正确
|
||||
- 手动运行 `node commonApi.js` 测试登录
|
||||
|
||||
### 问题 2: 数据库连接失败
|
||||
|
||||
```
|
||||
❌ 价格表创建失败: Access denied for user 'root'@'localhost'
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
- 检查 `.env` 中的数据库配置
|
||||
- 确认 MySQL 服务已启动
|
||||
- 验证用户权限
|
||||
|
||||
### 问题 3: API 返回空数据
|
||||
|
||||
```
|
||||
⚠️ 没有有效数据可导入
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
- 检查日期范围是否合理
|
||||
- 确认 API 接口地址正确
|
||||
- 尝试增加 `pageSize` 参数
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **定期导入**: 使用 `node-cron` 或系统定时任务定期执行 API 导入
|
||||
2. **增量更新**: 使用日期参数仅导入最新数据
|
||||
3. **数据备份**: 导入前备份现有数据
|
||||
4. **监控日志**: 保存导入日志用于审计和问题排查
|
||||
|
||||
## 示例:定时任务
|
||||
|
||||
使用 `node-cron` 设置每天凌晨 2 点自动导入:
|
||||
|
||||
```javascript
|
||||
// scripts/schedule-import.js
|
||||
const cron = require('node-cron');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
cron.schedule('0 2 * * *', () => {
|
||||
console.log('🕰️ 开始定时导入任务...');
|
||||
|
||||
exec('node scripts/import-data.js api --startDate 2025-01-01 --pageSize 100000',
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`执行出错: ${error}`);
|
||||
return;
|
||||
}
|
||||
console.log(stdout);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
console.log('✅ 定时任务已启动:每天 02:00 执行数据导入');
|
||||
```
|
||||
|
||||
运行定时任务:
|
||||
|
||||
```bash
|
||||
node scripts/schedule-import.js
|
||||
```
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 数据转换
|
||||
|
||||
- 原始数据格式: `rawData.data.page.result[]`
|
||||
- 过滤条件: `price_date` 存在且 `hang_price > 0`
|
||||
- 字段映射: 自动映射 API 字段到数据库字段
|
||||
|
||||
### 批量插入策略
|
||||
|
||||
- 批次大小: 1000 条/批
|
||||
- 重复处理: 使用 `ON DUPLICATE KEY UPDATE`
|
||||
- 事务支持: 每批独立事务
|
||||
|
||||
### 性能优化
|
||||
|
||||
- 批量插入优于单条插入
|
||||
- 数据库索引优化 (`idx_price_date`, `idx_region_material`)
|
||||
- 自动过滤无效数据,减少不必要的数据库操作
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 2026-01-06
|
||||
- ✅ 新增 API 接口导入功能
|
||||
- ✅ 支持命令行参数配置
|
||||
- ✅ 改进错误处理和进度显示
|
||||
- ✅ 保持向后兼容性
|
||||
@@ -2,23 +2,80 @@ require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Price = require('../src/models/Price');
|
||||
const SteelPriceCollector = require('../commonApi');
|
||||
|
||||
/**
|
||||
* 数据导入脚本
|
||||
* 从 JSON 文件导入钢材价格数据到数据库
|
||||
* 支持两种导入模式:
|
||||
* 1. 从本地 JSON 文件导入
|
||||
* 2. 从 commonApi.js 接口实时获取并导入
|
||||
*/
|
||||
|
||||
// 数据文件映射
|
||||
const dataFiles = [
|
||||
{ file: '刚协指导价.json', source: '云南钢协', priceField: 'PR_PRICESET_HANGPRICE' },
|
||||
{ file: '钢材网架.json', source: '我的钢铁', priceField: 'PR_PRICESET_HANGPRICE' },
|
||||
{ file: '钢厂指导价.json', source: '德钢指导价', priceField: 'PR_PRICESET_HANGPRICE' }
|
||||
];
|
||||
// 数据源映射配置
|
||||
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, source, priceField) {
|
||||
function transformData(rawData, config) {
|
||||
if (!rawData || !rawData.data || !rawData.data.page || !rawData.data.page.result) {
|
||||
return [];
|
||||
}
|
||||
@@ -30,12 +87,19 @@ function transformData(rawData, source, priceField) {
|
||||
goods_spec: item.GOODS_SPEC || '未知',
|
||||
partsname_name: item.PARTSNAME_NAME || '未知',
|
||||
productarea_name: item.PRODUCTAREA_NAME || '未知',
|
||||
price_source: item.PR_PRICE_SOURCE || source,
|
||||
// 数据源标识 - 使用 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[priceField] || 0,
|
||||
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,
|
||||
@@ -47,17 +111,22 @@ function transformData(rawData, source, priceField) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入单个数据文件
|
||||
* 导入单个本地数据文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {Object} config - 数据源配置
|
||||
*/
|
||||
async function importFile(filePath, source, priceField) {
|
||||
async function importLocalFile(filePath, config) {
|
||||
try {
|
||||
console.log(`\n📄 正在读取文件: ${path.basename(filePath)}`);
|
||||
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, source, priceField);
|
||||
const prices = transformData(rawData, config);
|
||||
console.log(`✅ 解析到 ${prices.length} 条有效数据`);
|
||||
|
||||
if (prices.length === 0) {
|
||||
@@ -80,40 +149,138 @@ async function importFile(filePath, source, priceField) {
|
||||
return totalImported;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 导入文件失败 ${path.basename(filePath)}:`, error.message);
|
||||
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 importAllData() {
|
||||
console.log('🚀 开始导入钢材价格数据...\n');
|
||||
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 { file, source, priceField } of dataFiles) {
|
||||
const filePath = path.join(dataDir, file);
|
||||
for (const [, config] of Object.entries(DATA_SOURCES.LOCAL)) {
|
||||
const filePath = path.join(dataDir, config.file);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(`⚠️ 文件不存在,跳过: ${file}`);
|
||||
console.log(`⚠️ 文件不存在,跳过: ${config.file}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const count = await importFile(filePath, source, priceField);
|
||||
const count = await importLocalFile(filePath, config);
|
||||
totalImported += count;
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log(`🎉 数据导入完成!总计导入 ${totalImported} 条数据`);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看导入统计
|
||||
*/
|
||||
@@ -144,7 +311,59 @@ async function showStats() {
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
importAllData()
|
||||
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✅ 脚本执行完成');
|
||||
@@ -156,4 +375,13 @@ if (require.main === module) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { importAllData, importFile, transformData };
|
||||
module.exports = {
|
||||
// 向后兼容的导出
|
||||
importAllData,
|
||||
importFile: importLocalFile,
|
||||
transformData,
|
||||
// 新增的 API 导入功能
|
||||
importFromAPI,
|
||||
importAllFromAPI,
|
||||
importAllLocalFiles
|
||||
};
|
||||
|
||||
35
scripts/migrate-add-source-fields.js
Normal file
35
scripts/migrate-add-source-fields.js
Normal file
@@ -0,0 +1,35 @@
|
||||
require('dotenv').config();
|
||||
const Price = require('../src/models/Price');
|
||||
|
||||
/**
|
||||
* 数据库迁移脚本:添加数据源标识字段
|
||||
* 为现有 prices 表添加数据源区分字段
|
||||
*/
|
||||
|
||||
async function migrate() {
|
||||
console.log('🔄 开始数据库迁移:添加数据源标识字段\n');
|
||||
|
||||
try {
|
||||
await Price.migrateAddSourceFields();
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('✅ 数据库迁移完成!');
|
||||
console.log('='.repeat(60));
|
||||
console.log('\n📊 新增字段说明:');
|
||||
console.log(' - price_source_code: 数据源代码(YUNNAN_STEEL_ASSOC / MY_STEEL / DE_STEEL_FACTORY)');
|
||||
console.log(' - price_source_desc: 数据源描述');
|
||||
console.log(' - data_origin: 数据来源标识(LOCAL_FILE 或 API:ENDPOINT)');
|
||||
console.log('\n🎨 数据源标识:');
|
||||
console.log(' 🔴 YUNNAN_STEEL_ASSOC - 云南钢协指导价(DEFAULT 接口)');
|
||||
console.log(' 🔵 MY_STEEL - 我的钢铁网价格(BACKUP 接口)');
|
||||
console.log(' 🟢 DE_STEEL_FACTORY - 德钢钢厂指导价(EXTENDED 接口)');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('\n❌ 迁移失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行迁移
|
||||
migrate();
|
||||
134
scripts/test-data-source-identification.js
Normal file
134
scripts/test-data-source-identification.js
Normal file
@@ -0,0 +1,134 @@
|
||||
require('dotenv').config();
|
||||
const db = require('../src/config/database');
|
||||
|
||||
/**
|
||||
* 测试数据源标识功能
|
||||
* 验证三个接口的数据是否有明显区分
|
||||
*/
|
||||
|
||||
async function testDataSourceIdentification() {
|
||||
console.log('🧪 测试数据源标识功能\n');
|
||||
|
||||
try {
|
||||
// 1. 测试查询所有数据源统计
|
||||
console.log('📊 查询所有数据源统计...\n');
|
||||
|
||||
const [stats] = await db.execute(`
|
||||
SELECT
|
||||
price_source_code AS '数据源代码',
|
||||
price_source_desc AS '数据源描述',
|
||||
data_origin AS '数据来源',
|
||||
COUNT(*) AS '记录数',
|
||||
AVG(hang_price) AS '平均价格',
|
||||
MIN(hang_price) AS '最低价格',
|
||||
MAX(hang_price) AS '最高价格'
|
||||
FROM prices
|
||||
GROUP BY price_source_code, price_source_desc, data_origin
|
||||
ORDER BY price_source_code
|
||||
`);
|
||||
|
||||
console.log('┌' + '─'.repeat(120) + '┐');
|
||||
console.log('│' + ' '.repeat(40) + '数据源统计报告' + ' '.repeat(56) + '│');
|
||||
console.log('└' + '─'.repeat(120) + '┘');
|
||||
console.log('');
|
||||
|
||||
if (stats.length === 0) {
|
||||
console.log('⚠️ 当前数据库中没有数据');
|
||||
console.log('💡 提示:运行以下命令导入数据:');
|
||||
console.log(' npm run db:import:api');
|
||||
console.log(' npm run db:import:local');
|
||||
} else {
|
||||
// 表头
|
||||
console.log('┌' + '─'.repeat(22) + '┬' + '─'.repeat(28) + '┬' + '─'.repeat(16) + '┬' + '─'.repeat(10) + '┬' + '─'.repeat(12) + '┬' + '─'.repeat(12) + '┬' + '─'.repeat(12) + '┐');
|
||||
console.log('│ ' + '数据源代码'.padEnd(20) + ' │ ' + '数据源描述'.padEnd(26) + ' │ ' + '数据来源'.padEnd(14) + ' │ ' + '记录数'.padEnd(8) + ' │ ' + '平均价格'.padEnd(10) + ' │ ' + '最低价格'.padEnd(10) + ' │ ' + '最高价格'.padEnd(10) + ' │');
|
||||
console.log('├' + '─'.repeat(22) + '┼' + '─'.repeat(28) + '┼' + '─'.repeat(16) + '┼' + '─'.repeat(10) + '┼' + '─'.repeat(12) + '┼' + '─'.repeat(12) + '┼' + '─'.repeat(12) + '┤');
|
||||
|
||||
// 数据行
|
||||
const colorMap = {
|
||||
'YUNNAN_STEEL_ASSOC': '🔴',
|
||||
'MY_STEEL': '🔵',
|
||||
'DE_STEEL_FACTORY': '🟢',
|
||||
'UNKNOWN': '⚪'
|
||||
};
|
||||
|
||||
stats.forEach(row => {
|
||||
const emoji = colorMap[row['数据源代码']] || '⚪';
|
||||
const avgPrice = row['平均价格'] ? parseFloat(row['平均价格']).toFixed(2) : 'N/A';
|
||||
const minPrice = row['最低价格'] || 'N/A';
|
||||
const maxPrice = row['最高价格'] || 'N/A';
|
||||
|
||||
console.log(
|
||||
'│ ' + `${emoji} ${row['数据源代码']}`.padEnd(20) +
|
||||
' │ ' + String(row['数据源描述']).padEnd(26) +
|
||||
' │ ' + String(row['数据来源']).padEnd(14) +
|
||||
' │ ' + String(row['记录数']).padEnd(8) +
|
||||
' │ ' + String(avgPrice).padEnd(10) +
|
||||
' │ ' + String(minPrice).padEnd(10) +
|
||||
' │ ' + String(maxPrice).padEnd(10) + ' │'
|
||||
);
|
||||
});
|
||||
|
||||
console.log('└' + '─'.repeat(22) + '┴' + '─'.repeat(28) + '┴' + '─'.repeat(16) + '┴' + '─'.repeat(10) + '┴' + '─'.repeat(12) + '┴' + '─'.repeat(12) + '┴' + '─'.repeat(12) + '┘');
|
||||
}
|
||||
|
||||
// 2. 查看最新导入的数据样本
|
||||
console.log('\n📋 最新数据样本(每个数据源取最新 3 条)...\n');
|
||||
|
||||
const samples = await db.execute(`
|
||||
SELECT
|
||||
price_source_code,
|
||||
price_source_desc,
|
||||
data_origin,
|
||||
goods_material,
|
||||
goods_spec,
|
||||
price_region,
|
||||
hang_price,
|
||||
price_date
|
||||
FROM prices
|
||||
WHERE (price_source_code, price_date) IN (
|
||||
SELECT price_source_code, price_date
|
||||
FROM prices p2
|
||||
WHERE p2.price_source_code = prices.price_source_code
|
||||
ORDER BY price_date DESC
|
||||
LIMIT 3
|
||||
)
|
||||
ORDER BY price_source_code, price_date DESC
|
||||
`);
|
||||
|
||||
if (samples[0].length > 0) {
|
||||
samples[0].forEach((row, index) => {
|
||||
const emoji = colorMap[row.price_source_code] || '⚪';
|
||||
console.log(`${index + 1}. ${emoji} [${row.price_source_code}] ${row.goods_material}/${row.goods_spec} - ${row.price_region} - ¥${row.hang_price} (${new Date(row.price_date).toLocaleDateString('zh-CN')})`);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 数据源配置信息
|
||||
console.log('\n🎨 数据源配置信息\n');
|
||||
console.log('┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐');
|
||||
console.log('│ ' + '数据源标识配置'.padEnd(119) + '│');
|
||||
console.log('├─────────────────────────────────────────────────────────────────────────────────────────────────────┤');
|
||||
console.log('│ 🔴 YUNNAN_STEEL_ASSOC → 云南钢协指导价(DEFAULT 接口) → Color: #FF6B6B' + ' '.repeat(44) + '│');
|
||||
console.log('│ 🔵 MY_STEEL → 我的钢铁网价格(BACKUP 接口) → Color: #4ECDC4' + ' '.repeat(44) + '│');
|
||||
console.log('│ 🟢 DE_STEEL_FACTORY → 德钢钢厂指导价(EXTENDED 接口) → Color: #95E1D3' + ' '.repeat(44) + '│');
|
||||
console.log('└─────────────────────────────────────────────────────────────────────────────────────────────────────┘');
|
||||
|
||||
// 4. 字段说明
|
||||
console.log('\n📝 字段说明\n');
|
||||
console.log(' • price_source_code - 数据源唯一代码(用于查询和筛选)');
|
||||
console.log(' • price_source_desc - 数据源描述(用于显示)');
|
||||
console.log(' • data_origin - 数据来源标识(LOCAL_FILE 或 API:ENDPOINT)');
|
||||
|
||||
console.log('\n' + '='.repeat(120));
|
||||
console.log('✅ 数据源标识测试完成!');
|
||||
console.log('='.repeat(120) + '\n');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('\n❌ 测试失败:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testDataSourceIdentification();
|
||||
Reference in New Issue
Block a user