From 0a28f235d3593ff87ee0bd6604135fdf40a34e1d Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Thu, 23 Oct 2025 07:40:34 +0800 Subject: [PATCH 1/4] update /api/events//stocks resp format --- app.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/app.py b/app.py index 4a188cab..d941a343 100755 --- a/app.py +++ b/app.py @@ -4023,18 +4023,31 @@ def get_related_stocks(event_id): 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, - 'retrieved_sources': stock.retrieved_sources, - 'correlation': stock.correlation, - 'momentum': stock.momentum, - 'created_at': stock.created_at.isoformat() if stock.created_at else None, - 'updated_at': stock.updated_at.isoformat() if stock.updated_at else None - }) + if stock.retrieved_sources is not None: + stocks_data.append({ + 'id': stock.id, + 'stock_code': stock.stock_code, + 'stock_name': stock.stock_name, + 'sector': stock.sector, + 'relation_desc': {"data":stock.retrieved_sources}, + 'retrieved_sources': stock.retrieved_sources, + 'correlation': stock.correlation, + 'momentum': stock.momentum, + 'created_at': stock.created_at.isoformat() if stock.created_at else None, + 'updated_at': stock.updated_at.isoformat() if stock.updated_at else None + }) + else: + 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, + 'updated_at': stock.updated_at.isoformat() if stock.updated_at else None + }) return jsonify({ 'success': True, From 0b1591c3ddeb9ee5e3ca05a76812ed8cdcd11598 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Thu, 23 Oct 2025 08:18:13 +0800 Subject: [PATCH 2/4] update /api/events//stocks resp format --- app.py | 81 ++- app/__init__.py | 45 -- app/__pycache__/__init__.cpython-310.pyc | Bin 1274 -> 0 bytes app/__pycache__/__init__.cpython-311.pyc | Bin 2483 -> 0 bytes app/__pycache__/models.cpython-311.pyc | Bin 33321 -> 0 bytes app/extensions.py | 30 - app/models.py | 504 ----------------- app/routes/__init__.py | 1 - .../__pycache__/__init__.cpython-311.pyc | Bin 194 -> 0 bytes .../__pycache__/calendar.cpython-311.pyc | Bin 6058 -> 0 bytes app/routes/__pycache__/events.cpython-311.pyc | Bin 19407 -> 0 bytes .../__pycache__/industries.cpython-311.pyc | Bin 12203 -> 0 bytes .../__pycache__/limitanalyse.cpython-311.pyc | Bin 15931 -> 0 bytes app/routes/__pycache__/stocks.cpython-311.pyc | Bin 12242 -> 0 bytes app/routes/calendar.py | 121 ----- app/routes/events.py | 385 ------------- app/routes/industries.py | 511 ------------------ app/routes/limitanalyse.py | 469 ---------------- app/routes/stocks.py | 241 --------- src/hooks/useEventNotifications.js | 23 +- src/services/socketService.js | 33 +- src/views/Community/components/EventList.js | 17 +- 22 files changed, 144 insertions(+), 2317 deletions(-) delete mode 100644 app/__init__.py delete mode 100644 app/__pycache__/__init__.cpython-310.pyc delete mode 100644 app/__pycache__/__init__.cpython-311.pyc delete mode 100644 app/__pycache__/models.cpython-311.pyc delete mode 100644 app/extensions.py delete mode 100644 app/models.py delete mode 100644 app/routes/__init__.py delete mode 100644 app/routes/__pycache__/__init__.cpython-311.pyc delete mode 100644 app/routes/__pycache__/calendar.cpython-311.pyc delete mode 100644 app/routes/__pycache__/events.cpython-311.pyc delete mode 100644 app/routes/__pycache__/industries.cpython-311.pyc delete mode 100644 app/routes/__pycache__/limitanalyse.cpython-311.pyc delete mode 100644 app/routes/__pycache__/stocks.cpython-311.pyc delete mode 100644 app/routes/calendar.py delete mode 100644 app/routes/events.py delete mode 100644 app/routes/industries.py delete mode 100644 app/routes/limitanalyse.py delete mode 100644 app/routes/stocks.py 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 6c23c104b548ec92b863dade22dd51173d7f616d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1274 zcmYjP&2Jk;6rb5$+w1i@aXy+fUtqYohXk%wgNQ03A*x2yRS=^{tF`CF9%nzy%(x*I zoYD)3P)_7lIX93HNPvpOpE6fSME?a2z?*f7GNXC>o44=dH}Ac?UJnSadq1A^fAI+U z!(MJKA209XQGH4XC!8iErHoRGoy199<`&pZytKqh1@@A1T49v}ml8kqnP1>?Qe{>2 zD<=U9fI{t*vJiqneKNx<+`n+xEdH$~jkLiUlt4IWOy-~gvn%8RGlB~tddx!!LLTti znZp*Kf;i-L9tbKZpE+|d(=2a1C2SF6%_X-UW1Pv-1$h=?g!#$x1)=04`?R! zz0;y>v71X3#YvJM1CMekdLmQf@>V<)TX$IHT7iaD2`V4SE~u{M^=jcn5EsvVJZcez zCKJl(1^t|8XX1W_U2Bt;Gn&U=e|`Pi<+E=uzdHZ(_aCm#U%Wj3bzI+3BHn8E88Y-M;5;y^svj|#L+F5RuOc^VZX=TbvV-=MN6OjrXXK`|(!1&!bfsDt} z1R~=jrKJF6{3uFekwwv{g+aErx10NMGJ@u=%rh-OHXlLU)y=EtFRq?Hz5M2<%P+rc zqSd(a)@HHA7Yl?uZn?(m=St&CbE!>)cfZ@!tPaV diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 3f4247f4d6a1d08aa507004cff05664cf3d21cea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2483 zcmbVN-D@0G6u+}G`#GCroA1pgc4Mg_E%qbWSS1zO*kWzkV7FE}28PMbZ8DqrSntdx zO>F751o}bMJOq=clBXC+u!2&-NBa-VmIap!LP1mQTcirbC(oVDZnF`J^v=EK{?57Q z+CZ#1MC^rV@6DhaR^5;mlR{YqBbzlR1l1)vxv*tT*cjqUWNuigaTI%4DLSs)9@seYdbWvW=^ zYU6SRIeyRDBN=AzwachhFNJ!2xkgy}UbgGdW(&OSq%K)184uUqMV1#e^#)o`MqO zmxyfg(*PvNbZi>19Z0NAqq=5jj$$hMw2f&ft?1ZP6+$DLspf2lXxOG9SvC~Sl;vD6 zY;xqtk>q*idh*1k>&dgs^<>%i^|^cNw^x#&H4{9r&wI36O$eRq<*6`k*$#xzA`bPd zcr-WG`zrNcWQ9jqdT7kUvi*s!lq-<1nx*xdVy_f6i?FoPNLk1#i?F|fmkPpeTI#br zr%lDcvP^l1CJkb9QUiY}KZYGDmWxwn8if69s0ev5WNS2o(+>5iqcmh!PGj+=5!hs( zVX0WRsR&qx9UOegp^-Es&cUp>%Syz`IoKvFL!_M%cC(WXMiM}1Bf(>u?LfvyA&6O` znQ-7}mio((Cpq(LMA(f?nt}Wi8h~SSI(~=r1Iy~mo`lbDbNgN}`B^^V`dv zZ6CfNUzZF1)}p_4194olM@mBA#_8*)uMN)*=Z9G!%VKZsPp0)(RrR^iHuUV-vu% diff --git a/app/__pycache__/models.cpython-311.pyc b/app/__pycache__/models.cpython-311.pyc deleted file mode 100644 index caeb96128056e441c123133958dc42c24db709ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33321 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 BSxEo@ 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 91144116e4fac57f9de7f6c60a40b5d0970c3cb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194 zcmZ3^%ge<81c7TRGo*p^V-N=h7@>^MY(U0zh7^Wi22Do4l?+8pK>lZtg diff --git a/app/routes/__pycache__/calendar.cpython-311.pyc b/app/routes/__pycache__/calendar.cpython-311.pyc deleted file mode 100644 index 935c5163e16322c4546b27426888894965b2bb84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6058 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 O3d{_+hbskLxPJp}vEdH@ diff --git a/app/routes/__pycache__/events.cpython-311.pyc b/app/routes/__pycache__/events.cpython-311.pyc deleted file mode 100644 index 1ef13cbeb51da303d420083f8b79bffb9aded16c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19407 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 o0=OHx^VvT!Xym^`Mg&;g4|0?DG0BPq9C;$Ke diff --git a/app/routes/__pycache__/industries.cpython-311.pyc b/app/routes/__pycache__/industries.cpython-311.pyc deleted file mode 100644 index d8e8b1ef42bcd8ebf4a8c014fbf8135225e925e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12203 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 Wm}fE^iV0;x2y8&2H_CZz>Hh*Sgv#vz diff --git a/app/routes/__pycache__/limitanalyse.cpython-311.pyc b/app/routes/__pycache__/limitanalyse.cpython-311.pyc deleted file mode 100644 index bd3e66f40ee520a8aef5d18a9d07dca04e71c42b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15931 zcmdU0dvF`adB4Nqzyk;p03RR;k{~Ejq9}@j2mZE--g-l@8%*SgO ziaJm66i*LOgYCm?6*M53=|C(?JuG!6BVL)bBN>FpIKf5 z4sXIw3Hz-ft4PMp)MQe=i0At0pIKisL3{Wqfwz5=dJVE~W-6%t#eDHMsr~kXeC&Do zlq}&>QjkMwUJi~W97+pvaOUOUTEf9mkV9Es4&_TYI16&9$jhN}2?tj}4(_}hs+Mpl zE6AZbFNc~X9LfuF@Z{xCyM#kUK@N3!In*!VP+5?Jmv6{3Gkr@qxOvlFs zTYCmZLnA_XC@L2Tp(CTA2xK0Q3=f45o!HY#%N!q!hN9uYkZd0a4~C<`q2R!YNNB9g z9~=q$NmaXA2d!Wu^LIIsb?rr=m1esDU@dU;$5>UW2+fv-NwaCMud;j1<*O3ERf$ zXa*jjFK>>~FHtWW`q6scY!vT74KZ3j25&Kr$`>6n^_jj#@!U9llHRMP$LM3!apr5( zF}l?{S`EzRzw^u4=e|Gt+{K$OOwFBtb#C&9v+?uu7p7X-Pi%p;f%WZcH@0_d(B#)^ z^6NDD4o!aTzBf#AQDn5QFBFN$W_Y^6v4&lOFf91`L(yn>sNa`;_CE6LxA=CyVG_#V zhGaH`4Hy0h&u>qwS-{66TX!Ar3ynm>!$UHwOa>+r6+oGZc9uIy`dVtGjw;pLzD?n->op4Z@`AJtPbdMZ+PXR|p0BqI0jj zJ@?8t4+KX>4hX}eFdYtP=hK>z6LLA+dvEr>d&$T`J&eb}FY*SEQ&igGWV`9K+2XRL zsSJs;i%!Z}m8z;uRn(+erW&pRxkWL~;y)sRqP6(XiyURIOj9OgprO~e5~;Lvng#+f z!L3Vj>m+X7HHTYV|Bs#1M}D#4tqoGu7KjPQ)}&+WrM;44yTlpepCP1KWDc2#2k!QC zWi>Tn>uz9v6zKLamn&$ykvga1hd|Rvz}+6#xe#kdJNh?zG%z>Yl0gC5yxAxp0`0Qhqc%~+oJdh zvZ@3VZy%!h6226!I5K>ACzM>-QrRS@UokXwcAs_)KAsh?#n7zujcLjKX?(tueugcS6IM%QLkO^8pZZ=FTXeY@_7Lt zBI!T(_SEc?lgg!^|9JM&rSSG12NizkeToX7`u+qJ_FX8VS~=O=H_V4*^H2~_7nfsS zNVfDH4i5E08VU79hXvV27Ef>V#0b3#pm2hf7#iYb(+DqHcf!@(0*ohA!OaQ?T&-rgh%CXVAe2K{ro%!lq!BX^ z9|WNeYaKXDFBt<@Y*5$$wYX>_WiUNq!J5lh?{$YOZocMnpNl;2dBKyWY*q&$@$Qt} z`P}_y?oU;>i1dU5|5DY>qHDrFVNcc6i?q1oLeY!fRBhda@Ztus`^7C&L(3u1DxwHwYA`PHag+XPjOL4KGK~R@6sXD7819I|A;b)xfr^YQYHorZgZwtWQdjQ_xc+of) zvxpi1%yEx#ye;>XqPZ*uR~)&b{!XK~a@7FsoVj+qOFuIV9LsVo^&I)~NwdBN0RJ1h znNkr0VH0Cw=9q=AoMd^oan=yvM-)*5QQWN2hD;fNzbaeE zDiI3R3~>s6rw zss%Tab|fu82;R^C@+?5Vuo}uT+lMeMn+HR|A(@Gc4z@DFN^ISRWDSyRaRC=JsRLDw z^e$GgR^ed_9_{Z{MT0#$91RZiD#Bo2D=Tb*uCiK7Iz<4jQHSpx9Z?WFBD<6de)!l> zrpm5}7$^p#5y1sa#_A2aC9wg*k*rX035XW5IVkjlmJezikpiG;4vs`mC>ka+5E_z; z`a$&@Ie0>5g8lt+u@H)Y#upyqL&s$<97VnKSVZVRRty>>EVLftb0SH=$=GUB{xK7O zNa%*L(1T0V)gbBLS?#FW`L<8xDZ28a4A6%UoN6^s{fB7!1hA3PWVMQc=o7q zt^%D7w868ntB!^%jt0>(WluQPCmrkKW)SP8^%MINCCic}%i^X~amm?DX^OV)rLUEH z#8GME{Zj3hAtuTnNR~hFQA8?#M6y2uy{c;`0?!|N;aJ>$%~L;dP%IO_oNVlve(Xx) zwxsv-2~Stj(-n7It7{NVVn7^7Hg!%PztYr|^leYnbtmh(C9Wn7Ts-H;zB?waOjNZc ztJbh= z#VPl)tM0Zd?zV)xJ?U--%^C$XkY{$syW^3Rz3Qsnd&TZe!Mz@yY`@m9Ozaeo{$y*S z!JlmKU-LGJJEpjwJTTpn@UBmK*I)Cg6$xJ;=?kQ-tPf-^kXsasG8YIGRkB#BsP5OR z>r%eesir_``G%CQEnRG>gic6UD=PFrKxCsr4+M&+(7OvO^c_<{sT5>%M}i9^xq!q4 zKAS?n9A?DnZtJe))Mfk5YDhj@Zri<@`OqKO-NO8?k%s*5T3DcCcE2(M(dg9+aRe!#w_|fxs4#L9JidbfFLEQMv%HS zf>g1E6p2XyT#=)~X1GC()I<>{Y(cUW$>)H8;LD0bMLPQNQm$?r7Kzxz3n2Iilh0#q z7n1EjGICHck@hKKtyVAAlx3S0YEB~5gk8vDHxjgLgfAdLQ%|@L$(NAakL1fpP<9Cq zAlZW?YfLGUt4R6BAH+*2tAvM;JdETKB*eTzJyj>794MkRLQOyy%t81?-c>9tOXIy3 zP@g7RP`X%IlrBI3)KR(s`I9XtU5Zh+!N_y6gI^c6gEI^Q5F<7K%l6S zoS&%SHhv=bHwyWQ{8QahsUKp3Tbtz8O5ECeB35>`fLPh#f#ic0+paat2OWW3t;~l_ zG~_>QWr2d#Ury2rzLKPFz6#TR8~C#{<`-WL#$Qcd4CAu@mEy%U>9EhE__Or5 z1YTS%?^>OqXuwIn&NwPxuX=I4n95{Yne}3-F zDFN{lyryP+RWeK5po=KW=Dt5Y|I}~i-Z{7MD3nN#*|Wc%fA-=6A`2sJe? z(vtGR)6dVp^6Ww&Q=XlizVXiM3usm-{o`*hoDvrh29@%+CvRN(!9o+1NAZqs{6?Jp z>30{<-9XAWzjN`%doM4bFjL{r;4#K~_L1qOLg4NkF7J7x$cTmn0}TMjD5|*?St-gi zfi#?*0BMTGO}xCM0U1kaHV7@^@QbWaA+5SFq@_8^?VVyI`)Y`(O5apKvaf-ds;r+f zN%l5~_cWWXEUvW0t&5(Fi@E;ppyOVOUzbw)7bD}Q0k=w|_405#@ zGV252!?4K{J~&GDeaci7V9h96M5L2})rpqMfI*6&-rd(|kSbxs75V2Ox zf>F*i#R;xlIW-~G<*~{ePPRA zwhab_CvdF?M^rE|xDv^H^r`c4i;uWY_b5oBntk|R%67%+skFTL0X9Ub_`&~61j&OP45%dK zdQq#zS5bwe8dULWP(^Xe z%#vLWWkDB06}9ELXK%hftr!M>1MgdS90`KGa0ChZV#24=pP0g@s@_^HTgVQ2$0&lN z0!!UO6XY{H9t2SkOw^VEP0Jn?teUo~7dtfT0~=Y6SPbt1anji(3lf6nHD{1gEO%Q?>Qr!%uk|QohDiU302#`SqH{sm=dn zmYlmFrbvO2oI4@v3b2__3+hivEr1x1xMR_qQGc554#e)YIi!ME4+9|926Tq8?XWR3 zC4n6*Gt)&w{(Y7OI_B_41vvPio?0_t80r9jS;Ghp*AOQOIGR<%$f>3-)gVFyLIbH% zDT84ICXm4}ssh8P`i}5N?@|HW%Ig-!1*-<$qtj}PE|ps2sC*886qiap8b}53Xa4QK zn?3WMia%NC5u7l#zykr>hSU1b89h<6zk65O*R8jWlk9Z_u}nI-R9 zK$D7CQ*eMLiYF3@K?NJSu@s(|Anu z<`8LF$Q&v|b4Y2C;U#>-*FaIAVqs0~=ReWaEk%H%HMqs0Q9}o&wsff~_rDL{Hn2 z5CeM%HnA*Ya0OQ|oU>ptuO99r^*m=hu>#vkZD+S@*E4Sgt@*^JJ})RW9HR5JE^$xgaVFagNTNeR@uOdI4sF;%@={1SPv{fuqWB{!Z8r zh5QB#&MhCztvn79Fq_QCa2Jx@$j%xXf`fW!83_cu$aWFF-uyd7atU(p1?~hn;KVOM zq=>K{yO0~CA1|Af1rJSWyP&6jqeMIR#faoKtWCk1Sz;Tjq>Fs-p4kl#P3B zF%e%&kEO>-%~)+aoXkvlU`H`17&PSHFJ^&`Rr>pe1tByLMDN;PRM`MW1A`x`fI)*f z+_kZj9M+bQoSE$1z*SHtaQ6nx9B7IAHg1xu;;Z=@%z?23W{pAFs0GMavXhe6*wHH+ z^>?6bc(veUgigL;31s+G$Y@l(uq4$$#xgBrymV>yTmLY3>h&f1Miwwsx9K0c;L`|o zxpibR0l8T7N)_iU`eplVSARUR&33aZhcW%{FRncB+{|oOzv7Sm*DiuO{r`!zi}PK= zlemIUW9K8=u={`DfS{YeQQZzT4G0&o(NjQF187};D|TP`?_FxedNtk9|HvipxBl?( zd>803<*MGIxz~tXruas*bt;}U>s1;}i#t(loSOE9)SGJJ?aeKQ-mH@IQ~2L@BO(P< zM1s@tg^pUKZB+g8>DrI>NzO+gg7q?8{ZX^zd=TR83N6!L%=7_;rCC*Xj(=p9oDV=m zeI|baa0xxWC3R@>)Y&AiI%o4V-yIBXntoU+-2^egZBBBVC2sRQ&~NPi(D9LBb(cpr zKlI?R|swHra$lylJi!)~3KCwM!{#2u;M29m5)eDaL8k9EP zq2i2l++cZ?c|Y$oUQ*${kWGmx<}ltmEg{e)h#k2qwM zUMh$9x!E&6g#9}-Q1FC(#1AW*I~0afl=xMEJyYKcA6$X5f~QgOyDIn~r$0oF>yNpW zb*;<|{0;z*qv=hQR%KIH=5T@HH($y*mwXBdOPDI#yTuAm^6`Q23m92ZL9iP+xT5Uy zW?JE}!rO2ygbV$@AcLw$xw6ocjt1qZecYUKls|X;%<&0}2&XICla6+%a(m7n{_f$4 zqvElI+n;p%gh+~t z(EkeL7FC3Y2!Y@k6gWg!R`|5XDiQ3FRS*+gTas&&xVF!# z>EH-CF$>CNJ@zHXmG)u|4-5&o#V#C2f{t+ky-vi)Bx<36NKD?+8`Q^;rN}${g!|m! zFh4pF`n>QS)FEs{aG|Fenx<1!yHq%)sB$S2Q&jOO@|U9OC4IckmY(`r{N%)OY26nk z_PzvrUy{A=l<7LnJY9XdTC&xNqhkN5>IA(eNv}zp%4qy_3+XL}GWm0LD=TRDLL*C7 zQzkPqh__7an%Ic_o04=>+O(SVM~eNMb9D#cb6};%8gaeYf<0Cx=~Zb{6X}5zd$i^1 zzLf3JE-n|Vut#f>ZcUp!qz6*$0bDY5n=(TfI(SJ3q`3~HZhK}3$>tFs6~BOEtWMIa z(QFRU8F4wi`!*H;}2zjuLMZ*@e3Wm5#EKF`ag)_#!~*ieZwHJf-dQ&~2FnQZNk?e?k)Rad!+t&L6mPpO0|D!=xe zZb??lHp$M+{+Qb9>#uL$ci(&ZeD|Ezugqox0b%CG+2eI93F5!;CL8EX;%f~_5LXC> zV8{S5MD~zFsvgx4)kBe(rwXWt=pI^1QvuD8wnrjK*hXv^R9`Pz3QV{L7)o!syQvJyxG3R+P=lzcL2K$e0FIH_Y#-pdI|nfw7z? z-ht%1*$kqmc*#Dj@4yqi3;%LpiWan8#uURWY|Ju1J7WVZVbonjt$hxg)Ka2;XJFJf z!up3pqL%eNKkDNkae@mD`v=a}lA?-v?m8)Ib_M*t;n0Cvm8fI9Az#Qp_5qk8S1?w{(9*K+8RdLK;H=K8}Og$&&*J|XaRAZzFrG(g$U*5Pr%MFDu!az z40VHk2lnmV?A{SDXU(V+8hrV;AI-e-_b{RUlaB>QIp55CZ_P$Oy7%t;qCODp_Xds! zxsZR&N`erzjBmg@8VHF*ty)x%1lf?Nh6b#t8}V}7=^)EUPvd>g3z;~n>%`Z9#QlQ+ zyIOh=LpImTab7Ri+u`F*hJqu#M>}`UT=*%B)_cke3)eTm28Tm_AKSY3d#-60HhgbKorBGo^TmSrM5IPpAJ)KXB>Vr>qJq;_Z9rA^a2N`bhW$snu zGV6&DbdU`va*NK8Cne!yyAtoF~FWN5ShHfxHEM z7?O_W3r|1Y1TD1SjORVu6t>$gEy7yA`!?+WXf@HZ+&<5=XvZ^?C{!FLL@ zUmsG)?$Hu#{r*R@uZ(~3$?I|lU;OqT?!|t1T_tL{(f)oP$BDF$WrOV8I4G|EobrN( zsCI1d|Mjx~SBOD)y`EVcA>qY<`-MR%1n-xAzj~Kc7wftR7AFeomgYjNe+0--wV^>& zOAm&M%;RCp;PS0uTfonS`h!Cw+sCR=>q{iiCoAp^+a#9awnHLY3nhtL4{(m)i+0E{ zR-M@NQOmDeKG^n~ZICl=zv6n;b*bWVMXrWK2&2oPS#IrSuU?q_S@hn|&*v}V0g>$a zR#Vr|(9+OwA2;fYkAL#TZ$E_nnY;3v+1T%9e)r1U<=4J^|NSv@JLmV-cOCZ*4<7gW z6|0PjQd;F3koYlm1y(r*sfB?dmE!a=6{QA%)z9lfc|?L7uY6$^s7X?4M;EjMb^9Id zR2Qf)8CKzojY_yyH(CuzSvbG=_?NR2e>Z#O?U`3EX4AL|GtnzwUWj zQ9L^8SLxBuLE>j*M#Neoq~hQr9bJ@1K+sF8P#vHx|b2z;@l!7@_z2A}^k!omS zz9n1W+(xsomA>n0b`=zfYB+eXqGrfH3`Y~u7-GGQe|WHu@t);yY>&rtP7+{t4cv|G zSz2lzZpiJg<= z^|l*rX*yeScg5;7O|0C&SMGtBP|H`q$t}6$d_VIkX|3mw|L0KmqXhxla{v`T`v_*J zMT^V@Im5y{6l8}{GO-dTTD=1yA3Gcj4$B!V%6?X2cSsT|I+qm5v5kL(9B;%CE&<>Q zX^8ULJFdnlS7TzW;CeLadNjH(t(h0od~u#$y)J$zp}H3SWjIY?m9Hd;|3<>-6KO+s zIp-G$#h*kzD1STuGYW{^G(PnV!A^MWjeMTMfq`k*G(=KcUXfwHuZ zq}$19Cm{;9 zgGt*#{;9*f?J#dT4Dw#wPQoc8UIQQ$?MN2wh|-z1ec850`SN2B1zUI0*3JLmS>E<6 zZ+aHmmb#*bl+l?c)cR-0w2Cr6Lr%MElJ1R(19vvX2i$LX@=@y&eeVZQPRM4{|xvg|0|-OHEt@!OprNfKs!iG9ag2J41kn!2HM^ou;9tt=j%j{ne#?B#`b%q~TJUTU zoLi!MQ>7JGhF%@I6ucaaQ$lH7oD)i#qPtk1>R?*pI$=o}Kk2DYQz<5It;`uJqhVshUFb~)U7pA zdM%0Rx}A?e!5^v(kJV9s*wFOYD(Z6&3F*&Q(ST#N`fc(l8Bf#O>rV#!!#)KsYhh44 zKz{uuyh%%YGh(Rm76f!FDUXf7=Xtf}y&+4qr<8|UIMu1ZhQ~Q&0=TSbMpJ07FphN^i%V04ULIopEKDam%vvvD)FyK+8;#pk26-5Fz;KzlKwhBJ#Q za~>@Y70*v9OoS=0zhUj?ots?*)Hda6!*ssqfhh{pi`v7AwF;GIdjNa5z~{5bDi&w#;bjr}MH=j3>6&K`+eUFt z`S$R>dmsPH?9URjKYdSBht7`pM4IvX17{_IDzJa^tuxjG{jg}>v$Y85FlFly9!F>e zkf{=17q$v+#~g=;xzQ1HcKR3(kUSgm;D9ZjvGT=eU3$@4JnR8zdjMz@X`RKRFM9|f zN8s$!czYP(2!Loss*|3fPwGOHxKB1MGC9RrzQxHwU0~o|Z2t@b8tb(h(J=qgiw0@7 zeK=dT59{m%$kxDP3X5k}4qq9Wd1ca-W+HbZ&4e9+3fMjO5u8+zb6rTez>~dqN@}J` zYGMQNL7`+zvSdrtFzxhQt+?Z?opRR3*9p$8N$1w6<*pUE=sS5*P3gP8i`$T-A52?; zKS-#Yj~Na=#Fsw>QLuF-ZC(7~W4!GcZ^}gAHfV&ZS4WiAO`PCM)a&vNm;UsoNw8>DyWwus_cUL7>4bamtMhrwkGiiXkIv zg=JshO9QL^KO%IPRJb!#VH$mdHwt(H^Oj(g0*_`y6IO?5hR*sVA*~q$QD?N0&rz4n zk7&bMDAP-22C2-LE7Rp`PN=6-vIt|6yq)@dHd?e6%o_ZtW*Enk>)RNnVmao5b<2gY zCf{!v)-nd=sEShELre7!Ej1LDg4fl0RioralDNp{`=f!6zytIB_8?3#MOO>s*O^Y&2p1+=17RqNmlXA|CZNa(0o?)1hi0MTWFpPA~W0=xWoPo?R z;Le(-7j`$~vrPctrv!8EEXSf_aq#D_Vfg2^Ih1$V^{@w0bCQC83~OwZ!hg19s{UJ> zf5%QMF?aAYNl~L^2-ae7#0Q2~e;~-g&2j(Waghpu(WW^S42%w;qiHpEzaKy}plK!P zeINS-WEAp`L>`_D|Lm5TPG%va<|_qb+ADM9GdM15J^9K3aM=tq&*;ne zd)X6M0U-ka+$S>YsFwrl+_e@*)u|FE5KVFEID5%?8JNJTw@MIgPuZ*9?oRB4V|WK& z(=609CjsrvcWk?+Y`bpm6>Lu?ZBIr!QqGmJ=i?i%ofe$+iAJcsd9v|?=9`qzv`cXA z0&~Mv9Vg@M*Xaa#U7zR@RzD)R9*ORoc2-RE#LETe=A?5on3B%c=-z2Z`NZD1QE+TZ zIyNOZ!O;?ZJXKOL@qBFaYiEU$HPPL7O+~NldwE~VQ8DdsPn?La5FG20j&;+{ zvOCgx8VwwPuLv3q900gQ1IMzof%E4kAM~ca)S>S%5w}c+P8)Trw5iiV-PVzizHOla z7pIXm@N%JPlBE&2(n=~R{AdbfatKtv2He6xK_EutMZKa0(122%LW`|H0-D@mT>~mt zdlDNHoC)%EnVx_sARo_j*lT&1j)7n<4A4Rku1W*chU*&z1P1E#k&91-@)UpJ@fhZF zUHTlSi|B#EbWDzpAZ?Op$}G{;Je3&21}L*iWkm%PW>ip^A#7AWJ@gKb03YA-3@Yet zVJ)OEGpJ@Avn)qp7ND@j*FYb_qd+Ev;S-{AR7I)rp{1sWmYNGo!xqNA2!;Klf;43c z%QINAGlkF=E_i5dBpmn6(~%) z=22Kho{=;Y{hPU0e^JO{=%8nJAUukIFNo|pM|xv7BIG>h_g=8Sx97jc&bQ4;w1LKb zChKzB94;NpA&dgdeF?8Ai<}1rh+1+8p2o5>2xk#c##l-1ug6pvpwKBQTRIP;uXE5! z)cb~k7g^v=_C@UKuMpp#ZEi%^h+PbUjz%6CQN{HhcQ0)`#uRcEhcx+X?>8-k0U1HN@)2~|+EjL$v z-t=kR&AODmB30zRQ&c@wR2^6IHQt|Yv-Ga}{u_ERc%IcG4^=ZO_ zyb5q(PqaN6l1X#ZwKIaVA<;gm`e66up%3=ntQI!z7M!~aN%Lu8b+h1VE+kF0;M`V7 znjyi_ilph9I25zLc1$R#`O75DKety1r-8U_Fzha-Zrhu7o2cJwNJ#(QL<1^F z6BRhThs4)-HvfM}n!Z1ZG|dueS_?@Nyj(h_I7f7*hY(#JjR4VE3y3bi6s#*{Y1qUp zDx?;YZm_knAmUzD38BEb$AMydwcq zs_bU$D+u%4gHCEC_sGl>z-kEp4*%SJkPBp<>c33pF(C5*WSGakJhmdn zTw5d9>)?kCP1jGsxAd{er-cpeg1tT3nR2*e#j!)zO5(-WoN-3*GzpHThcF6QUZXMZ zYn4|kr`=Vt=D1gIHzeH+Xlm(cG_?T0T0&C`0B$n?7So^K)M|`-c}pWiLD!VjHSxNp zKh4i4n_5(dhPb6MbegGKMNOSL>b8o6^lcptSX(O60|760l0~|Z9n`2^L<)QXEP8{* z;=G^LGOYiUk7ae3uNi@F$6iiUjWE(zXxXK342yJqedto|>l0PajYvPjmA){;In4*f zk`Gz7>!l--B(3Pdkw3pnzw**;8wxU`0pE6Z0t!HY32ql4IBiKXMXcwSjwxapuZ$_8 znpegYVdJwgMXcc$iz&i*PWrn`Th4Vy&rCSt0iJFZ=+-3NdQO#AtH>YV*T8wy5i0eM z9WObewG+PBk%_)@4uM>sB-f`^Pmm;@)}+f<6rtLXm;0', 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'); } }); From 9ad2dc7fabd0e535d5fafaac593310f3b24bd2c0 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Thu, 23 Oct 2025 08:31:06 +0800 Subject: [PATCH 3/4] update /api/events//stocks resp format --- app.py | 141 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 60 deletions(-) diff --git a/app.py b/app.py index edc4e934..73e2006e 100755 --- a/app.py +++ b/app.py @@ -7642,23 +7642,22 @@ def broadcast_new_event(event): # ==================== WebSocket 轮询机制(检测新事件) ==================== -# 内存变量:记录上次检查的时间戳和已推送的事件 ID 集合 -last_checked_time = None -pushed_event_ids = set() # 已推送的事件 ID 集合,防止重复推送 -MAX_PUSHED_IDS_SIZE = 1000 # 已推送 ID 集合的最大容量 +# 内存变量:记录近24小时内已知的事件ID集合和最大ID +known_event_ids_in_24h = set() # 近24小时内已知的所有事件ID +last_max_event_id = 0 # 已知的最大事件ID def poll_new_events(): """ 定期轮询数据库,检查是否有新事件 每 30 秒执行一次 - 设计思路: - 1. 使用时间戳查询(created_at),而不是 ID - 2. 维护已推送事件 ID 集合,避免重复推送 - 3. 使用重叠时间窗口(向前多查60秒),捕获延迟写入的事件 - 4. 定期清理已推送集合,防止内存泄漏 + 新的设计思路(修复 created_at 不是入库时间的问题): + 1. 查询近24小时内的所有活跃事件(按 created_at,因为这是事件发生时间) + 2. 通过对比事件ID(自增ID)来判断是否为新插入的事件 + 3. 推送 ID > last_max_event_id 的事件 + 4. 更新已知事件ID集合和最大ID """ - global last_checked_time, pushed_event_ids + global known_event_ids_in_24h, last_max_event_id try: with app.app_context(): @@ -7667,63 +7666,64 @@ def poll_new_events(): 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)}') + print(f'[轮询 DEBUG] 已知事件ID数量: {len(known_event_ids_in_24h)}') + print(f'[轮询 DEBUG] 当前最大事件ID: {last_max_event_id}') - # 如果是第一次运行,只查询最近 30 秒的事件 - if last_checked_time is None: - query_start_time = current_time - timedelta(seconds=30) - else: - # 向前多查 60 秒(重叠窗口),防止漏掉延迟写入的事件 - query_start_time = last_checked_time - timedelta(seconds=60) + # 查询近24小时内的所有活跃事件(按事件发生时间 created_at) + time_24h_ago = current_time - timedelta(hours=24) + print(f'[轮询 DEBUG] 查询时间范围: 近24小时({time_24h_ago.strftime("%Y-%m-%d %H:%M:%S")} ~ 现在)') - 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, - Event.created_at <= current_time, + # 查询所有近24小时内的活跃事件 + events_in_24h = Event.query.filter( + Event.created_at >= time_24h_ago, Event.status == 'active' - ).order_by(Event.created_at.asc()).all() + ).order_by(Event.id.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}') + print(f'[轮询 DEBUG] 数据库查询结果: 找到 {len(events_in_24h)} 个近24小时内的事件') - # 过滤掉已经推送过的事件 - unpushed_events = [ - event for event in new_events - if event.id not in pushed_event_ids + # 找出新插入的事件(ID > last_max_event_id) + new_events = [ + event for event in events_in_24h + if event.id > last_max_event_id ] - print(f'[轮询 DEBUG] 过滤后未推送事件: {len(unpushed_events)} 个') + print(f'[轮询 DEBUG] 新事件数量(ID > {last_max_event_id}): {len(new_events)} 个') - if unpushed_events: - print(f'[轮询] 发现 {len(unpushed_events)} 个新事件(查询到 {len(new_events)} 个,已过滤 {len(new_events) - len(unpushed_events)} 个重复)') + if new_events: + print(f'[轮询] 发现 {len(new_events)} 个新事件') + + for event in new_events: + print(f'[轮询 DEBUG] 新事件详情:') + print(f'[轮询 DEBUG] - ID: {event.id}') + print(f'[轮询 DEBUG] - 标题: {event.title}') + print(f'[轮询 DEBUG] - 事件发生时间(created_at): {event.created_at}') + print(f'[轮询 DEBUG] - 事件类型: {event.event_type}') - for event in unpushed_events: - print(f'[轮询 DEBUG] 准备推送事件: ID={event.id}, 标题={event.title}') # 推送新事件 + print(f'[轮询 DEBUG] 准备推送事件 ID={event.id}') broadcast_new_event(event) - # 记录已推送 - pushed_event_ids.add(event.id) - print(f'[轮询] 推送事件 ID={event.id}, 标题={event.title}') + print(f'[轮询] ✓ 已推送事件 ID={event.id}, 标题={event.title}') - # 更新检查时间 - last_checked_time = current_time + # 更新已知事件ID集合(所有近24小时内的事件ID) + known_event_ids_in_24h = set(event.id for event in events_in_24h) - # 清理已推送集合(防止无限增长) - if len(pushed_event_ids) > MAX_PUSHED_IDS_SIZE: - # 只保留最新的一半 - sorted_ids = sorted(pushed_event_ids) - pushed_event_ids = set(sorted_ids[-MAX_PUSHED_IDS_SIZE//2:]) - print(f'[轮询] 已清理推送记录,当前保留 {len(pushed_event_ids)} 个') + # 更新最大事件ID + new_max_id = max(event.id for event in events_in_24h) + print(f'[轮询 DEBUG] 更新最大事件ID: {last_max_event_id} -> {new_max_id}') + last_max_event_id = new_max_id + + print(f'[轮询 DEBUG] 更新后已知事件ID数量: {len(known_event_ids_in_24h)}') else: print(f'[轮询 DEBUG] 没有新事件需要推送') - # 没有新事件,也要更新检查时间 - last_checked_time = current_time + + # 即使没有新事件,也要更新已知事件集合(清理超过24小时的) + if events_in_24h: + known_event_ids_in_24h = set(event.id for event in events_in_24h) + current_max_id = max(event.id for event in events_in_24h) + if current_max_id != last_max_event_id: + print(f'[轮询 DEBUG] 更新最大事件ID: {last_max_event_id} -> {current_max_id}') + last_max_event_id = current_max_id print(f'[轮询 DEBUG] ========== 轮询结束 ==========\n') @@ -7738,22 +7738,43 @@ def initialize_event_polling(): 初始化事件轮询机制 在应用启动时调用 """ - global last_checked_time, pushed_event_ids + global known_event_ids_in_24h, last_max_event_id try: - from datetime import datetime + from datetime import datetime, timedelta with app.app_context(): - # 设置初始检查时间为当前时间 - # 这样启动后只会推送新创建的事件,不会推送历史事件 - last_checked_time = datetime.now() - pushed_event_ids.clear() + current_time = datetime.now() + time_24h_ago = current_time - timedelta(hours=24) + + print(f'\n[轮询] ========== 初始化事件轮询 ==========') + print(f'[轮询] 当前时间: {current_time.strftime("%Y-%m-%d %H:%M:%S")}') + + # 查询近24小时内的所有活跃事件 + events_in_24h = Event.query.filter( + Event.created_at >= time_24h_ago, + Event.status == 'active' + ).order_by(Event.id.asc()).all() + + # 初始化已知事件ID集合 + known_event_ids_in_24h = set(event.id for event in events_in_24h) + + # 初始化最大事件ID + if events_in_24h: + last_max_event_id = max(event.id for event in events_in_24h) + print(f'[轮询] 近24小时内共有 {len(events_in_24h)} 个活跃事件') + print(f'[轮询] 初始最大事件ID: {last_max_event_id}') + print(f'[轮询] 事件ID范围: {min(event.id for event in events_in_24h)} ~ {last_max_event_id}') + else: + last_max_event_id = 0 + print(f'[轮询] 近24小时内没有活跃事件') + print(f'[轮询] 初始最大事件ID: 0') # 统计数据库中的事件总数 total_events = Event.query.filter_by(status='active').count() - print(f'[轮询] 初始化完成,数据库中共有 {total_events} 个活跃事件') - print(f'[轮询] 起始时间: {last_checked_time.strftime("%Y-%m-%d %H:%M:%S")}') - print(f'[轮询] 只会推送此时间之后创建的新事件') + print(f'[轮询] 数据库中共有 {total_events} 个活跃事件(所有时间)') + print(f'[轮询] 只会推送 ID > {last_max_event_id} 的新事件') + print(f'[轮询] ========== 初始化完成 ==========\n') # 创建后台调度器 scheduler = BackgroundScheduler() From 37eba489066a8b55f3106ac285a7ca68441ced01 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Thu, 23 Oct 2025 10:09:24 +0800 Subject: [PATCH 4/4] update /api/events//stocks resp format --- src/hooks/useEventNotifications.js | 35 +++++++++++++++++---- src/views/Community/components/EventList.js | 16 +++++----- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/hooks/useEventNotifications.js b/src/hooks/useEventNotifications.js index 876c27c9..e23de785 100644 --- a/src/hooks/useEventNotifications.js +++ b/src/hooks/useEventNotifications.js @@ -37,28 +37,46 @@ export const useEventNotifications = (options = {}) => { const [error, setError] = useState(null); const unsubscribeRef = useRef(null); + // 使用 ref 存储 onNewEvent 回调,避免因回调函数引用改变导致重新连接 + const onNewEventRef = useRef(onNewEvent); + + // 每次 onNewEvent 改变时更新 ref useEffect(() => { + onNewEventRef.current = onNewEvent; + }, [onNewEvent]); + + useEffect(() => { + console.log('[useEventNotifications DEBUG] ========== useEffect 执行 =========='); + console.log('[useEventNotifications DEBUG] enabled:', enabled); + console.log('[useEventNotifications DEBUG] eventType:', eventType); + console.log('[useEventNotifications DEBUG] importance:', importance); + // 如果禁用,则不订阅 if (!enabled) { + console.log('[useEventNotifications DEBUG] ⚠️ 订阅已禁用,跳过'); return; } // 连接状态监听 const handleConnect = () => { + console.log('[useEventNotifications DEBUG] ✓ WebSocket 已连接'); setIsConnected(true); setError(null); }; const handleDisconnect = () => { + console.log('[useEventNotifications DEBUG] ⚠️ WebSocket 已断开'); setIsConnected(false); }; const handleConnectError = (err) => { + console.error('[useEventNotifications ERROR] WebSocket 连接错误:', err); setError(err); setIsConnected(false); }; // 连接 WebSocket + console.log('[useEventNotifications DEBUG] 准备连接 WebSocket...'); socketService.connect(); // 监听连接事件 @@ -66,7 +84,7 @@ export const useEventNotifications = (options = {}) => { socketService.on('disconnect', handleDisconnect); socketService.on('connect_error', handleConnectError); - // 新事件处理函数 + // 新事件处理函数 - 使用 ref 中的回调 const handleNewEvent = (eventData) => { console.log('\n[useEventNotifications DEBUG] ========== Hook 收到新事件 =========='); console.log('[useEventNotifications DEBUG] 事件数据:', eventData); @@ -77,10 +95,10 @@ export const useEventNotifications = (options = {}) => { setNewEvent(eventData); console.log('[useEventNotifications DEBUG] ✓ newEvent 状态已更新'); - // 调用外部回调 - if (onNewEvent) { + // 调用外部回调(从 ref 中获取最新的回调) + if (onNewEventRef.current) { console.log('[useEventNotifications DEBUG] 准备调用外部 onNewEvent 回调'); - onNewEvent(eventData); + onNewEventRef.current(eventData); console.log('[useEventNotifications DEBUG] ✓ 外部 onNewEvent 回调已调用'); } else { console.log('[useEventNotifications DEBUG] ⚠️ 没有外部 onNewEvent 回调'); @@ -114,22 +132,27 @@ export const useEventNotifications = (options = {}) => { // 组件卸载时清理 return () => { - console.log('清理 WebSocket 订阅'); + console.log('\n[useEventNotifications DEBUG] ========== 清理 WebSocket 订阅 =========='); // 取消订阅 if (unsubscribeRef.current) { + console.log('[useEventNotifications DEBUG] 取消订阅...'); unsubscribeRef.current(); } // 移除监听器 + console.log('[useEventNotifications DEBUG] 移除事件监听器...'); socketService.off('connect', handleConnect); socketService.off('disconnect', handleDisconnect); socketService.off('connect_error', handleConnectError); // 断开连接 + console.log('[useEventNotifications DEBUG] 断开 WebSocket 连接...'); socketService.disconnect(); + + console.log('[useEventNotifications DEBUG] ========== 清理完成 ==========\n'); }; - }, [eventType, importance, enabled, onNewEvent]); + }, [eventType, importance, enabled]); // 移除 onNewEvent 依赖 return { newEvent, // 最新收到的事件 diff --git a/src/views/Community/components/EventList.js b/src/views/Community/components/EventList.js index 28c8a1fa..7e7f7a77 100644 --- a/src/views/Community/components/EventList.js +++ b/src/views/Community/components/EventList.js @@ -187,17 +187,17 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai logger.info('EventList', '收到新事件推送', event); console.log('[EventList DEBUG] 准备显示 Toast 通知'); - // 显示 Toast 通知 - toast({ - title: '新事件发布', + // 显示 Toast 通知 - 更明显的配置 + const toastId = toast({ + title: '🔔 新事件发布', description: event.title, - status: 'info', - duration: 5000, + status: 'success', // 改为 success,更醒目 + duration: 8000, // 延长显示时间到 8 秒 isClosable: true, - position: 'top-right', - variant: 'left-accent', + position: 'top', // 改为顶部居中,更显眼 + variant: 'solid', // 改为 solid,背景更明显 }); - console.log('[EventList DEBUG] ✓ Toast 通知已显示'); + console.log('[EventList DEBUG] ✓ Toast 通知已调用,ID:', toastId); console.log('[EventList DEBUG] 准备更新事件列表'); // 将新事件添加到列表顶部(防止重复)