diff --git a/backend/app/api/v1/cache.py b/backend/app/api/v1/cache.py index c08ca95..1fe1b7a 100644 --- a/backend/app/api/v1/cache.py +++ b/backend/app/api/v1/cache.py @@ -194,19 +194,26 @@ async def detect_all_missing_data( current_user: User = Depends(get_current_user) ): """一键检测所有数据的缺失情况""" - service = CacheService(db) - start = parse_date(request.start_date) - end = parse_date(request.end_date) - - result = service.detect_all_missing_data( - request.security_type, - request.period_type, - start, - end, - request.contract_type - ) - - return ResponseModel(data=result) + try: + service = CacheService(db) + start = parse_date(request.start_date) + end = parse_date(request.end_date) + + result = service.detect_all_missing_data( + request.security_type, + request.period_type, + start, + end, + request.contract_type + ) + + return ResponseModel(data=result) + except ValueError as e: + return ResponseModel(code=400, message=f"参数错误: {str(e)}") + except RuntimeError as e: + return ResponseModel(code=500, message=f"服务错误: {str(e)}") + except Exception as e: + return ResponseModel(code=500, message=f"检测失败: {str(e)}") @router.post("/detect-missing", response_model=ResponseModel) diff --git a/backend/app/api/v1/configs.py b/backend/app/api/v1/configs.py index d712526..056df15 100644 --- a/backend/app/api/v1/configs.py +++ b/backend/app/api/v1/configs.py @@ -227,10 +227,11 @@ async def test_system_connection( result = { "database": False, - "redis": False + "redis": False, + "database_error": None, + "redis_error": None } - # 测试数据库连接 if "database" in configs: try: engine = sqlalchemy.create_engine( @@ -239,17 +240,26 @@ async def test_system_connection( ) with engine.connect() as conn: result["database"] = True + except sqlalchemy.exc.OperationalError as e: + result["database_error"] = f"数据库连接错误: {str(e)}" + except sqlalchemy.exc.ProgrammingError as e: + result["database_error"] = f"数据库配置错误: {str(e)}" + except sqlalchemy.exc.AuthenticationFailed as e: + result["database_error"] = f"数据库认证失败: 用户名或密码错误" except Exception as e: - pass + result["database_error"] = f"数据库连接失败: {str(e)}" - # 测试Redis连接 if "redis" in configs: try: redis_client = redis.from_url(configs["redis"]) redis_client.ping() result["redis"] = True + except redis.exceptions.ConnectionError as e: + result["redis_error"] = f"Redis连接错误: 无法连接到Redis服务器,请检查地址和端口" + except redis.exceptions.AuthenticationError as e: + result["redis_error"] = f"Redis认证失败: 密码错误" except Exception as e: - pass + result["redis_error"] = f"Redis连接失败: {str(e)}" return ResponseModel(data=result) @@ -283,19 +293,26 @@ async def check_database_structure( inspector = inspect(engine) existing_tables = inspector.get_table_names() - # 检查必要的表是否存在 required_tables = [ 'users', 'sdk_configs', 'system_configs', + 'stock_info', 'stock_kline_daily', - 'stock_kline_minute', - 'future_kline_daily', - 'future_kline_minute', - 'cache_tasks', + 'stock_kline_min', 'stock_basic', 'index_basic', - 'index_trade' + 'index_trade', + 'future_info', + 'future_kline_daily', + 'future_kline_min', + 'realtime_snapshot', + 'finance_balance_sheet', + 'finance_cash_flow', + 'finance_income', + 'cache_tasks', + 'cache_task_details', + 'api_test_logs' ] missing_tables = [table for table in required_tables if table not in existing_tables] @@ -304,7 +321,8 @@ async def check_database_structure( return ResponseModel(data={ "complete": complete, "missing_tables": missing_tables, - "existing_tables": existing_tables + "existing_tables": existing_tables, + "required_tables": required_tables }) except Exception as e: return ResponseModel( diff --git a/backend/app/api/v1/stock.py b/backend/app/api/v1/stock.py index cdfc7ff..2f922a0 100644 --- a/backend/app/api/v1/stock.py +++ b/backend/app/api/v1/stock.py @@ -101,27 +101,34 @@ async def get_stock_kline_chart( current_user: User = Depends(get_current_user) ): """获取股票K线图数据(ECharts格式,含基础信息)""" - service = StockService(db) - start = parse_date(start_date) - end = parse_date(end_date) - - chart_data = service.get_kline_chart_data(code, start, end, period) - - stock_basic = db.query(StockBasic).filter(StockBasic.code == code).first() - - chart_data["basic"] = { - "code": stock_basic.code if stock_basic else code, - "name": stock_basic.name if stock_basic else None, - "total_shares": stock_basic.total_shares if stock_basic else None, - "float_shares": stock_basic.float_shares if stock_basic else None, - "industry_index_name": stock_basic.industry_index_name if stock_basic else None, - "industry_index_code": stock_basic.industry_index_code if stock_basic else None, - "institution_hold_ratio": float(stock_basic.institution_hold_ratio) if stock_basic and stock_basic.institution_hold_ratio else None, - "industry_level3": stock_basic.industry_level3 if stock_basic else None, - "list_date": str(stock_basic.list_date) if stock_basic and stock_basic.list_date else None - } - - return ResponseModel(data=chart_data) + try: + service = StockService(db) + start = parse_date(start_date) + end = parse_date(end_date) + + chart_data = service.get_kline_chart_data(code, start, end, period) + + stock_basic = db.query(StockBasic).filter(StockBasic.code == code).first() + + chart_data["basic"] = { + "code": stock_basic.code if stock_basic else code, + "name": stock_basic.name if stock_basic else None, + "total_shares": stock_basic.total_shares if stock_basic else None, + "float_shares": stock_basic.float_shares if stock_basic else None, + "industry_index_name": stock_basic.industry_index_name if stock_basic else None, + "industry_index_code": stock_basic.industry_index_code if stock_basic else None, + "institution_hold_ratio": float(stock_basic.institution_hold_ratio) if stock_basic and stock_basic.institution_hold_ratio else None, + "industry_level3": stock_basic.industry_level3 if stock_basic else None, + "list_date": str(stock_basic.list_date) if stock_basic and stock_basic.list_date else None + } + + return ResponseModel(data=chart_data) + except ValueError as e: + return ResponseModel(code=400, message=f"参数错误: {str(e)}") + except RuntimeError as e: + return ResponseModel(code=500, message=f"服务错误: {str(e)}") + except Exception as e: + return ResponseModel(code=500, message=f"查询失败: {str(e)}") @router.get("/basic/{code}", response_model=ResponseModel) diff --git a/backend/app/db/session.py b/backend/app/db/session.py index 91eb3ba..e09d182 100644 --- a/backend/app/db/session.py +++ b/backend/app/db/session.py @@ -61,8 +61,12 @@ def init_db() -> None: """初始化数据库表""" from app.models import user, config, stock, future, realtime, finance, cache, test from app.models.user import User + from app.models.stock_basic import StockBasic, IndexBasic, IndexTrade from app.core.security import get_password_hash + + print("开始创建数据库表...") Base.metadata.create_all(bind=engine) + print("数据库表创建完成") db = SessionLocal() try: diff --git a/backend/app/models/stock_basic.py b/backend/app/models/stock_basic.py index 9a719f9..1bc6f29 100644 --- a/backend/app/models/stock_basic.py +++ b/backend/app/models/stock_basic.py @@ -71,8 +71,4 @@ class IndexTrade(Base): created_at = Column(DateTime(timezone=True), default=datetime.utcnow) updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow) - index = relationship("IndexBasic", back_populates="trades") - - __table_args__ = ( - {'unique_constraint': None}, - ) \ No newline at end of file + index = relationship("IndexBasic", back_populates="trades") \ No newline at end of file diff --git a/frontend/src/views/CacheManager/DetectMissing.vue b/frontend/src/views/CacheManager/DetectMissing.vue index 8ce8ef1..be6b32e 100644 --- a/frontend/src/views/CacheManager/DetectMissing.vue +++ b/frontend/src/views/CacheManager/DetectMissing.vue @@ -330,12 +330,15 @@ const handleDetectAll = async () => { if (res.data.status === 'completed') { ElMessage.success(`检测完成:完整${res.data.complete_count}个,缺失${res.data.missing_count}个,错误${res.data.error_count}个`) } else if (res.data.status === 'failed') { - ElMessage.error(`检测失败:${res.data.error_message}`) + ElMessage.error(`检测失败:${res.data.error_message || '未知错误'}`) } + } else { + ElMessage.error(res.message || '检测失败,请稍后重试') } - } catch (error) { + } catch (error: any) { console.error(error) - ElMessage.error('检测失败') + const errorMsg = error.response?.data?.message || error.message || '网络请求失败' + ElMessage.error(`检测失败: ${errorMsg}`) } finally { detectingAll.value = false } @@ -365,10 +368,13 @@ const handleCacheAll = async () => { } ElMessage.success(`缓存任务已启动,共${res.data.total_count}个代码`) pollTaskProgress(res.data.task_id) + } else { + ElMessage.error(res.message || '缓存任务启动失败') } - } catch (error) { + } catch (error: any) { console.error(error) - ElMessage.error('缓存失败') + const errorMsg = error.response?.data?.message || error.message || '网络请求失败' + ElMessage.error(`缓存失败: ${errorMsg}`) } finally { cachingAll.value = false } @@ -404,10 +410,13 @@ const handleFillMissing = async () => { pollTaskProgress(res.data.task_id) } } + } else { + ElMessage.error(res.message || '补齐任务启动失败') } - } catch (error) { + } catch (error: any) { console.error(error) - ElMessage.error('补齐失败') + const errorMsg = error.response?.data?.message || error.message || '网络请求失败' + ElMessage.error(`补齐失败: ${errorMsg}`) } finally { fillingMissing.value = false } @@ -439,9 +448,17 @@ const handleDetect = async () => { : 0, details: item.missing_dates })) + + if (batchDetectResult.value.length === 0) { + ElMessage.success('所有代码数据完整,无缺失') + } + } else { + ElMessage.error(res.message || '检测失败,请稍后重试') } - } catch (error) { + } catch (error: any) { console.error(error) + const errorMsg = error.response?.data?.message || error.message || '网络请求失败' + ElMessage.error(`检测失败: ${errorMsg}`) } finally { detecting.value = false } @@ -453,16 +470,23 @@ const handleCache = async () => { caching.value = true try { - await batchCacheData({ + const res: any = await batchCacheData({ security_type: form.securityType, period_type: form.periodType, start_date: form.startDate, end_date: form.endDate, code_list: codes }) - ElMessage.success('缓存任务已启动') - } catch (error) { + + if (res.data) { + ElMessage.success('缓存任务已启动') + } else { + ElMessage.error(res.message || '缓存任务启动失败') + } + } catch (error: any) { console.error(error) + const errorMsg = error.response?.data?.message || error.message || '网络请求失败' + ElMessage.error(`缓存失败: ${errorMsg}`) } finally { caching.value = false } diff --git a/frontend/src/views/ConfigManager.vue b/frontend/src/views/ConfigManager.vue index 8a7cd84..b9cc63f 100644 --- a/frontend/src/views/ConfigManager.vue +++ b/frontend/src/views/ConfigManager.vue @@ -306,9 +306,17 @@ const buildDatabaseUrl = () => { case 'sqlite': return `sqlite:///${systemForm.dbPath}` case 'postgresql': - return `postgresql://${systemForm.dbUsername}:${systemForm.dbPassword}@${systemForm.dbHost}:${systemForm.dbPort}/${systemForm.dbName}` + if (systemForm.dbPassword) { + return `postgresql://${systemForm.dbUsername}:${systemForm.dbPassword}@${systemForm.dbHost}:${systemForm.dbPort}/${systemForm.dbName}` + } else { + return `postgresql://${systemForm.dbUsername}@${systemForm.dbHost}:${systemForm.dbPort}/${systemForm.dbName}` + } case 'mysql': - return `mysql://${systemForm.dbUsername}:${systemForm.dbPassword}@${systemForm.dbHost}:${systemForm.dbPort}/${systemForm.dbName}` + if (systemForm.dbPassword) { + return `mysql://${systemForm.dbUsername}:${systemForm.dbPassword}@${systemForm.dbHost}:${systemForm.dbPort}/${systemForm.dbName}` + } else { + return `mysql://${systemForm.dbUsername}@${systemForm.dbHost}:${systemForm.dbPort}/${systemForm.dbName}` + } default: return 'sqlite:///./amazing_data.db' } @@ -320,18 +328,48 @@ const parseDatabaseUrl = (url: string) => { systemForm.dbPath = url.replace('sqlite:///', '') } else if (url.startsWith('postgresql://')) { systemForm.dbType = 'postgresql' - systemForm.dbHost = 'localhost' - systemForm.dbPort = 5432 - systemForm.dbUsername = 'postgres' - systemForm.dbPassword = '' - systemForm.dbName = 'amazing_data' + try { + const urlPart = url.replace('postgresql://', '') + const parts = urlPart.split('@') + if (parts.length === 2) { + const userPass = parts[0].split(':') + systemForm.dbUsername = userPass[0] || 'postgres' + systemForm.dbPassword = userPass.length > 1 ? userPass[1] : '' + const hostPortDb = parts[1].split('/') + systemForm.dbName = hostPortDb.length > 1 ? hostPortDb[1] : 'amazing_data' + const hostPort = hostPortDb[0].split(':') + systemForm.dbHost = hostPort[0] || 'localhost' + systemForm.dbPort = hostPort.length > 1 ? parseInt(hostPort[1]) : 5432 + } + } catch (e) { + systemForm.dbHost = 'localhost' + systemForm.dbPort = 5432 + systemForm.dbUsername = 'postgres' + systemForm.dbPassword = '' + systemForm.dbName = 'amazing_data' + } } else if (url.startsWith('mysql://')) { systemForm.dbType = 'mysql' - systemForm.dbHost = 'localhost' - systemForm.dbPort = 3306 - systemForm.dbUsername = 'root' - systemForm.dbPassword = '' - systemForm.dbName = 'amazing_data' + try { + const urlPart = url.replace('mysql://', '') + const parts = urlPart.split('@') + if (parts.length === 2) { + const userPass = parts[0].split(':') + systemForm.dbUsername = userPass[0] || 'root' + systemForm.dbPassword = userPass.length > 1 ? userPass[1] : '' + const hostPortDb = parts[1].split('/') + systemForm.dbName = hostPortDb.length > 1 ? hostPortDb[1] : 'amazing_data' + const hostPort = hostPortDb[0].split(':') + systemForm.dbHost = hostPort[0] || 'localhost' + systemForm.dbPort = hostPort.length > 1 ? parseInt(hostPort[1]) : 3306 + } + } catch (e) { + systemForm.dbHost = 'localhost' + systemForm.dbPort = 3306 + systemForm.dbUsername = 'root' + systemForm.dbPassword = '' + systemForm.dbName = 'amazing_data' + } } } @@ -467,12 +505,16 @@ const handleTestConnection = async () => { if (!valid) return testingConnection.value = true + dbStructureStatus.value = null + showInitButton.value = false + try { const databaseUrl = buildDatabaseUrl() const res: any = await testSystemConnection({ database: databaseUrl, redis: systemForm.redis }) + if (res.data?.database) { ElMessage.success('数据库连接成功') const structureRes: any = await checkDatabaseStructure() @@ -483,20 +525,25 @@ const handleTestConnection = async () => { } else { dbStructureStatus.value = 'incomplete' showInitButton.value = true - ElMessage.warning('数据结构不完整,需要初始化') + const missingTables = structureRes.data?.missing_tables || [] + ElMessage.warning(`数据结构不完整,缺少表: ${missingTables.join(', ')}`) } } else { - ElMessage.error('数据库连接失败') + const dbErrorMsg = res.data?.database_error || res.message || '未知错误' + ElMessage.error(`数据库连接失败: ${dbErrorMsg}`) dbStructureStatus.value = null showInitButton.value = false } + if (res.data?.redis) { ElMessage.success('Redis连接成功') } else { - ElMessage.error('Redis连接失败') + const redisErrorMsg = res.data?.redis_error || '请检查Redis服务是否启动' + ElMessage.error(`Redis连接失败: ${redisErrorMsg}`) } - } catch (error) { - ElMessage.error('检测连接失败') + } catch (error: any) { + const errorMsg = error.response?.data?.message || error.message || '网络请求失败' + ElMessage.error(`检测连接失败: ${errorMsg}`) dbStructureStatus.value = null showInitButton.value = false } finally { diff --git a/frontend/src/views/DataQuery/StockKlineQuery.vue b/frontend/src/views/DataQuery/StockKlineQuery.vue index f6c7ad1..aa62570 100644 --- a/frontend/src/views/DataQuery/StockKlineQuery.vue +++ b/frontend/src/views/DataQuery/StockKlineQuery.vue @@ -167,22 +167,29 @@ const handleQuery = async () => { chartData.values = res.data.values || [] chartData.volumes = res.data.volumes || [] - tableData.value = chartData.categoryData.map((date, index) => ({ - date, - open: chartData.values[index]?.[0], - close: chartData.values[index]?.[1], - low: chartData.values[index]?.[2], - high: chartData.values[index]?.[3], - volume: chartData.values[index]?.[4] - })).reverse() - - nextTick(() => { - renderChart() - }) + if (chartData.categoryData.length === 0) { + ElMessage.warning('未查询到K线数据,请检查日期范围或股票代码是否正确') + } else { + tableData.value = chartData.categoryData.map((date, index) => ({ + date, + open: chartData.values[index]?.[0], + close: chartData.values[index]?.[1], + low: chartData.values[index]?.[2], + high: chartData.values[index]?.[3], + volume: chartData.values[index]?.[4] + })).reverse() + + nextTick(() => { + renderChart() + }) + } + } else { + ElMessage.error(res.message || '查询失败,请稍后重试') } - } catch (error) { + } catch (error: any) { console.error(error) - ElMessage.error('查询失败') + const errorMsg = error.response?.data?.message || error.message || '查询失败,请检查网络连接' + ElMessage.error(`查询失败: ${errorMsg}`) } finally { loading.value = false }