From 955f22cd75d3b10f3b245d61bc20b30c84b3e296 Mon Sep 17 00:00:00 2001 From: Lxy Date: Tue, 24 Feb 2026 20:14:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0mysql=E3=80=81redis?= =?UTF-8?q?=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package-lock.json | 120 ++ backend/package.json | 1 + .../service/data/futures_analysis.db | Bin 12308480 -> 12312576 bytes backend/src/config/database/index.ts | 2 + backend/src/config/database/mysql.ts | 86 ++ backend/src/config/database/redis.ts | 84 ++ backend/src/config/index.ts | 7 + backend/src/services/cacheService.ts | 128 ++ .../src/services/datasource/TQDataSource.ts | 2 +- backend/src/services/marketService.ts | 1281 +++++++++-------- backend/src/services/pushService.ts | 4 +- src/pages/admin/AdminConfig.jsx | 315 +--- 12 files changed, 1140 insertions(+), 890 deletions(-) create mode 100644 backend/src/config/database/index.ts create mode 100644 backend/src/config/database/mysql.ts create mode 100644 backend/src/config/database/redis.ts create mode 100644 backend/src/services/cacheService.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 2108ca9..ff8bb55 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index 3faf7a3..7f629ff 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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" diff --git a/backend/service_implementation/service/data/futures_analysis.db b/backend/service_implementation/service/data/futures_analysis.db index d125453245854d98cdc6d7ad88fada05fa3f8610..13a0ef9977ea473194275ad5b065d2c2306878bb 100644 GIT binary patch delta 916 zcmb`_$#c|Y5P;zc$q;1m7Xbl5Oa=n7nDEYIhRF;n11N5Q0Lw z%BU=YqJRMef*`S^_yasx9<1WX$to^MRjRyr@iU%s=c{_YLw6s#ySn8_XT0TVXME47 zcy}x|;L6^b>1EyLnvO+P7152@f05{)=d&ZeEY`oh*xcS#{a4hdqjul5 zDdWpJw(?el4s>Pd0t+S7v&{+Syss_@~XTht7VNe%UW3{>t%y%lufc(w#e)9hHRB>@}|5c zZ_7LKuCz!|w#$36Lw1U?OLoiq@_~FPAIZn^iL}Zd*(;yQKKV>Om;F+bFXT)4N)E_D zIV6YWYdIoE&iOy&RVla`GZSRCJY3i?x<2YuYvq|0C8KuBP`Z6q`%e6K&PC zxqOm*vZ1~qlTBxmDNFC1T z=w0bVTW~5k9h?b%49*7Ug7ZOpa3T09xENdtehw}N9hcIHUw%8YpN@)nS@dfxD$Q>i nw7T?rRb}^0{zjA+ifemjOZ03Z)mYz{%{1gPy=M#iNj3fphJ-u} delta 671 zcmWO0HCL4Z0D$33Tv3dRC>A1MH?N9;1$K8gc41=|puVd{bhg* zltGdwgJp;em0>bm@})pV$VeF_qh*YYm2om&CdfpYB$H)|OqFReU1rEknI*Gjj?9&L zGG7+RLRln>Wr-}6Wl|{1WreJiRkB*v$XZz^>t%y%lp@(An`Mh^m2I+Jc1W@8lwGo0 z_Q+n@C;R1q9F#+HSW2W+%H)U~m1CkDmlJYQPRVIGBWLBDoR@OBAQ$D5T$U?xRibiD zuFDO%DYxXd+>yI-PwvYDc_IPCANUZc4%7rb20jHo*W_k@`Cj=SErRhM diff --git a/backend/src/config/database/index.ts b/backend/src/config/database/index.ts new file mode 100644 index 0000000..1a63127 --- /dev/null +++ b/backend/src/config/database/index.ts @@ -0,0 +1,2 @@ +export * from './mysql'; +export * from './redis'; \ No newline at end of file diff --git a/backend/src/config/database/mysql.ts b/backend/src/config/database/mysql.ts new file mode 100644 index 0000000..7ae1da1 --- /dev/null +++ b/backend/src/config/database/mysql.ts @@ -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(); \ No newline at end of file diff --git a/backend/src/config/database/redis.ts b/backend/src/config/database/redis.ts new file mode 100644 index 0000000..e71d112 --- /dev/null +++ b/backend/src/config/database/redis.ts @@ -0,0 +1,84 @@ +import { createClient } from 'redis'; +import { config } from '../index'; +import { logger } from '../../utils/logger'; + +class RedisConnection { + private client: ReturnType | 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(); \ No newline at end of file diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index f88bbcc..80bef1a 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -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: { diff --git a/backend/src/services/cacheService.ts b/backend/src/services/cacheService.ts new file mode 100644 index 0000000..7ebf0f0 --- /dev/null +++ b/backend/src/services/cacheService.ts @@ -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(key: string, symbol: string, type: string, fetchFromSource: () => Promise, options: CacheOptions = {}): Promise { + 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 { + 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 { + 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 { + try { + await redisConnection.del(key); + logger.log(`删除Redis缓存成功: ${key}`); + } catch (error) { + logger.error(`删除Redis缓存失败: ${key}`, error); + } + } + + /** + * 清除指定类型的缓存 + */ + async clearByType(symbol: string, type: string): Promise { + 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; \ No newline at end of file diff --git a/backend/src/services/datasource/TQDataSource.ts b/backend/src/services/datasource/TQDataSource.ts index b89a22f..ddc2859 100644 --- a/backend/src/services/datasource/TQDataSource.ts +++ b/backend/src/services/datasource/TQDataSource.ts @@ -64,7 +64,7 @@ class HttpClient { async post(endpoint: string, data?: any): Promise { const url = `${this.baseUrl}${endpoint}`; - logger.log('发送POST请求:', url, '数据:', data); + logger.log(`发送POST请求: ${url} 数据: ${JSON.stringify(data)}`); try { // 设置20秒超时 diff --git a/backend/src/services/marketService.ts b/backend/src/services/marketService.ts index 3d31b20..5f8c388 100644 --- a/backend/src/services/marketService.ts +++ b/backend/src/services/marketService.ts @@ -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,69 +14,290 @@ const getDataSourceConfig = () => { // 获取市场概览 export const fetchMarketOverview = async () => { - try { - // 首先尝试使用 service_implementation API + const cacheKey = 'market:overview'; + const symbol = 'market'; + const type = 'overview'; + + return cacheService.get(cacheKey, symbol, type, async () => { try { - logger.log('尝试使用 service_implementation API 获取市场概览...'); - - // 先获取合约列表 - logger.log('获取合约列表...'); - const contractsResponse = await serviceImplementationClient.getContracts(); - const allContracts = contractsResponse.data; - - // 去重,按品种代码分组 - const uniqueContracts = new Map(); - for (const contract of allContracts) { - // 提取品种代码(如从CU2603中提取CU) - const code = contract.symbol.slice(0, 2); - if (!uniqueContracts.has(code)) { - uniqueContracts.set(code, { - code: code, - name: contract.name || code, - exchange: contract.exchange || '' - }); + // 首先尝试使用 service_implementation API + try { + logger.log('尝试使用 service_implementation API 获取市场概览...'); + + // 先获取合约列表 + logger.log('获取合约列表...'); + const contractsResponse = await serviceImplementationClient.getContracts(); + const allContracts = contractsResponse.data; + + // 去重,按品种代码分组 + const uniqueContracts = new Map(); + for (const contract of allContracts) { + // 提取品种代码(如从CU2603中提取CU) + const code = contract.symbol.slice(0, 2); + if (!uniqueContracts.has(code)) { + uniqueContracts.set(code, { + code: code, + name: contract.name || code, + exchange: contract.exchange || '' + }); + } + } + + // 转换为数组 + const contractList = Array.from(uniqueContracts.values()); + logger.log(`获取到 ${contractList.length} 个独特品种,分别是: ${contractList.map(c => c.code).join(', ')}`); + + // 使用获取到的合约列表 + const overview = []; + for (const future of contractList) { + try { + // 构建合约符号(使用大写代码,因为 service_implementation API 期望大写) + const symbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}03`; + + // 获取合约详情 + logger.log(`获取合约${symbol}详情...`); + const contractsResponse = await serviceImplementationClient.getContracts(future.exchange, future.code); + const contract = contractsResponse.data.find((c: any) => c.symbol === symbol); + + if (!contract) { + logger.warn(`合约${symbol}不存在,跳过`); + continue; + } + + // 更新未来合约的中文名称 + future.name = `${contract.product_name || future.name}`; + + // 获取分析数据 + logger.log(`分析合约${symbol}...`); + logger.log(`before analyzeMarket: symbol=${symbol},time=${new Date().toISOString()}`); + const analysisResponse = await serviceImplementationClient.analyzeMarket(symbol); + const analysis = analysisResponse.data; + logger.log(`after analyzeMarket: symbol=${symbol}, analysis=${JSON.stringify(analysis)}, time=${new Date().toISOString()}`); + + overview.push({ + code: future.code, + name: future.name, + currentPrice: analysis.current_price || 0, + changePercent: analysis.change_percent || 0, + winRate: analysis.probability ? Math.round(analysis.probability * 100) : Math.floor(Math.random() * 50) + 30, // 使用分析结果或模拟胜率 + atr: analysis.atr || +(Math.random() * 5 + 0.5).toFixed(2), // 使用分析结果或模拟ATR + adx: analysis.adx || Math.floor(Math.random() * 60) + 10, // 使用分析结果或模拟ADX + adxStatus: (adx: number) => { + if (adx < 20) return '无趋势/震荡'; + if (adx < 40) return '弱趋势'; + return '强趋势'; + }, + trends: { + '5MIN': { + direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望', + status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡', + rsi: Math.floor(Math.random() * 80) + 10 + }, + '30MIN': { + direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望', + status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡', + rsi: Math.floor(Math.random() * 80) + 10 + }, + '1HOUR': { + direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望', + status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡', + rsi: Math.floor(Math.random() * 80) + 10 + }, + '1DAY': { + direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望', + status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡', + rsi: Math.floor(Math.random() * 80) + 10 + } + }, + tradingAdvice: { + entry: analysis.entry_price || 0, + stopLoss: analysis.stop_loss || 0, + target: analysis.target_price || 0, + resistance: analysis.resistance || 0, + support: analysis.support || 0 + }, + overallView: analysis.trend === 'up' ? '多头排列' : analysis.trend === 'down' ? '空头排列' : '震荡', + aiAnalysis: `趋势:${analysis.trend || '中性'} | 概率:${analysis.probability ? Math.round(analysis.probability * 100) : 50}% | 方向:${analysis.direction || '观望'}` + }); + } catch (error) { + logger.error(`获取合约${future.code}行情失败:`, error); + // 跳过获取失败的合约 + continue; + } + } + + if (overview.length > 0) { + logger.log('使用 service_implementation API 获取市场概览成功'); + return overview; } + + logger.warn('service_implementation API 未返回数据,尝试使用其他数据源'); + } catch (error) { + logger.error('service_implementation API 获取失败:', error); + // service_implementation API 失败,尝试使用其他数据源 } - // 转换为数组 - const contractList = Array.from(uniqueContracts.values()); - logger.log(`获取到 ${contractList.length} 个独特品种,分别是: ${contractList.map(c => c.code).join(', ')}`); + // 获取数据源配置 + const dataSourceConfig = getDataSourceConfig(); + logger.log('获取数据源配置:', dataSourceConfig); + // 检查是否有可用的数据源 + const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; + if (!hasAvailableDataSource) { + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); + } - // 使用获取到的合约列表 - const overview = []; - for (const future of contractList) { + // 尝试使用TQSDK数据源 + if (dataSourceConfig.tqsdk?.enabled) { try { - // 构建合约符号(使用大写代码,因为 service_implementation API 期望大写) - const symbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}03`; + const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - // 获取合约详情 - logger.log(`获取合约${symbol}详情...`); - const contractsResponse = await serviceImplementationClient.getContracts(future.exchange, future.code); - const contract = contractsResponse.data.find((c: any) => c.symbol === symbol); - - if (!contract) { - logger.warn(`合约${symbol}不存在,跳过`); - continue; + // 使用用户指定的合约列表 + 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; + // 查找合约信息 + const future = futuresList.find(item => item.code === code); + if (!future) { + throw new Error('品种不存在'); + } + + // 首先尝试使用 service_implementation API + try { + logger.log('尝试使用 service_implementation API 获取品种详情...'); + + // 构建合约符号(使用大写代码,因为 service_implementation API 期望大写) + const contractSymbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}03`; + + // 获取合约详情 + logger.log(`获取合约${contractSymbol}详情...`); + const contractsResponse = await serviceImplementationClient.getContracts(future.exchange, future.code); + const contract = contractsResponse.data.find((c: any) => c.symbol === contractSymbol); + + if (!contract) { + logger.warn(`合约${contractSymbol}不存在,尝试使用其他数据源`); + } else { // 更新未来合约的中文名称 - future.name = `${contract.product_name || future.name}`; + future.name = `${contract.product_name || future.name} - ${future.code}`; // 获取分析数据 - logger.log(`分析合约${symbol}...`); - logger.log(`before analyzeMarket: symbol=${symbol},time=${new Date().toISOString()}`); - const analysisResponse = await serviceImplementationClient.analyzeMarket(symbol); + logger.log(`分析合约${contractSymbol}...`); + logger.log(`before analyzeMarket: symbol=${contractSymbol},time=${new Date().toISOString()}`); + const analysisResponse = await serviceImplementationClient.analyzeMarket(contractSymbol); const analysis = analysisResponse.data; - logger.log(`after analyzeMarket: symbol=${symbol}, analysis=${JSON.stringify(analysis)}, time=${new Date().toISOString()}`); + logger.log(`after analyzeMarket: symbol=${contractSymbol},time=${new Date().toISOString()}`); + + // 获取交易建议 + logger.log(`获取合约${contractSymbol}交易建议...`); + const recommendationsResponse = await serviceImplementationClient.getRecommendations(contractSymbol); + const recommendation = recommendationsResponse.data[0]; - overview.push({ + // 转换为前端需要的格式 + const result = { code: future.code, name: future.name, + fullName: `${future.name}-${future.code}605`, currentPrice: analysis.current_price || 0, changePercent: analysis.change_percent || 0, - winRate: analysis.probability ? Math.round(analysis.probability * 100) : Math.floor(Math.random() * 50) + 30, // 使用分析结果或模拟胜率 - atr: analysis.atr || +(Math.random() * 5 + 0.5).toFixed(2), // 使用分析结果或模拟ATR - adx: analysis.adx || Math.floor(Math.random() * 60) + 10, // 使用分析结果或模拟ADX + winRate: analysis.probability ? Math.round(analysis.probability * 100) : Math.floor(Math.random() * 50) + 30, + atr: analysis.atr || +(Math.random() * 5 + 0.5).toFixed(2), + adx: analysis.adx || Math.floor(Math.random() * 60) + 10, adxStatus: (adx: number) => { if (adx < 20) return '无趋势/震荡'; if (adx < 40) return '弱趋势'; @@ -103,593 +325,341 @@ export const fetchMarketOverview = async () => { rsi: Math.floor(Math.random() * 80) + 10 } }, + indicators: { + macd: analysis.trend === 'up' ? '金叉向上' : analysis.trend === 'down' ? '死叉向下' : '走平', + rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`, + bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)], + kdj: analysis.trend === 'up' ? '金叉向上' : analysis.trend === 'down' ? '死叉向下' : '走平' + }, tradingAdvice: { - entry: analysis.entry_price || 0, - stopLoss: analysis.stop_loss || 0, - target: analysis.target_price || 0, + entry: analysis.entry_price || recommendation?.entry_price || 0, + stopLoss: analysis.stop_loss || recommendation?.stop_loss || 0, + target: analysis.target_price || recommendation?.target_price || 0, resistance: analysis.resistance || 0, support: analysis.support || 0 }, + riskLevel: analysis.risk_level || ['低', '中等', '高'][Math.floor(Math.random() * 3)], + volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)], overallView: analysis.trend === 'up' ? '多头排列' : analysis.trend === 'down' ? '空头排列' : '震荡', aiAnalysis: `趋势:${analysis.trend || '中性'} | 概率:${analysis.probability ? Math.round(analysis.probability * 100) : 50}% | 方向:${analysis.direction || '观望'}` - }); - } catch (error) { - logger.error(`获取合约${future.code}行情失败:`, error); - // 跳过获取失败的合约 - continue; + }; + + logger.log('使用 service_implementation API 获取品种详情成功'); + return result; } - } - - if (overview.length > 0) { - logger.log('使用 service_implementation API 获取市场概览成功'); - return overview; - } - - logger.warn('service_implementation API 未返回数据,尝试使用其他数据源'); - } catch (error) { - logger.error('service_implementation API 获取失败:', error); - // 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('无可用数据源,请在管理配置中启用至少一个数据源'); - // } - } catch (error) { - logger.error('获取市场概览失败:', error); - // 直接返回友好的错误提示 - throw new Error(error instanceof Error ? error.message : '获取市场概览失败,请检查数据源配置'); - } -}; - -// 获取品种详情 -export const fetchMarketDetail = async (symbol: string) => { - try { - // 提取合约代码(从SHFE.AU2605中提取AU) - const code = symbol.includes('.') ? symbol.split('.')[1].slice(0, 2) : symbol; - // 查找合约信息 - const future = futuresList.find(item => item.code === code); - if (!future) { - throw new Error('品种不存在'); - } - - // 首先尝试使用 service_implementation API - try { - logger.log('尝试使用 service_implementation API 获取品种详情...'); - - // 构建合约符号(使用大写代码,因为 service_implementation API 期望大写) - const contractSymbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}03`; + logger.warn('service_implementation API 未返回数据,尝试使用其他数据源'); + } catch (error) { + logger.error('service_implementation API 获取失败:', error); + // service_implementation API 失败,尝试使用其他数据源 + } - // 获取合约详情 - logger.log(`获取合约${contractSymbol}详情...`); - const contractsResponse = await serviceImplementationClient.getContracts(future.exchange, future.code); - const contract = contractsResponse.data.find((c: any) => c.symbol === contractSymbol); + // 获取数据源配置 + const dataSourceConfig = getDataSourceConfig(); - if (!contract) { - logger.warn(`合约${contractSymbol}不存在,尝试使用其他数据源`); - } else { - // 更新未来合约的中文名称 - future.name = `${contract.product_name || future.name} - ${future.code}`; - - // 获取分析数据 - logger.log(`分析合约${contractSymbol}...`); - logger.log(`before analyzeMarket: symbol=${contractSymbol},time=${new Date().toISOString()}`); - const analysisResponse = await serviceImplementationClient.analyzeMarket(contractSymbol); - const analysis = analysisResponse.data; - logger.log(`after analyzeMarket: symbol=${contractSymbol},time=${new Date().toISOString()}`); - - // 获取交易建议 - logger.log(`获取合约${contractSymbol}交易建议...`); - const recommendationsResponse = await serviceImplementationClient.getRecommendations(contractSymbol); - const recommendation = recommendationsResponse.data[0]; - - // 转换为前端需要的格式 - const result = { - code: future.code, - name: future.name, - fullName: `${future.name}-${future.code}605`, - currentPrice: analysis.current_price || 0, - changePercent: analysis.change_percent || 0, - winRate: analysis.probability ? Math.round(analysis.probability * 100) : Math.floor(Math.random() * 50) + 30, - atr: analysis.atr || +(Math.random() * 5 + 0.5).toFixed(2), - adx: analysis.adx || Math.floor(Math.random() * 60) + 10, - adxStatus: (adx: number) => { - if (adx < 20) return '无趋势/震荡'; - if (adx < 40) return '弱趋势'; - return '强趋势'; - }, - trends: { - '5MIN': { - direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望', - status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡', - rsi: Math.floor(Math.random() * 80) + 10 - }, - '30MIN': { - direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望', - status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡', - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1HOUR': { - direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望', - status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡', - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1DAY': { - direction: analysis.trend === 'up' ? '看多' : analysis.trend === 'down' ? '看空' : '观望', - status: analysis.trend === 'up' ? '多头趋势' : analysis.trend === 'down' ? '空头趋势' : '震荡', - rsi: Math.floor(Math.random() * 80) + 10 - } - }, - indicators: { - macd: analysis.trend === 'up' ? '金叉向上' : analysis.trend === 'down' ? '死叉向下' : '走平', - rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`, - bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)], - kdj: analysis.trend === 'up' ? '金叉向上' : analysis.trend === 'down' ? '死叉向下' : '走平' - }, - tradingAdvice: { - entry: analysis.entry_price || recommendation?.entry_price || 0, - stopLoss: analysis.stop_loss || recommendation?.stop_loss || 0, - target: analysis.target_price || recommendation?.target_price || 0, - resistance: analysis.resistance || 0, - support: analysis.support || 0 - }, - riskLevel: analysis.risk_level || ['低', '中等', '高'][Math.floor(Math.random() * 3)], - volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)], - overallView: analysis.trend === 'up' ? '多头排列' : analysis.trend === 'down' ? '空头排列' : '震荡', - aiAnalysis: `趋势:${analysis.trend || '中性'} | 概率:${analysis.probability ? Math.round(analysis.probability * 100) : 50}% | 方向:${analysis.direction || '观望'}` - }; - - logger.log('使用 service_implementation API 获取品种详情成功'); - return result; + // 检查是否有可用的数据源 + const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; + if (!hasAvailableDataSource) { + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); } - logger.warn('service_implementation API 未返回数据,尝试使用其他数据源'); - } catch (error) { - logger.error('service_implementation API 获取失败:', error); - // 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`; - - // 获取合约详情和实时行情 - const tick = await dataSource.getTickData(contractSymbol); - - // 转换为前端需要的格式 - return { - code: future.code, - name: future.name, - fullName: `${future.name}-${future.code}605`, - 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), - adx: Math.floor(Math.random() * 60) + 10, - 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 + // 尝试使用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`; + + // 获取合约详情和实时行情 + const tick = await dataSource.getTickData(contractSymbol); + + // 转换为前端需要的格式 + return { + code: future.code, + name: future.name, + fullName: `${future.name}-${future.code}605`, + 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), + adx: Math.floor(Math.random() * 60) + 10, + 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 + } }, - '30MIN': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 + indicators: { + macd: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)], + rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`, + bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)], + kdj: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)] }, - '1HOUR': { - 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)) }, - '1DAY': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - } - }, - indicators: { - macd: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)], - rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`, - bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)], - kdj: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)] - }, - 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)) - }, - riskLevel: ['低', '中等', '高'][Math.floor(Math.random() * 3)], - volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)], - overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)], - aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近` - }; - } catch (error) { - logger.error('TQSDK数据源获取失败:', error); - // TQSDK数据源失败,尝试使用测试数据源 - if (dataSourceConfig.test?.enabled) { - logger.log('切换到测试数据源'); - // 启用了测试数据源,使用测试数据 - await new Promise(resolve => setTimeout(resolve, 200)); - return generateFutureData(symbol, future.name); - } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取品种详情失败,所有数据源均不可用'); + riskLevel: ['低', '中等', '高'][Math.floor(Math.random() * 3)], + volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)], + overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)], + aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近` + }; + } catch (error) { + logger.error('TQSDK数据源获取失败:', error); + // TQSDK数据源失败,尝试使用测试数据源 + if (dataSourceConfig.test?.enabled) { + logger.log('切换到测试数据源'); + // 启用了测试数据源,使用测试数据 + await new Promise(resolve => setTimeout(resolve, 200)); + return generateFutureData(symbol, future.name); + } else { + // 未启用测试数据源,返回友好的错误提示 + throw new Error('获取品种详情失败,所有数据源均不可用'); + } } + } else if (dataSourceConfig.test?.enabled) { + // 直接使用测试数据源 + logger.log('使用测试数据源'); + await new Promise(resolve => setTimeout(resolve, 200)); + return generateFutureData(symbol, future.name); + } else { + // 无可用数据源 + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); } - } else if (dataSourceConfig.test?.enabled) { - // 直接使用测试数据源 - logger.log('使用测试数据源'); - await new Promise(resolve => setTimeout(resolve, 200)); - return generateFutureData(symbol, future.name); - } else { - // 无可用数据源 - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); + } catch (error) { + logger.error(`获取品种${symbol}详情失败:`, error); + // 直接返回友好的错误提示 + throw new Error(error instanceof Error ? error.message : '获取品种详情失败,请检查数据源配置'); } - } catch (error) { - logger.error(`获取品种${symbol}详情失败:`, error); - // 直接返回友好的错误提示 - throw new Error(error instanceof Error ? error.message : '获取品种详情失败,请检查数据源配置'); - } + }, { expireTime: 300 }); // 缓存5分钟 }; // 获取K线数据 export const fetchKlineData = async (symbol: string, period: string) => { - try { - // 查找合约信息 - const future = futuresList.find(item => item.code === symbol); - if (!future) { - throw new Error('品种不存在'); - } - - // 首先尝试使用 service_implementation API + const cacheKey = `market:kline:${symbol}:${period}`; + const type = 'kline'; + + return cacheService.get(cacheKey, symbol, type, async () => { try { - logger.log('尝试使用 service_implementation API 获取K线数据...'); - - // 构建合约符号(使用大写代码,因为 service_implementation API 期望大写) - const contractSymbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}3`; - - // 转换周期格式 - let duration = period; - switch (period) { - case '1M': - duration = '1m'; - break; - case '5M': - duration = '5m'; - break; - case '15M': - duration = '15m'; - break; - case '30M': - duration = '30m'; - break; - case '1H': - duration = '1h'; - break; - case '4H': - duration = '4h'; - break; - case '1D': - duration = '1d'; - break; - case '1W': - duration = '1w'; - break; - default: - duration = '1m'; + // 查找合约信息 + const future = futuresList.find(item => item.code === symbol); + if (!future) { + throw new Error('品种不存在'); } - // 获取K线数据 - logger.log(`获取合约${contractSymbol}K线数据,周期: ${duration}...`); - const klineResponse = await serviceImplementationClient.getKlineData(contractSymbol, duration, 30); - const klineData = klineResponse.data; - - if (klineData.length > 0) { - // 转换为前端需要的格式 - const result = klineData.map((item: any) => ({ - timestamp: new Date(item.datetime).getTime() / 1000, // 转换为秒 - open: item.open, - high: item.high, - low: item.low, - close: item.close, - volume: item.volume - })); + // 首先尝试使用 service_implementation API + try { + logger.log('尝试使用 service_implementation API 获取K线数据...'); - logger.log('使用 service_implementation API 获取K线数据成功'); - return result; - } - - logger.warn('service_implementation API 未返回K线数据,尝试使用其他数据源'); - } catch (error) { - logger.error('service_implementation API 获取K线数据失败:', error); - // 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); + // 构建合约符号(使用大写代码,因为 service_implementation API 期望大写) + const contractSymbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}3`; - // // 构建合约符号(使用小写代码,因为TQAPI期望小写) - // const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`; + // 转换周期格式 + let duration = period; + switch (period) { + case '1M': + duration = '1m'; + break; + case '5M': + duration = '5m'; + break; + case '15M': + duration = '15m'; + break; + case '30M': + duration = '30m'; + break; + case '1H': + duration = '1h'; + break; + case '4H': + duration = '4h'; + break; + case '1D': + duration = '1d'; + break; + case '1W': + duration = '1w'; + break; + default: + duration = '1m'; + } - // // 获取K线数据 - // const klineData = await dataSource.getKlineData(contractSymbol, period, 30); + // 获取K线数据 + logger.log(`获取合约${contractSymbol}K线数据,周期: ${duration}...`); + const klineResponse = await serviceImplementationClient.getKlineData(contractSymbol, duration, 30); + const klineData = klineResponse.data; - // // 转换为前端需要的格式 - // 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线数据失败,请检查数据源配置'); - } -}; - -// 获取市场热点 -export const fetchMarketHotspots = async () => { - try { - // 首先尝试使用 service_implementation API - try { - logger.log('尝试使用 service_implementation API 获取市场热点...'); - - // 先获取合约列表 - logger.log('获取合约列表...'); - const contractsResponse = await serviceImplementationClient.getContracts(); - const allContracts = contractsResponse.data; - - // 去重,按品种代码分组 - const uniqueContracts = new Map(); - for (const contract of allContracts) { - // 提取品种代码(如从CU2603中提取CU) - const code = contract.symbol.slice(0, 2); - if (!uniqueContracts.has(code)) { - uniqueContracts.set(code, { - code: code, - name: contract.name || code, - exchange: contract.exchange || '' - }); + if (klineData.length > 0) { + // 转换为前端需要的格式 + const result = klineData.map((item: any) => ({ + timestamp: new Date(item.datetime).getTime() / 1000, // 转换为秒 + open: item.open, + high: item.high, + low: item.low, + close: item.close, + volume: item.volume + })); + + logger.log('使用 service_implementation API 获取K线数据成功'); + return result; } + + logger.warn('service_implementation API 未返回K线数据,尝试使用其他数据源'); + } catch (error) { + logger.error('service_implementation API 获取K线数据失败:', error); + // service_implementation API 失败,尝试使用其他数据源 } - // 转换为数组 - const contractList = Array.from(uniqueContracts.values()); - logger.log(`获取到 ${contractList.length} 个独特品种`); + // 获取数据源配置 + const dataSourceConfig = getDataSourceConfig(); + + // 检查是否有可用的数据源 + const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; + if (!hasAvailableDataSource) { + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); + } - // 使用获取到的合约列表 - const hotspots = []; - for (const future of contractList) { + // 尝试使用TQSDK数据源 + if (dataSourceConfig.tqsdk?.enabled) { try { - // 构建合约符号(使用大写代码,因为 service_implementation API 期望大写) - const symbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}03`; - - // 获取合约详情 - logger.log(`获取合约${symbol}详情...`); - const contractsResponse = await serviceImplementationClient.getContracts(future.exchange, future.code); - const contract = contractsResponse.data.find((c: any) => c.symbol === symbol); - - if (!contract) { - logger.warn(`合约${symbol}不存在,跳过`); - continue; - } + const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - // 更新未来合约的中文名称 - future.name = `${contract.product_name || future.name} - ${future.code}`; + // 构建合约符号(使用小写代码,因为TQAPI期望小写) + const contractSymbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`; - // 获取分析数据 - logger.log(`分析合约${symbol}...`); - const analysisResponse = await serviceImplementationClient.analyzeMarket(symbol); - const analysis = analysisResponse.data; + // 获取K线数据 + const klineData = await dataSource.getKlineData(contractSymbol, period, 30); - hotspots.push({ - symbol: future.code, - name: future.name, - change: analysis.change_percent || 0, - volume: analysis.volume || Math.floor(Math.random() * 1000000) + 100000 - }); + // 转换为前端需要的格式 + 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(`获取合约${future.code}行情失败:`, error); - // 跳过获取失败的合约 - continue; + 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('无可用数据源,请在管理配置中启用至少一个数据源'); } - - if (hotspots.length > 0) { - // 按涨跌幅排序,返回前10个 - const sortedHotspots = hotspots - .sort((a, b) => Math.abs(b.change) - Math.abs(a.change)) - .slice(0, 10); - - logger.log('使用 service_implementation API 获取市场热点成功'); - return sortedHotspots; - } - - logger.warn('service_implementation API 未返回市场热点数据,尝试使用其他数据源'); } catch (error) { - logger.error('service_implementation API 获取市场热点失败:', error); - // service_implementation API 失败,尝试使用其他数据源 - } - - // 获取数据源配置 - const dataSourceConfig = getDataSourceConfig(); - - // 检查是否有可用的数据源 - const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; - if (!hasAvailableDataSource) { - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); + logger.error(`获取合约${symbol}K线数据失败:`, error); + // 直接返回友好的错误提示 + throw new Error(error instanceof Error ? error.message : '获取K线数据失败,请检查数据源配置'); } - - // 尝试使用TQSDK数据源 - if (dataSourceConfig.tqsdk?.enabled) { + }, { 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 { - const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); + logger.log('尝试使用 service_implementation API 获取市场热点...'); + + // 先获取合约列表 + logger.log('获取合约列表...'); + const contractsResponse = await serviceImplementationClient.getContracts(); + const allContracts = contractsResponse.data; - // 使用用户指定的合约列表 + // 去重,按品种代码分组 + const uniqueContracts = new Map(); + for (const contract of allContracts) { + // 提取品种代码(如从CU2603中提取CU) + const code = contract.symbol.slice(0, 2); + if (!uniqueContracts.has(code)) { + uniqueContracts.set(code, { + code: code, + name: contract.name || code, + exchange: contract.exchange || '' + }); + } + } + + // 转换为数组 + const contractList = Array.from(uniqueContracts.values()); + logger.log(`获取到 ${contractList.length} 个独特品种`); + + // 使用获取到的合约列表 const hotspots = []; - for (const future of futuresList) { + for (const future of contractList) { try { - // 构建合约符号(使用小写代码,因为TQAPI期望小写) - const symbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`; - // 获取合约详情和实时行情 - const tick = await dataSource.getTickData(symbol); + // 构建合约符号(使用大写代码,因为 service_implementation API 期望大写) + const symbol = `${future.code}${new Date().getFullYear().toString().slice(-2)}03`; + + // 获取合约详情 + logger.log(`获取合约${symbol}详情...`); + const contractsResponse = await serviceImplementationClient.getContracts(future.exchange, future.code); + const contract = contractsResponse.data.find((c: any) => c.symbol === symbol); + + if (!contract) { + logger.warn(`合约${symbol}不存在,跳过`); + continue; + } + + // 更新未来合约的中文名称 + future.name = `${contract.product_name || future.name} - ${future.code}`; + + // 获取分析数据 + logger.log(`分析合约${symbol}...`); + const analysisResponse = await serviceImplementationClient.analyzeMarket(symbol); + const analysis = analysisResponse.data; hotspots.push({ symbol: future.code, name: future.name, - change: tick.price_change / tick.pre_close * 100, - volume: tick.volume + change: analysis.change_percent || 0, + volume: analysis.volume || Math.floor(Math.random() * 1000000) + 100000 }); } catch (error) { logger.error(`获取合约${future.code}行情失败:`, error); @@ -698,57 +668,110 @@ export const fetchMarketHotspots = async () => { } } - // 按涨跌幅排序,返回前10个 - return hotspots - .sort((a, b) => Math.abs(b.change) - Math.abs(a.change)) - .slice(0, 10); + if (hotspots.length > 0) { + // 按涨跌幅排序,返回前10个 + const sortedHotspots = hotspots + .sort((a, b) => Math.abs(b.change) - Math.abs(a.change)) + .slice(0, 10); + + logger.log('使用 service_implementation API 获取市场热点成功'); + return sortedHotspots; + } + + logger.warn('service_implementation API 未返回市场热点数据,尝试使用其他数据源'); } catch (error) { - logger.error('TQSDK数据源获取失败:', error); - // TQSDK数据源失败,尝试使用测试数据源 - if (dataSourceConfig.test?.enabled) { - logger.log('切换到测试数据源'); - // 启用了测试数据源,使用测试数据 - await new Promise(resolve => setTimeout(resolve, 200)); - const overview = generateFuturesOverview(); + logger.error('service_implementation API 获取市场热点失败:', error); + // 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); + + // 使用用户指定的合约列表 + const hotspots = []; + 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); + + hotspots.push({ + symbol: future.code, + name: future.name, + change: tick.price_change / tick.pre_close * 100, + volume: tick.volume + }); + } catch (error) { + logger.error(`获取合约${future.code}行情失败:`, error); + // 跳过获取失败的合约 + continue; + } + } + // 按涨跌幅排序,返回前10个 - return overview - .sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)) - .slice(0, 10) - .map(item => ({ - symbol: item.code, - name: item.name, - change: item.changePercent, - volume: Math.floor(Math.random() * 1000000) + 100000 - })); - } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取市场热点失败,所有数据源均不可用'); + return hotspots + .sort((a, b) => Math.abs(b.change) - Math.abs(a.change)) + .slice(0, 10); + } catch (error) { + logger.error('TQSDK数据源获取失败:', error); + // TQSDK数据源失败,尝试使用测试数据源 + if (dataSourceConfig.test?.enabled) { + logger.log('切换到测试数据源'); + // 启用了测试数据源,使用测试数据 + await new Promise(resolve => setTimeout(resolve, 200)); + const overview = generateFuturesOverview(); + // 按涨跌幅排序,返回前10个 + return overview + .sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)) + .slice(0, 10) + .map(item => ({ + symbol: item.code, + name: item.name, + change: item.changePercent, + volume: Math.floor(Math.random() * 1000000) + 100000 + })); + } else { + // 未启用测试数据源,返回友好的错误提示 + throw new Error('获取市场热点失败,所有数据源均不可用'); + } } + } else if (dataSourceConfig.test?.enabled) { + // 直接使用测试数据源 + logger.log('使用测试数据源'); + await new Promise(resolve => setTimeout(resolve, 200)); + const overview = generateFuturesOverview(); + // 按涨跌幅排序,返回前10个 + return overview + .sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)) + .slice(0, 10) + .map(item => ({ + symbol: item.code, + name: item.name, + change: item.changePercent, + volume: Math.floor(Math.random() * 1000000) + 100000 + })); + } else { + // 无可用数据源 + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); } - } else if (dataSourceConfig.test?.enabled) { - // 直接使用测试数据源 - logger.log('使用测试数据源'); - await new Promise(resolve => setTimeout(resolve, 200)); - const overview = generateFuturesOverview(); - // 按涨跌幅排序,返回前10个 - return overview - .sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)) - .slice(0, 10) - .map(item => ({ - symbol: item.code, - name: item.name, - change: item.changePercent, - volume: Math.floor(Math.random() * 1000000) + 100000 - })); - } else { - // 无可用数据源 - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); + } catch (error) { + logger.error('获取市场热点失败:', error); + // 直接返回友好的错误提示 + throw new Error(error instanceof Error ? error.message : '获取市场热点失败,请检查数据源配置'); } - } catch (error) { - logger.error('获取市场热点失败:', error); - // 直接返回友好的错误提示 - throw new Error(error instanceof Error ? error.message : '获取市场热点失败,请检查数据源配置'); - } + }, { expireTime: 300 }); // 缓存5分钟 }; // 获取风险预警 @@ -756,4 +779,4 @@ export const fetchRiskAlerts = async () => { // 直接返回模拟数据 await new Promise(resolve => setTimeout(resolve, 100)); return riskAlerts; -}; \ No newline at end of file +}; diff --git a/backend/src/services/pushService.ts b/backend/src/services/pushService.ts index b29b7c4..2a5af9d 100644 --- a/backend/src/services/pushService.ts +++ b/backend/src/services/pushService.ts @@ -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: '测试推送成功' }; diff --git a/src/pages/admin/AdminConfig.jsx b/src/pages/admin/AdminConfig.jsx index fffebe6..ffad5b7 100644 --- a/src/pages/admin/AdminConfig.jsx +++ b/src/pages/admin/AdminConfig.jsx @@ -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配置 */} - - - - - handleDatabaseConfigChange('mongoDB', 'host', e.target.value)} - /> - - - - - handleDatabaseConfigChange('mongoDB', 'port', value)} - /> - - - - - handleDatabaseConfigChange('mongoDB', 'database', e.target.value)} - /> - - - - - handleDatabaseConfigChange('mongoDB', 'authSource', e.target.value)} - /> - - - - - handleDatabaseConfigChange('mongoDB', 'username', e.target.value)} - /> - - - - - handleDatabaseConfigChange('mongoDB', 'password', e.target.value)} - /> - - - - - handleDatabaseConfigChange('mongoDB', 'ssl', checked)} - /> - - - - - handleDatabaseConfigChange('mongoDB', 'enabled', checked)} - /> - - - -
- - -
-
- - {/* PostgreSQL配置 */} - + {/* MySQL配置 */} + - + handleDatabaseConfigChange('postgreSQL', 'host', e.target.value)} + onChange={(e) => handleDatabaseConfigChange('mysql', 'host', e.target.value)} /> - + handleDatabaseConfigChange('postgreSQL', 'port', value)} + onChange={(value) => handleDatabaseConfigChange('mysql', 'port', value)} /> - + handleDatabaseConfigChange('postgreSQL', 'database', e.target.value)} + onChange={(e) => handleDatabaseConfigChange('mysql', 'database', e.target.value)} /> - + handleDatabaseConfigChange('postgreSQL', 'username', e.target.value)} + onChange={(e) => handleDatabaseConfigChange('mysql', 'username', e.target.value)} /> - + handleDatabaseConfigChange('postgreSQL', 'password', e.target.value)} + onChange={(e) => handleDatabaseConfigChange('mysql', 'password', e.target.value)} /> - + handleDatabaseConfigChange('postgreSQL', 'ssl', checked)} + defaultChecked={config.database.mysql.ssl} + onChange={(checked) => handleDatabaseConfigChange('mysql', 'ssl', checked)} /> - + handleDatabaseConfigChange('postgreSQL', 'enabled', checked)} + defaultChecked={config.database.mysql.enabled} + onChange={(checked) => handleDatabaseConfigChange('mysql', 'enabled', checked)} /> @@ -910,7 +794,7 @@ const AdminConfig = () => {
- {/* InfluxDB配置 */} - - - - - handleDatabaseConfigChange('influxDB', 'host', e.target.value)} - /> - - - - - handleDatabaseConfigChange('influxDB', 'port', value)} - /> - - - - - handleDatabaseConfigChange('influxDB', 'database', e.target.value)} - /> - - - - - handleDatabaseConfigChange('influxDB', 'username', e.target.value)} - /> - - - - - handleDatabaseConfigChange('influxDB', 'password', e.target.value)} - /> - - - - - handleDatabaseConfigChange('influxDB', 'ssl', checked)} - /> - - - - - handleDatabaseConfigChange('influxDB', 'enabled', checked)} - /> - - - -
- - -
-
+ ) },