添加汇率数据
This commit is contained in:
parent
b61cf9628e
commit
d232591a38
@ -32,6 +32,10 @@ COPY --from=builder /app/public* ./public/
|
|||||||
# 设置淘宝源并安装生产依赖
|
# 设置淘宝源并安装生产依赖
|
||||||
RUN npm config set registry https://registry.npmmirror.com && npm install --only=production
|
RUN npm config set registry https://registry.npmmirror.com && npm install --only=production
|
||||||
|
|
||||||
|
|
||||||
|
ENV DATABASE_URL="mysql://root:fullstack@baishe.vps.honor3.com:6033/iboard"
|
||||||
|
|
||||||
|
|
||||||
# 暴露 3000 端口
|
# 暴露 3000 端口
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"mariadb": "^3.5.2",
|
"mariadb": "^3.5.2",
|
||||||
"marked": "^17.0.6",
|
"marked": "^17.0.6",
|
||||||
"next": "^14.2.3",
|
"next": "^14.2.3",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
"prisma": "^7.7.0",
|
"prisma": "^7.7.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@ -2117,6 +2118,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-cron": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/node-cron/-/node-cron-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch-native": {
|
"node_modules/node-fetch-native": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
"resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "iboard",
|
"name": "iboard",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
@ -14,6 +15,7 @@
|
|||||||
"mariadb": "^3.5.2",
|
"mariadb": "^3.5.2",
|
||||||
"marked": "^17.0.6",
|
"marked": "^17.0.6",
|
||||||
"next": "^14.2.3",
|
"next": "^14.2.3",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
"prisma": "^7.7.0",
|
"prisma": "^7.7.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
105
pages/api/exchange-rate.js
Normal file
105
pages/api/exchange-rate.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { createPool } from 'mariadb';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
function loadDatabaseConfig() {
|
||||||
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||||
|
const lines = envContent.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (trimmedLine.startsWith('DATABASE_URL=')) {
|
||||||
|
const url = trimmedLine.substring('DATABASE_URL='.length).replace(/^"|"$/g, '');
|
||||||
|
|
||||||
|
// 解析URL
|
||||||
|
const parts = url.split('://')[1].split('@');
|
||||||
|
const auth = parts[0].split(':');
|
||||||
|
const hostAndDb = parts[1].split('/');
|
||||||
|
const hostAndPort = hostAndDb[0].split(':');
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: auth[0],
|
||||||
|
password: auth[1],
|
||||||
|
host: hostAndPort[0],
|
||||||
|
port: parseInt(hostAndPort[1]),
|
||||||
|
database: hostAndDb[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
host: '192.168.111.111',
|
||||||
|
port: 3306,
|
||||||
|
user: 'root',
|
||||||
|
password: 'fullstack',
|
||||||
|
database: 'iboard'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let pool;
|
||||||
|
|
||||||
|
async function initPool() {
|
||||||
|
if (!pool) {
|
||||||
|
const config = loadDatabaseConfig();
|
||||||
|
pool = createPool({
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
user: config.user,
|
||||||
|
password: config.password,
|
||||||
|
database: config.database,
|
||||||
|
connectionLimit: 5
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
try {
|
||||||
|
await initPool();
|
||||||
|
|
||||||
|
// 获取查询参数
|
||||||
|
const { startDate, endDate, limit = 30 } = req.query;
|
||||||
|
|
||||||
|
let query = 'SELECT date, centerPrice FROM econ_ExchangeRate WHERE currency = ? ORDER BY date DESC';
|
||||||
|
const params = ['USD/CNY'];
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
query = 'SELECT date, centerPrice FROM econ_ExchangeRate WHERE currency = ? AND date BETWEEN ? AND ? ORDER BY date DESC';
|
||||||
|
params.push(startDate, endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(query, params);
|
||||||
|
|
||||||
|
// 限制返回数量
|
||||||
|
const limitedResult = result.slice(0, parseInt(limit));
|
||||||
|
|
||||||
|
// 反转顺序,使日期从早到晚
|
||||||
|
const sortedResult = limitedResult.reverse();
|
||||||
|
|
||||||
|
// 格式化数据
|
||||||
|
const formattedData = sortedResult.map(row => ({
|
||||||
|
date: row.date instanceof Date ? row.date.toISOString().split('T')[0] : row.date,
|
||||||
|
rate: parseFloat(row.centerPrice)
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: formattedData,
|
||||||
|
total: result.length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取汇率数据失败:', error.message);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取汇率数据失败',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (pool) {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import Navbar from '../components/Navbar'
|
import Navbar from '../components/Navbar'
|
||||||
import Footer from '../components/Footer'
|
import Footer from '../components/Footer'
|
||||||
@ -159,6 +159,34 @@ const sowInventoryData = [
|
|||||||
|
|
||||||
export default function Data() {
|
export default function Data() {
|
||||||
const [selectedIndicator, setSelectedIndicator] = useState('bond-china-treasury')
|
const [selectedIndicator, setSelectedIndicator] = useState('bond-china-treasury')
|
||||||
|
const [exchangeRateData, setExchangeRateData] = useState([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedIndicator === 'central-bank-usd-cny-rate') {
|
||||||
|
fetchExchangeRateData()
|
||||||
|
}
|
||||||
|
}, [selectedIndicator])
|
||||||
|
|
||||||
|
const fetchExchangeRateData = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/exchange-rate?limit=60')
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success) {
|
||||||
|
setExchangeRateData(data.data)
|
||||||
|
} else {
|
||||||
|
setError(data.message)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('获取汇率数据失败')
|
||||||
|
console.error('获取汇率数据失败:', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const indicators = [
|
const indicators = [
|
||||||
{
|
{
|
||||||
@ -180,7 +208,8 @@ export default function Data() {
|
|||||||
{ id: 'central-bank-china-gold-monthly', name: '中国黄金储备(月)' },
|
{ id: 'central-bank-china-gold-monthly', name: '中国黄金储备(月)' },
|
||||||
{ id: 'central-bank-us-gold-monthly', name: '美国黄金储备(月)' },
|
{ id: 'central-bank-us-gold-monthly', name: '美国黄金储备(月)' },
|
||||||
{ id: 'central-bank-china-gold-yearly', name: '中国黄金储备(年)' },
|
{ id: 'central-bank-china-gold-yearly', name: '中国黄金储备(年)' },
|
||||||
{ id: 'central-bank-us-gold-yearly', name: '美国黄金储备(年)' }
|
{ id: 'central-bank-us-gold-yearly', name: '美国黄金储备(年)' },
|
||||||
|
{ id: 'central-bank-usd-cny-rate', name: '人民币兑美元汇率' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -526,6 +555,54 @@ export default function Data() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedIndicator === 'central-bank-usd-cny-rate' && (
|
||||||
|
<div className="indicator-detail">
|
||||||
|
<h2 className="indicator-title">人民币兑美元汇率</h2>
|
||||||
|
<p className="indicator-description">展示人民币兑美元汇率的变化趋势</p>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="loading">加载中...</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="error">{error}</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="chart-container">
|
||||||
|
<ResponsiveContainer width="100%" height={500}>
|
||||||
|
<LineChart data={exchangeRateData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="date" />
|
||||||
|
<YAxis domain={['dataMin - 0.1', 'dataMax + 0.1']} tickFormatter={(value) => value.toFixed(4)} />
|
||||||
|
<Tooltip formatter={(value) => [value.toFixed(4), '汇率']} />
|
||||||
|
<Legend />
|
||||||
|
<Line type="monotone" dataKey="rate" name="汇率" stroke="#1e88e5" strokeWidth={2} activeDot={{ r: 8 }} />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="data-table-container">
|
||||||
|
<h3 className="table-title">详细数据</h3>
|
||||||
|
<table className="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>日期</th>
|
||||||
|
<th>汇率</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{exchangeRateData.map((row, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{row.date}</td>
|
||||||
|
<td>{row.rate.toFixed(4)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedIndicator === 'commodity-pork-price' && (
|
{selectedIndicator === 'commodity-pork-price' && (
|
||||||
<div className="indicator-detail">
|
<div className="indicator-detail">
|
||||||
<h2 className="indicator-title">猪肉价格</h2>
|
<h2 className="indicator-title">猪肉价格</h2>
|
||||||
|
|||||||
49
scripts/scheduler/index.js
Normal file
49
scripts/scheduler/index.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import cron from 'node-cron';
|
||||||
|
import { trigger as triggerUSDCNYRate } from '../scraper/usd-cny-rate.scraper.js';
|
||||||
|
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
console.log('🚀 启动定时任务调度器');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
// 注册定时任务
|
||||||
|
const job = cron.schedule('0 0 * * *', async () => {
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
console.log('⏰ 执行定时任务: USD-CNY 汇率数据更新');
|
||||||
|
console.log('📅 执行时间:', new Date().toISOString());
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await triggerUSDCNYRate();
|
||||||
|
console.log('\n✅ 任务执行成功!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ 任务执行失败:', error.message);
|
||||||
|
} finally {
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
scheduled: true,
|
||||||
|
timezone: 'Asia/Shanghai'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 定时任务已启动: USD-CNY 汇率数据更新');
|
||||||
|
console.log('📅 执行表达式: 0 0 * * * (每天 0 点)');
|
||||||
|
console.log('📅 下次执行时间: 明天 00:00');
|
||||||
|
console.log('');
|
||||||
|
console.log('按 Ctrl+C 停止...');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
// 优雅处理退出
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
console.log('🛑 停止定时任务调度器');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
job.stop();
|
||||||
|
console.log('✅ 定时任务已停止');
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
console.log('📊 定时任务调度器已停止');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
49
scripts/scheduler/test.js
Normal file
49
scripts/scheduler/test.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import cron from 'node-cron';
|
||||||
|
import { trigger as triggerUSDCNYRate } from '../scraper/usd-cny-rate.scraper.js';
|
||||||
|
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
console.log('🚀 启动定时任务调度器');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
// 注册定时任务
|
||||||
|
const job = cron.schedule('0 0 * * *', async () => {
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
console.log('⏰ 执行定时任务: USD-CNY 汇率数据更新');
|
||||||
|
console.log('📅 执行时间:', new Date().toISOString());
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await triggerUSDCNYRate();
|
||||||
|
console.log('\n✅ 任务执行成功!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ 任务执行失败:', error.message);
|
||||||
|
} finally {
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
scheduled: true,
|
||||||
|
timezone: 'Asia/Shanghai'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 定时任务已启动: USD-CNY 汇率数据更新');
|
||||||
|
console.log('📅 执行表达式: 0 0 * * * (每天 0 点)');
|
||||||
|
console.log('📅 下次执行时间: 明天 00:00');
|
||||||
|
console.log('');
|
||||||
|
console.log('按 Ctrl+C 停止...');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
// 优雅处理退出
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
console.log('🛑 停止定时任务调度器');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
job.stop();
|
||||||
|
console.log('✅ 定时任务已停止');
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
console.log('📊 定时任务调度器已停止');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
@ -1,23 +1,82 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { PrismaClient } from '../../generated/prisma/client.js';
|
|
||||||
import { PrismaMariaDb } from '@prisma/adapter-mariadb';
|
|
||||||
import { createPool } from 'mariadb';
|
import { createPool } from 'mariadb';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
function loadDatabaseConfig() {
|
||||||
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
console.log('读取 .env 文件...');
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||||
|
const lines = envContent.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (trimmedLine.startsWith('DATABASE_URL=')) {
|
||||||
|
const url = trimmedLine.substring('DATABASE_URL='.length).replace(/^"|"$/g, '');
|
||||||
|
console.log('从 .env 文件获取数据库配置成功!');
|
||||||
|
console.log(`DATABASE_URL: ${url}`);
|
||||||
|
|
||||||
|
// 解析URL
|
||||||
|
const parts = url.split('://')[1].split('@');
|
||||||
|
const auth = parts[0].split(':');
|
||||||
|
const hostAndDb = parts[1].split('/');
|
||||||
|
const hostAndPort = hostAndDb[0].split(':');
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: auth[0],
|
||||||
|
password: auth[1],
|
||||||
|
host: hostAndPort[0],
|
||||||
|
port: parseInt(hostAndPort[1]),
|
||||||
|
database: hostAndDb[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('未找到 .env 文件或 DATABASE_URL 配置,使用默认配置');
|
||||||
|
return {
|
||||||
|
host: '192.168.111.111',
|
||||||
|
port: 3306,
|
||||||
|
user: 'root',
|
||||||
|
password: 'fullstack',
|
||||||
|
database: 'iboard'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 创建数据库连接池
|
// 创建数据库连接池
|
||||||
const pool = createPool({
|
const config = loadDatabaseConfig();
|
||||||
host: '192.168.111.111',
|
console.log('数据库配置:');
|
||||||
port: 3306,
|
console.log(` 主机: ${config.host}`);
|
||||||
user: 'root',
|
console.log(` 端口: ${config.port}`);
|
||||||
password: 'fullstack',
|
console.log(` 用户: ${config.user}`);
|
||||||
database: 'iboard',
|
console.log(` 数据库: ${config.database}`);
|
||||||
connectionLimit: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
// 构造 PrismaClient,提供 adapter
|
let pool;
|
||||||
const prisma = new PrismaClient({
|
|
||||||
adapter: new PrismaMariaDb(pool)
|
async function initPool() {
|
||||||
});
|
if (!pool) {
|
||||||
|
pool = createPool({
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
user: config.user,
|
||||||
|
password: config.password,
|
||||||
|
database: config.database,
|
||||||
|
connectionLimit: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conn = await pool.getConnection();
|
||||||
|
console.log('数据库连接成功!');
|
||||||
|
conn.release();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据库连接失败:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
// 浙江数据开放网站URL(示例,实际需要根据真实网站修改)
|
// 浙江数据开放网站URL(示例,实际需要根据真实网站修改)
|
||||||
const ZHEJIANG_OPEN_DATA_URL = 'https://data.zj.gov.cn/';
|
const ZHEJIANG_OPEN_DATA_URL = 'https://data.zj.gov.cn/';
|
||||||
@ -31,10 +90,9 @@ const MIN_UPDATE_INTERVAL = 30 * 60 * 1000;
|
|||||||
// 从数据库读取缓存
|
// 从数据库读取缓存
|
||||||
async function readCache() {
|
async function readCache() {
|
||||||
try {
|
try {
|
||||||
const cache = await prisma.scraperCache.findUnique({
|
await initPool();
|
||||||
where: { scraperName: SCRAPER_NAME }
|
const result = await pool.query('SELECT * FROM ScraperCache WHERE scraperName = ?', [SCRAPER_NAME]);
|
||||||
});
|
return result.length > 0 ? result[0] : { scraperName: SCRAPER_NAME, lastDataHash: null, lastUpdateAt: null };
|
||||||
return cache || { scraperName: SCRAPER_NAME, lastDataHash: null, lastUpdateAt: null };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('读取数据库缓存失败:', error.message);
|
console.error('读取数据库缓存失败:', error.message);
|
||||||
return { scraperName: SCRAPER_NAME, lastDataHash: null, lastUpdateAt: null };
|
return { scraperName: SCRAPER_NAME, lastDataHash: null, lastUpdateAt: null };
|
||||||
@ -44,18 +102,11 @@ async function readCache() {
|
|||||||
// 写入数据库缓存
|
// 写入数据库缓存
|
||||||
async function writeCache(lastDataHash) {
|
async function writeCache(lastDataHash) {
|
||||||
try {
|
try {
|
||||||
await prisma.scraperCache.upsert({
|
await initPool();
|
||||||
where: { scraperName: SCRAPER_NAME },
|
await pool.query(
|
||||||
update: {
|
'INSERT INTO ScraperCache (scraperName, lastDataHash, lastUpdateAt) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE lastDataHash = VALUES(lastDataHash), lastUpdateAt = VALUES(lastUpdateAt)',
|
||||||
lastDataHash,
|
[SCRAPER_NAME, lastDataHash, new Date()]
|
||||||
lastUpdateAt: new Date()
|
);
|
||||||
},
|
|
||||||
create: {
|
|
||||||
scraperName: SCRAPER_NAME,
|
|
||||||
lastDataHash,
|
|
||||||
lastUpdateAt: new Date()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('写入数据库缓存失败:', error.message);
|
console.error('写入数据库缓存失败:', error.message);
|
||||||
}
|
}
|
||||||
@ -76,12 +127,9 @@ function computeDataHash(data) {
|
|||||||
// 获取数据库中最新的月份
|
// 获取数据库中最新的月份
|
||||||
async function getLatestMonth() {
|
async function getLatestMonth() {
|
||||||
try {
|
try {
|
||||||
const latest = await prisma.econ_SowInventory.findFirst({
|
await initPool();
|
||||||
orderBy: {
|
const result = await pool.query('SELECT MAX(month) as latestMonth FROM Econ_SowInventory');
|
||||||
month: 'desc'
|
return result[0].latestMonth || null;
|
||||||
}
|
|
||||||
});
|
|
||||||
return latest ? latest.month : null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('查询最新月份失败:', error.message);
|
console.error('查询最新月份失败:', error.message);
|
||||||
return null;
|
return null;
|
||||||
@ -108,6 +156,7 @@ function shouldSkipRequest(cache) {
|
|||||||
// 能繁母猪数量数据爬取函数(增量更新版本)
|
// 能繁母猪数量数据爬取函数(增量更新版本)
|
||||||
async function scrapeSowInventory(incremental = true) {
|
async function scrapeSowInventory(incremental = true) {
|
||||||
try {
|
try {
|
||||||
|
await initPool();
|
||||||
console.log('='.repeat(50));
|
console.log('='.repeat(50));
|
||||||
console.log('开始爬取浙江省能繁母猪数量数据...');
|
console.log('开始爬取浙江省能繁母猪数量数据...');
|
||||||
console.log(`增量更新模式: ${incremental ? '开启' : '关闭'}`);
|
console.log(`增量更新模式: ${incremental ? '开启' : '关闭'}`);
|
||||||
@ -224,26 +273,25 @@ async function scrapeSowInventory(incremental = true) {
|
|||||||
for (const item of dataToUpdate) {
|
for (const item of dataToUpdate) {
|
||||||
try {
|
try {
|
||||||
// 检查数据是否已存在
|
// 检查数据是否已存在
|
||||||
const existing = await prisma.econ_SowInventory.findUnique({
|
const existing = await pool.query('SELECT * FROM Econ_SowInventory WHERE month = ?', [item.month]);
|
||||||
where: { month: item.month }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
if (existing.length > 0) {
|
||||||
// 检查数据是否真的需要更新(值变化了才更新)
|
// 检查数据是否真的需要更新(值变化了才更新)
|
||||||
if (existing.inventory !== item.inventory) {
|
if (existing[0].inventory !== item.inventory) {
|
||||||
await prisma.econ_SowInventory.update({
|
await pool.query(
|
||||||
where: { month: item.month },
|
'UPDATE Econ_SowInventory SET inventory = ? WHERE month = ?',
|
||||||
data: { inventory: item.inventory }
|
[item.inventory, item.month]
|
||||||
});
|
);
|
||||||
updateCount++;
|
updateCount++;
|
||||||
console.log(`更新数据: ${item.month} - ${item.inventory} (原值: ${existing.inventory})`);
|
console.log(`更新数据: ${item.month} - ${item.inventory} (原值: ${existing[0].inventory})`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`数据未变化,跳过: ${item.month}`);
|
console.log(`数据未变化,跳过: ${item.month}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await prisma.econ_SowInventory.create({
|
await pool.query(
|
||||||
data: item
|
'INSERT INTO Econ_SowInventory (month, inventory) VALUES (?, ?)',
|
||||||
});
|
[item.month, item.inventory]
|
||||||
|
);
|
||||||
newCount++;
|
newCount++;
|
||||||
console.log(`新增数据: ${item.month} - ${item.inventory}`);
|
console.log(`新增数据: ${item.month} - ${item.inventory}`);
|
||||||
}
|
}
|
||||||
@ -272,10 +320,11 @@ async function scrapeSowInventory(incremental = true) {
|
|||||||
console.error('爬取数据时出错:', error.message);
|
console.error('爬取数据时出错:', error.message);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// 关闭Prisma连接
|
|
||||||
await prisma.$disconnect();
|
|
||||||
// 关闭数据库连接池
|
// 关闭数据库连接池
|
||||||
await pool.end();
|
if (pool) {
|
||||||
|
await pool.end();
|
||||||
|
console.log('数据库连接池已关闭');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,68 @@
|
|||||||
import { createPool } from 'mariadb';
|
import { createPool } from 'mariadb';
|
||||||
import { fetch as fetchUSDToCNYRate } from '../fetchers/usd-cny-rate.fetcher.js';
|
import { fetch as fetchUSDToCNYRate } from '../fetchers/usd-cny-rate.fetcher.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
let pool;
|
let pool;
|
||||||
|
|
||||||
|
function loadDatabaseConfig() {
|
||||||
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
console.log('读取 .env 文件...');
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||||
|
const lines = envContent.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (trimmedLine.startsWith('DATABASE_URL=')) {
|
||||||
|
const url = trimmedLine.substring('DATABASE_URL='.length).replace(/^"|"$/g, '');
|
||||||
|
console.log('从 .env 文件获取数据库配置成功!');
|
||||||
|
console.log(`DATABASE_URL: ${url}`);
|
||||||
|
|
||||||
|
// 解析URL
|
||||||
|
const parts = url.split('://')[1].split('@');
|
||||||
|
const auth = parts[0].split(':');
|
||||||
|
const hostAndDb = parts[1].split('/');
|
||||||
|
const hostAndPort = hostAndDb[0].split(':');
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: auth[0],
|
||||||
|
password: auth[1],
|
||||||
|
host: hostAndPort[0],
|
||||||
|
port: parseInt(hostAndPort[1]),
|
||||||
|
database: hostAndDb[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('未找到 .env 文件或 DATABASE_URL 配置,使用默认配置');
|
||||||
|
return {
|
||||||
|
host: '192.168.111.111',
|
||||||
|
port: 3306,
|
||||||
|
user: 'root',
|
||||||
|
password: 'fullstack',
|
||||||
|
database: 'iboard'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function initPool() {
|
async function initPool() {
|
||||||
if (!pool) {
|
if (!pool) {
|
||||||
console.log('初始化数据库连接池...');
|
console.log('初始化数据库连接池...');
|
||||||
|
const config = loadDatabaseConfig();
|
||||||
|
|
||||||
|
console.log('数据库配置:');
|
||||||
|
console.log(` 主机: ${config.host}`);
|
||||||
|
console.log(` 端口: ${config.port}`);
|
||||||
|
console.log(` 用户: ${config.user}`);
|
||||||
|
console.log(` 数据库: ${config.database}`);
|
||||||
|
|
||||||
pool = createPool({
|
pool = createPool({
|
||||||
host: '192.168.111.111',
|
host: config.host,
|
||||||
port: 3306,
|
port: config.port,
|
||||||
user: 'root',
|
user: config.user,
|
||||||
password: 'fullstack',
|
password: config.password,
|
||||||
database: 'iboard',
|
database: config.database,
|
||||||
connectionLimit: 5
|
connectionLimit: 5
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -212,12 +263,23 @@ export async function trigger(startDate = null, endDate = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 直接运行测试
|
// 直接运行测试
|
||||||
console.log('='.repeat(60));
|
async function runTest() {
|
||||||
console.log('测试 usd-cny-rate.scraper.js');
|
console.log('='.repeat(60));
|
||||||
console.log('='.repeat(60));
|
console.log('测试 usd-cny-rate.scraper.js');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
|
||||||
trigger()
|
// 测试配置读取
|
||||||
.then(result => {
|
console.log('\n测试数据库配置读取...');
|
||||||
|
const config = loadDatabaseConfig();
|
||||||
|
console.log('配置读取结果:');
|
||||||
|
console.log(` 主机: ${config.host}`);
|
||||||
|
console.log(` 端口: ${config.port}`);
|
||||||
|
console.log(` 用户: ${config.user}`);
|
||||||
|
console.log(` 数据库: ${config.database}`);
|
||||||
|
console.log('配置读取测试完成!');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await trigger();
|
||||||
console.log('\n' + '='.repeat(60));
|
console.log('\n' + '='.repeat(60));
|
||||||
console.log('测试结果:');
|
console.log('测试结果:');
|
||||||
console.log(` 开始日期: ${result.startDate}`);
|
console.log(` 开始日期: ${result.startDate}`);
|
||||||
@ -227,8 +289,11 @@ trigger()
|
|||||||
console.log(` 最终数据: ${result.finalCount} 条`);
|
console.log(` 最终数据: ${result.finalCount} 条`);
|
||||||
console.log(` 操作成功: ${result.success}`);
|
console.log(` 操作成功: ${result.success}`);
|
||||||
console.log('='.repeat(60));
|
console.log('='.repeat(60));
|
||||||
})
|
} catch (error) {
|
||||||
.catch(error => {
|
|
||||||
console.error('测试失败:', error);
|
console.error('测试失败:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
runTest();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user