fix: 增加mysql、redis缓存

master
Lxy 3 months ago
parent f45ee680d4
commit 955f22cd75

@ -16,6 +16,7 @@
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.3",
"morgan": "^1.10.0",
"mysql2": "^3.9.1",
"pg": "^8.11.3",
"redis": "^4.6.12",
"socket.io": "^4.7.4"
@ -974,6 +975,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1488,6 +1498,15 @@
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -2264,6 +2283,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
@ -2690,6 +2718,12 @@
"node": ">=8"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@ -2902,6 +2936,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/loupe": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
@ -2912,6 +2952,21 @@
"get-func-name": "^2.0.1"
}
},
"node_modules/lru.min": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@ -3270,6 +3325,56 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/mysql2": {
"version": "3.18.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.18.0.tgz",
"integrity": "sha512-3rupyOFks7Vq0jcjBpmg1gtgfGuCcmgrRJPEfpGzzrB/ydutupbjKkoDJGsGkrJRU6j44o2tb0McduL03/v/dQ==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.2",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.2",
"long": "^5.3.2",
"lru.min": "^1.1.4",
"named-placeholders": "^1.1.6",
"sql-escaper": "^1.3.3"
},
"engines": {
"node": ">= 8.0"
},
"peerDependencies": {
"@types/node": ">= 8"
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/named-placeholders": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
"license": "MIT",
"dependencies": {
"lru.min": "^1.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -4228,6 +4333,21 @@
"node": ">= 10.x"
}
},
"node_modules/sql-escaper": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
"integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=2.0.0",
"node": ">=12.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",

@ -19,6 +19,7 @@
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.3",
"morgan": "^1.10.0",
"mysql2": "^3.9.1",
"pg": "^8.11.3",
"redis": "^4.6.12",
"socket.io": "^4.7.4"

@ -0,0 +1,2 @@
export * from './mysql';
export * from './redis';

@ -0,0 +1,86 @@
import mysql from 'mysql2/promise';
import { config } from '../index';
import { logger } from '../../utils/logger';
class MySQLConnection {
private pool: mysql.Pool | null = null;
async connect() {
try {
this.pool = mysql.createPool({
host: config.database.mysql.host,
port: config.database.mysql.port,
user: config.database.mysql.user,
password: config.database.mysql.password,
database: config.database.mysql.database,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// 测试连接
const connection = await this.pool.getConnection();
logger.log('MySQL连接成功');
connection.release();
// 初始化表结构
await this.initTables();
} catch (error) {
logger.error('MySQL连接失败:', error);
throw error;
}
}
async initTables() {
if (!this.pool) {
throw new Error('MySQL连接未初始化');
}
try {
// 创建市场数据表格
await this.pool.execute(`
CREATE TABLE IF NOT EXISTS market_data (
id INT AUTO_INCREMENT PRIMARY KEY,
symbol VARCHAR(20) NOT NULL,
type VARCHAR(20) NOT NULL,
data JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_symbol_type (symbol, type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
logger.log('MySQL表结构初始化完成');
} catch (error) {
logger.error('初始化MySQL表结构失败:', error);
throw error;
}
}
async getConnection() {
if (!this.pool) {
await this.connect();
}
return this.pool!.getConnection();
}
async query(sql: string, values?: any[]) {
if (!this.pool) {
await this.connect();
}
if (values) {
return this.pool!.execute(sql, values);
} else {
return this.pool!.execute(sql);
}
}
async close() {
if (this.pool) {
await this.pool.end();
logger.log('MySQL连接已关闭');
}
}
}
export const mysqlConnection = new MySQLConnection();

@ -0,0 +1,84 @@
import { createClient } from 'redis';
import { config } from '../index';
import { logger } from '../../utils/logger';
class RedisConnection {
private client: ReturnType<typeof createClient> | null = null;
async connect() {
try {
this.client = createClient({
url: config.redis.url
});
this.client.on('error', (error) => {
logger.error('Redis连接错误:', error);
});
this.client.on('connect', () => {
logger.log('Redis连接成功');
});
this.client.on('end', () => {
logger.log('Redis连接已关闭');
});
await this.client.connect();
} catch (error) {
logger.error('Redis连接失败:', error);
throw error;
}
}
async getClient() {
if (!this.client || !this.client.isReady) {
await this.connect();
}
return this.client!;
}
async get(key: string) {
const client = await this.getClient();
return client.get(key);
}
async set(key: string, value: string | number | Buffer, options?: {
ex?: number;
px?: number;
nx?: boolean;
xx?: boolean;
}) {
const client = await this.getClient();
const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
if (options) {
// 使用Redis v4的正确set方法参数格式
const setOptions: any = {};
if (options.ex) setOptions.EX = options.ex;
if (options.px) setOptions.PX = options.px;
if (options.nx) setOptions.NX = options.nx;
if (options.xx) setOptions.XX = options.xx;
return client.set(key, stringValue, setOptions);
} else {
return client.set(key, stringValue);
}
}
async del(key: string) {
const client = await this.getClient();
return client.del(key);
}
async exists(key: string) {
const client = await this.getClient();
return client.exists(key);
}
async close() {
if (this.client) {
await this.client.quit();
logger.log('Redis连接已关闭');
}
}
}
export const redisConnection = new RedisConnection();

@ -61,6 +61,13 @@ export const config = {
user: process.env.PG_USER || (fileConfig.database?.postgreSQL?.username || 'postgres'),
password: process.env.PG_PASSWORD || (fileConfig.database?.postgreSQL?.password || 'password'),
database: process.env.PG_DATABASE || (fileConfig.database?.postgreSQL?.database || 'alpha-futures')
},
mysql: {
host: process.env.MYSQL_HOST || 'localhost',
port: parseInt(process.env.MYSQL_PORT || '3306'),
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || 'password',
database: process.env.MYSQL_DATABASE || 'alpha-futures'
}
},
redis: {

@ -0,0 +1,128 @@
import { redisConnection, mysqlConnection } from '../config/database';
import { logger } from '../utils/logger';
interface CacheOptions {
expireTime?: number; // Redis过期时间
}
class CacheService {
/**
*
* Redis MySQL
*/
async get<T>(key: string, symbol: string, type: string, fetchFromSource: () => Promise<T>, options: CacheOptions = {}): Promise<T> {
try {
// 1. 先从Redis中读取
logger.log(`尝试从Redis获取缓存: ${key}`);
const redisData = await redisConnection.get(key);
if (redisData) {
logger.log(`从Redis获取缓存成功: ${key}`);
return JSON.parse(redisData) as T;
}
// 2. Redis中不存在从MySQL中获取
logger.log(`尝试从MySQL获取缓存: ${symbol}, ${type}`);
const [mysqlResults]: any = await mysqlConnection.query(
'SELECT data FROM market_data WHERE symbol = ? AND type = ?',
[symbol, type]
);
if (mysqlResults && mysqlResults.length > 0) {
const mysqlData = mysqlResults[0].data;
logger.log(`从MySQL获取缓存成功: ${symbol}, ${type}`);
// 将MySQL数据同步到Redis
await this.set(key, mysqlData, options);
return mysqlData as T;
}
// 3. MySQL中也不存在从数据源获取
logger.log(`从数据源获取数据: ${key}`);
const sourceData = await fetchFromSource();
// 将数据源数据同步到MySQL和Redis
await this.set(key, sourceData, options);
await this.saveToMySQL(symbol, type, sourceData);
return sourceData;
} catch (error) {
logger.error(`获取缓存数据失败: ${key}`, error);
// 出错时直接从数据源获取
return fetchFromSource();
}
}
/**
* Redis
*/
async set(key: string, value: any, options: CacheOptions = {}): Promise<void> {
try {
const jsonValue = JSON.stringify(value);
const redisOptions: any = {};
if (options.expireTime) {
redisOptions.ex = options.expireTime;
}
await redisConnection.set(key, jsonValue, redisOptions);
logger.log(`设置Redis缓存成功: ${key}`);
} catch (error) {
logger.error(`设置Redis缓存失败: ${key}`, error);
}
}
/**
* MySQL
*/
async saveToMySQL(symbol: string, type: string, data: any): Promise<void> {
try {
// 使用UPSERT操作存在则更新不存在则插入
await mysqlConnection.query(
`INSERT INTO market_data (symbol, type, data)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE data = VALUES(data)`,
[symbol, type, JSON.stringify(data)]
);
logger.log(`保存数据到MySQL成功: ${symbol}, ${type}`);
} catch (error) {
logger.error(`保存数据到MySQL失败: ${symbol}, ${type}`, error);
}
}
/**
*
*/
async delete(key: string): Promise<void> {
try {
await redisConnection.del(key);
logger.log(`删除Redis缓存成功: ${key}`);
} catch (error) {
logger.error(`删除Redis缓存失败: ${key}`, error);
}
}
/**
*
*/
async clearByType(symbol: string, type: string): Promise<void> {
try {
// 清除Redis缓存
const key = `${symbol}:${type}`;
await this.delete(key);
// 清除MySQL缓存
await mysqlConnection.query(
'DELETE FROM market_data WHERE symbol = ? AND type = ?',
[symbol, type]
);
logger.log(`清除缓存成功: ${symbol}, ${type}`);
} catch (error) {
logger.error(`清除缓存失败: ${symbol}, ${type}`, error);
}
}
}
export const cacheService = new CacheService();
export default cacheService;

@ -64,7 +64,7 @@ class HttpClient {
async post<T>(endpoint: string, data?: any): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
logger.log('发送POST请求:', url, '数据:', data);
logger.log(`发送POST请求: ${url} 数据: ${JSON.stringify(data)}`);
try {
// 设置20秒超时

@ -4,6 +4,7 @@ import { futuresList, generateFuturesOverview, generateFutureData, generateKline
import { config } from '../config';
import { serviceImplementationClient } from './ServiceImplementationClient';
import { logger } from '../utils/logger';
import { cacheService } from './cacheService';
// 获取数据源配置
const getDataSourceConfig = () => {
@ -13,6 +14,11 @@ const getDataSourceConfig = () => {
// 获取市场概览
export const fetchMarketOverview = async () => {
const cacheKey = 'market:overview';
const symbol = 'market';
const type = 'overview';
return cacheService.get(cacheKey, symbol, type, async () => {
try {
// 首先尝试使用 service_implementation API
try {
@ -131,113 +137,118 @@ export const fetchMarketOverview = async () => {
// service_implementation API 失败,尝试使用其他数据源
}
// // 获取数据源配置
// const dataSourceConfig = getDataSourceConfig();
// logger.log('获取数据源配置:', dataSourceConfig);
// // 检查是否有可用的数据源
// const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
// if (!hasAvailableDataSource) {
// throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
// }
// // 尝试使用TQSDK数据源
// if (dataSourceConfig.tqsdk?.enabled) {
// try {
// const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// // 使用用户指定的合约列表
// const overview = [];
// for (const future of futuresList) {
// try {
// // 构建合约符号使用小写代码因为TQAPI期望小写
// const symbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// // 获取合约详情和实时行情
// const tick = await dataSource.getTickData(symbol);
// overview.push({
// code: future.code,
// name: future.name,
// currentPrice: tick.last_price,
// changePercent: tick.price_change / tick.pre_close * 100,
// winRate: Math.floor(Math.random() * 50) + 30, // 模拟胜率
// atr: +(Math.random() * 5 + 0.5).toFixed(2), // 模拟ATR
// adx: Math.floor(Math.random() * 60) + 10, // 模拟ADX
// adxStatus: (adx: number) => {
// if (adx < 20) return '无趋势/震荡';
// if (adx < 40) return '弱趋势';
// return '强趋势';
// },
// trends: {
// '5MIN': {
// direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
// status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
// rsi: Math.floor(Math.random() * 80) + 10
// },
// '30MIN': {
// direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
// status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
// rsi: Math.floor(Math.random() * 80) + 10
// },
// '1HOUR': {
// direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
// status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
// rsi: Math.floor(Math.random() * 80) + 10
// },
// '1DAY': {
// direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
// status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
// rsi: Math.floor(Math.random() * 80) + 10
// }
// },
// tradingAdvice: {
// entry: tick.last_price,
// stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)),
// target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)),
// resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)),
// support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5))
// },
// overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)],
// aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近`
// });
// } catch (error) {
// logger.error(`获取合约${future.code}行情失败:`, error);
// // 跳过获取失败的合约
// continue;
// }
// }
// return overview;
// } catch (error) {
// logger.error('TQSDK数据源获取失败:', error);
// // TQSDK数据源失败尝试使用测试数据源
// if (dataSourceConfig.test?.enabled) {
// logger.log('切换到测试数据源');
// // 启用了测试数据源,使用测试数据
// await new Promise(resolve => setTimeout(resolve, 300));
// return generateFuturesOverview();
// } else {
// // 未启用测试数据源,返回友好的错误提示
// throw new Error('获取市场概览失败,所有数据源均不可用');
// }
// }
// } else if (dataSourceConfig.test?.enabled) {
// // 直接使用测试数据源
// logger.log('使用测试数据源');
// await new Promise(resolve => setTimeout(resolve, 300));
// return generateFuturesOverview();
// } else {
// // 无可用数据源
// throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
// }
// 获取数据源配置
const dataSourceConfig = getDataSourceConfig();
logger.log('获取数据源配置:', dataSourceConfig);
// 检查是否有可用的数据源
const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
// 尝试使用TQSDK数据源
if (dataSourceConfig.tqsdk?.enabled) {
try {
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// 使用用户指定的合约列表
const overview = [];
for (const future of futuresList) {
try {
// 构建合约符号使用小写代码因为TQAPI期望小写
const symbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// 获取合约详情和实时行情
const tick = await dataSource.getTickData(symbol);
overview.push({
code: future.code,
name: future.name,
currentPrice: tick.last_price,
changePercent: tick.price_change / tick.pre_close * 100,
winRate: Math.floor(Math.random() * 50) + 30, // 模拟胜率
atr: +(Math.random() * 5 + 0.5).toFixed(2), // 模拟ATR
adx: Math.floor(Math.random() * 60) + 10, // 模拟ADX
adxStatus: (adx: number) => {
if (adx < 20) return '无趋势/震荡';
if (adx < 40) return '弱趋势';
return '强趋势';
},
trends: {
'5MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'30MIN': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1HOUR': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
},
'1DAY': {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10
}
},
tradingAdvice: {
entry: tick.last_price,
stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)),
target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)),
resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)),
support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5))
},
overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)],
aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近`
});
} catch (error) {
logger.error(`获取合约${future.code}行情失败:`, error);
// 跳过获取失败的合约
continue;
}
}
return overview;
} catch (error) {
logger.error('TQSDK数据源获取失败:', error);
// TQSDK数据源失败尝试使用测试数据源
if (dataSourceConfig.test?.enabled) {
logger.log('切换到测试数据源');
// 启用了测试数据源,使用测试数据
await new Promise(resolve => setTimeout(resolve, 300));
return generateFuturesOverview();
} else {
// 未启用测试数据源,返回友好的错误提示
throw new Error('获取市场概览失败,所有数据源均不可用');
}
}
} else if (dataSourceConfig.test?.enabled) {
// 直接使用测试数据源
logger.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 300));
return generateFuturesOverview();
} else {
// 无可用数据源
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
} catch (error) {
logger.error('获取市场概览失败:', error);
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取市场概览失败,请检查数据源配置');
}
}, { expireTime: 300 }); // 缓存5分钟
};
// 获取品种详情
export const fetchMarketDetail = async (symbol: string) => {
const cacheKey = `market:detail:${symbol}`;
const type = 'detail';
return cacheService.get(cacheKey, symbol, type, async () => {
try {
// 提取合约代码从SHFE.AU2605中提取AU
const code = symbol.includes('.') ? symbol.split('.')[1].slice(0, 2) : symbol;
@ -445,10 +456,15 @@ export const fetchMarketDetail = async (symbol: string) => {
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取品种详情失败,请检查数据源配置');
}
}, { expireTime: 300 }); // 缓存5分钟
};
// 获取K线数据
export const fetchKlineData = async (symbol: string, period: string) => {
const cacheKey = `market:kline:${symbol}:${period}`;
const type = 'kline';
return cacheService.get(cacheKey, symbol, type, async () => {
try {
// 查找合约信息
const future = futuresList.find(item => item.code === symbol);
@ -520,66 +536,72 @@ export const fetchKlineData = async (symbol: string, period: string) => {
// service_implementation API 失败,尝试使用其他数据源
}
// // 获取数据源配置
// const dataSourceConfig = getDataSourceConfig();
// // 检查是否有可用的数据源
// const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
// if (!hasAvailableDataSource) {
// throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
// }
// // 尝试使用TQSDK数据源
// if (dataSourceConfig.tqsdk?.enabled) {
// try {
// const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// // 构建合约符号使用小写代码因为TQAPI期望小写
// const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// // 获取K线数据
// const klineData = await dataSource.getKlineData(contractSymbol, period, 30);
// // 转换为前端需要的格式
// return klineData.map(item => ({
// timestamp: item.datetime / 1000000000, // 转换为秒
// open: item.open,
// high: item.high,
// low: item.low,
// close: item.close,
// volume: item.volume
// }));
// } catch (error) {
// logger.error('TQSDK数据源获取失败:', error);
// // TQSDK数据源失败尝试使用测试数据源
// if (dataSourceConfig.test?.enabled) {
// logger.log('切换到测试数据源');
// // 启用了测试数据源,使用测试数据
// await new Promise(resolve => setTimeout(resolve, 200));
// return generateKlineData(30);
// } else {
// // 未启用测试数据源,返回友好的错误提示
// throw new Error('获取K线数据失败所有数据源均不可用');
// }
// }
// } else if (dataSourceConfig.test?.enabled) {
// // 直接使用测试数据源
// logger.log('使用测试数据源');
// await new Promise(resolve => setTimeout(resolve, 200));
// return generateKlineData(30);
// } else {
// // 无可用数据源
// throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
// }
// 获取数据源配置
const dataSourceConfig = getDataSourceConfig();
// 检查是否有可用的数据源
const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
// 尝试使用TQSDK数据源
if (dataSourceConfig.tqsdk?.enabled) {
try {
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
// 构建合约符号使用小写代码因为TQAPI期望小写
const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`;
// 获取K线数据
const klineData = await dataSource.getKlineData(contractSymbol, period, 30);
// 转换为前端需要的格式
return klineData.map(item => ({
timestamp: item.datetime / 1000000000, // 转换为秒
open: item.open,
high: item.high,
low: item.low,
close: item.close,
volume: item.volume
}));
} catch (error) {
logger.error('TQSDK数据源获取失败:', error);
// TQSDK数据源失败尝试使用测试数据源
if (dataSourceConfig.test?.enabled) {
logger.log('切换到测试数据源');
// 启用了测试数据源,使用测试数据
await new Promise(resolve => setTimeout(resolve, 200));
return generateKlineData(30);
} else {
// 未启用测试数据源,返回友好的错误提示
throw new Error('获取K线数据失败所有数据源均不可用');
}
}
} else if (dataSourceConfig.test?.enabled) {
// 直接使用测试数据源
logger.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 200));
return generateKlineData(30);
} else {
// 无可用数据源
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
}
} catch (error) {
logger.error(`获取合约${symbol}K线数据失败:`, error);
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取K线数据失败请检查数据源配置');
}
}, { expireTime: 600 }); // 缓存10分钟
};
// 获取市场热点
export const fetchMarketHotspots = async () => {
const cacheKey = 'market:hotspots';
const symbol = 'market';
const type = 'hotspots';
return cacheService.get(cacheKey, symbol, type, async () => {
try {
// 首先尝试使用 service_implementation API
try {
@ -749,6 +771,7 @@ export const fetchMarketHotspots = async () => {
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取市场热点失败,请检查数据源配置');
}
}, { expireTime: 300 }); // 缓存5分钟
};
// 获取风险预警

@ -43,7 +43,7 @@ export const savePushSettings = async (symbol: string, settings: any) => {
await new Promise(resolve => setTimeout(resolve, 300));
pushSettings[symbol] = settings;
logger.log('保存推送设置:', symbol, settings);
logger.log(`保存推送设置: ${symbol} ${JSON.stringify(settings)}`);
return settings;
};
@ -53,7 +53,7 @@ export const testPush = async (method: string, content: string) => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
logger.log('测试推送:', method, content);
logger.log(`测试推送: ${method} ${content}`);
// 模拟推送成功
return { success: true, message: '测试推送成功' };

@ -22,23 +22,12 @@ const AdminConfig = () => {
//
const databaseConfig = {
// MongoDB
mongoDB: {
// MySQL
mysql: {
host: 'localhost',
port: 27017,
port: 3306,
database: 'alpha-futures',
username: '',
password: '',
authSource: 'admin',
ssl: false,
enabled: true
},
// PostgreSQL
postgreSQL: {
host: 'localhost',
port: 5432,
database: 'alpha-futures',
username: 'postgres',
username: 'root',
password: 'password',
ssl: false,
enabled: true
@ -50,16 +39,6 @@ const AdminConfig = () => {
password: '',
db: 0,
enabled: true
},
// InfluxDB
influxDB: {
host: 'localhost',
port: 8086,
database: 'alpha-futures',
username: '',
password: '',
ssl: false,
enabled: true
}
};
@ -339,11 +318,25 @@ const AdminConfig = () => {
//
const completeConfig = {
database: newConfig.database || {
mongoDB: { host: 'localhost', port: 27017, database: 'alpha-futures', username: '', password: '', authSource: 'admin', ssl: false, enabled: true },
postgreSQL: { host: 'localhost', port: 5432, database: 'alpha-futures', username: 'postgres', password: 'password', ssl: false, enabled: true },
redis: { host: 'localhost', port: 6379, password: '', db: 0, enabled: true },
influxDB: { host: 'localhost', port: 8086, database: 'alpha-futures', username: '', password: '', ssl: false, enabled: true }
database: {
mysql: {
host: 'localhost',
port: 3306,
database: 'alpha-futures',
username: 'root',
password: 'password',
ssl: false,
enabled: true,
...newConfig.database?.mysql
},
redis: {
host: 'localhost',
port: 6379,
password: '',
db: 0,
enabled: true,
...newConfig.database?.redis
}
},
server: newConfig.server || {
port: 3007, host: '0.0.0.0', environment: 'development', debug: true, timeout: 30000, maxBodySize: '10mb'
@ -496,22 +489,13 @@ const AdminConfig = () => {
//
form.setFieldsValue({
'database.mongoDB.host': completeConfig.database.mongoDB.host,
'database.mongoDB.port': completeConfig.database.mongoDB.port,
'database.mongoDB.database': completeConfig.database.mongoDB.database,
'database.mongoDB.username': completeConfig.database.mongoDB.username,
'database.mongoDB.password': completeConfig.database.mongoDB.password,
'database.mongoDB.authSource': completeConfig.database.mongoDB.authSource,
'database.mongoDB.ssl': completeConfig.database.mongoDB.ssl,
'database.mongoDB.enabled': completeConfig.database.mongoDB.enabled,
'database.postgreSQL.host': completeConfig.database.postgreSQL.host,
'database.postgreSQL.port': completeConfig.database.postgreSQL.port,
'database.postgreSQL.database': completeConfig.database.postgreSQL.database,
'database.postgreSQL.username': completeConfig.database.postgreSQL.username,
'database.postgreSQL.password': completeConfig.database.postgreSQL.password,
'database.postgreSQL.ssl': completeConfig.database.postgreSQL.ssl,
'database.postgreSQL.enabled': completeConfig.database.postgreSQL.enabled,
'database.mysql.host': completeConfig.database.mysql.host,
'database.mysql.port': completeConfig.database.mysql.port,
'database.mysql.database': completeConfig.database.mysql.database,
'database.mysql.username': completeConfig.database.mysql.username,
'database.mysql.password': completeConfig.database.mysql.password,
'database.mysql.ssl': completeConfig.database.mysql.ssl,
'database.mysql.enabled': completeConfig.database.mysql.enabled,
'database.redis.host': completeConfig.database.redis.host,
'database.redis.port': completeConfig.database.redis.port,
@ -519,14 +503,6 @@ const AdminConfig = () => {
'database.redis.db': completeConfig.database.redis.db,
'database.redis.enabled': completeConfig.database.redis.enabled,
'database.influxDB.host': completeConfig.database.influxDB.host,
'database.influxDB.port': completeConfig.database.influxDB.port,
'database.influxDB.database': completeConfig.database.influxDB.database,
'database.influxDB.username': completeConfig.database.influxDB.username,
'database.influxDB.password': completeConfig.database.influxDB.password,
'database.influxDB.ssl': completeConfig.database.influxDB.ssl,
'database.influxDB.enabled': completeConfig.database.influxDB.enabled,
'server.port': completeConfig.server.port,
'server.host': completeConfig.server.host,
'server.environment': completeConfig.server.environment,
@ -753,156 +729,64 @@ const AdminConfig = () => {
key: 'database',
children: (
<>
{/* MongoDB配置 */}
<Card title="MongoDB配置" className="admin-config-card" style={{ marginBottom: 24 }}>
{/* MySQL配置 */}
<Card title="MySQL配置" className="admin-config-card" style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Item label="主机" name="database.mongoDB.host">
<Input
onChange={(e) => handleDatabaseConfigChange('mongoDB', 'host', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="端口" name="database.mongoDB.port">
<InputNumber
min={1}
max={65535}
onChange={(value) => handleDatabaseConfigChange('mongoDB', 'port', value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="数据库" name="database.mongoDB.database">
<Input
onChange={(e) => handleDatabaseConfigChange('mongoDB', 'database', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="认证源" name="database.mongoDB.authSource">
<Item label="主机" name="database.mysql.host">
<Input
onChange={(e) => handleDatabaseConfigChange('mongoDB', 'authSource', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="用户名" name="database.mongoDB.username">
<Input
onChange={(e) => handleDatabaseConfigChange('mongoDB', 'username', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="密码" name="database.mongoDB.password">
<Input.Password
onChange={(e) => handleDatabaseConfigChange('mongoDB', 'password', e.target.value)}
onChange={(e) => handleDatabaseConfigChange('mysql', 'host', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="SSL" name="database.mongoDB.ssl">
<Switch
defaultChecked={config.database.mongoDB.ssl}
onChange={(checked) => handleDatabaseConfigChange('mongoDB', 'ssl', checked)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="启用" name="database.mongoDB.enabled">
<Switch
defaultChecked={config.database.mongoDB.enabled}
onChange={(checked) => handleDatabaseConfigChange('mongoDB', 'enabled', checked)}
/>
</Item>
</Col>
</Row>
<div style={{ marginTop: 16 }}>
<Button
type="primary"
onClick={() => testDatabaseConnection('MongoDB')}
style={{ marginRight: 8 }}
>
测试连接
</Button>
<Button
type="default"
onClick={() => {
handleDatabaseConfigChange('mongoDB', 'host', 'localhost');
handleDatabaseConfigChange('mongoDB', 'port', 27017);
handleDatabaseConfigChange('mongoDB', 'database', 'alpha-futures');
handleDatabaseConfigChange('mongoDB', 'username', '');
handleDatabaseConfigChange('mongoDB', 'password', '');
handleDatabaseConfigChange('mongoDB', 'authSource', 'admin');
handleDatabaseConfigChange('mongoDB', 'ssl', false);
handleDatabaseConfigChange('mongoDB', 'enabled', true);
}}
>
恢复默认
</Button>
</div>
</Card>
{/* PostgreSQL配置 */}
<Card title="PostgreSQL配置" className="admin-config-card" style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Item label="主机" name="database.postgreSQL.host">
<Input
onChange={(e) => handleDatabaseConfigChange('postgreSQL', 'host', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="端口" name="database.postgreSQL.port">
<Item label="端口" name="database.mysql.port">
<InputNumber
min={1}
max={65535}
onChange={(value) => handleDatabaseConfigChange('postgreSQL', 'port', value)}
onChange={(value) => handleDatabaseConfigChange('mysql', 'port', value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="数据库" name="database.postgreSQL.database">
<Item label="数据库" name="database.mysql.database">
<Input
onChange={(e) => handleDatabaseConfigChange('postgreSQL', 'database', e.target.value)}
onChange={(e) => handleDatabaseConfigChange('mysql', 'database', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="用户名" name="database.postgreSQL.username">
<Item label="用户名" name="database.mysql.username">
<Input
onChange={(e) => handleDatabaseConfigChange('postgreSQL', 'username', e.target.value)}
onChange={(e) => handleDatabaseConfigChange('mysql', 'username', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="密码" name="database.postgreSQL.password">
<Item label="密码" name="database.mysql.password">
<Input.Password
onChange={(e) => handleDatabaseConfigChange('postgreSQL', 'password', e.target.value)}
onChange={(e) => handleDatabaseConfigChange('mysql', 'password', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="SSL" name="database.postgreSQL.ssl">
<Item label="SSL" name="database.mysql.ssl">
<Switch
defaultChecked={config.database.postgreSQL.ssl}
onChange={(checked) => handleDatabaseConfigChange('postgreSQL', 'ssl', checked)}
defaultChecked={config.database.mysql.ssl}
onChange={(checked) => handleDatabaseConfigChange('mysql', 'ssl', checked)}
/>
</Item>
</Col>
<Col span={12}>
<Item label="启用" name="database.postgreSQL.enabled">
<Item label="启用" name="database.mysql.enabled">
<Switch
defaultChecked={config.database.postgreSQL.enabled}
onChange={(checked) => handleDatabaseConfigChange('postgreSQL', 'enabled', checked)}
defaultChecked={config.database.mysql.enabled}
onChange={(checked) => handleDatabaseConfigChange('mysql', 'enabled', checked)}
/>
</Item>
</Col>
@ -910,7 +794,7 @@ const AdminConfig = () => {
<div style={{ marginTop: 16 }}>
<Button
type="primary"
onClick={() => testDatabaseConnection('PostgreSQL')}
onClick={() => testDatabaseConnection('MySQL')}
style={{ marginRight: 8 }}
>
测试连接
@ -918,13 +802,13 @@ const AdminConfig = () => {
<Button
type="default"
onClick={() => {
handleDatabaseConfigChange('postgreSQL', 'host', 'localhost');
handleDatabaseConfigChange('postgreSQL', 'port', 5432);
handleDatabaseConfigChange('postgreSQL', 'database', 'alpha-futures');
handleDatabaseConfigChange('postgreSQL', 'username', 'postgres');
handleDatabaseConfigChange('postgreSQL', 'password', 'password');
handleDatabaseConfigChange('postgreSQL', 'ssl', false);
handleDatabaseConfigChange('postgreSQL', 'enabled', true);
handleDatabaseConfigChange('mysql', 'host', 'localhost');
handleDatabaseConfigChange('mysql', 'port', 3306);
handleDatabaseConfigChange('mysql', 'database', 'alpha-futures');
handleDatabaseConfigChange('mysql', 'username', 'root');
handleDatabaseConfigChange('mysql', 'password', 'password');
handleDatabaseConfigChange('mysql', 'ssl', false);
handleDatabaseConfigChange('mysql', 'enabled', true);
}}
>
恢复默认
@ -1003,92 +887,7 @@ const AdminConfig = () => {
</div>
</Card>
{/* InfluxDB配置 */}
<Card title="InfluxDB配置" className="admin-config-card" style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Item label="主机" name="database.influxDB.host">
<Input
onChange={(e) => handleDatabaseConfigChange('influxDB', 'host', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="端口" name="database.influxDB.port">
<InputNumber
min={1}
max={65535}
onChange={(value) => handleDatabaseConfigChange('influxDB', 'port', value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="数据库" name="database.influxDB.database">
<Input
onChange={(e) => handleDatabaseConfigChange('influxDB', 'database', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="用户名" name="database.influxDB.username">
<Input
onChange={(e) => handleDatabaseConfigChange('influxDB', 'username', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="密码" name="database.influxDB.password">
<Input.Password
onChange={(e) => handleDatabaseConfigChange('influxDB', 'password', e.target.value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="SSL" name="database.influxDB.ssl">
<Switch
defaultChecked={config.database.influxDB.ssl}
onChange={(checked) => handleDatabaseConfigChange('influxDB', 'ssl', checked)}
/>
</Item>
</Col>
<Col span={12}>
<Item label="启用" name="database.influxDB.enabled">
<Switch
defaultChecked={config.database.influxDB.enabled}
onChange={(checked) => handleDatabaseConfigChange('influxDB', 'enabled', checked)}
/>
</Item>
</Col>
</Row>
<div style={{ marginTop: 16 }}>
<Button
type="primary"
onClick={() => testDatabaseConnection('InfluxDB')}
style={{ marginRight: 8 }}
>
测试连接
</Button>
<Button
type="default"
onClick={() => {
handleDatabaseConfigChange('influxDB', 'host', 'localhost');
handleDatabaseConfigChange('influxDB', 'port', 8086);
handleDatabaseConfigChange('influxDB', 'database', 'alpha-futures');
handleDatabaseConfigChange('influxDB', 'username', '');
handleDatabaseConfigChange('influxDB', 'password', '');
handleDatabaseConfigChange('influxDB', 'ssl', false);
handleDatabaseConfigChange('influxDB', 'enabled', true);
}}
>
恢复默认
</Button>
</div>
</Card>
</>
)
},

Loading…
Cancel
Save