Skip to content

Commit d2ef47d

Browse files
author
Taois
committed
feat: 增加简易的定时脚本执行器,下个版本增加完整版本
1 parent fce4bce commit d2ef47d

File tree

6 files changed

+776
-0
lines changed

6 files changed

+776
-0
lines changed

.env.development

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ ENV='staging'
66

77

88
LOG_WITH_FILE = 1
9+
ENABLE_TASKER = 0
10+
TASKER_INTERVAL = 0
911
FORCE_HEADER = 0
1012
# dr2的api,默认0取public目录。设置1取壳子内部assets
1113
DR2_API_TYPE = 0

controllers/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import decoderController from './decoder.js';
88
import authCoderController from './authcoder.js';
99
import webController from './web.js';
1010
import httpController from './http.js';
11+
import taskController from './tasker.js';
1112

1213
export const registerRoutes = (fastify, options) => {
1314
fastify.register(docsController, options);
@@ -20,4 +21,5 @@ export const registerRoutes = (fastify, options) => {
2021
fastify.register(authCoderController, options);
2122
fastify.register(webController, options);
2223
fastify.register(httpController, options);
24+
fastify.register(taskController, options);
2325
};

controllers/tasker.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import path from 'path';
2+
import {readdir, stat} from 'fs/promises';
3+
import {pathToFileURL} from 'url'; // 添加此导入
4+
5+
6+
const scripts_exclude = ['moontv.mjs', 'kzz.mjs'];
7+
const enable_tasker = Number(process.env.ENABLE_TASKER) || 0;
8+
const tasker_interval = Number(process.env.TASKER_INTERVAL) || 30 * 60 * 1000; // 30分钟执行一次;
9+
10+
11+
export default (fastify, options, done) => {
12+
const config = {
13+
scriptsDir: path.join(options.rootDir, 'scripts'), // 脚本目录
14+
interval: tasker_interval,
15+
};
16+
17+
// 加载并执行单个脚本 (ESM 动态导入)
18+
async function executeScript(scriptPath) {
19+
try {
20+
fastify.log.info(`Executing script: ${scriptPath}`);
21+
22+
const scriptUrl = pathToFileURL(scriptPath).href;
23+
// 动态导入 ES 模块
24+
const module = await import(scriptUrl);
25+
const script = module.default || module;
26+
27+
if (typeof script.run === 'function') {
28+
await script.run(fastify);
29+
} else {
30+
fastify.log.warn(`Script ${scriptPath} does not export a 'run' function`);
31+
}
32+
} catch (err) {
33+
fastify.log.error(`Error executing script ${scriptPath}: ${err}`);
34+
}
35+
}
36+
37+
// 执行目录下所有脚本
38+
async function executeAllScripts() {
39+
try {
40+
fastify.log.info('Starting script execution...');
41+
42+
const files = await readdir(config.scriptsDir);
43+
44+
for (const file of files) {
45+
const filePath = path.join(config.scriptsDir, file);
46+
const fileStat = await stat(filePath);
47+
48+
// 只处理.mjs和.js文件
49+
if (fileStat.isFile() && ['.mjs', '.js'].includes(path.extname(file)) && !scripts_exclude.includes(file)) {
50+
console.log(`Starting script execution:${file}| ${filePath}`);
51+
await executeScript(filePath);
52+
}
53+
}
54+
55+
fastify.log.info('Script execution completed');
56+
} catch (err) {
57+
fastify.log.error(`Error reading scripts directory:${err}`);
58+
}
59+
}
60+
61+
// 启动定时任务
62+
function startScheduledTasks() {
63+
// 立即执行一次
64+
executeAllScripts().then(r => {
65+
});
66+
// 设置定时器
67+
setInterval(executeAllScripts, config.interval);
68+
69+
fastify.log.info(`Scheduled tasks started, running every ${config.interval / 1000} seconds`);
70+
}
71+
72+
fastify.get('/execute-now', async (request, reply) => {
73+
await executeAllScripts();
74+
return {message: 'Scripts executed manually'};
75+
});
76+
77+
fastify.get('/status', async (request, reply) => {
78+
return {
79+
running: true,
80+
lastRun: new Date(),
81+
nextRun: new Date(Date.now() + config.interval)
82+
};
83+
});
84+
if (enable_tasker) {
85+
console.log('enable_tasker:', enable_tasker);
86+
console.log(`tasker_interval: ${tasker_interval} (ms) => ${tasker_interval / 60000}(m)`);
87+
// 启动定时任务
88+
startScheduledTasks();
89+
}
90+
done()
91+
}

scripts/example-task.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// scripts/example-task.mjs
2+
export default {
3+
run: async (fastify) => {
4+
const timestamp = new Date().toISOString();
5+
fastify.log.info(`📝 Example task started at ${timestamp}`);
6+
7+
// 模拟任务执行
8+
await new Promise(resolve => setTimeout(resolve, 1500));
9+
10+
// 随机生成成功/失败
11+
const success = Math.random() > 0.2;
12+
13+
if (success) {
14+
fastify.log.info('✅ Example task completed successfully');
15+
return {status: 'success', message: 'Task completed'};
16+
} else {
17+
fastify.log.error('❌ Example task failed');
18+
throw new Error('Simulated task failure');
19+
}
20+
}
21+
};

scripts/kzz.mjs

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// kzz.mjs
2+
import axios from 'axios';
3+
import dayjs from 'dayjs';
4+
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
5+
6+
// 扩展dayjs以支持自定义格式解析
7+
dayjs.extend(customParseFormat);
8+
9+
// 默认用户代理
10+
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36';
11+
12+
/**
13+
* 获取可转债数据
14+
* @param {string|number} taskId - 任务ID
15+
* @param {string} [url='http://data.eastmoney.com/kzz/'] - 东方财富网查询地址
16+
* @param {number} [dayeExtra=0] - 日期偏移量。正数:未来天数,负数:过去天数
17+
* @param {Object} [kwargs] - 其他可选参数
18+
* @returns {Promise<Array>} - 可转债列表
19+
*/
20+
export async function getNowKzz(taskId, url = 'http://data.eastmoney.com/kzz/', dayeExtra = 0, kwargs = {}) {
21+
console.log(`[任务 ${taskId}] 开始获取可转债数据,日期偏移量: ${dayeExtra}, 参数: ${JSON.stringify(kwargs)}`);
22+
23+
const headers = {
24+
'User-Agent': DEFAULT_USER_AGENT,
25+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
26+
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
27+
'Connection': 'keep-alive',
28+
'Upgrade-Insecure-Requests': '1',
29+
'Pragma': 'no-cache',
30+
'Cache-Control': 'no-cache'
31+
};
32+
33+
// 创建axios会话实例
34+
const session = axios.create({
35+
headers,
36+
timeout: 15000,
37+
withCredentials: true
38+
});
39+
40+
try {
41+
// 先访问首页获取必要的cookies
42+
console.log(`[任务 ${taskId}] 访问首页获取cookies...`);
43+
await session.get(url);
44+
45+
// 构建数据API URL - 使用随机回调函数名
46+
const callbackName = `jQuery${Math.floor(Math.random() * 1000000000)}_${Date.now()}`;
47+
const dataUrl = `https://datacenter-web.eastmoney.com/api/data/v1/get?callback=${callbackName}&sortColumns=PUBLIC_START_DATE&sortTypes=-1&pageSize=50&pageNumber=1&reportName=RPT_BOND_CB_LIST&columns=ALL&quoteColumns=f2~01~CONVERT_STOCK_CODE~CONVERT_STOCK_PRICE%2Cf235~10~SECURITY_CODE~TRANSFER_PRICE%2Cf236~10~SECURITY_CODE~TRANSFER_VALUE%2Cf2~10~SECURITY_CODE~CURRENT_BOND_PRICE%2Cf237~10~SECURITY_CODE~TRANSFER_PREMIUM_RATIO%2Cf239~10~SECURITY_CODE~RESALE_TRIG_PRICE%2Cf240~10~SECURITY_CODE~REDEEM_TRIG_PRICE%2Cf23~01~CONVERT_STOCK_CODE~PBV_RATIO&quoteType=0&source=WEB&client=WEB`;
48+
49+
console.log(`[任务 ${taskId}] 请求API数据...`);
50+
const response = await session.get(dataUrl);
51+
52+
// 解析JSONP响应
53+
console.log(`[任务 ${taskId}] 解析响应数据...`);
54+
const jsonStr = extractJsonFromJsonp(response.data);
55+
const data = safeParseJson(jsonStr);
56+
57+
console.log(`[任务 ${taskId}] 处理可转债数据...`);
58+
return processKzzData(data, dayeExtra);
59+
} catch (error) {
60+
const errorMsg = `[任务 ${taskId}] 获取可转债数据失败: ${error.message}`;
61+
console.error(errorMsg);
62+
throw new Error(errorMsg);
63+
}
64+
}
65+
66+
/**
67+
* 从JSONP响应中提取JSON字符串
68+
* @param {string} jsonp - JSONP响应字符串
69+
* @returns {string} - JSON字符串
70+
*/
71+
function extractJsonFromJsonp(jsonp) {
72+
// 尝试多种解析方式
73+
const patterns = [
74+
/^[^{]*?\(({.*})\)[^}]*?$/,
75+
/^.*?\(({.*})\)$/,
76+
/^.*?\(({.*})\);?$/,
77+
/({.*})/
78+
];
79+
80+
for (const pattern of patterns) {
81+
const match = jsonp.match(pattern);
82+
if (match && match[1]) {
83+
return match[1];
84+
}
85+
}
86+
87+
// 尝试直接解析为JSON
88+
try {
89+
JSON.parse(jsonp);
90+
return jsonp;
91+
} catch (e) {
92+
throw new Error('无法解析JSONP响应: ' + jsonp.substring(0, 100) + '...');
93+
}
94+
}
95+
96+
/**
97+
* 安全解析JSON字符串
98+
* @param {string} jsonStr - JSON字符串
99+
* @returns {Object} - 解析后的对象
100+
*/
101+
function safeParseJson(jsonStr) {
102+
try {
103+
return JSON.parse(jsonStr);
104+
} catch (e) {
105+
// 尝试修复常见的JSON格式问题
106+
try {
107+
const fixedJson = jsonStr
108+
.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/g, '$1"$2"$3')
109+
.replace(/:\s*'([^']*)'/g, ': "$1"')
110+
.replace(/(\w+)\s*:/g, '"$1":') // 修复键名缺少引号
111+
.replace(/:\s*([a-zA-Z_][\w]*)([,\}])/g, ':"$1"$2'); // 修复字符串值缺少引号
112+
113+
return JSON.parse(fixedJson);
114+
} catch (e2) {
115+
console.error('JSON解析错误:', e2.message);
116+
console.error('原始JSON字符串:', jsonStr.substring(0, 200));
117+
throw new Error('JSON解析失败: ' + e2.message);
118+
}
119+
}
120+
}
121+
122+
function compareDates(dateStr1, dateStr2) {
123+
const date1 = new Date(dateStr1);
124+
const date2 = new Date(dateStr2);
125+
return date1.getTime() - date2.getTime();
126+
}
127+
128+
// 返回值>0表示date1较晚,<0表示date1较早,=0表示相同
129+
130+
131+
/**
132+
* 处理可转债数据
133+
* @param {Object} data - 原始数据
134+
* @param {number} dayeExtra - 日期偏移量
135+
* @returns {Array} - 处理后的可转债列表
136+
*/
137+
function processKzzData(data, dayeExtra) {
138+
if (!data || !data.result || !Array.isArray(data.result.data)) {
139+
throw new Error('API返回的数据结构无效');
140+
}
141+
142+
const resultData = data.result.data;
143+
144+
// 计算目标日期范围
145+
const today = dayjs();
146+
const targetDate = today.add(dayeExtra, 'day');
147+
const targetDateStr = targetDate.format('YYYY-MM-DD');
148+
149+
console.log(`筛选日期: ${targetDateStr} (偏移量: ${dayeExtra}天)`);
150+
151+
// 处理数据
152+
const kzzList = [];
153+
const dateFormats = [
154+
'YYYY-MM-DD HH:mm:ss',
155+
'YYYY/MM/DD HH:mm:ss',
156+
'YYYY-MM-DD',
157+
'YYYY/MM/DD',
158+
'YYYY-M-DD',
159+
'YYYY-M-D',
160+
'YYYY/MM/DD'
161+
];
162+
163+
for (const item of resultData) {
164+
if (!item.PUBLIC_START_DATE) {
165+
console.warn(`跳过无发行日期的项目: ${item.SECURITY_NAME_ABBR}`);
166+
continue;
167+
}
168+
169+
// 解析日期
170+
let publicDate;
171+
for (const format of dateFormats) {
172+
publicDate = dayjs(item.PUBLIC_START_DATE, format, true);
173+
if (publicDate.isValid()) break;
174+
}
175+
176+
if (!publicDate.isValid()) {
177+
console.warn(`无效的日期格式: ${item.PUBLIC_START_DATE} (项目: ${item.SECURITY_NAME_ABBR})`);
178+
continue;
179+
}
180+
181+
const dateStr = publicDate.format('YYYY-MM-DD');
182+
183+
// 检查是否匹配目标日期
184+
// if (dateStr === targetDateStr) {
185+
if (compareDates(dateStr, targetDateStr) >= 0) {
186+
kzzList.push({
187+
name: item.SECURITY_NAME_ABBR,
188+
code: item.SECURITY_CODE,
189+
date: dateStr
190+
});
191+
}
192+
}
193+
194+
console.log(`找到 ${kzzList.length} 个匹配的可转债`);
195+
return kzzList;
196+
}
197+
198+
/**
199+
* 主函数 - 测试用
200+
*/
201+
async function main() {
202+
try {
203+
// 支持正负偏移量测试
204+
const testCases = [
205+
{offset: 0, desc: "今天"},
206+
{offset: 1, desc: "明天"},
207+
{offset: -1, desc: "昨天"},
208+
{offset: 7, desc: "7天后"},
209+
{offset: -7, desc: "7天前"}
210+
];
211+
212+
for (const testCase of testCases) {
213+
console.log(`\n===== 测试 ${testCase.desc} (偏移量: ${testCase.offset}) =====`);
214+
const kzzData = await getNowKzz(
215+
`test_${testCase.offset}`,
216+
'http://data.eastmoney.com/kzz/',
217+
testCase.offset
218+
);
219+
220+
if (kzzData.length > 0) {
221+
console.log(`找到 ${kzzData.length} 个可转债:`);
222+
kzzData.forEach((item, index) => {
223+
console.log(`${index + 1}. ${item.name} (${item.code}) - ${item.date}`);
224+
});
225+
} else {
226+
console.log(`未找到 ${testCase.desc} 的可转债数据`);
227+
}
228+
}
229+
230+
console.log('\n所有测试完成');
231+
} catch (error) {
232+
console.error('测试失败:', error.message);
233+
}
234+
}
235+
236+
// 使用立即执行的异步函数调用main
237+
(async () => {
238+
try {
239+
// await main();
240+
const kzzData = await getNowKzz(
241+
`test_7`,
242+
'http://data.eastmoney.com/kzz/',
243+
7
244+
);
245+
console.log(kzzData);
246+
console.log('程序执行完成');
247+
} catch (error) {
248+
console.error('程序执行失败:', error.message);
249+
process.exit(1);
250+
}
251+
})();

0 commit comments

Comments
 (0)