update app_vx

This commit is contained in:
2025-11-13 10:20:03 +08:00
parent d64349b606
commit 1c49ddf42c
7 changed files with 3578 additions and 8772 deletions

295
app_vx.py
View File

@@ -16,6 +16,28 @@ 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
@@ -1518,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({
@@ -1597,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()
@@ -3853,17 +3863,171 @@ 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:
print(f"正在处理股票 {stock.stock_code}价格数据...")
print(f"正在组装股票 {stock.stock_code} 的数据...")
# 获取股票基本信息
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()
# 从批量查询结果中获取股票基本信息
stock_info = stock_info_map.get(stock.stock_code)
# 从批量查询结果中获取价格数据
price_info = price_data_map.get(stock.stock_code)
# 使用与 get_stock_quotes 完全相同的逻辑计算涨跌幅
latest_price = None
first_price = None
change_pct = None
@@ -3875,79 +4039,20 @@ def api_event_related_stocks(event_id):
amount = None
trade_date = trading_day
try:
# 使用与 get_stock_quotes 完全相同的 SQL 查询
# 获取事件时间点的第一个价格 (first_price) 和当前时间的最后一个价格 (last_price)
data = client.execute("""
WITH first_price AS (
SELECT close
FROM stock_minute
WHERE code = %(code)s
AND timestamp >= %(start)s
AND timestamp <= %(end)s
ORDER BY timestamp
LIMIT 1
),
last_price AS (
SELECT close
FROM stock_minute
WHERE code = %(code)s
AND timestamp >= %(start)s
AND timestamp <= %(end)s
ORDER BY timestamp DESC
LIMIT 1
)
SELECT
last_price.close as last_price,
(last_price.close - first_price.close) / first_price.close * 100 as change,
first_price.close as first_price
FROM last_price
CROSS JOIN first_price
WHERE EXISTS (SELECT 1 FROM first_price)
AND EXISTS (SELECT 1 FROM last_price)
""", {
'code': stock.stock_code,
'start': start_datetime,
'end': end_datetime
})
print(f"股票 {stock.stock_code} 查询结果: {data}")
if data and data[0] and data[0][0] is not None:
latest_price = float(data[0][0])
change_pct = float(data[0][1]) if data[0][1] is not None else None
first_price = float(data[0][2]) if len(data[0]) > 2 and data[0][2] is not None else None
# 计算涨跌额
if latest_price is not None and first_price is not None:
change_amount = latest_price - first_price
# 获取额外的价格信息(开盘价、最高价、最低价等)
extra_data = client.execute("""
SELECT
open, high, low, volume, amt
FROM stock_minute
WHERE code = %(code)s
AND timestamp >= %(start)s
AND timestamp <= %(end)s
ORDER BY timestamp DESC
LIMIT 1
""", {
'code': stock.stock_code,
'start': start_datetime,
'end': end_datetime
})
if extra_data and extra_data[0]:
open_price = float(extra_data[0][0]) if extra_data[0][0] else None
high_price = float(extra_data[0][1]) if extra_data[0][1] else None
low_price = float(extra_data[0][2]) if extra_data[0][2] else None
volume = int(extra_data[0][3]) if extra_data[0][3] else None
amount = float(extra_data[0][4]) if extra_data[0][4] else None
except Exception as e:
print(f"Error fetching price data for {stock.stock_code}: {e}")
# 如果 ClickHouse 查询失败,尝试使用 TradeData 作为降级方案
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]]
@@ -3974,10 +4079,10 @@ def api_event_related_stocks(event_id):
if latest_trade.F009N:
change_amount = float(latest_trade.F009N)
except Exception as fallback_error:
print(f"Fallback query also failed for {stock.stock_code}: {fallback_error}")
print(f"降级查询也失败 {stock.stock_code}: {fallback_error}")
# 获取分时图数据
minute_chart_data = get_minute_chart_data(stock.stock_code)
# 从批量查询结果中获取分时图数据
minute_chart_data = minute_chart_map.get(stock.stock_code, [])
stock_data = {
'id': stock.id,