diff --git a/app.py b/app.py index d941a343..edc4e934 100755 --- a/app.py +++ b/app.py @@ -7475,12 +7475,18 @@ def add_event_comment(event_id): @socketio.on('connect') def handle_connect(): """客户端连接事件""" + print(f'\n[WebSocket DEBUG] ========== 客户端连接 ==========') + print(f'[WebSocket DEBUG] Socket ID: {request.sid}') + print(f'[WebSocket DEBUG] Remote Address: {request.remote_addr if hasattr(request, "remote_addr") else "N/A"}') print(f'[WebSocket] 客户端已连接: {request.sid}') + emit('connection_response', { 'status': 'connected', 'sid': request.sid, 'message': '已连接到事件推送服务' }) + print(f'[WebSocket DEBUG] ✓ 已发送 connection_response') + print(f'[WebSocket DEBUG] ========== 连接完成 ==========\n') @socketio.on('subscribe_events') @@ -7494,25 +7500,40 @@ def handle_subscribe(data): } """ try: + print(f'\n[WebSocket DEBUG] ========== 收到订阅请求 ==========') + print(f'[WebSocket DEBUG] Socket ID: {request.sid}') + print(f'[WebSocket DEBUG] 订阅数据: {data}') + event_type = data.get('event_type', 'all') importance = data.get('importance', 'all') + print(f'[WebSocket DEBUG] 事件类型: {event_type}') + print(f'[WebSocket DEBUG] 重要性: {importance}') + # 加入对应的房间 room_name = f"events_{event_type}" + print(f'[WebSocket DEBUG] 准备加入房间: {room_name}') join_room(room_name) + print(f'[WebSocket DEBUG] ✓ 已加入房间: {room_name}') print(f'[WebSocket] 客户端 {request.sid} 订阅了房间: {room_name}') - emit('subscription_confirmed', { + response_data = { 'success': True, 'room': room_name, 'event_type': event_type, 'importance': importance, 'message': f'已订阅 {event_type} 类型的事件推送' - }) + } + print(f'[WebSocket DEBUG] 准备发送 subscription_confirmed: {response_data}') + emit('subscription_confirmed', response_data) + print(f'[WebSocket DEBUG] ✓ 已发送 subscription_confirmed') + print(f'[WebSocket DEBUG] ========== 订阅完成 ==========\n') except Exception as e: - print(f'[WebSocket] 订阅失败: {e}') + print(f'[WebSocket ERROR] 订阅失败: {e}') + import traceback + traceback.print_exc() emit('subscription_error', { 'success': False, 'error': str(e) @@ -7523,9 +7544,16 @@ def handle_subscribe(data): def handle_unsubscribe(data): """取消订阅事件推送""" try: + print(f'\n[WebSocket DEBUG] ========== 收到取消订阅请求 ==========') + print(f'[WebSocket DEBUG] Socket ID: {request.sid}') + print(f'[WebSocket DEBUG] 数据: {data}') + event_type = data.get('event_type', 'all') room_name = f"events_{event_type}" + + print(f'[WebSocket DEBUG] 准备离开房间: {room_name}') leave_room(room_name) + print(f'[WebSocket DEBUG] ✓ 已离开房间: {room_name}') print(f'[WebSocket] 客户端 {request.sid} 取消订阅房间: {room_name}') @@ -7534,9 +7562,12 @@ def handle_unsubscribe(data): 'room': room_name, 'message': f'已取消订阅 {event_type} 类型的事件推送' }) + print(f'[WebSocket DEBUG] ========== 取消订阅完成 ==========\n') except Exception as e: - print(f'[WebSocket] 取消订阅失败: {e}') + print(f'[WebSocket ERROR] 取消订阅失败: {e}') + import traceback + traceback.print_exc() emit('unsubscription_error', { 'success': False, 'error': str(e) @@ -7546,7 +7577,10 @@ def handle_unsubscribe(data): @socketio.on('disconnect') def handle_disconnect(): """客户端断开连接事件""" + print(f'\n[WebSocket DEBUG] ========== 客户端断开 ==========') + print(f'[WebSocket DEBUG] Socket ID: {request.sid}') print(f'[WebSocket] 客户端已断开: {request.sid}') + print(f'[WebSocket DEBUG] ========== 断开完成 ==========\n') # ==================== WebSocket 辅助函数 ==================== @@ -7560,6 +7594,12 @@ def broadcast_new_event(event): event: Event 模型实例 """ try: + print(f'\n[WebSocket DEBUG] ========== 广播新事件 ==========') + print(f'[WebSocket DEBUG] 事件ID: {event.id}') + print(f'[WebSocket DEBUG] 事件标题: {event.title}') + print(f'[WebSocket DEBUG] 事件类型: {event.event_type}') + print(f'[WebSocket DEBUG] 重要性: {event.importance}') + event_data = { 'id': event.id, 'title': event.title, @@ -7575,19 +7615,29 @@ def broadcast_new_event(event): 'keywords': event.keywords_list if hasattr(event, 'keywords_list') else event.keywords, } + print(f'[WebSocket DEBUG] 准备发送的数据: {event_data}') + # 发送到所有订阅者(all 房间) + print(f'[WebSocket DEBUG] 正在发送到房间: events_all') socketio.emit('new_event', event_data, room='events_all', namespace='/') + print(f'[WebSocket DEBUG] ✓ 已发送到 events_all') # 发送到特定类型订阅者 if event.event_type: room_name = f"events_{event.event_type}" + print(f'[WebSocket DEBUG] 正在发送到房间: {room_name}') socketio.emit('new_event', event_data, room=room_name, namespace='/') + print(f'[WebSocket DEBUG] ✓ 已发送到 {room_name}') print(f'[WebSocket] 已推送新事件到房间: events_all, {room_name}') else: print(f'[WebSocket] 已推送新事件到房间: events_all') + print(f'[WebSocket DEBUG] ========== 广播完成 ==========\n') + except Exception as e: - print(f'[WebSocket] 推送新事件失败: {e}') + print(f'[WebSocket ERROR] 推送新事件失败: {e}') + import traceback + traceback.print_exc() # ==================== WebSocket 轮询机制(检测新事件) ==================== @@ -7615,6 +7665,10 @@ def poll_new_events(): from datetime import datetime, timedelta current_time = datetime.now() + print(f'\n[轮询 DEBUG] ========== 开始轮询 ==========') + print(f'[轮询 DEBUG] 当前时间: {current_time.strftime("%Y-%m-%d %H:%M:%S")}') + print(f'[轮询 DEBUG] 上次检查时间: {last_checked_time.strftime("%Y-%m-%d %H:%M:%S") if last_checked_time else "None"}') + print(f'[轮询 DEBUG] 已推送事件数量: {len(pushed_event_ids)}') # 如果是第一次运行,只查询最近 30 秒的事件 if last_checked_time is None: @@ -7623,6 +7677,8 @@ def poll_new_events(): # 向前多查 60 秒(重叠窗口),防止漏掉延迟写入的事件 query_start_time = last_checked_time - timedelta(seconds=60) + print(f'[轮询 DEBUG] 查询时间范围: {query_start_time.strftime("%Y-%m-%d %H:%M:%S")} ~ {current_time.strftime("%Y-%m-%d %H:%M:%S")}') + # 查询时间范围内的新事件 new_events = Event.query.filter( Event.created_at >= query_start_time, @@ -7630,16 +7686,24 @@ def poll_new_events(): Event.status == 'active' ).order_by(Event.created_at.asc()).all() + print(f'[轮询 DEBUG] 数据库查询结果: 找到 {len(new_events)} 个事件') + if new_events: + for evt in new_events: + print(f'[轮询 DEBUG] - ID={evt.id}, 标题={evt.title}, 创建时间={evt.created_at}, 已推送={evt.id in pushed_event_ids}') + # 过滤掉已经推送过的事件 unpushed_events = [ event for event in new_events if event.id not in pushed_event_ids ] + print(f'[轮询 DEBUG] 过滤后未推送事件: {len(unpushed_events)} 个') + if unpushed_events: print(f'[轮询] 发现 {len(unpushed_events)} 个新事件(查询到 {len(new_events)} 个,已过滤 {len(new_events) - len(unpushed_events)} 个重复)') for event in unpushed_events: + print(f'[轮询 DEBUG] 准备推送事件: ID={event.id}, 标题={event.title}') # 推送新事件 broadcast_new_event(event) # 记录已推送 @@ -7657,11 +7721,16 @@ def poll_new_events(): print(f'[轮询] 已清理推送记录,当前保留 {len(pushed_event_ids)} 个') else: + print(f'[轮询 DEBUG] 没有新事件需要推送') # 没有新事件,也要更新检查时间 last_checked_time = current_time + print(f'[轮询 DEBUG] ========== 轮询结束 ==========\n') + except Exception as e: - print(f'[轮询] 检查新事件时出错: {e}') + print(f'[轮询 ERROR] 检查新事件时出错: {e}') + import traceback + traceback.print_exc() def initialize_event_polling(): diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index a29325fd..00000000 --- a/app/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - -from flask_cors import CORS -from datetime import datetime -import pytz -import os - -# 创建Flask应用 -app = Flask(__name__) - -# 配置 -config_name = os.environ.get('FLASK_ENV', 'development') -from config import config -app.config.from_object(config[config_name]) - -# 初始化扩展 -db = SQLAlchemy(app) -CORS(app, resources={r"/api/*": {"origins": "*"}}) - -# 时区设置 -def beijing_now(): - """获取北京时间""" - tz = pytz.timezone('Asia/Shanghai') - return datetime.now(tz) - -# 导入模型 -from app.models import * - -# 创建数据库表 -with app.app_context(): - db.create_all() - -# 注册路由 -from app.routes import events, stocks, limitanalyse, calendar, industries - -app.register_blueprint(events.bp) -app.register_blueprint(stocks.bp) -app.register_blueprint(limitanalyse.bp) -app.register_blueprint(calendar.bp) -app.register_blueprint(industries.bp) - -if __name__ == '__main__': - print("=== Value Frontier React 架构启动 ===") - app.run(host='0.0.0.0', port=5001, debug=True) \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-310.pyc b/app/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 6c23c104..00000000 Binary files a/app/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 3f4247f4..00000000 Binary files a/app/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/app/__pycache__/models.cpython-311.pyc b/app/__pycache__/models.cpython-311.pyc deleted file mode 100644 index caeb9612..00000000 Binary files a/app/__pycache__/models.cpython-311.pyc and /dev/null differ diff --git a/app/extensions.py b/app/extensions.py deleted file mode 100644 index 5732bc83..00000000 --- a/app/extensions.py +++ /dev/null @@ -1,30 +0,0 @@ -# app/extensions.py -from flask_sqlalchemy import SQLAlchemy -from flask_login import LoginManager -from flask_compress import Compress -from flask_cors import CORS -from clickhouse_driver import Client as Cclient -from sqlalchemy import create_engine - -# Database instances -db = SQLAlchemy() - -# Other extensions -login_manager = LoginManager() -compress = Compress() -cors = CORS() - -# Database engines (如果仍然需要直接使用 engine) -engine = create_engine("mysql+pymysql://root:Zzl33818!@111.198.58.126:33060/stock", echo=False) -engine_med = create_engine("mysql+pymysql://root:Zzl33818!@111.198.58.126:33060/med", echo=False) -engine_2 = create_engine("mysql+pymysql://root:Zzl33818!@111.198.58.126:33060/valuefrontier", echo=False) - -# ClickHouse client factory -def get_clickhouse_client(): - return Cclient( - host='111.198.58.126', - port=18778, - user='default', - password='Zzl5588161!', - database='stock' - ) \ No newline at end of file diff --git a/app/models.py b/app/models.py deleted file mode 100644 index e4ea3af7..00000000 --- a/app/models.py +++ /dev/null @@ -1,504 +0,0 @@ -from app import db -from datetime import datetime -import pytz -import json - -def beijing_now(): - """获取北京时间""" - tz = pytz.timezone('Asia/Shanghai') - return datetime.now(tz) - -class Post(db.Model): - """帖子模型""" - id = db.Column(db.Integer, primary_key=True) - event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - - # 内容 - title = db.Column(db.String(200)) # 标题(可选) - content = db.Column(db.Text, nullable=False) # 内容 - content_type = db.Column(db.String(20), default='text') # 内容类型:text/rich_text/link - - # 时间 - created_at = db.Column(db.DateTime, default=beijing_now) - updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now) - - # 统计 - likes_count = db.Column(db.Integer, default=0) - comments_count = db.Column(db.Integer, default=0) - view_count = db.Column(db.Integer, default=0) - - # 状态 - status = db.Column(db.String(20), default='active') # active/hidden/deleted - is_top = db.Column(db.Boolean, default=False) # 是否置顶 - - # 关系 - user = db.relationship('User', backref='posts') - likes = db.relationship('PostLike', backref='post', lazy='dynamic') - comments = db.relationship('Comment', backref='post', lazy='dynamic') - -class User(db.Model): - """用户模型""" - id = db.Column(db.Integer, primary_key=True) - - # 基础账号信息(注册时必填) - username = db.Column(db.String(80), unique=True, nullable=False) # 用户名 - email = db.Column(db.String(120), unique=True, nullable=False) # 邮箱 - password_hash = db.Column(db.String(128), nullable=False) # 密码哈希 - email_confirmed = db.Column(db.Boolean, default=False) # 邮箱是否验证 - - # 账号状态 - created_at = db.Column(db.DateTime, default=beijing_now) # 注册时间 - last_seen = db.Column(db.DateTime, default=beijing_now) # 最后活跃时间 - status = db.Column(db.String(20), default='active') # 账号状态 active/banned/deleted - - # 个人资料(可选,后续在个人中心完善) - nickname = db.Column(db.String(30)) # 社区昵称 - avatar_url = db.Column(db.String(200)) # 头像URL - banner_url = db.Column(db.String(200)) # 个人主页背景图 - bio = db.Column(db.String(200)) # 个人简介 - gender = db.Column(db.String(10)) # 性别 - birth_date = db.Column(db.Date) # 生日 - location = db.Column(db.String(100)) # 所在地 - - # 联系方式(可选) - phone = db.Column(db.String(20)) # 手机号 - wechat_id = db.Column(db.String(80)) # 微信号 - - # 实名认证信息(可选) - real_name = db.Column(db.String(30)) # 真实姓名 - id_number = db.Column(db.String(18)) # 身份证号(加密存储) - is_verified = db.Column(db.Boolean, default=False) # 是否实名认证 - verify_time = db.Column(db.DateTime) # 实名认证时间 - - # 投资相关信息(可选) - trading_experience = db.Column(db.Integer) # 炒股年限 - investment_style = db.Column(db.String(50)) # 投资风格 - risk_preference = db.Column(db.String(20)) # 风险偏好 - investment_amount = db.Column(db.String(20)) # 投资规模 - preferred_markets = db.Column(db.String(200), default='[]') # 偏好市场 JSON - - # 社区信息(系统自动更新) - user_level = db.Column(db.Integer, default=1) # 用户等级 - reputation_score = db.Column(db.Integer, default=0) # 信用积分 - contribution_point = db.Column(db.Integer, default=0) # 贡献点数 - post_count = db.Column(db.Integer, default=0) # 发帖数 - comment_count = db.Column(db.Integer, default=0) # 评论数 - follower_count = db.Column(db.Integer, default=0) # 粉丝数 - following_count = db.Column(db.Integer, default=0) # 关注数 - - # 创作者信息(可选) - is_creator = db.Column(db.Boolean, default=False) # 是否创作者 - creator_type = db.Column(db.String(20)) # 创作者类型 - creator_tags = db.Column(db.String(200), default='[]') # 创作者标签 JSON - - # 系统设置 - email_notifications = db.Column(db.Boolean, default=True) # 邮件通知 - sms_notifications = db.Column(db.Boolean, default=False) # 短信通知 - wechat_notifications = db.Column(db.Boolean, default=False) # 微信通知 - notification_preferences = db.Column(db.String(500), default='{}') # 通知偏好 JSON - privacy_level = db.Column(db.String(20), default='public') # 隐私级别 - theme_preference = db.Column(db.String(20), default='light') # 主题偏好 - blocked_keywords = db.Column(db.String(500), default='[]') # 屏蔽关键词 JSON - # 手机号验证 - phone_confirmed = db.Column(db.Boolean, default=False) # 手机是否验证 - phone_confirm_time = db.Column(db.DateTime) # 手机验证时间 - - def __init__(self, username, email=None, password=None, phone=None): - self.username = username - if email: - self.email = email - if password: - self.set_password(password) - if phone: - self.phone = phone - - def set_password(self, password): - from werkzeug.security import generate_password_hash - self.password_hash = generate_password_hash(password) - - def check_password(self, password): - from werkzeug.security import check_password_hash - return check_password_hash(self.password_hash, password) - - def update_last_seen(self): - self.last_seen = beijing_now() - db.session.commit() - - def get_preferred_markets(self): - try: - return json.loads(self.preferred_markets) - except (json.JSONDecodeError, TypeError): - return [] - - def get_blocked_keywords(self): - try: - return json.loads(self.blocked_keywords) - except (json.JSONDecodeError, TypeError): - return [] - - def get_notification_preferences(self): - try: - return json.loads(self.notification_preferences) - except (json.JSONDecodeError, TypeError): - return {} - - def get_creator_tags(self): - try: - return json.loads(self.creator_tags) - except (json.JSONDecodeError, TypeError): - return [] - - def set_preferred_markets(self, markets): - self.preferred_markets = json.dumps(markets) - - def set_blocked_keywords(self, keywords): - self.blocked_keywords = json.dumps(keywords) - - def set_notification_preferences(self, preferences): - self.notification_preferences = json.dumps(preferences) - - def set_creator_tags(self, tags): - self.creator_tags = json.dumps(tags) - - def to_dict(self): - return { - 'id': self.id, - 'username': self.username, - 'email': self.email, - 'nickname': self.nickname, - 'avatar_url': self.avatar_url, - 'bio': self.bio, - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'last_seen': self.last_seen.isoformat() if self.last_seen else None, - 'status': self.status, - 'user_level': self.user_level, - 'reputation_score': self.reputation_score, - 'post_count': self.post_count, - 'follower_count': self.follower_count, - 'following_count': self.following_count - } - - def __repr__(self): - return f'' - -class Comment(db.Model): - """评论""" - id = db.Column(db.Integer, primary_key=True) - post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - content = db.Column(db.Text, nullable=False) - parent_id = db.Column(db.Integer, db.ForeignKey('comment.id')) # 父评论ID,用于回复 - created_at = db.Column(db.DateTime, default=beijing_now) - status = db.Column(db.String(20), default='active') - - user = db.relationship('User', backref='comments') - replies = db.relationship('Comment', backref=db.backref('parent', remote_side=[id])) - - -class CommentLike(db.Model): - """评论点赞记录(基于session_id以兼容匿名点赞)""" - __tablename__ = 'comment_like' - - id = db.Column(db.Integer, primary_key=True) - comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False) - session_id = db.Column(db.String(100), nullable=False) - created_at = db.Column(db.DateTime, default=beijing_now) - - __table_args__ = (db.UniqueConstraint('comment_id', 'session_id'),) - -class EventFollow(db.Model): - """事件关注""" - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) - created_at = db.Column(db.DateTime, default=beijing_now) - - user = db.relationship('User', backref='event_follows') - - __table_args__ = (db.UniqueConstraint('user_id', 'event_id'),) - -class PostLike(db.Model): - """帖子点赞""" - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False) - created_at = db.Column(db.DateTime, default=beijing_now) - - user = db.relationship('User', backref='post_likes') - - __table_args__ = (db.UniqueConstraint('user_id', 'post_id'),) - -class Event(db.Model): - """事件模型""" - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(200), nullable=False) - description = db.Column(db.Text) - - # 事件类型与状态 - event_type = db.Column(db.String(50)) - status = db.Column(db.String(20), default='active') - - # 时间相关 - start_time = db.Column(db.DateTime, default=beijing_now) - end_time = db.Column(db.DateTime) - created_at = db.Column(db.DateTime, default=beijing_now) - updated_at = db.Column(db.DateTime, default=beijing_now) - - # 热度与统计 - hot_score = db.Column(db.Float, default=0) - view_count = db.Column(db.Integer, default=0) - trending_score = db.Column(db.Float, default=0) - post_count = db.Column(db.Integer, default=0) - follower_count = db.Column(db.Integer, default=0) - - # 关联信息 - related_industries = db.Column(db.JSON) - keywords = db.Column(db.JSON) - files = db.Column(db.JSON) - importance = db.Column(db.String(20)) - related_avg_chg = db.Column(db.Float, default=0) - related_max_chg = db.Column(db.Float, default=0) - related_week_chg = db.Column(db.Float, default=0) - - # 新增字段 - invest_score = db.Column(db.Integer) # 超预期得分 - expectation_surprise_score = db.Column(db.Integer) - # 创建者信息 - creator_id = db.Column(db.Integer, db.ForeignKey('user.id')) - creator = db.relationship('User', backref='created_events') - - # 关系 - posts = db.relationship('Post', backref='event', lazy='dynamic') - followers = db.relationship('EventFollow', backref='event', lazy='dynamic') - related_stocks = db.relationship('RelatedStock', backref='event', lazy='dynamic') - historical_events = db.relationship('HistoricalEvent', backref='event', lazy='dynamic') - related_data = db.relationship('RelatedData', backref='event', lazy='dynamic') - related_concepts = db.relationship('RelatedConcepts', backref='event', lazy='dynamic') - - @property - def keywords_list(self): - if isinstance(self.keywords, list): - return self.keywords - elif isinstance(self.keywords, str): - try: - return json.loads(self.keywords) - except (json.JSONDecodeError, TypeError): - return [] - return [] - - def set_keywords(self, keywords): - if isinstance(keywords, list): - self.keywords = keywords - elif isinstance(keywords, str): - try: - self.keywords = json.loads(keywords) - except json.JSONDecodeError: - self.keywords = [keywords] - else: - self.keywords = [] - -class RelatedStock(db.Model): - """相关标的模型""" - id = db.Column(db.Integer, primary_key=True) - event_id = db.Column(db.Integer, db.ForeignKey('event.id')) - stock_code = db.Column(db.String(20)) # 股票代码 - stock_name = db.Column(db.String(100)) # 股票名称 - sector = db.Column(db.String(100)) # 关联类型 - relation_desc = db.Column(db.String(1024)) # 关联原因描述 - created_at = db.Column(db.DateTime, default=beijing_now) - updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now) - correlation = db.Column(db.Float()) - momentum = db.Column(db.String(1024)) #动量 - -class RelatedData(db.Model): - """关联数据模型""" - id = db.Column(db.Integer, primary_key=True) - event_id = db.Column(db.Integer, db.ForeignKey('event.id')) - title = db.Column(db.String(200)) # 数据标题 - data_type = db.Column(db.String(50)) # 数据类型 - data_content = db.Column(db.JSON) # 数据内容(JSON格式) - description = db.Column(db.Text) # 数据描述 - created_at = db.Column(db.DateTime, default=beijing_now) - -class RelatedConcepts(db.Model): - """关联数据模型""" - id = db.Column(db.Integer, primary_key=True) - event_id = db.Column(db.Integer, db.ForeignKey('event.id')) - concept_code = db.Column(db.String(20)) # 数据标题 - concept = db.Column(db.String(100)) # 数据类型 - reason = db.Column(db.Text) # 数据描述 - image_paths = db.Column(db.JSON) # 数据内容(JSON格式) - created_at = db.Column(db.DateTime, default=beijing_now) - - @property - def image_paths_list(self): - if isinstance(self.image_paths, list): - return self.image_paths - elif isinstance(self.image_paths, str): - try: - return json.loads(self.image_paths) - except (json.JSONDecodeError, TypeError): - return [] - return [] - - def set_image_paths(self, image_paths): - if isinstance(image_paths, list): - self.image_paths = image_paths - elif isinstance(image_paths, str): - try: - self.image_paths = json.loads(image_paths) - except json.JSONDecodeError: - self.image_paths = [image_paths] - else: - self.image_paths = [] - - def get_first_image_path(self): - paths = self.image_paths_list - return paths[0] if paths else None - -class EventHotHistory(db.Model): - """事件热度历史记录""" - id = db.Column(db.Integer, primary_key=True) - event_id = db.Column(db.Integer, db.ForeignKey('event.id')) - score = db.Column(db.Float) # 总分 - interaction_score = db.Column(db.Float) # 互动分数 - follow_score = db.Column(db.Float) # 关注度分数 - view_score = db.Column(db.Float) # 浏览量分数 - recent_activity_score = db.Column(db.Float) # 最近活跃度分数 - time_decay = db.Column(db.Float) # 时间衰减因子 - created_at = db.Column(db.DateTime, default=beijing_now) - - event = db.relationship('Event', backref='hot_history') - -class EventTransmissionNode(db.Model): - """事件传导节点模型""" - __tablename__ = 'event_transmission_nodes' - - id = db.Column(db.Integer, primary_key=True) - event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) - node_type = db.Column(db.Enum('company', 'industry', 'policy', 'technology', - 'market', 'event', 'other'), nullable=False) - node_name = db.Column(db.String(200), nullable=False) - node_description = db.Column(db.Text) - importance_score = db.Column(db.Integer, default=50) - stock_code = db.Column(db.String(20)) - is_main_event = db.Column(db.Boolean, default=False) - - created_at = db.Column(db.DateTime, default=beijing_now) - updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now) - - # Relationships - event = db.relationship('Event', backref='transmission_nodes') - outgoing_edges = db.relationship('EventTransmissionEdge', - foreign_keys='EventTransmissionEdge.from_node_id', - backref='from_node', cascade='all, delete-orphan') - incoming_edges = db.relationship('EventTransmissionEdge', - foreign_keys='EventTransmissionEdge.to_node_id', - backref='to_node', cascade='all, delete-orphan') - - __table_args__ = ( - db.Index('idx_event_node_type', 'event_id', 'node_type'), - db.Index('idx_node_name', 'node_name'), - ) - -class EventTransmissionEdge(db.Model): - """事件传导边模型""" - __tablename__ = 'event_transmission_edges' - - id = db.Column(db.Integer, primary_key=True) - event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) - from_node_id = db.Column(db.Integer, db.ForeignKey('event_transmission_nodes.id'), nullable=False) - to_node_id = db.Column(db.Integer, db.ForeignKey('event_transmission_nodes.id'), nullable=False) - - transmission_type = db.Column(db.Enum('supply_chain', 'competition', 'policy', - 'technology', 'capital_flow', 'expectation', - 'cyclic_effect', 'other'), nullable=False) - transmission_mechanism = db.Column(db.Text) - direction = db.Column(db.Enum('positive', 'negative', 'neutral', 'mixed'), default='neutral') - strength = db.Column(db.Integer, default=50) - impact = db.Column(db.Text) - is_circular = db.Column(db.Boolean, default=False) - - created_at = db.Column(db.DateTime, default=beijing_now) - updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now) - - # Relationship - event = db.relationship('Event', backref='transmission_edges') - - __table_args__ = ( - db.Index('idx_event_edge_type', 'event_id', 'transmission_type'), - db.Index('idx_from_to_nodes', 'from_node_id', 'to_node_id'), - ) - -class EventSankeyFlow(db.Model): - """事件桑基流模型""" - __tablename__ = 'event_sankey_flows' - - id = db.Column(db.Integer, primary_key=True) - event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) - - # 流的基本信息 - source_node = db.Column(db.String(200), nullable=False) - source_type = db.Column(db.Enum('event', 'policy', 'technology', 'industry', - 'company', 'product'), nullable=False) - source_level = db.Column(db.Integer, nullable=False, default=0) - - target_node = db.Column(db.String(200), nullable=False) - target_type = db.Column(db.Enum('policy', 'technology', 'industry', - 'company', 'product'), nullable=False) - target_level = db.Column(db.Integer, nullable=False, default=1) - - # 流量信息 - flow_value = db.Column(db.Numeric(10, 2), nullable=False) - flow_ratio = db.Column(db.Numeric(5, 4), nullable=False) - - # 传导机制 - transmission_path = db.Column(db.String(500)) - impact_description = db.Column(db.Text) - evidence_strength = db.Column(db.Integer, default=50) - - # 时间戳 - created_at = db.Column(db.DateTime, default=beijing_now) - updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now) - - # 关系 - event = db.relationship('Event', backref='sankey_flows') - - __table_args__ = ( - db.Index('idx_event_flow', 'event_id'), - db.Index('idx_source_target', 'source_node', 'target_node'), - ) - -class HistoricalEvent(db.Model): - """历史事件模型""" - id = db.Column(db.Integer, primary_key=True) - event_id = db.Column(db.Integer, db.ForeignKey('event.id')) - title = db.Column(db.String(200)) - content = db.Column(db.Text) - event_date = db.Column(db.DateTime) - relevance = db.Column(db.Integer) # 相关性 - importance = db.Column(db.Integer) # 重要程度 - related_stock = db.Column(db.JSON) # 保留JSON字段 - created_at = db.Column(db.DateTime, default=beijing_now) - - # 新增关系 - stocks = db.relationship('HistoricalEventStock', backref='historical_event', lazy='dynamic', - cascade='all, delete-orphan') - -class HistoricalEventStock(db.Model): - """历史事件相关股票模型""" - __tablename__ = 'historical_event_stocks' - - id = db.Column(db.Integer, primary_key=True) - historical_event_id = db.Column(db.Integer, db.ForeignKey('historical_event.id'), nullable=False) - stock_code = db.Column(db.String(20), nullable=False) - stock_name = db.Column(db.String(50)) - relation_desc = db.Column(db.Text) - correlation = db.Column(db.Float, default=0.5) - sector = db.Column(db.String(100)) - created_at = db.Column(db.DateTime, default=beijing_now) - - __table_args__ = ( - db.Index('idx_historical_event_stock', 'historical_event_id', 'stock_code'), - ) \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py deleted file mode 100644 index a3d724bf..00000000 --- a/app/routes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# 路由包初始化文件 \ No newline at end of file diff --git a/app/routes/__pycache__/__init__.cpython-311.pyc b/app/routes/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 91144116..00000000 Binary files a/app/routes/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/app/routes/__pycache__/calendar.cpython-311.pyc b/app/routes/__pycache__/calendar.cpython-311.pyc deleted file mode 100644 index 935c5163..00000000 Binary files a/app/routes/__pycache__/calendar.cpython-311.pyc and /dev/null differ diff --git a/app/routes/__pycache__/events.cpython-311.pyc b/app/routes/__pycache__/events.cpython-311.pyc deleted file mode 100644 index 1ef13cbe..00000000 Binary files a/app/routes/__pycache__/events.cpython-311.pyc and /dev/null differ diff --git a/app/routes/__pycache__/industries.cpython-311.pyc b/app/routes/__pycache__/industries.cpython-311.pyc deleted file mode 100644 index d8e8b1ef..00000000 Binary files a/app/routes/__pycache__/industries.cpython-311.pyc and /dev/null differ diff --git a/app/routes/__pycache__/limitanalyse.cpython-311.pyc b/app/routes/__pycache__/limitanalyse.cpython-311.pyc deleted file mode 100644 index bd3e66f4..00000000 Binary files a/app/routes/__pycache__/limitanalyse.cpython-311.pyc and /dev/null differ diff --git a/app/routes/__pycache__/stocks.cpython-311.pyc b/app/routes/__pycache__/stocks.cpython-311.pyc deleted file mode 100644 index fcd8fa84..00000000 Binary files a/app/routes/__pycache__/stocks.cpython-311.pyc and /dev/null differ diff --git a/app/routes/calendar.py b/app/routes/calendar.py deleted file mode 100644 index 81de26c8..00000000 --- a/app/routes/calendar.py +++ /dev/null @@ -1,121 +0,0 @@ -from flask import Blueprint, request, jsonify -from datetime import datetime, timedelta -import json - -bp = Blueprint('calendar', __name__, url_prefix='/api/v1/calendar') - -@bp.route('/event-counts', methods=['GET']) -def get_event_counts(): - """获取事件数量统计""" - try: - year = request.args.get('year', '2027') - month = request.args.get('month', '10') - - # 模拟事件数量数据 - event_counts = [] - for day in range(1, 32): - count = (day % 7) + 1 # 模拟每天1-7个事件 - event_counts.append({ - 'date': f'{year}-{month.zfill(2)}-{day:02d}', - 'count': count - }) - - return jsonify({ - 'success': True, - 'data': event_counts - }) - - except Exception as e: - print(f"Error getting event counts: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/events', methods=['GET']) -def get_calendar_events(): - """获取日历事件""" - try: - year = request.args.get('year', '2027') - month = request.args.get('month', '10') - event_type = request.args.get('type', 'all') - - # 模拟日历事件数据 - events = [] - for day in range(1, 32): - for i in range((day % 7) + 1): - event = { - 'id': f'{year}{month.zfill(2)}{day:02d}{i}', - 'title': f'事件{day}-{i+1}', - 'date': f'{year}-{month.zfill(2)}-{day:02d}', - 'type': ['政策', '技术', '产业', '公司'][i % 4], - 'importance': ['高', '中', '低'][i % 3], - 'status': 'active' - } - events.append(event) - - # 根据类型过滤 - if event_type != 'all': - events = [e for e in events if e['type'] == event_type] - - return jsonify({ - 'success': True, - 'data': events - }) - - except Exception as e: - print(f"Error getting calendar events: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/events/', methods=['GET']) -def get_calendar_event_detail(event_id): - """获取日历事件详情""" - try: - # 模拟事件详情 - event_detail = { - 'id': event_id, - 'title': f'事件{event_id}详情', - 'description': f'这是事件{event_id}的详细描述', - 'date': '2027-10-15', - 'type': '政策', - 'importance': '高', - 'status': 'active', - 'related_stocks': [ - {'code': '000001', 'name': '股票A'}, - {'code': '000002', 'name': '股票B'} - ], - 'keywords': ['政策', '改革', '创新'], - 'files': [ - {'name': '报告.pdf', 'url': '/files/report.pdf'}, - {'name': '数据.xlsx', 'url': '/files/data.xlsx'} - ] - } - - return jsonify({ - 'success': True, - 'data': event_detail - }) - - except Exception as e: - print(f"Error getting calendar event detail: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -def get_event_class(count): - """根据事件数量获取CSS类""" - if count == 0: - return 'no-events' - elif count <= 3: - return 'few-events' - elif count <= 6: - return 'medium-events' - else: - return 'many-events' - -def parse_json_field(field_value): - """解析JSON字段""" - if isinstance(field_value, str): - try: - return json.loads(field_value) - except (json.JSONDecodeError, TypeError): - return [] - elif isinstance(field_value, (list, dict)): - return field_value - else: - return [] \ No newline at end of file diff --git a/app/routes/events.py b/app/routes/events.py deleted file mode 100644 index 7bcb29d1..00000000 --- a/app/routes/events.py +++ /dev/null @@ -1,385 +0,0 @@ -from flask import Blueprint, request, jsonify -from app import db -from app.models import Event, RelatedStock, RelatedConcepts, HistoricalEvent, EventTransmissionNode, EventTransmissionEdge, EventSankeyFlow -from datetime import datetime -import json - -bp = Blueprint('events', __name__, url_prefix='/api/events') - - - -@bp.route('/', methods=['GET']) -def get_event_detail(event_id): - """获取事件详情""" - try: - event = Event.query.get(event_id) - if not event: - return jsonify({'success': False, 'error': '事件不存在'}), 404 - - # 获取相关股票 - related_stocks = RelatedStock.query.filter_by(event_id=event_id).all() - stocks_data = [] - for stock in related_stocks: - stocks_data.append({ - 'id': stock.id, - 'stock_code': stock.stock_code, - 'stock_name': stock.stock_name, - 'sector': stock.sector, - 'relation_desc': stock.relation_desc, - 'correlation': stock.correlation, - 'momentum': stock.momentum, - 'created_at': stock.created_at.isoformat() if stock.created_at else None - }) - - # 获取相关概念 - related_concepts = RelatedConcepts.query.filter_by(event_id=event_id).all() - concepts_data = [] - for concept in related_concepts: - concepts_data.append({ - 'id': concept.id, - 'concept_code': concept.concept_code, - 'concept': concept.concept, - 'reason': concept.reason, - 'image_paths': concept.image_paths_list, - 'created_at': concept.created_at.isoformat() if concept.created_at else None - }) - - event_data = { - 'id': event.id, - 'title': event.title, - 'description': event.description, - 'event_type': event.event_type, - 'status': event.status, - 'start_time': event.start_time.isoformat() if event.start_time else None, - 'end_time': event.end_time.isoformat() if event.end_time else None, - 'created_at': event.created_at.isoformat() if event.created_at else None, - 'updated_at': event.updated_at.isoformat() if event.updated_at else None, - 'hot_score': event.hot_score, - 'view_count': event.view_count, - 'trending_score': event.trending_score, - 'post_count': event.post_count, - 'follower_count': event.follower_count, - 'related_industries': event.related_industries, - 'keywords': event.keywords_list, - 'files': event.files, - 'importance': event.importance, - 'related_avg_chg': event.related_avg_chg, - 'related_max_chg': event.related_max_chg, - 'related_week_chg': event.related_week_chg, - 'invest_score': event.invest_score, - 'expectation_surprise_score': event.expectation_surprise_score, - 'related_stocks': stocks_data, - 'related_concepts': concepts_data - } - - return jsonify({ - 'success': True, - 'data': event_data - }) - - except Exception as e: - print(f"Error getting event detail: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//stocks', methods=['GET']) -def get_related_stocks(event_id): - """获取事件相关股票""" - try: - stocks = RelatedStock.query.filter_by(event_id=event_id).all() - stocks_data = [] - for stock in stocks: - stocks_data.append({ - 'id': stock.id, - 'stock_code': stock.stock_code, - 'stock_name': stock.stock_name, - 'sector': stock.sector, - 'relation_desc': stock.relation_desc, - 'correlation': stock.correlation, - 'momentum': stock.momentum, - 'created_at': stock.created_at.isoformat() if stock.created_at else None - }) - - return jsonify({ - 'success': True, - 'data': stocks_data - }) - - except Exception as e: - print(f"Error getting related stocks: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//stocks', methods=['POST']) -def add_related_stock(event_id): - """添加相关股票""" - try: - data = request.get_json() - if not data: - return jsonify({'success': False, 'error': '请提供数据'}), 400 - - # 检查事件是否存在 - event = Event.query.get(event_id) - if not event: - return jsonify({'success': False, 'error': '事件不存在'}), 404 - - # 创建新的相关股票记录 - new_stock = RelatedStock( - event_id=event_id, - stock_code=data['stock_code'], - stock_name=data.get('stock_name', ''), - sector=data.get('sector', ''), - relation_desc=data['relation_desc'], - correlation=data.get('correlation', 0.5), - momentum=data.get('momentum', '') - ) - - db.session.add(new_stock) - db.session.commit() - - return jsonify({ - 'success': True, - 'message': '相关股票添加成功', - 'data': { - 'id': new_stock.id, - 'stock_code': new_stock.stock_code, - 'stock_name': new_stock.stock_name, - 'sector': new_stock.sector, - 'relation_desc': new_stock.relation_desc, - 'correlation': new_stock.correlation, - 'momentum': new_stock.momentum - } - }) - - except Exception as e: - db.session.rollback() - print(f"Error adding related stock: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/stocks/', methods=['DELETE']) -def delete_related_stock(stock_id): - """删除相关股票""" - try: - stock = RelatedStock.query.get(stock_id) - if not stock: - return jsonify({'success': False, 'error': '相关股票不存在'}), 404 - - db.session.delete(stock) - db.session.commit() - - return jsonify({ - 'success': True, - 'message': '相关股票删除成功' - }) - - except Exception as e: - db.session.rollback() - print(f"Error deleting related stock: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//concepts', methods=['GET']) -def get_related_concepts(event_id): - """获取事件相关概念""" - try: - concepts = RelatedConcepts.query.filter_by(event_id=event_id).all() - concepts_data = [] - for concept in concepts: - concepts_data.append({ - 'id': concept.id, - 'concept_code': concept.concept_code, - 'concept': concept.concept, - 'reason': concept.reason, - 'image_paths': concept.image_paths_list, - 'created_at': concept.created_at.isoformat() if concept.created_at else None - }) - - return jsonify({ - 'success': True, - 'data': concepts_data - }) - - except Exception as e: - print(f"Error getting related concepts: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//historical', methods=['GET']) -def get_historical_events(event_id): - """获取历史事件""" - try: - historical_events = HistoricalEvent.query.filter_by(event_id=event_id).all() - events_data = [] - for event in historical_events: - events_data.append({ - 'id': event.id, - 'title': event.title, - 'content': event.content, - 'event_date': event.event_date.isoformat() if event.event_date else None, - 'relevance': event.relevance, - 'importance': event.importance, - 'related_stock': event.related_stock, - 'created_at': event.created_at.isoformat() if event.created_at else None - }) - - return jsonify({ - 'success': True, - 'data': events_data - }) - - except Exception as e: - print(f"Error getting historical events: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//expectation-score', methods=['GET']) -def get_expectation_score(event_id): - """获取超预期得分""" - try: - event = Event.query.get(event_id) - if not event: - return jsonify({'success': False, 'error': '事件不存在'}), 404 - - return jsonify({ - 'success': True, - 'data': { - 'invest_score': event.invest_score, - 'expectation_surprise_score': event.expectation_surprise_score - } - }) - - except Exception as e: - print(f"Error getting expectation score: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//follow', methods=['POST']) -def toggle_event_follow(event_id): - """关注/取消关注事件""" - try: - # 这里需要用户认证,暂时返回成功 - return jsonify({ - 'success': True, - 'message': '关注状态更新成功' - }) - - except Exception as e: - print(f"Error toggling event follow: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//transmission', methods=['GET']) -def get_transmission_chain(event_id): - """获取事件传导链""" - try: - # 获取传导节点 - nodes = EventTransmissionNode.query.filter_by(event_id=event_id).all() - nodes_data = [] - for node in nodes: - nodes_data.append({ - 'id': node.id, - 'node_type': node.node_type, - 'node_name': node.node_name, - 'node_description': node.node_description, - 'importance_score': node.importance_score, - 'stock_code': node.stock_code, - 'is_main_event': node.is_main_event - }) - - # 获取传导边 - edges = EventTransmissionEdge.query.filter_by(event_id=event_id).all() - edges_data = [] - for edge in edges: - edges_data.append({ - 'id': edge.id, - 'from_node_id': edge.from_node_id, - 'to_node_id': edge.to_node_id, - 'transmission_type': edge.transmission_type, - 'transmission_mechanism': edge.transmission_mechanism, - 'direction': edge.direction, - 'strength': edge.strength, - 'impact': edge.impact, - 'is_circular': edge.is_circular - }) - - return jsonify({ - 'success': True, - 'data': { - 'nodes': nodes_data, - 'edges': edges_data - } - }) - - except Exception as e: - print(f"Error getting transmission chain: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//sankey-data') -def get_event_sankey_data(event_id): - """获取事件桑基图数据""" - try: - flows = EventSankeyFlow.query.filter_by(event_id=event_id).all() - flows_data = [] - for flow in flows: - flows_data.append({ - 'id': flow.id, - 'source_node': flow.source_node, - 'source_type': flow.source_type, - 'source_level': flow.source_level, - 'target_node': flow.target_node, - 'target_type': flow.target_type, - 'target_level': flow.target_level, - 'flow_value': float(flow.flow_value), - 'flow_ratio': float(flow.flow_ratio), - 'transmission_path': flow.transmission_path, - 'impact_description': flow.impact_description, - 'evidence_strength': flow.evidence_strength - }) - - return jsonify({ - 'success': True, - 'data': flows_data - }) - - except Exception as e: - print(f"Error getting sankey data: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//chain-analysis') -def get_event_chain_analysis(event_id): - """获取事件链分析""" - try: - # 这里可以添加更复杂的链分析逻辑 - return jsonify({ - 'success': True, - 'data': { - 'event_id': event_id, - 'analysis': '链分析数据' - } - }) - - except Exception as e: - print(f"Error getting chain analysis: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//chain-node/', methods=['GET']) -def get_chain_node_detail(event_id, node_id): - """获取链节点详情""" - try: - node = EventTransmissionNode.query.filter_by( - event_id=event_id, - id=node_id - ).first() - - if not node: - return jsonify({'success': False, 'error': '节点不存在'}), 404 - - return jsonify({ - 'success': True, - 'data': { - 'id': node.id, - 'node_type': node.node_type, - 'node_name': node.node_name, - 'node_description': node.node_description, - 'importance_score': node.importance_score, - 'stock_code': node.stock_code, - 'is_main_event': node.is_main_event - } - }) - - except Exception as e: - print(f"Error getting chain node detail: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 \ No newline at end of file diff --git a/app/routes/industries.py b/app/routes/industries.py deleted file mode 100644 index c2db94b1..00000000 --- a/app/routes/industries.py +++ /dev/null @@ -1,511 +0,0 @@ -from flask import Blueprint, request, jsonify -import json - -bp = Blueprint('industries', __name__, url_prefix='/api') - -@bp.route('/classifications', methods=['GET']) -def get_classifications(): - """获取行业分类""" - try: - # 模拟行业分类数据 - classifications = [ - { - 'id': 1, - 'name': '申万一级行业', - 'description': '申万一级行业分类标准', - 'levels': [ - {'id': 1, 'name': '农林牧渔'}, - {'id': 2, 'name': '采掘'}, - {'id': 3, 'name': '化工'}, - {'id': 4, 'name': '钢铁'}, - {'id': 5, 'name': '有色金属'}, - {'id': 6, 'name': '建筑材料'}, - {'id': 7, 'name': '建筑装饰'}, - {'id': 8, 'name': '电气设备'}, - {'id': 9, 'name': '国防军工'}, - {'id': 10, 'name': '汽车'}, - {'id': 11, 'name': '家用电器'}, - {'id': 12, 'name': '纺织服装'}, - {'id': 13, 'name': '轻工制造'}, - {'id': 14, 'name': '医药生物'}, - {'id': 15, 'name': '公用事业'}, - {'id': 16, 'name': '交通运输'}, - {'id': 17, 'name': '房地产'}, - {'id': 18, 'name': '商业贸易'}, - {'id': 19, 'name': '休闲服务'}, - {'id': 20, 'name': '银行'}, - {'id': 21, 'name': '非银金融'}, - {'id': 22, 'name': '综合'}, - {'id': 23, 'name': '计算机'}, - {'id': 24, 'name': '传媒'}, - {'id': 25, 'name': '通信'}, - {'id': 26, 'name': '电子'}, - {'id': 27, 'name': '机械设备'}, - {'id': 28, 'name': '食品饮料'} - ] - } - ] - - return jsonify({ - 'success': True, - 'data': classifications - }) - - except Exception as e: - print(f"Error getting classifications: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/levels', methods=['GET']) -def get_industry_levels(): - """获取行业层级""" - try: - classification_id = request.args.get('classification_id', '1') - - # 模拟行业层级数据 - levels = [ - { - 'id': 1, - 'name': '农林牧渔', - 'code': '801010', - 'description': '农业、林业、畜牧业、渔业', - 'stock_count': 45, - 'avg_change': 1.2, - 'total_market_cap': 500000000000, - 'sub_industries': [ - {'id': 101, 'name': '种植业', 'stock_count': 20}, - {'id': 102, 'name': '林业', 'stock_count': 8}, - {'id': 103, 'name': '畜牧业', 'stock_count': 12}, - {'id': 104, 'name': '渔业', 'stock_count': 5} - ] - }, - { - 'id': 2, - 'name': '采掘', - 'code': '801020', - 'description': '煤炭、石油、天然气、有色金属矿采选', - 'stock_count': 38, - 'avg_change': 0.8, - 'total_market_cap': 800000000000, - 'sub_industries': [ - {'id': 201, 'name': '煤炭开采', 'stock_count': 15}, - {'id': 202, 'name': '石油开采', 'stock_count': 8}, - {'id': 203, 'name': '有色金属矿采选', 'stock_count': 15} - ] - }, - { - 'id': 3, - 'name': '化工', - 'code': '801030', - 'description': '化学原料、化学制品、化学纤维', - 'stock_count': 156, - 'avg_change': 1.5, - 'total_market_cap': 1200000000000, - 'sub_industries': [ - {'id': 301, 'name': '化学原料', 'stock_count': 45}, - {'id': 302, 'name': '化学制品', 'stock_count': 78}, - {'id': 303, 'name': '化学纤维', 'stock_count': 33} - ] - }, - { - 'id': 4, - 'name': '钢铁', - 'code': '801040', - 'description': '钢铁冶炼、钢铁制品', - 'stock_count': 32, - 'avg_change': 0.6, - 'total_market_cap': 600000000000, - 'sub_industries': [ - {'id': 401, 'name': '钢铁冶炼', 'stock_count': 18}, - {'id': 402, 'name': '钢铁制品', 'stock_count': 14} - ] - }, - { - 'id': 5, - 'name': '有色金属', - 'code': '801050', - 'description': '有色金属冶炼、有色金属制品', - 'stock_count': 67, - 'avg_change': 1.8, - 'total_market_cap': 900000000000, - 'sub_industries': [ - {'id': 501, 'name': '有色金属冶炼', 'stock_count': 35}, - {'id': 502, 'name': '有色金属制品', 'stock_count': 32} - ] - }, - { - 'id': 6, - 'name': '建筑材料', - 'code': '801060', - 'description': '水泥、玻璃、陶瓷、其他建材', - 'stock_count': 89, - 'avg_change': 1.1, - 'total_market_cap': 700000000000, - 'sub_industries': [ - {'id': 601, 'name': '水泥', 'stock_count': 25}, - {'id': 602, 'name': '玻璃', 'stock_count': 18}, - {'id': 603, 'name': '陶瓷', 'stock_count': 12}, - {'id': 604, 'name': '其他建材', 'stock_count': 34} - ] - }, - { - 'id': 7, - 'name': '建筑装饰', - 'code': '801070', - 'description': '房屋建设、装修装饰、园林工程', - 'stock_count': 45, - 'avg_change': 0.9, - 'total_market_cap': 400000000000, - 'sub_industries': [ - {'id': 701, 'name': '房屋建设', 'stock_count': 15}, - {'id': 702, 'name': '装修装饰', 'stock_count': 20}, - {'id': 703, 'name': '园林工程', 'stock_count': 10} - ] - }, - { - 'id': 8, - 'name': '电气设备', - 'code': '801080', - 'description': '电机、电气自动化设备、电源设备', - 'stock_count': 134, - 'avg_change': 2.1, - 'total_market_cap': 1500000000000, - 'sub_industries': [ - {'id': 801, 'name': '电机', 'stock_count': 25}, - {'id': 802, 'name': '电气自动化设备', 'stock_count': 45}, - {'id': 803, 'name': '电源设备', 'stock_count': 64} - ] - }, - { - 'id': 9, - 'name': '国防军工', - 'code': '801090', - 'description': '航天装备、航空装备、地面兵装', - 'stock_count': 28, - 'avg_change': 1.6, - 'total_market_cap': 300000000000, - 'sub_industries': [ - {'id': 901, 'name': '航天装备', 'stock_count': 8}, - {'id': 902, 'name': '航空装备', 'stock_count': 12}, - {'id': 903, 'name': '地面兵装', 'stock_count': 8} - ] - }, - { - 'id': 10, - 'name': '汽车', - 'code': '801100', - 'description': '汽车整车、汽车零部件', - 'stock_count': 78, - 'avg_change': 1.3, - 'total_market_cap': 1100000000000, - 'sub_industries': [ - {'id': 1001, 'name': '汽车整车', 'stock_count': 25}, - {'id': 1002, 'name': '汽车零部件', 'stock_count': 53} - ] - }, - { - 'id': 11, - 'name': '家用电器', - 'code': '801110', - 'description': '白色家电、小家电、家电零部件', - 'stock_count': 56, - 'avg_change': 1.0, - 'total_market_cap': 800000000000, - 'sub_industries': [ - {'id': 1101, 'name': '白色家电', 'stock_count': 20}, - {'id': 1102, 'name': '小家电', 'stock_count': 18}, - {'id': 1103, 'name': '家电零部件', 'stock_count': 18} - ] - }, - { - 'id': 12, - 'name': '纺织服装', - 'code': '801120', - 'description': '纺织制造、服装家纺', - 'stock_count': 67, - 'avg_change': 0.7, - 'total_market_cap': 500000000000, - 'sub_industries': [ - {'id': 1201, 'name': '纺织制造', 'stock_count': 35}, - {'id': 1202, 'name': '服装家纺', 'stock_count': 32} - ] - }, - { - 'id': 13, - 'name': '轻工制造', - 'code': '801130', - 'description': '造纸、包装印刷、家用轻工', - 'stock_count': 89, - 'avg_change': 0.9, - 'total_market_cap': 600000000000, - 'sub_industries': [ - {'id': 1301, 'name': '造纸', 'stock_count': 25}, - {'id': 1302, 'name': '包装印刷', 'stock_count': 30}, - {'id': 1303, 'name': '家用轻工', 'stock_count': 34} - ] - }, - { - 'id': 14, - 'name': '医药生物', - 'code': '801140', - 'description': '化学制药、中药、生物制品、医疗器械', - 'stock_count': 234, - 'avg_change': 1.9, - 'total_market_cap': 2500000000000, - 'sub_industries': [ - {'id': 1401, 'name': '化学制药', 'stock_count': 78}, - {'id': 1402, 'name': '中药', 'stock_count': 45}, - {'id': 1403, 'name': '生物制品', 'stock_count': 56}, - {'id': 1404, 'name': '医疗器械', 'stock_count': 55} - ] - }, - { - 'id': 15, - 'name': '公用事业', - 'code': '801150', - 'description': '电力、燃气、水务', - 'stock_count': 78, - 'avg_change': 0.5, - 'total_market_cap': 900000000000, - 'sub_industries': [ - {'id': 1501, 'name': '电力', 'stock_count': 45}, - {'id': 1502, 'name': '燃气', 'stock_count': 18}, - {'id': 1503, 'name': '水务', 'stock_count': 15} - ] - }, - { - 'id': 16, - 'name': '交通运输', - 'code': '801160', - 'description': '港口、公路、铁路、航空', - 'stock_count': 67, - 'avg_change': 0.8, - 'total_market_cap': 800000000000, - 'sub_industries': [ - {'id': 1601, 'name': '港口', 'stock_count': 15}, - {'id': 1602, 'name': '公路', 'stock_count': 20}, - {'id': 1603, 'name': '铁路', 'stock_count': 12}, - {'id': 1604, 'name': '航空', 'stock_count': 20} - ] - }, - { - 'id': 17, - 'name': '房地产', - 'code': '801170', - 'description': '房地产开发、房地产服务', - 'stock_count': 89, - 'avg_change': 0.6, - 'total_market_cap': 1200000000000, - 'sub_industries': [ - {'id': 1701, 'name': '房地产开发', 'stock_count': 65}, - {'id': 1702, 'name': '房地产服务', 'stock_count': 24} - ] - }, - { - 'id': 18, - 'name': '商业贸易', - 'code': '801180', - 'description': '贸易、零售', - 'stock_count': 78, - 'avg_change': 0.7, - 'total_market_cap': 600000000000, - 'sub_industries': [ - {'id': 1801, 'name': '贸易', 'stock_count': 35}, - {'id': 1802, 'name': '零售', 'stock_count': 43} - ] - }, - { - 'id': 19, - 'name': '休闲服务', - 'code': '801190', - 'description': '景点、酒店、旅游综合', - 'stock_count': 34, - 'avg_change': 1.2, - 'total_market_cap': 300000000000, - 'sub_industries': [ - {'id': 1901, 'name': '景点', 'stock_count': 12}, - {'id': 1902, 'name': '酒店', 'stock_count': 15}, - {'id': 1903, 'name': '旅游综合', 'stock_count': 7} - ] - }, - { - 'id': 20, - 'name': '银行', - 'code': '801200', - 'description': '银行', - 'stock_count': 28, - 'avg_change': 0.4, - 'total_market_cap': 8000000000000, - 'sub_industries': [ - {'id': 2001, 'name': '银行', 'stock_count': 28} - ] - }, - { - 'id': 21, - 'name': '非银金融', - 'code': '801210', - 'description': '保险、证券、多元金融', - 'stock_count': 45, - 'avg_change': 0.8, - 'total_market_cap': 2000000000000, - 'sub_industries': [ - {'id': 2101, 'name': '保险', 'stock_count': 8}, - {'id': 2102, 'name': '证券', 'stock_count': 25}, - {'id': 2103, 'name': '多元金融', 'stock_count': 12} - ] - }, - { - 'id': 22, - 'name': '综合', - 'code': '801220', - 'description': '综合', - 'stock_count': 23, - 'avg_change': 0.6, - 'total_market_cap': 200000000000, - 'sub_industries': [ - {'id': 2201, 'name': '综合', 'stock_count': 23} - ] - }, - { - 'id': 23, - 'name': '计算机', - 'code': '801230', - 'description': '计算机设备、计算机应用', - 'stock_count': 156, - 'avg_change': 2.3, - 'total_market_cap': 1800000000000, - 'sub_industries': [ - {'id': 2301, 'name': '计算机设备', 'stock_count': 45}, - {'id': 2302, 'name': '计算机应用', 'stock_count': 111} - ] - }, - { - 'id': 24, - 'name': '传媒', - 'code': '801240', - 'description': '文化传媒、营销传播', - 'stock_count': 78, - 'avg_change': 1.4, - 'total_market_cap': 700000000000, - 'sub_industries': [ - {'id': 2401, 'name': '文化传媒', 'stock_count': 45}, - {'id': 2402, 'name': '营销传播', 'stock_count': 33} - ] - }, - { - 'id': 25, - 'name': '通信', - 'code': '801250', - 'description': '通信设备、通信运营', - 'stock_count': 45, - 'avg_change': 1.7, - 'total_market_cap': 600000000000, - 'sub_industries': [ - {'id': 2501, 'name': '通信设备', 'stock_count': 30}, - {'id': 2502, 'name': '通信运营', 'stock_count': 15} - ] - }, - { - 'id': 26, - 'name': '电子', - 'code': '801260', - 'description': '半导体、电子制造、光学光电子', - 'stock_count': 178, - 'avg_change': 2.0, - 'total_market_cap': 2000000000000, - 'sub_industries': [ - {'id': 2601, 'name': '半导体', 'stock_count': 45}, - {'id': 2602, 'name': '电子制造', 'stock_count': 78}, - {'id': 2603, 'name': '光学光电子', 'stock_count': 55} - ] - }, - { - 'id': 27, - 'name': '机械设备', - 'code': '801270', - 'description': '通用机械、专用设备、仪器仪表', - 'stock_count': 234, - 'avg_change': 1.1, - 'total_market_cap': 1500000000000, - 'sub_industries': [ - {'id': 2701, 'name': '通用机械', 'stock_count': 89}, - {'id': 2702, 'name': '专用设备', 'stock_count': 98}, - {'id': 2703, 'name': '仪器仪表', 'stock_count': 47} - ] - }, - { - 'id': 28, - 'name': '食品饮料', - 'code': '801280', - 'description': '食品加工、饮料制造', - 'stock_count': 67, - 'avg_change': 1.3, - 'total_market_cap': 1000000000000, - 'sub_industries': [ - {'id': 2801, 'name': '食品加工', 'stock_count': 35}, - {'id': 2802, 'name': '饮料制造', 'stock_count': 32} - ] - } - ] - - return jsonify({ - 'success': True, - 'data': levels - }) - - except Exception as e: - print(f"Error getting industry levels: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/info', methods=['GET']) -def get_industry_info(): - """获取行业信息""" - try: - industry_id = request.args.get('industry_id') - - if not industry_id: - return jsonify({'success': False, 'error': '请提供行业ID'}), 400 - - # 模拟行业信息 - industry_info = { - 'id': industry_id, - 'name': f'行业{industry_id}', - 'code': f'801{industry_id.zfill(3)}', - 'description': f'这是行业{industry_id}的详细描述', - 'stock_count': 50, - 'avg_change': 1.5, - 'total_market_cap': 800000000000, - 'pe_ratio': 15.6, - 'pb_ratio': 2.3, - 'roe': 8.5, - 'top_stocks': [ - {'code': '000001', 'name': '龙头股A', 'weight': 0.15}, - {'code': '000002', 'name': '龙头股B', 'weight': 0.12}, - {'code': '000003', 'name': '龙头股C', 'weight': 0.10} - ], - 'sub_industries': [ - {'id': 1, 'name': '子行业A', 'stock_count': 20}, - {'id': 2, 'name': '子行业B', 'stock_count': 18}, - {'id': 3, 'name': '子行业C', 'stock_count': 12} - ], - 'performance': { - 'daily': 1.5, - 'weekly': 3.2, - 'monthly': 8.5, - 'quarterly': 12.3, - 'yearly': 25.6 - }, - 'trend': { - 'direction': 'up', - 'strength': 'medium', - 'duration': '3 months' - } - } - - return jsonify({ - 'success': True, - 'data': industry_info - }) - - except Exception as e: - print(f"Error getting industry info: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 \ No newline at end of file diff --git a/app/routes/limitanalyse.py b/app/routes/limitanalyse.py deleted file mode 100644 index 6f400b45..00000000 --- a/app/routes/limitanalyse.py +++ /dev/null @@ -1,469 +0,0 @@ -from flask import Blueprint, request, jsonify -import pandas as pd -import json -from datetime import datetime - -bp = Blueprint('limitanalyse', __name__, url_prefix='/api/limit-analyse') - -@bp.route('/available-dates', methods=['GET']) -def get_available_dates(): - """获取可用日期列表""" - try: - # 模拟可用日期 - dates = [ - '2025-07-16', - '2025-07-15', - '2025-07-14', - '2025-07-11', - '2025-07-10' - ] - - return jsonify({ - 'success': True, - 'data': dates - }) - except Exception as e: - print(f"Error getting available dates: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -def load_stock_data(datestr): - """加载股票数据""" - try: - # 模拟股票数据 - data = [] - for i in range(100): - data.append({ - 'code': f'00000{i:03d}', - 'name': f'股票{i}', - 'price': 10.0 + i * 0.1, - 'change': (i % 10 - 5) * 0.5, - 'sector': f'板块{i % 5}', - 'limit_type': '涨停' if i % 10 == 0 else '正常', - 'volume': 1000000 + i * 50000, - 'amount': 10000000 + i * 500000 - }) - - return pd.DataFrame(data) - except Exception as e: - print(f"Error loading stock data: {e}") - return pd.DataFrame() - -@bp.route('/data', methods=['GET']) -def get_analysis_data(): - """获取分析数据""" - try: - date = request.args.get('date', '2025-07-16') - - # 加载数据 - df = load_stock_data(date) - if df.empty: - return jsonify({'success': False, 'error': '数据加载失败'}), 500 - - # 统计信息 - total_stocks = len(df) - limit_up_stocks = len(df[df['limit_type'] == '涨停']) - limit_down_stocks = len(df[df['limit_type'] == '跌停']) - - # 板块统计 - sector_stats = df.groupby('sector').agg({ - 'code': 'count', - 'change': 'mean', - 'volume': 'sum' - }).reset_index() - - sector_data = [] - for _, row in sector_stats.iterrows(): - sector_data.append({ - 'sector': row['sector'], - 'stock_count': int(row['code']), - 'avg_change': round(row['change'], 2), - 'total_volume': int(row['volume']) - }) - - return jsonify({ - 'success': True, - 'data': { - 'date': date, - 'total_stocks': total_stocks, - 'limit_up_stocks': limit_up_stocks, - 'limit_down_stocks': limit_down_stocks, - 'sector_stats': sector_data - } - }) - - except Exception as e: - print(f"Error getting analysis data: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/sector-data', methods=['GET']) -def get_sector_data(): - """获取板块数据""" - try: - date = request.args.get('date', '2025-07-16') - - # 加载数据 - df = load_stock_data(date) - if df.empty: - return jsonify({'success': False, 'error': '数据加载失败'}), 500 - - # 板块统计 - sector_stats = df.groupby('sector').agg({ - 'code': 'count', - 'change': 'mean', - 'volume': 'sum', - 'amount': 'sum' - }).reset_index() - - sector_data = [] - for _, row in sector_stats.iterrows(): - sector_data.append({ - 'sector': row['sector'], - 'stock_count': int(row['code']), - 'avg_change': round(row['change'], 2), - 'total_volume': int(row['volume']), - 'total_amount': int(row['amount']) - }) - - return jsonify({ - 'success': True, - 'data': sector_data - }) - - except Exception as e: - print(f"Error getting sector data: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/word-cloud', methods=['GET']) -def get_word_cloud_data(): - """获取词云数据""" - try: - date = request.args.get('date', '2025-07-16') - - # 模拟词云数据 - word_data = [ - {'word': '科技', 'value': 100}, - {'word': '新能源', 'value': 85}, - {'word': '医药', 'value': 70}, - {'word': '消费', 'value': 65}, - {'word': '金融', 'value': 50}, - {'word': '地产', 'value': 45}, - {'word': '制造', 'value': 40}, - {'word': '农业', 'value': 35}, - {'word': '传媒', 'value': 30}, - {'word': '环保', 'value': 25} - ] - - return jsonify({ - 'success': True, - 'data': word_data - }) - - except Exception as e: - print(f"Error getting word cloud data: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/chart-data', methods=['GET']) -def get_chart_data(): - """获取图表数据""" - try: - date = request.args.get('date', '2025-07-16') - - # 模拟图表数据 - chart_data = { - 'limit_up_distribution': [ - {'sector': '科技', 'count': 15}, - {'sector': '新能源', 'count': 12}, - {'sector': '医药', 'count': 10}, - {'sector': '消费', 'count': 8}, - {'sector': '金融', 'count': 6} - ], - 'sector_performance': [ - {'sector': '科技', 'change': 2.5}, - {'sector': '新能源', 'change': 1.8}, - {'sector': '医药', 'change': 1.2}, - {'sector': '消费', 'change': 0.8}, - {'sector': '金融', 'change': 0.5} - ] - } - - return jsonify({ - 'success': True, - 'data': chart_data - }) - - except Exception as e: - print(f"Error getting chart data: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/stock-details', methods=['GET']) -def get_stock_details(): - """获取股票详情""" - try: - code = request.args.get('code') - date = request.args.get('date', '2025-07-16') - - if not code: - return jsonify({'success': False, 'error': '请提供股票代码'}), 400 - - # 模拟股票详情 - stock_detail = { - 'code': code, - 'name': f'股票{code}', - 'price': 15.50, - 'change': 2.5, - 'sector': '科技', - 'volume': 1500000, - 'amount': 23250000, - 'limit_type': '涨停', - 'turnover_rate': 3.2, - 'market_cap': 15500000000 - } - - return jsonify({ - 'success': True, - 'data': stock_detail - }) - - except Exception as e: - print(f"Error getting stock details: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/sector-analysis', methods=['GET']) -def get_sector_analysis(): - """获取板块分析""" - try: - sector = request.args.get('sector') - date = request.args.get('date', '2025-07-16') - - if not sector: - return jsonify({'success': False, 'error': '请提供板块名称'}), 400 - - # 模拟板块分析数据 - sector_analysis = { - 'sector': sector, - 'stock_count': 25, - 'avg_change': 1.8, - 'limit_up_count': 8, - 'limit_down_count': 2, - 'total_volume': 50000000, - 'total_amount': 750000000, - 'top_stocks': [ - {'code': '000001', 'name': '股票A', 'change': 10.0}, - {'code': '000002', 'name': '股票B', 'change': 9.5}, - {'code': '000003', 'name': '股票C', 'change': 8.8} - ] - } - - return jsonify({ - 'success': True, - 'data': sector_analysis - }) - - except Exception as e: - print(f"Error getting sector analysis: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/trend-analysis', methods=['GET']) -def get_trend_analysis(): - """获取趋势分析""" - try: - date = request.args.get('date', '2025-07-16') - - # 模拟趋势分析数据 - trend_data = { - 'limit_up_trend': [ - {'date': '2025-07-10', 'count': 45}, - {'date': '2025-07-11', 'count': 52}, - {'date': '2025-07-14', 'count': 48}, - {'date': '2025-07-15', 'count': 55}, - {'date': '2025-07-16', 'count': 51} - ], - 'sector_trend': [ - {'sector': '科技', 'trend': 'up'}, - {'sector': '新能源', 'trend': 'up'}, - {'sector': '医药', 'trend': 'stable'}, - {'sector': '消费', 'trend': 'down'}, - {'sector': '金融', 'trend': 'stable'} - ] - } - - return jsonify({ - 'success': True, - 'data': trend_data - }) - - except Exception as e: - print(f"Error getting trend analysis: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/heat-map', methods=['GET']) -def get_heat_map_data(): - """获取热力图数据""" - try: - date = request.args.get('date', '2025-07-16') - - # 模拟热力图数据 - heat_map_data = [] - sectors = ['科技', '新能源', '医药', '消费', '金融', '地产', '制造', '农业'] - - for i, sector in enumerate(sectors): - for j in range(8): - heat_map_data.append({ - 'sector': sector, - 'metric': f'指标{j+1}', - 'value': (i + j) % 10 + 1 - }) - - return jsonify({ - 'success': True, - 'data': heat_map_data - }) - - except Exception as e: - print(f"Error getting heat map data: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/correlation-analysis', methods=['GET']) -def get_correlation_analysis(): - """获取相关性分析""" - try: - date = request.args.get('date', '2025-07-16') - - # 模拟相关性分析数据 - correlation_data = { - 'sector_correlations': [ - {'sector1': '科技', 'sector2': '新能源', 'correlation': 0.85}, - {'sector1': '医药', 'sector2': '消费', 'correlation': 0.72}, - {'sector1': '金融', 'sector2': '地产', 'correlation': 0.68}, - {'sector1': '科技', 'sector2': '医药', 'correlation': 0.45}, - {'sector1': '新能源', 'sector2': '制造', 'correlation': 0.78} - ], - 'stock_correlations': [ - {'stock1': '000001', 'stock2': '000002', 'correlation': 0.92}, - {'stock1': '000003', 'stock2': '000004', 'correlation': 0.88}, - {'stock1': '000005', 'stock2': '000006', 'correlation': 0.76} - ] - } - - return jsonify({ - 'success': True, - 'data': correlation_data - }) - - except Exception as e: - print(f"Error getting correlation analysis: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/export-data', methods=['POST']) -def export_data(): - """导出数据""" - try: - data = request.get_json() - date = data.get('date', '2025-07-16') - export_type = data.get('type', 'excel') - - # 模拟导出 - filename = f'limit_analyse_{date}.{export_type}' - - return jsonify({ - 'success': True, - 'message': '数据导出成功', - 'data': { - 'filename': filename, - 'download_url': f'/downloads/{filename}' - } - }) - - except Exception as e: - print(f"Error exporting data: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/high-position-stocks', methods=['GET']) -def get_high_position_stocks(): - """获取高位股统计数据""" - try: - date = request.args.get('date', datetime.now().strftime('%Y%m%d')) - - # 模拟高位股数据 - 实际使用时需要连接真实的数据库 - # 根据用户提供的表结构,查询连续涨停天数较多的股票 - high_position_stocks = [ - { - 'stock_code': '000001', - 'stock_name': '平安银行', - 'price': 15.68, - 'increase_rate': 10.02, - 'limit_up_days': 5, - 'continuous_limit_up': 3, - 'industry': '银行', - 'turnover_rate': 3.45, - 'market_cap': 32000000000 - }, - { - 'stock_code': '000002', - 'stock_name': '万科A', - 'price': 18.92, - 'increase_rate': 9.98, - 'limit_up_days': 4, - 'continuous_limit_up': 2, - 'industry': '房地产', - 'turnover_rate': 5.67, - 'market_cap': 21000000000 - }, - { - 'stock_code': '600036', - 'stock_name': '招商银行', - 'price': 42.15, - 'increase_rate': 8.45, - 'limit_up_days': 6, - 'continuous_limit_up': 4, - 'industry': '银行', - 'turnover_rate': 2.89, - 'market_cap': 105000000000 - }, - { - 'stock_code': '000858', - 'stock_name': '五粮液', - 'price': 168.50, - 'increase_rate': 7.23, - 'limit_up_days': 3, - 'continuous_limit_up': 2, - 'industry': '白酒', - 'turnover_rate': 1.56, - 'market_cap': 650000000000 - }, - { - 'stock_code': '002415', - 'stock_name': '海康威视', - 'price': 35.68, - 'increase_rate': 6.89, - 'limit_up_days': 4, - 'continuous_limit_up': 3, - 'industry': '安防', - 'turnover_rate': 4.12, - 'market_cap': 33000000000 - } - ] - - # 统计信息 - total_count = len(high_position_stocks) - avg_continuous_days = sum(stock['continuous_limit_up'] for stock in high_position_stocks) / total_count if total_count > 0 else 0 - - # 按连续涨停天数排序 - high_position_stocks.sort(key=lambda x: x['continuous_limit_up'], reverse=True) - - return jsonify({ - 'success': True, - 'data': { - 'stocks': high_position_stocks, - 'statistics': { - 'total_count': total_count, - 'avg_continuous_days': round(avg_continuous_days, 2), - 'max_continuous_days': max([stock['continuous_limit_up'] for stock in high_position_stocks], default=0), - 'industry_distribution': {} - } - } - }) - - except Exception as e: - print(f"Error getting high position stocks: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 \ No newline at end of file diff --git a/app/routes/stocks.py b/app/routes/stocks.py deleted file mode 100644 index 54baebfc..00000000 --- a/app/routes/stocks.py +++ /dev/null @@ -1,241 +0,0 @@ -from flask import Blueprint, request, jsonify -from app import db -from clickhouse_driver import Client -import pandas as pd -from datetime import datetime, timedelta -import pytz - -bp = Blueprint('stocks', __name__, url_prefix='/api/stock') - -def get_clickhouse_client(): - """获取ClickHouse客户端""" - return Client('localhost', port=9000, user='default', password='', database='default') - -@bp.route('/quotes', methods=['GET', 'POST']) -def get_stock_quotes(): - """获取股票实时报价""" - try: - if request.method == 'GET': - # GET 请求从 URL 参数获取数据 - codes = request.args.get('codes', '').split(',') - event_time_str = request.args.get('event_time') - else: - # POST 请求从 JSON 获取数据 - codes = request.json.get('codes', []) - event_time_str = request.json.get('event_time') - - if not codes: - return jsonify({'success': False, 'error': '请提供股票代码'}), 400 - - # 过滤空字符串 - codes = [code.strip() for code in codes if code.strip()] - - if not codes: - return jsonify({'success': False, 'error': '请提供有效的股票代码'}), 400 - - # 解析事件时间 - event_time = None - if event_time_str: - try: - event_time = datetime.fromisoformat(event_time_str.replace('Z', '+00:00')) - except ValueError: - return jsonify({'success': False, 'error': '事件时间格式错误'}), 400 - - # 获取当前时间 - now = datetime.now(pytz.timezone('Asia/Shanghai')) - - # 如果提供了事件时间,使用事件时间;否则使用当前时间 - target_time = event_time if event_time else now - - # 获取交易日和交易时间 - def get_trading_day_and_times(event_datetime): - """获取交易日和交易时间列表""" - # 这里简化处理,实际应该查询交易日历 - trading_day = event_datetime.strftime('%Y-%m-%d') - - # 生成交易时间列表 (9:30-11:30, 13:00-15:00) - morning_times = [f"{trading_day} {hour:02d}:{minute:02d}" - for hour in range(9, 12) - for minute in range(0, 60, 1) - if not (hour == 9 and minute < 30) and not (hour == 11 and minute > 30)] - - afternoon_times = [f"{trading_day} {hour:02d}:{minute:02d}" - for hour in range(13, 16) - for minute in range(0, 60, 1)] - - return trading_day, morning_times + afternoon_times - - trading_day, trading_times = get_trading_day_and_times(target_time) - - # 模拟股票数据 - results = {} - for code in codes: - # 这里应该从ClickHouse或其他数据源获取真实数据 - # 现在使用模拟数据 - import random - base_price = 10.0 + random.random() * 20.0 - change = (random.random() - 0.5) * 2.0 - - results[code] = { - 'price': round(base_price, 2), - 'change': round(change, 2), - 'name': f'股票{code}' - } - - return jsonify({ - 'success': True, - 'data': results - }) - - except Exception as e: - print(f"Error getting stock quotes: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('//kline') -def get_stock_kline(stock_code): - """获取股票K线数据""" - try: - chart_type = request.args.get('type', 'daily') - event_time_str = request.args.get('event_time') - - if not event_time_str: - return jsonify({'success': False, 'error': '请提供事件时间'}), 400 - - try: - event_datetime = datetime.fromisoformat(event_time_str.replace('Z', '+00:00')) - except ValueError: - return jsonify({'success': False, 'error': '事件时间格式错误'}), 400 - - # 获取股票名称(这里简化处理) - stock_name = f'股票{stock_code}' - - if chart_type == 'daily': - return get_daily_kline(stock_code, event_datetime, stock_name) - elif chart_type == 'minute': - return get_minute_kline(stock_code, event_datetime, stock_name) - elif chart_type == 'timeline': - return get_timeline_data(stock_code, event_datetime, stock_name) - else: - return jsonify({'error': f'Unsupported chart type: {chart_type}'}), 400 - - except Exception as e: - print(f"Error getting stock kline: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -def get_daily_kline(stock_code, event_datetime, stock_name): - """获取日K线数据""" - try: - # 模拟日K线数据 - data = [] - base_price = 10.0 - for i in range(30): - date = (event_datetime - timedelta(days=30-i)).strftime('%Y-%m-%d') - open_price = base_price + (i * 0.1) + (i % 3 - 1) * 0.5 - close_price = open_price + (i % 5 - 2) * 0.3 - high_price = max(open_price, close_price) + 0.2 - low_price = min(open_price, close_price) - 0.2 - volume = 1000000 + i * 50000 - - data.append({ - 'date': date, - 'open': round(open_price, 2), - 'close': round(close_price, 2), - 'high': round(high_price, 2), - 'low': round(low_price, 2), - 'volume': volume - }) - - return jsonify({ - 'code': stock_code, - 'name': stock_name, - 'trade_date': event_datetime.strftime('%Y-%m-%d'), - 'data': data - }) - - except Exception as e: - print(f"Error getting daily kline: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -def get_minute_kline(stock_code, event_datetime, stock_name): - """获取分钟K线数据""" - try: - # 模拟分钟K线数据 - data = [] - base_price = 10.0 - trading_times = [] - - # 生成交易时间 - for hour in range(9, 16): - if hour == 12: - continue - for minute in range(0, 60): - if (hour == 9 and minute < 30) or (hour == 11 and minute > 30): - continue - trading_times.append(f"{hour:02d}:{minute:02d}") - - for i, time in enumerate(trading_times): - open_price = base_price + (i * 0.01) + (i % 10 - 5) * 0.02 - close_price = open_price + (i % 7 - 3) * 0.01 - high_price = max(open_price, close_price) + 0.01 - low_price = min(open_price, close_price) - 0.01 - volume = 50000 + i * 1000 - - data.append({ - 'time': time, - 'open': round(open_price, 2), - 'close': round(close_price, 2), - 'high': round(high_price, 2), - 'low': round(low_price, 2), - 'volume': volume - }) - - return jsonify({ - 'code': stock_code, - 'name': stock_name, - 'trade_date': event_datetime.strftime('%Y-%m-%d'), - 'data': data - }) - - except Exception as e: - print(f"Error getting minute kline: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 - -def get_timeline_data(stock_code, event_datetime, stock_name): - """获取分时图数据""" - try: - # 模拟分时图数据 - data = [] - base_price = 10.0 - trading_times = [] - - # 生成交易时间 - for hour in range(9, 16): - if hour == 12: - continue - for minute in range(0, 60): - if (hour == 9 and minute < 30) or (hour == 11 and minute > 30): - continue - trading_times.append(f"{hour:02d}:{minute:02d}") - - for i, time in enumerate(trading_times): - price = base_price + (i * 0.01) + (i % 10 - 5) * 0.02 - avg_price = price + (i % 5 - 2) * 0.01 - volume = 50000 + i * 1000 - - data.append({ - 'time': time, - 'price': round(price, 2), - 'avg_price': round(avg_price, 2), - 'volume': volume - }) - - return jsonify({ - 'code': stock_code, - 'name': stock_name, - 'trade_date': event_datetime.strftime('%Y-%m-%d'), - 'data': data - }) - - except Exception as e: - print(f"Error getting timeline data: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 \ No newline at end of file diff --git a/src/hooks/useEventNotifications.js b/src/hooks/useEventNotifications.js index 9b99a039..876c27c9 100644 --- a/src/hooks/useEventNotifications.js +++ b/src/hooks/useEventNotifications.js @@ -68,23 +68,44 @@ export const useEventNotifications = (options = {}) => { // 新事件处理函数 const handleNewEvent = (eventData) => { + console.log('\n[useEventNotifications DEBUG] ========== Hook 收到新事件 =========='); + console.log('[useEventNotifications DEBUG] 事件数据:', eventData); + console.log('[useEventNotifications DEBUG] 事件 ID:', eventData?.id); + console.log('[useEventNotifications DEBUG] 事件标题:', eventData?.title); + + console.log('[useEventNotifications DEBUG] 设置 newEvent 状态'); setNewEvent(eventData); + console.log('[useEventNotifications DEBUG] ✓ newEvent 状态已更新'); // 调用外部回调 if (onNewEvent) { + console.log('[useEventNotifications DEBUG] 准备调用外部 onNewEvent 回调'); onNewEvent(eventData); + console.log('[useEventNotifications DEBUG] ✓ 外部 onNewEvent 回调已调用'); + } else { + console.log('[useEventNotifications DEBUG] ⚠️ 没有外部 onNewEvent 回调'); } + + console.log('[useEventNotifications DEBUG] ========== Hook 事件处理完成 ==========\n'); }; // 订阅事件推送 + console.log('\n[useEventNotifications DEBUG] ========== 开始订阅事件 =========='); + console.log('[useEventNotifications DEBUG] eventType:', eventType); + console.log('[useEventNotifications DEBUG] importance:', importance); + console.log('[useEventNotifications DEBUG] enabled:', enabled); + socketService.subscribeToEvents({ eventType, importance, onNewEvent: handleNewEvent, onSubscribed: (data) => { - console.log('订阅成功:', data); + console.log('\n[useEventNotifications DEBUG] ========== 订阅成功回调 =========='); + console.log('[useEventNotifications DEBUG] 订阅数据:', data); + console.log('[useEventNotifications DEBUG] ========== 订阅成功处理完成 ==========\n'); }, }); + console.log('[useEventNotifications DEBUG] ========== 订阅请求已发送 ==========\n'); // 保存取消订阅函数 unsubscribeRef.current = () => { diff --git a/src/services/socketService.js b/src/services/socketService.js index a41218c1..b6a39b6e 100644 --- a/src/services/socketService.js +++ b/src/services/socketService.js @@ -301,35 +301,64 @@ class SocketService { * 执行订阅操作(内部方法) */ _doSubscribe(eventType, importance, onNewEvent, onSubscribed) { + console.log('\n========== [SocketService DEBUG] 开始订阅 =========='); + console.log('[SocketService DEBUG] 事件类型:', eventType); + console.log('[SocketService DEBUG] 重要性:', importance); + console.log('[SocketService DEBUG] Socket 连接状态:', this.connected); + console.log('[SocketService DEBUG] Socket ID:', this.socket?.id); + // 发送订阅请求 - this.emit('subscribe_events', { + const subscribeData = { event_type: eventType, importance: importance, - }); + }; + console.log('[SocketService DEBUG] 准备发送 subscribe_events:', subscribeData); + this.emit('subscribe_events', subscribeData); + console.log('[SocketService DEBUG] ✓ 已发送 subscribe_events'); // 监听订阅确认 this.socket.once('subscription_confirmed', (data) => { + console.log('\n[SocketService DEBUG] ========== 收到订阅确认 =========='); + console.log('[SocketService DEBUG] 订阅确认数据:', data); logger.info('socketService', 'Subscription confirmed', data); if (onSubscribed) { + console.log('[SocketService DEBUG] 调用 onSubscribed 回调'); onSubscribed(data); } + console.log('[SocketService DEBUG] ========== 订阅确认处理完成 ==========\n'); }); // 监听订阅错误 this.socket.once('subscription_error', (error) => { + console.error('\n[SocketService ERROR] ========== 订阅错误 =========='); + console.error('[SocketService ERROR] 错误信息:', error); logger.error('socketService', 'Subscription error', error); + console.error('[SocketService ERROR] ========== 订阅错误处理完成 ==========\n'); }); // 监听新事件推送 if (onNewEvent) { + console.log('[SocketService DEBUG] 设置 new_event 监听器'); // 先移除之前的监听器(避免重复) this.socket.off('new_event'); + console.log('[SocketService DEBUG] ✓ 已移除旧的 new_event 监听器'); + // 添加新的监听器 this.socket.on('new_event', (eventData) => { + console.log('\n[SocketService DEBUG] ========== 收到新事件推送 =========='); + console.log('[SocketService DEBUG] 事件数据:', eventData); + console.log('[SocketService DEBUG] 事件 ID:', eventData?.id); + console.log('[SocketService DEBUG] 事件标题:', eventData?.title); logger.info('socketService', 'New event received', eventData); + console.log('[SocketService DEBUG] 准备调用 onNewEvent 回调'); onNewEvent(eventData); + console.log('[SocketService DEBUG] ✓ onNewEvent 回调已调用'); + console.log('[SocketService DEBUG] ========== 新事件处理完成 ==========\n'); }); + console.log('[SocketService DEBUG] ✓ new_event 监听器已设置'); } + + console.log('[SocketService DEBUG] ========== 订阅完成 ==========\n'); } /** diff --git a/src/views/Community/components/EventList.js b/src/views/Community/components/EventList.js index 7736d9cb..28c8a1fa 100644 --- a/src/views/Community/components/EventList.js +++ b/src/views/Community/components/EventList.js @@ -180,8 +180,13 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai importance: 'all', enabled: true, onNewEvent: (event) => { + console.log('\n[EventList DEBUG] ========== EventList 收到新事件 =========='); + console.log('[EventList DEBUG] 事件数据:', event); + console.log('[EventList DEBUG] 事件 ID:', event?.id); + console.log('[EventList DEBUG] 事件标题:', event?.title); logger.info('EventList', '收到新事件推送', event); + console.log('[EventList DEBUG] 准备显示 Toast 通知'); // 显示 Toast 通知 toast({ title: '新事件发布', @@ -192,18 +197,28 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai position: 'top-right', variant: 'left-accent', }); + console.log('[EventList DEBUG] ✓ Toast 通知已显示'); + console.log('[EventList DEBUG] 准备更新事件列表'); // 将新事件添加到列表顶部(防止重复) setLocalEvents((prevEvents) => { + console.log('[EventList DEBUG] 当前事件列表数量:', prevEvents.length); const exists = prevEvents.some(e => e.id === event.id); + console.log('[EventList DEBUG] 事件是否已存在:', exists); if (exists) { logger.debug('EventList', '事件已存在,跳过添加', { eventId: event.id }); + console.log('[EventList DEBUG] ⚠️ 事件已存在,跳过添加'); return prevEvents; } logger.info('EventList', '新事件添加到列表顶部', { eventId: event.id }); + console.log('[EventList DEBUG] ✓ 新事件添加到列表顶部'); // 添加到顶部,最多保留 100 个 - return [event, ...prevEvents].slice(0, 100); + const updatedEvents = [event, ...prevEvents].slice(0, 100); + console.log('[EventList DEBUG] 更新后事件列表数量:', updatedEvents.length); + return updatedEvents; }); + console.log('[EventList DEBUG] ✓ 事件列表更新完成'); + console.log('[EventList DEBUG] ========== EventList 处理完成 ==========\n'); } });