From 05604c9d7117a05191a773be4259de0ad3e974d6 Mon Sep 17 00:00:00 2001 From: Lxy Date: Fri, 20 Feb 2026 22:44:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=85=8D=E7=BD=AE=E9=A1=B9=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config.json | 101 +++++ backend/src/services/datasource/DataSource.ts | 26 ++ .../services/datasource/DataSourceFactory.ts | 68 ++++ .../src/services/datasource/MockDataSource.ts | 199 ++++++++++ .../src/services/datasource/TQDataSource.ts | 207 +++++++++++ docs/开发文档/配置管理逻辑.md | 345 ++++++++++++++++++ src/pages/admin/AdminConfig.jsx | 219 +++++------ 系统启动指南.md | 141 +++++++ 8 files changed, 1202 insertions(+), 104 deletions(-) create mode 100644 backend/config.json create mode 100644 backend/src/services/datasource/DataSource.ts create mode 100644 backend/src/services/datasource/DataSourceFactory.ts create mode 100644 backend/src/services/datasource/MockDataSource.ts create mode 100644 backend/src/services/datasource/TQDataSource.ts create mode 100644 docs/开发文档/配置管理逻辑.md create mode 100644 系统启动指南.md diff --git a/backend/config.json b/backend/config.json new file mode 100644 index 0000000..57d16d1 --- /dev/null +++ b/backend/config.json @@ -0,0 +1,101 @@ +{ + "database": { + "mongoDB": { + "host": "127.0.0.1", + "port": 10000, + "database": "aaa", + "username": "aaa", + "password": "aaaa", + "authSource": "aaa", + "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 + } + }, + "server": { + "port": 3006, + "host": "0.0.0.0", + "environment": "development", + "debug": true, + "timeout": 30000, + "maxBodySize": "10mb" + }, + "security": { + "jwtSecret": "your-secret-key", + "jwtExpiresIn": "7d", + "rateLimit": { + "windowMs": 60000, + "max": 120 + }, + "cors": { + "origin": "*", + "methods": [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS" + ], + "allowedHeaders": [ + "Content-Type", + "Authorization" + ] + } + }, + "dataSource": { + "test": { + "enabled": true, + "timeout": 10000, + "retries": 3, + "refreshInterval": 60000 + }, + "tqsdk": { + "enabled": true, + "username": "windsdreamer", + "password": "1qazse42W3", + "timeout": 10000, + "retries": 10, + "maxConnections": 20 + }, + "wind": { + "enabled": false, + "apiKey": "", + "apiSecret": "", + "url": "https://api.wind.com.cn", + "timeout": 30000, + "retries": 3 + }, + "sina": { + "enabled": false, + "url": "https://finance.sina.com.cn", + "timeout": 10000, + "retries": 3, + "refreshInterval": 60000 + }, + "defaultDataSource": "tqsdk" + } +} \ No newline at end of file diff --git a/backend/src/services/datasource/DataSource.ts b/backend/src/services/datasource/DataSource.ts new file mode 100644 index 0000000..2dbf54f --- /dev/null +++ b/backend/src/services/datasource/DataSource.ts @@ -0,0 +1,26 @@ +// 数据源抽象接口 +export interface DataSource { + // 获取所有合约列表 + getContractList(): Promise; + + // 获取单个合约详情 + getContractDetail(symbol: string): Promise; + + // 获取K线数据 + getKlineData(symbol: string, period: string, count: number): Promise; + + // 获取实时行情数据 + getTickData(symbol: string): Promise; + + // 获取市场概览 + getMarketOverview(): Promise; + + // 获取历史成交数据 + getHistoricalTrades(symbol: string, start: number, end: number): Promise; + + // 初始化数据源 + initialize(): Promise; + + // 关闭数据源连接 + close(): Promise; +} \ No newline at end of file diff --git a/backend/src/services/datasource/DataSourceFactory.ts b/backend/src/services/datasource/DataSourceFactory.ts new file mode 100644 index 0000000..30d8e1b --- /dev/null +++ b/backend/src/services/datasource/DataSourceFactory.ts @@ -0,0 +1,68 @@ +// 数据源工厂类 +import { DataSource } from './DataSource'; +import { TQDataSource } from './TQDataSource'; + +// 数据源类型 +export enum DataSourceType { + TQSDK = 'tqsdk', + MOCK = 'mock', + WIND = 'wind', + SINA = 'sina', + TEST = 'test' +} + +export class DataSourceFactory { + private static dataSources: Map = new Map(); + + // 获取数据源实例 + static async getDataSource(type: DataSourceType = DataSourceType.TQSDK, config: any = {}): Promise { + if (!this.dataSources.has(type)) { + let dataSource: DataSource; + + switch (type) { + case DataSourceType.TQSDK: + dataSource = new TQDataSource(config.tqsdk || {}); + break; + case DataSourceType.MOCK: + case DataSourceType.TEST: + // 导入模拟数据源 + const { MockDataSource } = await import('./MockDataSource'); + dataSource = new MockDataSource(); + break; + default: + throw new Error(`不支持的数据源类型: ${type}`); + } + + // 初始化数据源 + const initialized = await dataSource.initialize(); + if (!initialized) { + throw new Error(`数据源${type}初始化失败`); + } + + this.dataSources.set(type, dataSource); + } + + return this.dataSources.get(type)!; + } + + // 关闭所有数据源 + static async closeAllDataSources(): Promise { + for (const [type, dataSource] of this.dataSources) { + try { + await dataSource.close(); + console.log(`数据源${type}已关闭`); + } catch (error) { + console.error(`关闭数据源${type}失败:`, error); + } + } + this.dataSources.clear(); + } + + // 切换数据源 + static async switchDataSource(type: DataSourceType): Promise { + // 关闭当前数据源 + await this.closeAllDataSources(); + // 获取新数据源 + return this.getDataSource(type); + } +} \ No newline at end of file diff --git a/backend/src/services/datasource/MockDataSource.ts b/backend/src/services/datasource/MockDataSource.ts new file mode 100644 index 0000000..de3fd6e --- /dev/null +++ b/backend/src/services/datasource/MockDataSource.ts @@ -0,0 +1,199 @@ +// 模拟数据源实现 +import { DataSource } from './DataSource'; +import { futuresList, generateFuturesOverview, generateFutureData, generateKlineData, riskAlerts } from '../../utils/mockData'; + +export class MockDataSource implements DataSource { + private initialized: boolean = false; + + async initialize(): Promise { + // 模拟初始化 + this.initialized = true; + console.log('模拟数据源初始化成功'); + return true; + } + + async getContractList(): Promise { + if (!this.initialized) { + throw new Error('模拟数据源未初始化'); + } + + // 返回模拟合约列表 + return futuresList.map(item => ({ + symbol: item.code, + name: item.name, + exchange: this.getExchangeBySymbol(item.code), + product_class: 'futures', + price_tick: 0.01, + size: 1, + margin_rate: 0.05 + })); + } + + async getContractDetail(symbol: string): Promise { + if (!this.initialized) { + throw new Error('模拟数据源未初始化'); + } + + // 返回模拟合约详情 + const future = futuresList.find(item => item.code === symbol); + if (!future) { + throw new Error(`合约${symbol}不存在`); + } + + return { + symbol: future.code, + name: future.name, + exchange: this.getExchangeBySymbol(future.code), + product_class: 'futures', + price_tick: 0.01, + size: 1, + margin_rate: 0.05, + delivery_month: '202412', + trading_hours: '09:00-11:30, 13:30-15:00, 21:00-02:30', + pre_close: 2000, + open: 2005, + high: 2010, + low: 1995, + last_price: 2003, + volume: 10000, + open_interest: 50000 + }; + } + + async getKlineData(symbol: string, period: string, count: number): Promise { + if (!this.initialized) { + throw new Error('模拟数据源未初始化'); + } + + // 返回模拟K线数据 + const klineData = generateKlineData(count); + return klineData.map(item => ({ + datetime: item.time * 1000000000, // 转换为纳秒 + open: item.open, + high: item.high, + low: item.low, + close: item.close, + volume: item.volume, + open_interest: Math.floor(Math.random() * 100000) + 50000 + })); + } + + async getTickData(symbol: string): Promise { + if (!this.initialized) { + throw new Error('模拟数据源未初始化'); + } + + // 返回模拟实时行情数据 + const future = futuresList.find(item => item.code === symbol); + if (!future) { + throw new Error(`合约${symbol}不存在`); + } + + const pre_close = 2000; + const last_price = pre_close + (Math.random() * 20 - 10); + const price_change = last_price - pre_close; + + return { + datetime: Date.now() * 1000000, // 转换为纳秒 + last_price: +last_price.toFixed(2), + pre_close, + open: pre_close + (Math.random() * 10 - 5), + high: Math.max(last_price, pre_close + (Math.random() * 15 - 5)), + low: Math.min(last_price, pre_close + (Math.random() * 15 - 10)), + volume: Math.floor(Math.random() * 10000) + 5000, + open_interest: Math.floor(Math.random() * 100000) + 50000, + price_change: +price_change.toFixed(2), + bid_price1: last_price - 0.01, + bid_volume1: Math.floor(Math.random() * 100) + 50, + ask_price1: last_price + 0.01, + ask_volume1: Math.floor(Math.random() * 100) + 50 + }; + } + + async getMarketOverview(): Promise { + if (!this.initialized) { + throw new Error('模拟数据源未初始化'); + } + + // 返回模拟市场概览 + const overview = generateFuturesOverview(); + return overview.map(item => ({ + symbol: item.code, + name: item.name, + price: item.currentPrice, + change: item.changePercent, + change_percent: item.changePercent, + volume: Math.floor(Math.random() * 100000) + 50000, + open_interest: Math.floor(Math.random() * 1000000) + 500000 + })); + } + + async getHistoricalTrades(symbol: string, start: number, end: number): Promise { + if (!this.initialized) { + throw new Error('模拟数据源未初始化'); + } + + // 返回模拟历史成交数据 + const trades = []; + const count = 100; // 生成100条模拟数据 + const basePrice = 2000; + + for (let i = 0; i < count; i++) { + const price = basePrice + (Math.random() * 10 - 5); + const volume = Math.floor(Math.random() * 100) + 10; + const timestamp = start + Math.floor((end - start) * (i / count)); + + trades.push({ + datetime: timestamp * 1000000, // 转换为纳秒 + price: +price.toFixed(2), + volume, + direction: Math.random() > 0.5 ? 'BUY' : 'SELL' + }); + } + + return trades; + } + + async close(): Promise { + // 模拟关闭 + this.initialized = false; + console.log('模拟数据源已关闭'); + } + + // 根据合约代码获取交易所 + private getExchangeBySymbol(symbol: string): string { + // 简单的交易所映射 + const exchangeMap: Record = { + 'AU': 'SHFE', + 'AG': 'SHFE', + 'CU': 'SHFE', + 'NI': 'SHFE', + 'SN': 'SHFE', + 'AL': 'SHFE', + 'ZN': 'SHFE', + 'FG': 'CZCE', + 'SJS': 'CZCE', + 'SCA': 'CZCE', + 'JM': 'DCE', + 'RB': 'SHFE', + 'ALO': 'SHFE', + 'MA': 'DCE', + 'PVC': 'DCE', + 'FU': 'SHFE', + 'SC': 'INE', + 'L': 'DCE', + 'NR': 'SHFE', + 'BU': 'SHFE', + 'LU': 'INE', + 'P': 'DCE', + 'LC': 'SHFE', + 'SI': 'SHFE', + 'PGS': 'SHFE', + 'IC': 'CFFEX', + 'IM': 'CFFEX', + 'IH': 'CFFEX' + }; + + return exchangeMap[symbol] || 'SHFE'; + } +} \ No newline at end of file diff --git a/backend/src/services/datasource/TQDataSource.ts b/backend/src/services/datasource/TQDataSource.ts new file mode 100644 index 0000000..ce8c777 --- /dev/null +++ b/backend/src/services/datasource/TQDataSource.ts @@ -0,0 +1,207 @@ +// TQSDK数据源实现 +import { DataSource } from './DataSource'; +import { TqSdk, TqAccount } from 'tqsdk'; + +export class TQDataSource implements DataSource { + private tq: TqSdk | null = null; + private initialized: boolean = false; + private config: { + username?: string; + password?: string; + timeout?: number; + retries?: number; + maxConnections?: number; + }; + + constructor(config: any = {}) { + this.config = { + username: config.username || '', + password: config.password || '', + timeout: config.timeout || 30000, + retries: config.retries || 3, + maxConnections: config.maxConnections || 5 + }; + } + + async initialize(): Promise { + try { + // 创建TQSDK实例,使用配置的参数 + const tqConfig: any = {}; + + // 如果有用户名和密码,使用用户名密码登录 + if (this.config.username && this.config.password) { + tqConfig.account = this.config.username; + tqConfig.password = this.config.password; + } + + this.tq = new TqSdk(tqConfig); + + // 等待初始化完成 + await new Promise((resolve) => { + this.tq?.on('connected', () => { + resolve(true); + }); + }); + + this.initialized = true; + console.log('TQSDK数据源初始化成功'); + return true; + } catch (error) { + console.error('TQSDK数据源初始化失败:', error); + this.initialized = false; + return false; + } + } + + async getContractList(): Promise { + if (!this.initialized || !this.tq) { + throw new Error('TQSDK数据源未初始化'); + } + + try { + // 获取所有合约 + const contracts = await this.tq.get_contracts(); + // 过滤期货合约,排除期权等其他类型 + const futuresContracts = contracts.filter((contract: any) => { + return contract.exchange.includes('SHFE') || + contract.exchange.includes('DCE') || + contract.exchange.includes('CZCE') || + contract.exchange.includes('INE'); + }); + return futuresContracts; + } catch (error) { + console.error('获取合约列表失败:', error); + throw error; + } + } + + async getContractDetail(symbol: string): Promise { + if (!this.initialized || !this.tq) { + throw new Error('TQSDK数据源未初始化'); + } + + try { + // 获取合约详情 + const contract = await this.tq.get_contract(symbol); + return contract; + } catch (error) { + console.error(`获取合约${symbol}详情失败:`, error); + throw error; + } + } + + async getKlineData(symbol: string, period: string, count: number): Promise { + if (!this.initialized || !this.tq) { + throw new Error('TQSDK数据源未初始化'); + } + + try { + // 转换周期格式 + const tqPeriod = this.convertPeriod(period); + // 获取K线数据 + const kline = await this.tq.get_kline_serial(symbol, tqPeriod, count); + return kline; + } catch (error) { + console.error(`获取合约${symbol}K线数据失败:`, error); + throw error; + } + } + + async getTickData(symbol: string): Promise { + if (!this.initialized || !this.tq) { + throw new Error('TQSDK数据源未初始化'); + } + + try { + // 获取实时行情数据 + const tick = await this.tq.get_tick_serial(symbol, 1); + return tick[0]; + } catch (error) { + console.error(`获取合约${symbol}实时行情数据失败:`, error); + throw error; + } + } + + async getMarketOverview(): Promise { + if (!this.initialized || !this.tq) { + throw new Error('TQSDK数据源未初始化'); + } + + try { + // 获取所有合约 + const contracts = await this.getContractList(); + // 限制获取的合约数量,避免请求过多 + const limitedContracts = contracts.slice(0, 20); + // 获取每个合约的实时行情 + const overview = []; + for (const contract of limitedContracts) { + try { + const tick = await this.getTickData(contract.symbol); + overview.push({ + symbol: contract.symbol, + name: contract.name, + price: tick.last_price, + change: tick.price_change, + change_percent: tick.price_change / tick.pre_close * 100, + volume: tick.volume, + open_interest: tick.open_interest + }); + } catch (error) { + console.error(`获取合约${contract.symbol}行情失败:`, error); + } + } + return overview; + } catch (error) { + console.error('获取市场概览失败:', error); + throw error; + } + } + + async getHistoricalTrades(symbol: string, start: number, end: number): Promise { + if (!this.initialized || !this.tq) { + throw new Error('TQSDK数据源未初始化'); + } + + try { + // 获取历史成交数据 + const trades = await this.tq.get_trade_serial(symbol, start, end); + return trades; + } catch (error) { + console.error(`获取合约${symbol}历史成交数据失败:`, error); + throw error; + } + } + + async close(): Promise { + if (this.tq) { + this.tq.close(); + this.tq = null; + this.initialized = false; + console.log('TQSDK数据源已关闭'); + } + } + + // 转换周期格式 + private convertPeriod(period: string): string { + switch (period) { + case '1M': + return '1min'; + case '5M': + return '5min'; + case '15M': + return '15min'; + case '30M': + return '30min'; + case '1H': + return '60min'; + case '4H': + return '240min'; + case '1D': + return '1d'; + case '1W': + return '1w'; + default: + return '1min'; + } + } +} \ No newline at end of file diff --git a/docs/开发文档/配置管理逻辑.md b/docs/开发文档/配置管理逻辑.md new file mode 100644 index 0000000..c00ec8d --- /dev/null +++ b/docs/开发文档/配置管理逻辑.md @@ -0,0 +1,345 @@ +# 配置管理逻辑文档 + +本文档详细介绍了 Alpha Futures Pro 系统中配置管理的实现逻辑,包括配置的获取、保存、存储位置等内容。 + +## 配置管理架构 + +- **前端组件**:`AdminConfig.jsx`(管理配置界面) +- **后端 API**:`config.ts`(配置管理接口) +- **配置存储**:`backend/config.json`(JSON 格式文件) + +## 前端配置管理逻辑 + +### 1. 获取配置 + +**文件位置**:`src/pages/admin/AdminConfig.jsx` +**函数**:`fetchConfig`(第174-205行) + +**实现逻辑**: + +```javascript +const fetchConfig = async () => { + try { + // 调用后端 API 获取配置 + const response = await fetch('http://localhost:3007/api/config/get'); + const result = await response.json(); + if (result.success) { + // 更新本地配置状态 + const newConfig = result.data; + setConfig(newConfig); + + // 更新表单字段值 + form.setFieldsValue({ + database: newConfig.database, + server: newConfig.server, + security: { + ...newConfig.security, + cors: { + ...newConfig.security.cors, + methods: newConfig.security.cors.methods.join(', '), + allowedHeaders: newConfig.security.cors.allowedHeaders.join(', ') + } + }, + dataSource: newConfig.dataSource + }); + + messageApi.success('配置加载成功'); + } else { + messageApi.error('配置加载失败'); + } + } catch (error) { + console.error('获取配置失败:', error); + messageApi.error('获取配置失败,请检查网络连接'); + } +}; +``` + +**调用时机**: +- 组件挂载时(通过 useEffect 钩子) +- 保存配置成功后(重新获取最新配置) + +### 2. 保存配置 + +**文件位置**:`src/pages/admin/AdminConfig.jsx` +**函数**:`handleSubmit`(第208-228行) + +**实现逻辑**: + +```javascript +const handleSubmit = async (values) => { + try { + // 调用后端 API 保存配置 + const response = await fetch('http://localhost:3007/api/config/save', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(config) + }); + + const result = await response.json(); + if (result.success) { + messageApi.success(result.message); + // 保存成功后重新获取配置 + fetchConfig(); + } else { + messageApi.error(result.message); + } + } catch (error) { + console.error('保存配置失败:', error); + messageApi.error('保存配置失败,请检查网络连接'); + } +}; +``` + +**调用时机**: +- 用户点击 "保存配置" 按钮时 + +## 后端配置管理逻辑 + +### 1. 获取配置 API + +**文件位置**:`backend/src/api/config.ts` +**路由**:`/api/config/get`(第151-169行) + +**实现逻辑**: + +```typescript +// 获取配置 +router.get('/get', async (req, res) => { + try { + // 从文件读取配置 + const fs = require('fs'); + const path = require('path'); + const configPath = path.join(__dirname, '../../config.json'); + + let config = {}; + if (fs.existsSync(configPath)) { + const configData = fs.readFileSync(configPath, 'utf8'); + config = JSON.parse(configData); + } + + res.status(200).json({ success: true, data: config }); + } catch (error) { + console.error('获取配置失败:', error); + res.status(500).json({ success: false, message: '获取配置失败' }); + } +}); +``` + +**功能**: +- 从 `backend/config.json` 文件读取配置 +- 如果文件不存在,返回空对象 +- 将配置数据以 JSON 格式返回给前端 + +### 2. 保存配置 API + +**文件位置**:`backend/src/api/config.ts` +**路由**:`/api/config/save`(第131-148行) + +**实现逻辑**: + +```typescript +// 保存配置 +router.post('/save', async (req, res) => { + try { + // 获取前端发送的配置数据 + const config = req.body; + console.log('保存配置:', config); + + // 保存配置到文件 + const fs = require('fs'); + const path = require('path'); + const configPath = path.join(__dirname, '../../config.json'); + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + + res.status(200).json({ success: true, message: '配置保存成功' }); + } catch (error) { + console.error('保存配置失败:', error); + res.status(500).json({ success: false, message: '保存配置失败' }); + } +}); +``` + +**功能**: +- 接收前端发送的配置数据 +- 将配置数据写入 `backend/config.json` 文件 +- 返回保存成功的提示信息 + +## 配置存储位置 + +**配置文件**:`backend/config.json` + +**文件格式**:JSON 格式 + +**存储内容**: +- 数据库配置(MongoDB、PostgreSQL、Redis、InfluxDB) +- 服务器配置(端口、主机、环境等) +- 安全配置(JWT、速率限制、CORS 等) +- 数据源配置(测试数据、TQSDK、Wind、新浪财经等) + +**文件示例**: + +```json +{ + "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 + } + }, + "server": { + "port": 3007, + "host": "0.0.0.0", + "environment": "development", + "debug": true, + "timeout": 30000, + "maxBodySize": "10mb" + }, + "security": { + "jwtSecret": "your-secret-key", + "jwtExpiresIn": "7d", + "rateLimit": { + "windowMs": 60000, + "max": 120 + }, + "cors": { + "origin": "*", + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allowedHeaders": ["Content-Type", "Authorization"] + } + }, + "dataSource": { + "test": { + "enabled": true, + "timeout": 10000, + "retries": 3, + "refreshInterval": 60000 + }, + "tqsdk": { + "enabled": true, + "username": "", + "password": "", + "timeout": 30000, + "retries": 3, + "maxConnections": 5 + }, + "wind": { + "enabled": false, + "apiKey": "", + "apiSecret": "", + "url": "https://api.wind.com.cn", + "timeout": 30000, + "retries": 3 + }, + "sina": { + "enabled": false, + "url": "https://finance.sina.com.cn", + "timeout": 10000, + "retries": 3, + "refreshInterval": 60000 + }, + "defaultDataSource": "tqsdk" + } +} +``` + +## 配置管理流程 + +### 1. 系统启动时 + +1. 后端服务启动,从 `backend/config.json` 文件读取配置 +2. 如果配置文件不存在,使用默认配置 +3. 前端应用启动,加载 `AdminConfig` 组件 +4. `AdminConfig` 组件挂载时调用 `fetchConfig` 函数获取配置 + +### 2. 用户修改配置时 + +1. 用户在管理配置界面修改配置项 +2. 前端实时更新本地配置状态 +3. 用户点击 "保存配置" 按钮 +4. 前端调用 `handleSubmit` 函数,将配置发送到后端 +5. 后端接收配置数据,写入 `config.json` 文件 +6. 后端返回保存成功的响应 +7. 前端显示成功提示,并重���获取配置以更新本地状态 + +### 3. 配置生效 + +- **前端**:配置修改后立即在界面上生效 +- **后端**:配置保存后,新的配置会在下次服务重启时生效 +- **数据源配置**:修改后需要重启后端服务才能完全生效 + +## 注意事项 + +1. **配置文件权限**:确保后端服务有读写 `config.json` 文件的权限 +2. **配置格式**:配置数据必须是有效的 JSON 格式 +3. **敏感信息**:配置文件中包含敏感信息(如数据库密码、TQSDK 用户名密码),请妥善保管 +4. **配置备份**:建议定期备份 `config.json` 文件,以防止配置丢失 +5. **服务重启**:修改某些配置项(如服务器端口、数据库连接信息)后,需要重启后端服务才能生效 + +## 故障排查 + +### 1. 获取配置失败 + +**症状**:前端显示 "获取配置失败" 错误 + +**解决方案**: +- 检查后端服务是否正常运行 +- 检查 `config.json` 文件是否存在且格式正确 +- 检查后端 API 是否可以正常访问 + +### 2. 保存配置失败 + +**症状**:前端显示 "保存配置失败" 错误 + +**解决方案**: +- 检查后端服务是否正常运行 +- 检查 `config.json` 文件是否有写入权限 +- 检查配置数据是否格式正确 +- 检查后端日志中的错误信息 + +### 3. 配置不生效 + +**症状**:修改配置后,系统行为没有改变 + +**解决方案**: +- 检查配置是否成功保存到 `config.json` 文件 +- 尝试重启后端服务 +- 检查配置项是否正确设置 + +--- + +**文档版本**:v1.0 +**最后更新**:2026-02-20 \ No newline at end of file diff --git a/src/pages/admin/AdminConfig.jsx b/src/pages/admin/AdminConfig.jsx index 3f8b61d..1602489 100644 --- a/src/pages/admin/AdminConfig.jsx +++ b/src/pages/admin/AdminConfig.jsx @@ -177,23 +177,126 @@ const AdminConfig = () => { const result = await response.json(); if (result.success) { const newConfig = result.data; - setConfig(newConfig); + console.log('当前配置:', newConfig); + + // 确保配置结构完整 + 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 } + }, + server: newConfig.server || { + port: 3007, host: '0.0.0.0', environment: 'development', debug: true, timeout: 30000, maxBodySize: '10mb' + }, + security: newConfig.security || { + jwtSecret: 'your-secret-key', jwtExpiresIn: '7d', + rateLimit: { windowMs: 60000, max: 120 }, + cors: { origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] } + }, + dataSource: newConfig.dataSource || { + test: { enabled: true, timeout: 10000, retries: 3, refreshInterval: 60000 }, + tqsdk: { enabled: true, username: '', password: '', timeout: 30000, retries: 3, maxConnections: 5 }, + wind: { enabled: false, apiKey: '', apiSecret: '', url: 'https://api.wind.com.cn', timeout: 30000, retries: 3 }, + sina: { enabled: false, url: 'https://finance.sina.com.cn', timeout: 10000, retries: 3, refreshInterval: 60000 }, + defaultDataSource: 'tqsdk' + } + }; + + // 确保 security.cors 结构正确 + if (completeConfig.security.cors && Array.isArray(completeConfig.security.cors.methods)) { + completeConfig.security.cors.methods = completeConfig.security.cors.methods.join(', '); + } + if (completeConfig.security.cors && Array.isArray(completeConfig.security.cors.allowedHeaders)) { + completeConfig.security.cors.allowedHeaders = completeConfig.security.cors.allowedHeaders.join(', '); + } + + console.log('完整配置:', completeConfig); // 更新表单字段值 form.setFieldsValue({ - database: newConfig.database, - server: newConfig.server, - security: { - ...newConfig.security, - cors: { - ...newConfig.security.cors, - methods: newConfig.security.cors.methods.join(', '), - allowedHeaders: newConfig.security.cors.allowedHeaders.join(', ') - } - }, - dataSource: newConfig.dataSource + '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.redis.host': completeConfig.database.redis.host, + 'database.redis.port': completeConfig.database.redis.port, + 'database.redis.password': completeConfig.database.redis.password, + '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, + 'server.debug': completeConfig.server.debug, + 'server.timeout': completeConfig.server.timeout, + 'server.maxBodySize': completeConfig.server.maxBodySize, + + 'security.jwtSecret': completeConfig.security.jwtSecret, + 'security.jwtExpiresIn': completeConfig.security.jwtExpiresIn, + 'security.rateLimit.windowMs': completeConfig.security.rateLimit.windowMs, + 'security.rateLimit.max': completeConfig.security.rateLimit.max, + 'security.cors.origin': completeConfig.security.cors.origin, + 'security.cors.methods': completeConfig.security.cors.methods, + 'security.cors.allowedHeaders': completeConfig.security.cors.allowedHeaders, + + 'dataSource.defaultDataSource': completeConfig.dataSource.defaultDataSource, + 'dataSource.test.enabled': completeConfig.dataSource.test.enabled, + 'dataSource.test.timeout': completeConfig.dataSource.test.timeout, + 'dataSource.test.retries': completeConfig.dataSource.test.retries, + 'dataSource.test.refreshInterval': completeConfig.dataSource.test.refreshInterval, + 'dataSource.tqsdk.enabled': completeConfig.dataSource.tqsdk.enabled, + 'dataSource.tqsdk.username': completeConfig.dataSource.tqsdk.username, + 'dataSource.tqsdk.password': completeConfig.dataSource.tqsdk.password, + 'dataSource.tqsdk.timeout': completeConfig.dataSource.tqsdk.timeout, + 'dataSource.tqsdk.retries': completeConfig.dataSource.tqsdk.retries, + 'dataSource.tqsdk.maxConnections': completeConfig.dataSource.tqsdk.maxConnections, + 'dataSource.wind.enabled': completeConfig.dataSource.wind.enabled, + 'dataSource.wind.apiKey': completeConfig.dataSource.wind.apiKey, + 'dataSource.wind.apiSecret': completeConfig.dataSource.wind.apiSecret, + 'dataSource.wind.url': completeConfig.dataSource.wind.url, + 'dataSource.wind.timeout': completeConfig.dataSource.wind.timeout, + 'dataSource.wind.retries': completeConfig.dataSource.wind.retries, + 'dataSource.sina.enabled': completeConfig.dataSource.sina.enabled, + 'dataSource.sina.url': completeConfig.dataSource.sina.url, + 'dataSource.sina.timeout': completeConfig.dataSource.sina.timeout, + 'dataSource.sina.retries': completeConfig.dataSource.sina.retries, + 'dataSource.sina.refreshInterval': completeConfig.dataSource.sina.refreshInterval }); + // 验证表单字段值是否正确设置 + setTimeout(() => { + console.log('表单字段值:', form.getFieldsValue()); + }, 200); + + // 延迟更新 config 状态,确保表单字段值已更新 + setTimeout(() => { + setConfig(completeConfig); + console.log('表单字段值已更新'); + }, 100); + messageApi.success('配置加载成功'); } else { messageApi.error('配置加载失败'); @@ -296,98 +399,6 @@ const AdminConfig = () => { form={form} layout="vertical" onFinish={handleSubmit} - initialValues={{ - database: { - mongoDB: { - host: config.database.mongoDB.host, - port: config.database.mongoDB.port, - database: config.database.mongoDB.database, - username: config.database.mongoDB.username, - password: config.database.mongoDB.password, - authSource: config.database.mongoDB.authSource, - ssl: config.database.mongoDB.ssl, - enabled: config.database.mongoDB.enabled - }, - postgreSQL: { - host: config.database.postgreSQL.host, - port: config.database.postgreSQL.port, - database: config.database.postgreSQL.database, - username: config.database.postgreSQL.username, - password: config.database.postgreSQL.password, - ssl: config.database.postgreSQL.ssl, - enabled: config.database.postgreSQL.enabled - }, - redis: { - host: config.database.redis.host, - port: config.database.redis.port, - password: config.database.redis.password, - db: config.database.redis.db, - enabled: config.database.redis.enabled - }, - influxDB: { - host: config.database.influxDB.host, - port: config.database.influxDB.port, - database: config.database.influxDB.database, - username: config.database.influxDB.username, - password: config.database.influxDB.password, - ssl: config.database.influxDB.ssl, - enabled: config.database.influxDB.enabled - } - }, - server: { - port: config.server.port, - host: config.server.host, - environment: config.server.environment, - debug: config.server.debug, - timeout: config.server.timeout, - maxBodySize: config.server.maxBodySize - }, - security: { - jwtSecret: config.security.jwtSecret, - jwtExpiresIn: config.security.jwtExpiresIn, - rateLimit: { - windowMs: config.security.rateLimit.windowMs, - max: config.security.rateLimit.max - }, - cors: { - origin: config.security.cors.origin, - methods: config.security.cors.methods.join(', '), - allowedHeaders: config.security.cors.allowedHeaders.join(', ') - } - }, - dataSource: { - defaultDataSource: config.dataSource.defaultDataSource, - test: { - enabled: config.dataSource.test.enabled, - timeout: config.dataSource.test.timeout, - retries: config.dataSource.test.retries, - refreshInterval: config.dataSource.test.refreshInterval - }, - tqsdk: { - enabled: config.dataSource.tqsdk.enabled, - username: config.dataSource.tqsdk.username, - password: config.dataSource.tqsdk.password, - timeout: config.dataSource.tqsdk.timeout, - retries: config.dataSource.tqsdk.retries, - maxConnections: config.dataSource.tqsdk.maxConnections - }, - wind: { - enabled: config.dataSource.wind.enabled, - apiKey: config.dataSource.wind.apiKey, - apiSecret: config.dataSource.wind.apiSecret, - url: config.dataSource.wind.url, - timeout: config.dataSource.wind.timeout, - retries: config.dataSource.wind.retries - }, - sina: { - enabled: config.dataSource.sina.enabled, - url: config.dataSource.sina.url, - timeout: config.dataSource.sina.timeout, - retries: config.dataSource.sina.retries, - refreshInterval: config.dataSource.sina.refreshInterval - } - } - }} >