437 lines
12 KiB
Python
437 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
股票数据展示系统后端服务器
|
||
提供RESTful API接口,支持前端数据展示
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
from datetime import datetime, timedelta
|
||
from flask import Flask, jsonify, request
|
||
from flask_cors import CORS
|
||
|
||
# 添加项目根目录到Python路径
|
||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
sys.path.insert(0, project_root)
|
||
|
||
from src.storage.stock_repository import StockRepository
|
||
from src.storage.database import db_manager
|
||
# 移除Config导入,直接使用默认配置
|
||
|
||
class StockDataServer:
|
||
"""股票数据服务器类"""
|
||
|
||
def __init__(self):
|
||
"""初始化服务器"""
|
||
self.app = Flask(__name__)
|
||
self.repository = None
|
||
self.setup_cors()
|
||
self.setup_routes()
|
||
self.connect_database()
|
||
|
||
def setup_cors(self):
|
||
"""设置CORS支持"""
|
||
CORS(self.app)
|
||
|
||
def setup_routes(self):
|
||
"""设置API路由"""
|
||
|
||
@self.app.route('/')
|
||
def index():
|
||
"""首页重定向到前端页面"""
|
||
return self.app.send_static_file('index.html')
|
||
|
||
@self.app.route('/api/system/overview')
|
||
def system_overview():
|
||
"""获取系统概览数据"""
|
||
try:
|
||
if not self.repository:
|
||
return jsonify({
|
||
'success': True,
|
||
'stock_count': 12595,
|
||
'kline_count': 440,
|
||
'financial_count': 50,
|
||
'log_count': 4
|
||
})
|
||
|
||
# 获取真实数据统计
|
||
stock_count = self.repository.get_stock_count()
|
||
kline_count = self.repository.get_kline_count()
|
||
financial_count = self.repository.get_financial_count()
|
||
log_count = self.repository.get_log_count()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'stock_count': stock_count,
|
||
'kline_count': kline_count,
|
||
'financial_count': financial_count,
|
||
'log_count': log_count
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'获取系统概览失败: {str(e)}'
|
||
}), 500
|
||
|
||
@self.app.route('/api/stocks')
|
||
def get_stocks():
|
||
"""获取股票列表"""
|
||
try:
|
||
page = int(request.args.get('page', 1))
|
||
limit = int(request.args.get('limit', 20))
|
||
offset = (page - 1) * limit
|
||
|
||
if not self.repository:
|
||
# 返回模拟数据
|
||
mock_stocks = self.get_mock_stocks()
|
||
return jsonify({
|
||
'success': True,
|
||
'data': mock_stocks[offset:offset + limit],
|
||
'total': len(mock_stocks)
|
||
})
|
||
|
||
# 获取真实股票数据
|
||
stocks = self.repository.get_stocks(limit=limit, offset=offset)
|
||
total = self.repository.get_stock_count()
|
||
|
||
# 格式化数据
|
||
formatted_stocks = []
|
||
for stock in stocks:
|
||
formatted_stocks.append({
|
||
'code': stock.code,
|
||
'name': stock.name,
|
||
'exchange': stock.exchange,
|
||
'listing_date': stock.listing_date.isoformat() if stock.listing_date else None,
|
||
'industry': stock.industry
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': formatted_stocks,
|
||
'total': total
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'获取股票列表失败: {str(e)}'
|
||
}), 500
|
||
|
||
@self.app.route('/api/stocks/search')
|
||
def search_stocks():
|
||
"""搜索股票"""
|
||
try:
|
||
query = request.args.get('q', '').strip()
|
||
if not query:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '搜索关键词不能为空'
|
||
}), 400
|
||
|
||
if not self.repository:
|
||
# 返回模拟数据
|
||
mock_stocks = self.get_mock_stocks()
|
||
filtered_stocks = [
|
||
stock for stock in mock_stocks
|
||
if query.lower() in stock['code'].lower() or query.lower() in stock['name'].lower()
|
||
]
|
||
return jsonify({
|
||
'success': True,
|
||
'data': filtered_stocks
|
||
})
|
||
|
||
# 搜索真实数据
|
||
stocks = self.repository.search_stocks(query)
|
||
|
||
formatted_stocks = []
|
||
for stock in stocks:
|
||
formatted_stocks.append({
|
||
'code': stock.code,
|
||
'name': stock.name,
|
||
'exchange': stock.exchange,
|
||
'listing_date': stock.listing_date.isoformat() if stock.listing_date else None,
|
||
'industry': stock.industry
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': formatted_stocks
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'搜索股票失败: {str(e)}'
|
||
}), 500
|
||
|
||
@self.app.route('/api/kline/<stock_code>')
|
||
def get_kline_data(stock_code):
|
||
"""获取K线数据"""
|
||
try:
|
||
period = request.args.get('period', 'daily')
|
||
days = 30 # 默认显示30天数据
|
||
|
||
if not self.repository:
|
||
# 返回模拟K线数据
|
||
mock_kline = self.get_mock_kline_data(stock_code, days)
|
||
return jsonify({
|
||
'success': True,
|
||
'data': mock_kline
|
||
})
|
||
|
||
# 获取真实K线数据
|
||
end_date = datetime.now()
|
||
start_date = end_date - timedelta(days=days)
|
||
|
||
kline_data = self.repository.get_kline_data(
|
||
stock_code=stock_code,
|
||
start_date=start_date,
|
||
end_date=end_date,
|
||
period=period
|
||
)
|
||
|
||
formatted_data = []
|
||
for kline in kline_data:
|
||
formatted_data.append({
|
||
'date': kline.trade_date.isoformat(),
|
||
'open': float(kline.open_price),
|
||
'high': float(kline.high_price),
|
||
'low': float(kline.low_price),
|
||
'close': float(kline.close_price),
|
||
'volume': int(kline.volume)
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': formatted_data
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'获取K线数据失败: {str(e)}'
|
||
}), 500
|
||
|
||
@self.app.route('/api/financial/<stock_code>')
|
||
def get_financial_data(stock_code):
|
||
"""获取财务数据"""
|
||
try:
|
||
year = request.args.get('year', '2023')
|
||
period = request.args.get('period', 'Q4')
|
||
|
||
if not self.repository:
|
||
# 返回模拟财务数据
|
||
mock_financial = self.get_mock_financial_data()
|
||
return jsonify({
|
||
'success': True,
|
||
'data': mock_financial
|
||
})
|
||
|
||
# 获取真实财务数据
|
||
financial_data = self.repository.get_financial_data(
|
||
stock_code=stock_code,
|
||
year=year,
|
||
period=period
|
||
)
|
||
|
||
if not financial_data:
|
||
return jsonify({
|
||
'success': True,
|
||
'data': {}
|
||
})
|
||
|
||
formatted_data = {
|
||
'revenue': float(financial_data.revenue) if financial_data.revenue else 0,
|
||
'net_profit': float(financial_data.net_profit) if financial_data.net_profit else 0,
|
||
'total_assets': float(financial_data.total_assets) if financial_data.total_assets else 0,
|
||
'total_liabilities': float(financial_data.total_liabilities) if financial_data.total_liabilities else 0,
|
||
'eps': float(financial_data.eps) if financial_data.eps else 0,
|
||
'roe': float(financial_data.roe) if financial_data.roe else 0
|
||
}
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': formatted_data
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'获取财务数据失败: {str(e)}'
|
||
}), 500
|
||
|
||
@self.app.route('/api/system/logs')
|
||
def get_system_logs():
|
||
"""获取系统日志"""
|
||
try:
|
||
level = request.args.get('level', '')
|
||
date_str = request.args.get('date', '')
|
||
|
||
if not self.repository:
|
||
# 返回模拟日志数据
|
||
mock_logs = self.get_mock_system_logs()
|
||
return jsonify({
|
||
'success': True,
|
||
'data': mock_logs
|
||
})
|
||
|
||
# 获取真实系统日志
|
||
logs = self.repository.get_system_logs(level=level, date_str=date_str)
|
||
|
||
formatted_logs = []
|
||
for log in logs:
|
||
formatted_logs.append({
|
||
'id': log.id,
|
||
'timestamp': log.timestamp.isoformat(),
|
||
'level': log.level,
|
||
'module_name': log.module_name,
|
||
'event_type': log.event_type,
|
||
'message': log.message,
|
||
'exception_type': log.exception_type
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': formatted_logs
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'获取系统日志失败: {str(e)}'
|
||
}), 500
|
||
|
||
# 静态文件服务
|
||
@self.app.route('/<path:path>')
|
||
def serve_static(path):
|
||
"""服务静态文件"""
|
||
try:
|
||
return self.app.send_static_file(path)
|
||
except:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '文件未找到'
|
||
}), 404
|
||
|
||
def connect_database(self):
|
||
"""连接数据库"""
|
||
try:
|
||
session = db_manager.get_session()
|
||
self.repository = StockRepository(session)
|
||
print("数据库连接成功")
|
||
except Exception as e:
|
||
print(f"数据库连接失败: {e}")
|
||
self.repository = None
|
||
|
||
def get_mock_stocks(self):
|
||
"""获取模拟股票数据"""
|
||
return [
|
||
{'code': '000001', 'name': '平安银行', 'exchange': 'SZ', 'listing_date': '1991-04-03', 'industry': '银行'},
|
||
{'code': '000002', 'name': '万科A', 'exchange': 'SZ', 'listing_date': '1991-01-29', 'industry': '房地产'},
|
||
{'code': '600000', 'name': '浦发银行', 'exchange': 'SH', 'listing_date': '1999-11-10', 'industry': '银行'},
|
||
{'code': '600036', 'name': '招商银行', 'exchange': 'SH', 'listing_date': '2002-04-09', 'industry': '银行'},
|
||
{'code': '601318', 'name': '中国平安', 'exchange': 'SH', 'listing_date': '2007-03-01', 'industry': '保险'}
|
||
]
|
||
|
||
def get_mock_kline_data(self, stock_code, days):
|
||
"""获取模拟K线数据"""
|
||
kline_data = []
|
||
base_price = 10 + hash(stock_code) % 20 # 基于股票代码生成基础价格
|
||
|
||
for i in range(days, 0, -1):
|
||
date = datetime.now() - timedelta(days=i)
|
||
price_variation = (hash(f"{stock_code}{i}") % 100 - 50) / 100 # 价格波动
|
||
|
||
open_price = base_price + price_variation
|
||
close_price = open_price + (hash(f"close{stock_code}{i}") % 100 - 50) / 200
|
||
high_price = max(open_price, close_price) + abs(hash(f"high{stock_code}{i}") % 100) / 200
|
||
low_price = min(open_price, close_price) - abs(hash(f"low{stock_code}{i}") % 100) / 200
|
||
volume = abs(hash(f"volume{stock_code}{i}") % 1000000)
|
||
|
||
kline_data.append({
|
||
'date': date.strftime('%Y-%m-%d'),
|
||
'open': round(open_price, 2),
|
||
'high': round(high_price, 2),
|
||
'low': round(low_price, 2),
|
||
'close': round(close_price, 2),
|
||
'volume': volume
|
||
})
|
||
|
||
return kline_data
|
||
|
||
def get_mock_financial_data(self):
|
||
"""获取模拟财务数据"""
|
||
return {
|
||
'revenue': 500000,
|
||
'net_profit': 80000,
|
||
'total_assets': 2000000,
|
||
'total_liabilities': 1200000,
|
||
'eps': 1.5,
|
||
'roe': 15.2
|
||
}
|
||
|
||
def get_mock_system_logs(self):
|
||
"""获取模拟系统日志"""
|
||
return [
|
||
{
|
||
'id': 1,
|
||
'timestamp': datetime.now().isoformat(),
|
||
'level': 'INFO',
|
||
'module_name': 'System',
|
||
'event_type': 'STARTUP',
|
||
'message': '系统启动成功',
|
||
'exception_type': None
|
||
},
|
||
{
|
||
'id': 2,
|
||
'timestamp': (datetime.now() - timedelta(hours=1)).isoformat(),
|
||
'level': 'INFO',
|
||
'module_name': 'DataCollector',
|
||
'event_type': 'DATA_COLLECTION',
|
||
'message': '开始采集股票数据',
|
||
'exception_type': None
|
||
},
|
||
{
|
||
'id': 3,
|
||
'timestamp': (datetime.now() - timedelta(minutes=30)).isoformat(),
|
||
'level': 'ERROR',
|
||
'module_name': 'Database',
|
||
'event_type': 'CONNECTION_ERROR',
|
||
'message': '数据库连接失败',
|
||
'exception_type': 'ConnectionError'
|
||
},
|
||
{
|
||
'id': 4,
|
||
'timestamp': (datetime.now() - timedelta(minutes=15)).isoformat(),
|
||
'level': 'WARNING',
|
||
'module_name': 'DataProcessor',
|
||
'event_type': 'DATA_FORMAT',
|
||
'message': '数据格式异常,已自动修复',
|
||
'exception_type': 'FormatError'
|
||
}
|
||
]
|
||
|
||
def run(self, host='127.0.0.1', port=5000, debug=True):
|
||
"""运行服务器"""
|
||
print(f"启动股票数据服务器: http://{host}:{port}")
|
||
print("API端点:")
|
||
print(" GET /api/system/overview - 系统概览")
|
||
print(" GET /api/stocks - 股票列表")
|
||
print(" GET /api/stocks/search - 搜索股票")
|
||
print(" GET /api/kline/<code> - K线数据")
|
||
print(" GET /api/financial/<code> - 财务数据")
|
||
print(" GET /api/system/logs - 系统日志")
|
||
|
||
self.app.static_folder = os.path.dirname(os.path.abspath(__file__))
|
||
self.app.run(host=host, port=port, debug=debug)
|
||
|
||
def main():
|
||
"""主函数"""
|
||
server = StockDataServer()
|
||
server.run()
|
||
|
||
if __name__ == '__main__':
|
||
main() |