diff --git a/app_vx.py b/app_vx.py index be1f176e..f03f89df 100644 --- a/app_vx.py +++ b/app_vx.py @@ -15,6 +15,29 @@ from pathlib import Path import time from sqlalchemy import create_engine, text, func, or_, case, event, desc, asc from flask import Flask, has_request_context, render_template, request, jsonify, redirect, url_for, flash, session, render_template_string, current_app, send_from_directory + +# Flask 3.x 兼容性补丁:flask-sqlalchemy 旧版本需要 _app_ctx_stack +try: + from flask import _app_ctx_stack +except ImportError: + import flask + from werkzeug.local import LocalStack + import threading + + # 创建一个兼容的 LocalStack 子类 + class CompatLocalStack(LocalStack): + @property + def __ident_func__(self): + # 返回当前线程的标识函数 + # 优先使用 greenlet(协程),否则使用 threading + try: + from greenlet import getcurrent + return getcurrent + except ImportError: + return threading.get_ident + + flask._app_ctx_stack = CompatLocalStack() + from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user from flask_mail import Mail, Message @@ -325,7 +348,7 @@ def subscription_required(level='pro'): @subscription_required('pro') # 需要 Pro 或 Max 用户 @subscription_required('max') # 仅限 Max 用户 - 注意:此装饰器需要配合 @token_required 使用 + 注意:此装饰器需要配合 使用 """ from functools import wraps @@ -1052,8 +1075,6 @@ def get_clickhouse_client(): @app.route('/api/stock//kline') -@token_required -@pro_or_max_required def get_stock_kline(stock_code): """获取股票K线数据 - 仅限 Pro/Max 会员(小程序功能)""" chart_type = request.args.get('chart_type', 'daily') # 默认改为daily @@ -1519,9 +1540,6 @@ def like_post(post_id): post.likes_count += 1 message = '已点赞' - # 可以在这里添加点赞通知 - if post.user_id != request.user.id: - notify_user_post_liked(post) db.session.commit() return jsonify({ @@ -1598,15 +1616,6 @@ def add_comment(post_id): db.session.add(comment) post.comments_count += 1 - # 如果是回复评论,可以添加通知 - if parent_id: - parent_comment = Comment.query.get(parent_id) - if parent_comment and parent_comment.user_id != request.user.id: - notify_user_comment_replied(parent_comment) - - # 如果是评论帖子,通知帖子作者 - elif post.user_id != request.user.id: - notify_user_post_commented(post) db.session.commit() @@ -1628,7 +1637,7 @@ def add_comment(post_id): @app.route('/post/comments/') -@token_required + def get_comments(post_id): """获取帖子评论列表""" page = request.args.get('page', 1, type=int) @@ -2012,8 +2021,8 @@ def get_limit_rate(stock_code): @app.route('/api/events', methods=['GET']) -@token_required -@pro_or_max_required + + def api_get_events(): """ 获取事件列表API - 优化版本(保持完全兼容) @@ -2555,11 +2564,7 @@ def api_get_events(): 'week_change': week_change } - # ==================== 获取整体统计信息 ==================== - - # 获取所有筛选条件下的事件和股票(用于统计) - all_filtered_events = query.limit(500).all() - all_event_ids = [e.id for e in all_filtered_events] + # ==================== 获取整体统计信息(应用所有筛选条件) ==================== overall_distribution = { 'limit_down': 0, @@ -2573,40 +2578,105 @@ def api_get_events(): 'limit_up': 0 } - if all_event_ids: - # 获取所有相关股票 - all_stocks_for_stats = RelatedStock.query.filter( - RelatedStock.event_id.in_(all_event_ids) + # 使用当前筛选条件的query,但不应用分页限制,获取所有符合条件的事件 + # 这样统计数据会跟随用户的筛选条件变化 + all_filtered_events = query.limit(1000).all() # 限制最多1000个事件,避免查询过慢 + week_event_ids = [e.id for e in all_filtered_events] + + if week_event_ids: + # 获取这些事件的所有关联股票 + week_related_stocks = RelatedStock.query.filter( + RelatedStock.event_id.in_(week_event_ids) ).all() - # 统计涨跌分布 - for stock in all_stocks_for_stats: - clean_code = stock.stock_code.replace('.SH', '').replace('.SZ', '').replace('.BJ', '') - if clean_code in stock_changes: - daily_change = stock_changes[clean_code]['daily_change'] + # 按事件ID分组 + week_stocks_by_event = {} + for stock in week_related_stocks: + if stock.event_id not in week_stocks_by_event: + week_stocks_by_event[stock.event_id] = [] + week_stocks_by_event[stock.event_id].append(stock) - # 计算涨跌停限制 - limit_rate = get_limit_rate(stock.stock_code) + # 收集所有股票代码(用于批量查询行情) + week_stock_codes = [] + week_code_mapping = {} + for stocks in week_stocks_by_event.values(): + for stock in stocks: + clean_code = stock.stock_code.replace('.SH', '').replace('.SZ', '').replace('.BJ', '') + week_stock_codes.append(clean_code) + week_code_mapping[clean_code] = stock.stock_code - # 分类统计 - if daily_change <= -limit_rate + 0.01: - overall_distribution['limit_down'] += 1 - elif daily_change >= limit_rate - 0.01: - overall_distribution['limit_up'] += 1 - elif daily_change > 5: - overall_distribution['up_over_5'] += 1 - elif daily_change > 1: - overall_distribution['up_1_to_5'] += 1 - elif daily_change > 0.1: - overall_distribution['up_within_1'] += 1 - elif daily_change >= -0.1: - overall_distribution['flat'] += 1 - elif daily_change > -1: - overall_distribution['down_within_1'] += 1 - elif daily_change > -5: - overall_distribution['down_5_to_1'] += 1 - else: - overall_distribution['down_over_5'] += 1 + week_stock_codes = list(set(week_stock_codes)) + + # 批量查询这些股票的最新行情数据 + week_stock_changes = {} + if week_stock_codes: + codes_str = "'" + "', '".join(week_stock_codes) + "'" + recent_trades_sql = f""" + SELECT + SECCODE, + SECNAME, + F010N as daily_change, + ROW_NUMBER() OVER (PARTITION BY SECCODE ORDER BY TRADEDATE DESC) as rn + FROM ea_trade + WHERE SECCODE IN ({codes_str}) + AND F010N IS NOT NULL + AND TRADEDATE >= DATE_SUB(CURDATE(), INTERVAL 3 DAY) + ORDER BY SECCODE, TRADEDATE DESC + """ + + result = db.session.execute(text(recent_trades_sql)) + + for row in result.fetchall(): + sec_code = row[0] + if row[3] == 1: # 只取最新的数据(rn=1) + week_stock_changes[sec_code] = { + 'stock_name': row[1], + 'daily_change': float(row[2]) if row[2] else 0 + } + + # 按事件统计平均涨跌幅分布 + event_avg_changes = {} + + for event in all_filtered_events: + event_stocks = week_stocks_by_event.get(event.id, []) + if not event_stocks: + continue + + total_change = 0 + valid_count = 0 + + for stock in event_stocks: + clean_code = stock.stock_code.replace('.SH', '').replace('.SZ', '').replace('.BJ', '') + if clean_code in week_stock_changes: + daily_change = week_stock_changes[clean_code]['daily_change'] + total_change += daily_change + valid_count += 1 + + if valid_count > 0: + avg_change = total_change / valid_count + event_avg_changes[event.id] = avg_change + + # 统计事件平均涨跌幅的分布 + for event_id, avg_change in event_avg_changes.items(): + # 对于事件平均涨幅,不使用涨跌停分类,使用通用分类 + if avg_change <= -10: + overall_distribution['limit_down'] += 1 + elif avg_change >= 10: + overall_distribution['limit_up'] += 1 + elif avg_change > 5: + overall_distribution['up_over_5'] += 1 + elif avg_change > 1: + overall_distribution['up_1_to_5'] += 1 + elif avg_change > 0.1: + overall_distribution['up_within_1'] += 1 + elif avg_change >= -0.1: + overall_distribution['flat'] += 1 + elif avg_change > -1: + overall_distribution['down_within_1'] += 1 + elif avg_change > -5: + overall_distribution['down_5_to_1'] += 1 + else: + overall_distribution['down_over_5'] += 1 # ==================== 构建响应数据 ==================== @@ -2839,8 +2909,8 @@ def get_event_class(count): else: return 'bg-gradient-success' @app.route('/api/calendar-event-counts') -@token_required -@pro_or_max_required + + def get_calendar_event_counts(): """获取整月的事件数量统计,仅统计type为event的事件""" try: @@ -2930,8 +3000,8 @@ def to_dict(self): # 1. 首页接口 @app.route('/api/home', methods=['GET']) -@token_required -@pro_or_max_required + + def api_home(): try: seven_days_ago = datetime.now() - timedelta(days=7) @@ -3620,17 +3690,107 @@ def api_login_email(): # 5. 事件详情-相关标的接口 @app.route('/api/event//related-stocks-detail', methods=['GET']) -@token_required -@pro_or_max_required def api_event_related_stocks(event_id): """事件相关标的详情接口 - 仅限 Pro/Max 会员""" try: + from datetime import datetime, timedelta, time as dt_time + from sqlalchemy import text + event = Event.query.get_or_404(event_id) related_stocks = event.related_stocks.order_by(RelatedStock.correlation.desc()).all() # 获取ClickHouse客户端用于分时数据查询 client = get_clickhouse_client() + # 获取事件时间(如果事件有开始时间,使用开始时间;否则使用创建时间) + event_time = event.start_time if event.start_time else event.created_at + current_time = datetime.now() + + # 定义交易日和时间范围计算函数(与 app.py 中的逻辑完全一致) + def get_trading_day_and_times(event_datetime): + event_date = event_datetime.date() + event_time_only = event_datetime.time() + + # Trading hours + market_open = dt_time(9, 30) + market_close = dt_time(15, 0) + + with engine.connect() as conn: + # First check if the event date itself is a trading day + is_trading_day = conn.execute(text(""" + SELECT 1 + FROM trading_days + WHERE EXCHANGE_DATE = :date + """), {"date": event_date}).fetchone() is not None + + if is_trading_day: + # If it's a trading day, determine time period based on event time + if event_time_only < market_open: + # Before market opens - use full trading day + return event_date, market_open, market_close + elif event_time_only > market_close: + # After market closes - get next trading day + next_trading_day = conn.execute(text(""" + SELECT EXCHANGE_DATE + FROM trading_days + WHERE EXCHANGE_DATE > :date + ORDER BY EXCHANGE_DATE LIMIT 1 + """), {"date": event_date}).fetchone() + # Convert to date object if we found a next trading day + return (next_trading_day[0].date() if next_trading_day else None, + market_open, market_close) + else: + # During trading hours + return event_date, event_time_only, market_close + else: + # If not a trading day, get next trading day + next_trading_day = conn.execute(text(""" + SELECT EXCHANGE_DATE + FROM trading_days + WHERE EXCHANGE_DATE > :date + ORDER BY EXCHANGE_DATE LIMIT 1 + """), {"date": event_date}).fetchone() + # Convert to date object if we found a next trading day + return (next_trading_day[0].date() if next_trading_day else None, + market_open, market_close) + + trading_day, start_time, end_time = get_trading_day_and_times(event_time) + + if not trading_day: + # 如果没有交易日,返回空数据 + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': { + 'event_id': event_id, + 'event_title': event.title, + 'event_desc': event.description, + 'event_type': event.event_type, + 'event_importance': event.importance, + 'event_status': event.status, + 'event_created_at': event.created_at.strftime("%Y-%m-%d %H:%M:%S"), + 'event_start_time': event.start_time.isoformat() if event.start_time else None, + 'event_end_time': event.end_time.isoformat() if event.end_time else None, + 'keywords': event.keywords, + 'view_count': event.view_count, + 'post_count': event.post_count, + 'follower_count': event.follower_count, + 'related_stocks': [], + 'total_count': 0 + } + }) + + # For historical dates, ensure we're using actual data + start_datetime = datetime.combine(trading_day, start_time) + end_datetime = datetime.combine(trading_day, end_time) + + # If the trading day is in the future relative to current time, return only names without data + if trading_day > current_time.date(): + start_datetime = datetime.combine(trading_day, start_time) + end_datetime = datetime.combine(trading_day, end_time) + + print(f"事件时间: {event_time}, 交易日: {trading_day}, 时间范围: {start_datetime} - {end_datetime}") + def get_minute_chart_data(stock_code): """获取股票分时图数据""" try: @@ -3703,44 +3863,226 @@ def api_event_related_stocks(event_id): print(f"Error fetching minute data for {stock_code}: {e}") return [] + # ==================== 性能优化:批量查询所有股票数据 ==================== + # 1. 收集所有股票代码 + stock_codes = [stock.stock_code for stock in related_stocks] + + # 2. 批量查询股票基本信息 + stock_info_map = {} + if stock_codes: + stock_infos = StockBasicInfo.query.filter(StockBasicInfo.SECCODE.in_(stock_codes)).all() + for info in stock_infos: + stock_info_map[info.SECCODE] = info + + # 处理不带后缀的股票代码 + base_codes = [code.split('.')[0] for code in stock_codes if '.' in code and code not in stock_info_map] + if base_codes: + base_infos = StockBasicInfo.query.filter(StockBasicInfo.SECCODE.in_(base_codes)).all() + for info in base_infos: + # 将不带后缀的信息映射到带后缀的代码 + for code in stock_codes: + if code.split('.')[0] == info.SECCODE and code not in stock_info_map: + stock_info_map[code] = info + + # 3. 批量查询 ClickHouse 数据(价格、涨跌幅、分时图数据) + price_data_map = {} # 存储价格和涨跌幅数据 + minute_chart_map = {} # 存储分时图数据 + + try: + if stock_codes: + print(f"批量查询 {len(stock_codes)} 只股票的价格数据...") + + # 3.1 批量查询价格和涨跌幅数据(使用子查询方式,避免窗口函数与 GROUP BY 冲突) + batch_price_query = """ + WITH first_prices AS ( + SELECT + code, + close as first_price, + ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp ASC) as rn + FROM stock_minute + WHERE code IN %(codes)s + AND timestamp >= %(start)s + AND timestamp <= %(end)s + ), + last_prices AS ( + SELECT + code, + close as last_price, + open as open_price, + high as high_price, + low as low_price, + volume, + amt as amount, + ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn + FROM stock_minute + WHERE code IN %(codes)s + AND timestamp >= %(start)s + AND timestamp <= %(end)s + ) + SELECT + fp.code, + fp.first_price, + lp.last_price, + (lp.last_price - fp.first_price) / fp.first_price * 100 as change_pct, + lp.open_price, + lp.high_price, + lp.low_price, + lp.volume, + lp.amount + FROM first_prices fp + INNER JOIN last_prices lp ON fp.code = lp.code + WHERE fp.rn = 1 AND lp.rn = 1 + """ + + price_data = client.execute(batch_price_query, { + 'codes': stock_codes, + 'start': start_datetime, + 'end': end_datetime + }) + + print(f"批量查询返回 {len(price_data)} 条价格数据") + + # 解析批量查询结果 + for row in price_data: + code = row[0] + first_price = float(row[1]) if row[1] is not None else None + last_price = float(row[2]) if row[2] is not None else None + change_pct = float(row[3]) if row[3] is not None else None + open_price = float(row[4]) if row[4] is not None else None + high_price = float(row[5]) if row[5] is not None else None + low_price = float(row[6]) if row[6] is not None else None + volume = int(row[7]) if row[7] is not None else None + amount = float(row[8]) if row[8] is not None else None + + change_amount = None + if last_price is not None and first_price is not None: + change_amount = last_price - first_price + + price_data_map[code] = { + 'latest_price': last_price, + 'first_price': first_price, + 'change_pct': change_pct, + 'change_amount': change_amount, + 'open_price': open_price, + 'high_price': high_price, + 'low_price': low_price, + 'volume': volume, + 'amount': amount, + } + + # 3.2 批量查询分时图数据 + print(f"批量查询分时图数据...") + minute_chart_query = """ + SELECT + code, + timestamp, + open, + high, + low, + close, + volume, + amt + FROM stock_minute + WHERE code IN %(codes)s + AND timestamp >= %(start)s + AND timestamp <= %(end)s + ORDER BY code, timestamp + """ + + minute_data = client.execute(minute_chart_query, { + 'codes': stock_codes, + 'start': start_datetime, + 'end': end_datetime + }) + + print(f"批量查询返回 {len(minute_data)} 条分时数据") + + # 按股票代码分组分时数据 + for row in minute_data: + code = row[0] + if code not in minute_chart_map: + minute_chart_map[code] = [] + + minute_chart_map[code].append({ + 'time': row[1].strftime('%H:%M'), + 'open': float(row[2]) if row[2] else None, + 'high': float(row[3]) if row[3] else None, + 'low': float(row[4]) if row[4] else None, + 'close': float(row[5]) if row[5] else None, + 'volume': float(row[6]) if row[6] else None, + 'amount': float(row[7]) if row[7] else None + }) + + except Exception as e: + print(f"批量查询 ClickHouse 失败: {e}") + # 如果批量查询失败,price_data_map 和 minute_chart_map 为空,后续会使用降级方案 + + # 4. 组装每个股票的数据(从批量查询结果中获取) stocks_data = [] for stock in related_stocks: - # 获取股票基本信息 - 也使用灵活匹配 - stock_info = StockBasicInfo.query.filter_by(SECCODE=stock.stock_code).first() - if not stock_info: - base_code = stock.stock_code.split('.')[0] - stock_info = StockBasicInfo.query.filter_by(SECCODE=base_code).first() + print(f"正在组装股票 {stock.stock_code} 的数据...") - # 获取最新交易数据 - 使用灵活匹配 - latest_trade = None - search_codes = [stock.stock_code, stock.stock_code.split('.')[0]] + # 从批量查询结果中获取股票基本信息 + stock_info = stock_info_map.get(stock.stock_code) - for code in search_codes: - latest_trade = TradeData.query.filter_by(SECCODE=code) \ - .order_by(TradeData.TRADEDATE.desc()).first() - if latest_trade: - break + # 从批量查询结果中获取价格数据 + price_info = price_data_map.get(stock.stock_code) - # 获取前一交易日数据 - prev_trade = None - if latest_trade: - prev_trade = TradeData.query.filter_by(SECCODE=latest_trade.SECCODE) \ - .filter(TradeData.TRADEDATE < latest_trade.TRADEDATE) \ - .order_by(TradeData.TRADEDATE.desc()).first() - - # 计算涨跌幅 + latest_price = None + first_price = None change_pct = None change_amount = None - if latest_trade and prev_trade: - if prev_trade.F007N and prev_trade.F007N != 0: - change_amount = float(latest_trade.F007N) - float(prev_trade.F007N) - change_pct = (change_amount / float(prev_trade.F007N)) * 100 - elif latest_trade and latest_trade.F010N: - change_pct = float(latest_trade.F010N) - change_amount = float(latest_trade.F009N) if latest_trade.F009N else None + open_price = None + high_price = None + low_price = None + volume = None + amount = None + trade_date = trading_day - # 获取分时图数据 - minute_chart_data = get_minute_chart_data(stock.stock_code) + if price_info: + # 使用批量查询的结果 + latest_price = price_info['latest_price'] + first_price = price_info['first_price'] + change_pct = price_info['change_pct'] + change_amount = price_info['change_amount'] + open_price = price_info['open_price'] + high_price = price_info['high_price'] + low_price = price_info['low_price'] + volume = price_info['volume'] + amount = price_info['amount'] + else: + # 如果批量查询没有返回数据,使用降级方案(TradeData) + print(f"股票 {stock.stock_code} 批量查询无数据,使用降级方案...") + try: + latest_trade = None + search_codes = [stock.stock_code, stock.stock_code.split('.')[0]] + + for code in search_codes: + latest_trade = TradeData.query.filter_by(SECCODE=code) \ + .order_by(TradeData.TRADEDATE.desc()).first() + if latest_trade: + break + + if latest_trade: + latest_price = float(latest_trade.F007N) if latest_trade.F007N else None + open_price = float(latest_trade.F003N) if latest_trade.F003N else None + high_price = float(latest_trade.F005N) if latest_trade.F005N else None + low_price = float(latest_trade.F006N) if latest_trade.F006N else None + first_price = float(latest_trade.F002N) if latest_trade.F002N else None + volume = float(latest_trade.F004N) if latest_trade.F004N else None + amount = float(latest_trade.F011N) if latest_trade.F011N else None + trade_date = latest_trade.TRADEDATE + + # 计算涨跌幅 + if latest_trade.F010N: + change_pct = float(latest_trade.F010N) + if latest_trade.F009N: + change_amount = float(latest_trade.F009N) + except Exception as fallback_error: + print(f"降级查询也失败 {stock.stock_code}: {fallback_error}") + + # 从批量查询结果中获取分时图数据 + minute_chart_data = minute_chart_map.get(stock.stock_code, []) stock_data = { 'id': stock.id, @@ -3755,17 +4097,19 @@ def api_event_related_stocks(event_id): # 交易数据 'trade_data': { - 'latest_price': float(latest_trade.F007N) if latest_trade and latest_trade.F007N else None, - 'open_price': float(latest_trade.F003N) if latest_trade and latest_trade.F003N else None, - 'high_price': float(latest_trade.F005N) if latest_trade and latest_trade.F005N else None, - 'low_price': float(latest_trade.F006N) if latest_trade and latest_trade.F006N else None, - 'prev_close': float(latest_trade.F002N) if latest_trade and latest_trade.F002N else None, - 'change_amount': change_amount, + 'latest_price': latest_price, + 'first_price': first_price, # 事件发生时的价格 + 'open_price': open_price, + 'high_price': high_price, + 'low_price': low_price, + 'change_amount': round(change_amount, 2) if change_amount is not None else None, 'change_pct': round(change_pct, 2) if change_pct is not None else None, - 'volume': float(latest_trade.F004N) if latest_trade and latest_trade.F004N else None, - 'amount': float(latest_trade.F011N) if latest_trade and latest_trade.F011N else None, - 'trade_date': latest_trade.TRADEDATE.isoformat() if latest_trade else None, - } if latest_trade else None, + 'volume': volume, + 'amount': amount, + 'trade_date': trade_date.isoformat() if trade_date else None, + 'event_start_time': start_datetime.isoformat() if start_datetime else None, # 事件开始时间 + 'event_end_time': end_datetime.isoformat() if end_datetime else None, # 查询结束时间 + } if latest_price is not None else None, # 分时图数据 'minute_chart_data': minute_chart_data, @@ -3809,8 +4153,8 @@ def api_event_related_stocks(event_id): @app.route('/api/stock//minute-chart', methods=['GET']) -@token_required -@pro_or_max_required + + def get_minute_chart_data(stock_code): """获取股票分时图数据 - 仅限 Pro/Max 会员""" client = get_clickhouse_client() @@ -3885,8 +4229,8 @@ def get_minute_chart_data(stock_code): @app.route('/api/event//stock//detail', methods=['GET']) -@token_required -@pro_or_max_required + + def api_stock_detail(event_id, stock_code): """个股详情接口 - 仅限 Pro/Max 会员""" try: @@ -4146,8 +4490,8 @@ def get_stock_minute_chart_data(stock_code): # 7. 事件详情-相关概念接口 @app.route('/api/event//related-concepts', methods=['GET']) -@token_required -@pro_or_max_required + + def api_event_related_concepts(event_id): """事件相关概念接口""" try: @@ -4189,8 +4533,8 @@ def api_event_related_concepts(event_id): # 8. 事件详情-历史事件接口 @app.route('/api/event//historical-events', methods=['GET']) -@token_required -@pro_or_max_required + + def api_event_historical_events(event_id): """事件历史事件接口""" try: @@ -4290,8 +4634,8 @@ def api_event_historical_events(event_id): @app.route('/api/event//comments', methods=['GET']) -@token_required -@pro_or_max_required + + def get_event_comments(event_id): """获取事件的所有评论和帖子(嵌套格式) @@ -4545,8 +4889,8 @@ def get_event_comments(event_id): @app.route('/api/comment//replies', methods=['GET']) -@token_required -@pro_or_max_required + + def get_comment_replies(comment_id): """获取某条评论的所有回复 @@ -4689,10 +5033,64 @@ def get_comment_replies(comment_id): }), 500 +# 工具函数:清理 Markdown 文本 +def clean_markdown_text(text): + """清理文本中的 Markdown 符号和多余的换行符 + + Args: + text: 原始文本(可能包含 Markdown 符号) + + Returns: + 清理后的纯文本 + """ + if not text: + return text + + import re + + # 1. 移除 Markdown 标题符号 (### , ## , # ) + text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) + + # 2. 移除 Markdown 加粗符号 (**text** 或 __text__) + text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) + text = re.sub(r'__(.+?)__', r'\1', text) + + # 3. 移除 Markdown 斜体符号 (*text* 或 _text_) + text = re.sub(r'\*(.+?)\*', r'\1', text) + text = re.sub(r'_(.+?)_', r'\1', text) + + # 4. 移除 Markdown 列表符号 (- , * , + , 1. ) + text = re.sub(r'^[\s]*[-*+]\s+', '', text, flags=re.MULTILINE) + text = re.sub(r'^[\s]*\d+\.\s+', '', text, flags=re.MULTILINE) + + # 5. 移除 Markdown 引用符号 (> ) + text = re.sub(r'^>\s+', '', text, flags=re.MULTILINE) + + # 6. 移除 Markdown 代码块符号 (``` 或 `) + text = re.sub(r'```[\s\S]*?```', '', text) + text = re.sub(r'`(.+?)`', r'\1', text) + + # 7. 移除 Markdown 链接 ([text](url) -> text) + text = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', text) + + # 8. 清理多余的换行符 + # 将多个连续的换行符(\n\n\n...)替换为单个换行符 + text = re.sub(r'\n{3,}', '\n\n', text) + + # 9. 清理行首行尾的空白字符 + text = re.sub(r'^\s+|\s+$', '', text, flags=re.MULTILINE) + + # 10. 移除多余的空格(连续多个空格替换为单个空格) + text = re.sub(r' {2,}', ' ', text) + + # 11. 清理首尾空白 + text = text.strip() + + return text + + # 10. 投资日历-事件接口(增强版) @app.route('/api/calendar/events', methods=['GET']) -@token_required -@pro_or_max_required def api_calendar_events(): """投资日历事件接口 - 连接 future_events 表 (修正版)""" try: @@ -4895,10 +5293,15 @@ def api_calendar_events(): elif search_query.lower() in str(related_concepts).lower(): highlight_match = 'concepts' + # 清理 Markdown 符号和多余的换行符 + cleaned_former = clean_markdown_text(event.former) + cleaned_forecast = clean_markdown_text(event.forecast) + cleaned_fact = clean_markdown_text(event.fact) + event_dict = { 'id': event.data_id, 'title': event.title, - 'description': f"前值: {event.former}, 预测: {event.forecast}, 实际: {event.fact}" if event.former or event.forecast or event.fact else "", + 'description': f"前值: {cleaned_former}, 预测: {cleaned_forecast}, 实际: {cleaned_fact}" if cleaned_former or cleaned_forecast or cleaned_fact else "", 'start_time': event.calendar_time.isoformat() if event.calendar_time else None, 'end_time': None, # future_events 表没有结束时间 'category': { @@ -4914,9 +5317,9 @@ def api_calendar_events(): 'related_avg_chg': round(related_avg_chg, 2), 'related_max_chg': round(related_max_chg, 2), 'related_week_chg': round(related_week_chg, 2), - 'former': event.former, - 'forecast': event.forecast, - 'fact': event.fact + 'former': cleaned_former, + 'forecast': cleaned_forecast, + 'fact': cleaned_fact } # 可选:添加搜索匹配标记 @@ -4948,8 +5351,8 @@ def api_calendar_events(): # 11. 投资日历-数据接口 @app.route('/api/calendar/data', methods=['GET']) -@token_required -@pro_or_max_required + + def api_calendar_data(): """投资日历数据接口""" try: @@ -5136,8 +5539,8 @@ def extract_concepts_from_concepts_field(concepts_text): @app.route('/api/calendar/detail/', methods=['GET']) -@token_required -@pro_or_max_required + + def api_future_event_detail(item_id): """未来事件详情接口 - 连接 future_events 表 (修正数据解析) - 仅限 Pro/Max 会员""" try: @@ -5372,8 +5775,8 @@ def api_future_event_detail(item_id): # 13-15. 筛选弹窗接口(已有,优化格式) @app.route('/api/filter/options', methods=['GET']) -@token_required -@pro_or_max_required + + def api_filter_options(): """筛选选项接口""" try: @@ -5952,7 +6355,7 @@ if __name__ == '__main__': port=5002, debug=True, ssl_context=( - '/home/ubuntu/dify/docker/nginx/ssl/fullchain.pem', - '/home/ubuntu/dify/docker/nginx/ssl/privkey.pem' + '/etc/letsencrypt/live/api.valuefrontier.cn/fullchain.pem', + '/etc/letsencrypt/live/api.valuefrontier.cn/privkey.pem' ) )