update app_vx

This commit is contained in:
2025-11-13 15:22:02 +08:00
parent 49b31a5a89
commit 1d9b50a94e

453
app_vx.py
View File

@@ -14,7 +14,8 @@ from functools import wraps
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
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:
@@ -24,6 +25,7 @@ except ImportError:
from werkzeug.local import LocalStack
import threading
# 创建一个兼容的 LocalStack 子类
class CompatLocalStack(LocalStack):
@property
@@ -36,6 +38,7 @@ except ImportError:
except ImportError:
return threading.get_ident
flask._app_ctx_stack = CompatLocalStack()
from flask_sqlalchemy import SQLAlchemy
@@ -137,6 +140,15 @@ MAX_MEMORY_PERCENT = 75
MEMORY_CHECK_INTERVAL = 300
MAX_CACHE_ITEMS = 50
# 申银万国行业分类缓存(启动时初始化,避免每次请求都查询数据库)
# 结构: {industry_level: {industry_name: [code_prefix1, code_prefix2, ...]}}
SYWG_INDUSTRY_CACHE = {
2: {}, # level2: 一级行业
3: {}, # level3: 二级行业
4: {}, # level4: 三级行业
5: {} # level5: 四级行业
}
# 初始化扩展
db = SQLAlchemy(app)
mail = Mail(app)
@@ -193,8 +205,6 @@ def token_required(f):
return decorated_function
def beijing_now():
# 使用 pytz 处理时区
beijing_tz = pytz.timezone('Asia/Shanghai')
@@ -640,6 +650,7 @@ class Event(db.Model):
related_data = db.relationship('RelatedData', backref='event', lazy='dynamic')
related_concepts = db.relationship('RelatedConcepts', backref='event', lazy='dynamic')
ind_type = db.Column(db.String(255))
@property
def keywords_list(self):
"""返回解析后的关键词列表"""
@@ -871,7 +882,6 @@ class CompanyInfo(db.Model):
F032V = db.Column(db.String(60)) # CSRC industry second level
class TradeData(db.Model):
__tablename__ = 'ea_trade'
@@ -901,12 +911,88 @@ class SectorInfo(db.Model):
F005V = db.Column(db.String(50)) # Sector level 2 name
F006V = db.Column(db.String(50)) # Sector level 3 name
F007V = db.Column(db.String(50)) # Sector level 4 name
def init_sywg_industry_cache():
"""
初始化申银万国行业分类缓存
在程序启动时调用,将所有行业分类数据加载到内存中
"""
global SYWG_INDUSTRY_CACHE
try:
app.logger.info('开始初始化申银万国行业分类缓存...')
# 定义层级映射关系
level_column_map = {
2: 'f004v', # level2 对应一级行业
3: 'f005v', # level3 对应二级行业
4: 'f006v', # level4 对应三级行业
5: 'f007v' # level5 对应四级行业
}
# 定义代码前缀长度映射
prefix_length_map = {
2: 3, # S + 2位
3: 5, # S + 2位 + 2位
4: 7, # S + 2位 + 2位 + 2位
5: 9 # 完整代码
}
# 遍历所有层级
for level, column_name in level_column_map.items():
# 查询该层级的所有行业及其代码
query_sql = f"""
SELECT DISTINCT {column_name} as industry_name, f003v as code
FROM ea_sector
WHERE f002v = '申银万国行业分类'
AND {column_name} IS NOT NULL
AND {column_name} != ''
"""
result = db.session.execute(text(query_sql))
rows = result.fetchall()
# 构建该层级的缓存
industry_dict = {}
for row in rows:
industry_name = row[0]
code = row[1]
if industry_name and code:
# 获取代码前缀
prefix_length = prefix_length_map[level]
code_prefix = code[:prefix_length]
# 将前缀添加到对应行业的列表中
if industry_name not in industry_dict:
industry_dict[industry_name] = set()
industry_dict[industry_name].add(code_prefix)
# 将set转换为list并存储到缓存中
for industry_name, prefixes in industry_dict.items():
SYWG_INDUSTRY_CACHE[level][industry_name] = list(prefixes)
app.logger.info(f'Level {level} 缓存完成,共 {len(industry_dict)} 个行业')
# 统计总数
total_count = sum(len(industries) for industries in SYWG_INDUSTRY_CACHE.values())
app.logger.info(f'申银万国行业分类缓存初始化完成,共缓存 {total_count} 个行业分类')
except Exception as e:
app.logger.error(f'初始化申银万国行业分类缓存失败: {str(e)}')
import traceback
app.logger.error(traceback.format_exc())
def send_async_email(msg):
"""异步发送邮件"""
try:
mail.send(msg)
except Exception as e:
app.logger.error(f"Error sending async email: {str(e)}")
def verify_sms_code(phone_number, code):
"""验证短信验证码"""
stored_code = session.get('sms_verification_code')
@@ -1039,7 +1125,6 @@ def update_profile():
return jsonify({'success': False, 'message': '更新失败,请重试'})
# 投资偏好设置
@app.route('/settings/investment_preferences', methods=['POST'])
@token_required
@@ -1119,15 +1204,13 @@ def get_daily_kline(stock_code, event_datetime, stock_name):
with engine.connect() as conn:
# 获取事件日期前后的数据
kline_sql = """
WITH date_range AS (
SELECT TRADEDATE
FROM ea_trade
WHERE SECCODE = :stock_code
AND TRADEDATE BETWEEN DATE_SUB(:trade_date, INTERVAL 60 DAY)
AND :trade_date
GROUP BY TRADEDATE
ORDER BY TRADEDATE
)
WITH date_range AS (SELECT TRADEDATE \
FROM ea_trade \
WHERE SECCODE = :stock_code \
AND TRADEDATE BETWEEN DATE_SUB(:trade_date, INTERVAL 60 DAY) \
AND :trade_date \
GROUP BY TRADEDATE \
ORDER BY TRADEDATE)
SELECT t.TRADEDATE,
CAST(t.F003N AS FLOAT) as open,
CAST(t.F007N AS FLOAT) as close,
@@ -1135,9 +1218,10 @@ def get_daily_kline(stock_code, event_datetime, stock_name):
CAST(t.F006N AS FLOAT) as low,
CAST(t.F004N AS FLOAT) as volume
FROM ea_trade t
JOIN date_range d ON t.TRADEDATE = d.TRADEDATE
JOIN date_range d \
ON t.TRADEDATE = d.TRADEDATE
WHERE t.SECCODE = :stock_code
ORDER BY t.TRADEDATE
ORDER BY t.TRADEDATE \
"""
result = conn.execute(text(kline_sql), {
@@ -1166,7 +1250,7 @@ def get_daily_kline(stock_code, event_datetime, stock_name):
AND F006N IS NOT NULL
AND F004N IS NOT NULL
ORDER BY TRADEDATE
LIMIT 100
LIMIT 100 \
"""
result = conn.execute(text(fallback_sql), {
@@ -1271,7 +1355,7 @@ def get_minute_kline(stock_code, event_datetime, stock_name):
WHERE SECCODE = :stock_code
AND TRADEDATE = :prev_date
AND F007N IS NOT NULL
LIMIT 1
LIMIT 1 \
"""
result = conn.execute(text(sql), {
"stock_code": stock_code_short,
@@ -1286,10 +1370,11 @@ def get_minute_kline(stock_code, event_datetime, stock_name):
SELECT CAST(F007N AS FLOAT) as close, TRADEDATE
FROM ea_trade
WHERE SECCODE = :stock_code
AND TRADEDATE < :target_date
AND TRADEDATE \
< :target_date
AND F007N IS NOT NULL
ORDER BY TRADEDATE DESC
LIMIT 1
LIMIT 1 \
"""
result = conn.execute(text(fallback_sql), {
"stock_code": stock_code_short,
@@ -1339,16 +1424,11 @@ def get_minute_kline(stock_code, event_datetime, stock_name):
# 获取目标日期的完整交易时段数据
data = client.execute("""
SELECT
timestamp,
open,
high,
low,
close,
volume,
amt
timestamp, open, high, low, close, volume, amt
FROM stock_minute
WHERE code = %(code)s
AND timestamp BETWEEN %(start)s AND %(end)s
AND timestamp BETWEEN %(start)s
AND %(end)s
ORDER BY timestamp
""", {
'code': stock_code,
@@ -1398,6 +1478,7 @@ def get_minute_kline(stock_code, event_datetime, stock_name):
return jsonify(response_data)
class HistoricalEvent(db.Model):
"""历史事件模型"""
id = db.Column(db.Integer, primary_key=True)
@@ -1433,7 +1514,6 @@ class HistoricalEventStock(db.Model):
)
@app.route('/event/follow/<int:event_id>', methods=['POST'])
@token_required
def follow_event(event_id):
@@ -1540,7 +1620,6 @@ def like_post(post_id):
post.likes_count += 1
message = '已点赞'
db.session.commit()
return jsonify({
'success': True,
@@ -1616,7 +1695,6 @@ def add_comment(post_id):
db.session.add(comment)
post.comments_count += 1
db.session.commit()
return jsonify({
@@ -1637,7 +1715,6 @@ def add_comment(post_id):
@app.route('/post/comments/<int:post_id>')
def get_comments(post_id):
"""获取帖子评论列表"""
page = request.args.get('page', 1, type=int)
@@ -2021,8 +2098,6 @@ def get_limit_rate(stock_code):
@app.route('/api/events', methods=['GET'])
def api_get_events():
"""
获取事件列表API - 优化版本(保持完全兼容)
@@ -2170,51 +2245,16 @@ def api_get_events():
]
if industry_classification not in classification_systems:
# 根据层级和名称查询对应的行业代码
# 前端发送的level值直接对应数据库字段
# level=2 -> f004v一级行业
# level=3 -> f005v二级行业
# level=4 -> f006v三级行业
# level=5 -> f007v四级行业
level_column_map = {
2: 'f004v', # level2 对应一级行业
3: 'f005v', # level3 对应二级行业
4: 'f006v', # level4 对应三级行业
5: 'f007v' # level5 对应四级行业
}
# 使用内存缓存获取行业代码前缀(性能优化:避免每次请求都查询数据库)
# 前端发送的level值
# level=2 -> 一级行业
# level=3 -> 二级行业
# level=4 -> 三级行业
# level=5 -> 四级行业
if industry_level in level_column_map:
level_column = level_column_map[industry_level]
# 查询所有匹配该行业名称的代码
sector_codes_sql = f"""
SELECT DISTINCT f003v
FROM ea_sector
WHERE f002v = '申银万国行业分类'
AND {level_column} = :industry_name
"""
result = db.session.execute(
text(sector_codes_sql),
{'industry_name': industry_classification}
)
matching_codes = [row[0] for row in result.fetchall()]
if matching_codes:
# 根据层级确定代码前缀长度
# 申银万国代码规则S + 2位一级 + 2位二级 + 2位三级 + 2位四级
prefix_length_map = {
2: 3, # level2: S + 2位一级行业
3: 5, # level3: S + 2位 + 2位二级行业
4: 7, # level4: S + 2位 + 2位 + 2位三级行业
5: 9 # level5: 完整代码(四级行业)
}
prefix_length = prefix_length_map.get(industry_level, 9)
# 获取所有代码的共同前缀(用于模糊匹配)
code_prefixes = list(set([code[:prefix_length] for code in matching_codes if code]))
if industry_level in SYWG_INDUSTRY_CACHE:
# 直接从缓存中获取代码前缀列表
code_prefixes = SYWG_INDUSTRY_CACHE[industry_level].get(industry_classification, [])
if code_prefixes:
# 构建查询条件查找related_industries中包含这些前缀的事件
@@ -2908,9 +2948,9 @@ def get_event_class(count):
return 'bg-gradient-info'
else:
return 'bg-gradient-success'
@app.route('/api/calendar-event-counts')
def get_calendar_event_counts():
"""获取整月的事件数量统计仅统计type为event的事件"""
try:
@@ -2926,9 +2966,10 @@ def get_calendar_event_counts():
query = """
SELECT DATE (calendar_time) as date, COUNT (*) as count
FROM future_events
WHERE calendar_time BETWEEN :start_date AND :end_date
WHERE calendar_time BETWEEN :start_date \
AND :end_date
AND type = 'event'
GROUP BY DATE(calendar_time)
GROUP BY DATE (calendar_time) \
"""
result = db.session.execute(text(query), {
@@ -2949,7 +2990,6 @@ def get_calendar_event_counts():
return jsonify({'error': str(e)}), 500
def get_full_avatar_url(avatar_url):
"""
统一处理头像URL确保返回完整的可访问URL
@@ -2996,12 +3036,11 @@ def to_dict(self):
'last_seen': self.last_seen.isoformat() if self.last_seen else None
}
# ==================== 标准化API接口 ====================
# 1. 首页接口
@app.route('/api/home', methods=['GET'])
def api_home():
try:
seven_days_ago = datetime.now() - timedelta(days=7)
@@ -3031,10 +3070,10 @@ def api_home():
# 获取最新交易日数据
latest_trade = db.session.execute(text("""
SELECT * FROM ea_trade
SELECT *
FROM ea_trade
WHERE SECCODE = :stock_code
ORDER BY TRADEDATE DESC
LIMIT 1
ORDER BY TRADEDATE DESC LIMIT 1
"""), {"stock_code": stock_code}).first()
week_change = 0
@@ -3051,11 +3090,11 @@ def api_home():
# 获取最近5条交易记录
week_ago_trades = db.session.execute(text("""
SELECT * FROM ea_trade
SELECT *
FROM ea_trade
WHERE SECCODE = :stock_code
AND TRADEDATE < :latest_date
ORDER BY TRADEDATE DESC
LIMIT 5
ORDER BY TRADEDATE DESC LIMIT 5
"""), {
"stock_code": stock_code,
"latest_date": latest_date
@@ -3128,6 +3167,7 @@ def api_home():
"data": None
}), 500
@app.route('/api/auth/logout', methods=['POST'])
def logout_with_token():
"""使用token登出"""
@@ -3146,6 +3186,8 @@ def logout_with_token():
session.clear()
return jsonify({'message': '登出成功'}), 200
def send_sms_code(phone, code, template_id):
"""发送短信验证码"""
try:
@@ -3173,10 +3215,12 @@ def send_sms_code(phone, code, template_id):
print(f"SMS Error: {err}")
return False
def generate_verification_code():
"""生成6位数字验证码"""
return ''.join(random.choices(string.digits, k=6))
@app.route('/api/auth/send-sms', methods=['POST'])
def send_sms_verification():
"""发送手机验证码(统一接口,自动判断场景)"""
@@ -3218,7 +3262,6 @@ def generate_token(length=32):
return ''.join(secrets.choice(characters) for _ in range(length))
@app.route('/api/auth/login/phone', methods=['POST'])
def login_with_phone():
"""统一的手机号登录/注册接口"""
@@ -3367,8 +3410,6 @@ def verify_token():
}), 200
def generate_jwt_token(user_id):
"""
生成JWT Token - 与原系统保持一致
@@ -3389,8 +3430,6 @@ def generate_jwt_token(user_id):
return token
@app.route('/api/auth/login/wechat', methods=['POST'])
def api_login_wechat():
try:
@@ -3489,7 +3528,6 @@ def api_login_wechat():
user = None
is_new_user = False
logger.info(f"开始查找用户 - UnionID: {unionid}, OpenID: {openid[:8]}...")
if unionid:
@@ -3801,13 +3839,7 @@ def api_event_related_stocks(event_id):
# 获取最新交易日的分时数据
data = client.execute("""
SELECT
timestamp,
open,
high,
low,
close,
volume,
amt
timestamp, open, high, low, close, volume, amt
FROM stock_minute
WHERE code = %(code)s
AND timestamp >= %(start)s
@@ -3824,13 +3856,7 @@ def api_event_related_stocks(event_id):
# 获取最近的交易日数据
recent_data = client.execute("""
SELECT
timestamp,
open,
high,
low,
close,
volume,
amt
timestamp, open, high, low, close, volume, amt
FROM stock_minute
WHERE code = %(code)s
AND timestamp >= (
@@ -3894,44 +3920,35 @@ def api_event_related_stocks(event_id):
# 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
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 (
) \
, 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
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,
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
WHERE fp.rn = 1 \
AND lp.rn = 1 \
"""
price_data = client.execute(batch_price_query, {
@@ -3973,20 +3990,12 @@ def api_event_related_stocks(event_id):
# 3.2 批量查询分时图数据
print(f"批量查询分时图数据...")
minute_chart_query = """
SELECT
code,
timestamp,
open,
high,
low,
close,
volume,
amt
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
ORDER BY code, timestamp \
"""
minute_data = client.execute(minute_chart_query, {
@@ -4153,8 +4162,6 @@ def api_event_related_stocks(event_id):
@app.route('/api/stock/<stock_code>/minute-chart', methods=['GET'])
def get_minute_chart_data(stock_code):
"""获取股票分时图数据 - 仅限 Pro/Max 会员"""
client = get_clickhouse_client()
@@ -4166,13 +4173,7 @@ def get_minute_chart_data(stock_code):
# 获取最新交易日的分时数据
data = client.execute("""
SELECT
timestamp,
open,
high,
low,
close,
volume,
amt
timestamp, open, high, low, close, volume, amt
FROM stock_minute
WHERE code = %(code)s
AND timestamp >= %(start)s
@@ -4189,13 +4190,7 @@ def get_minute_chart_data(stock_code):
# 获取最近的交易日数据
recent_data = client.execute("""
SELECT
timestamp,
open,
high,
low,
close,
volume,
amt
timestamp, open, high, low, close, volume, amt
FROM stock_minute
WHERE code = %(code)s
AND timestamp >= (
@@ -4229,8 +4224,6 @@ def get_minute_chart_data(stock_code):
@app.route('/api/event/<int:event_id>/stock/<stock_code>/detail', methods=['GET'])
def api_stock_detail(event_id, stock_code):
"""个股详情接口 - 仅限 Pro/Max 会员"""
try:
@@ -4417,6 +4410,7 @@ def api_stock_detail(event_id, stock_code):
'data': None
}), 500
def get_stock_minute_chart_data(stock_code):
"""获取股票分时图数据"""
try:
@@ -4452,16 +4446,11 @@ def get_stock_minute_chart_data(stock_code):
# 获取分时数据
data = client.execute("""
SELECT
timestamp,
open,
high,
low,
close,
volume,
amt
timestamp, open, high, low, close, volume, amt
FROM stock_minute
WHERE code = %(code)s
AND timestamp BETWEEN %(start)s AND %(end)s
AND timestamp BETWEEN %(start)s
AND %(end)s
ORDER BY timestamp
""", {
'code': stock_code,
@@ -4490,8 +4479,6 @@ def get_stock_minute_chart_data(stock_code):
# 7. 事件详情-相关概念接口
@app.route('/api/event/<int:event_id>/related-concepts', methods=['GET'])
def api_event_related_concepts(event_id):
"""事件相关概念接口"""
try:
@@ -4533,8 +4520,6 @@ def api_event_related_concepts(event_id):
# 8. 事件详情-历史事件接口
@app.route('/api/event/<int:event_id>/historical-events', methods=['GET'])
def api_event_historical_events(event_id):
"""事件历史事件接口"""
try:
@@ -4634,8 +4619,6 @@ def api_event_historical_events(event_id):
@app.route('/api/event/<int:event_id>/comments', methods=['GET'])
def get_event_comments(event_id):
"""获取事件的所有评论和帖子(嵌套格式)
@@ -4889,8 +4872,6 @@ def get_event_comments(event_id):
@app.route('/api/comment/<int:comment_id>/replies', methods=['GET'])
def get_comment_replies(comment_id):
"""获取某条评论的所有回复
@@ -5033,6 +5014,24 @@ def get_comment_replies(comment_id):
}), 500
# 工具函数:处理转义字符,保留 Markdown 格式
def unescape_markdown_text(text):
"""
将数据库中存储的转义字符串转换为真正的换行符和特殊字符
例如:'\\n\\n#### 标题' -> '\n\n#### 标题'
"""
if not text:
return text
# 将转义的换行符转换为真正的换行符
# 注意:这里处理的是字符串字面量 '\\n',不是转义序列
text = text.replace('\\n', '\n')
text = text.replace('\\r', '\r')
text = text.replace('\\t', '\t')
return text.strip()
# 工具函数:清理 Markdown 文本
def clean_markdown_text(text):
"""清理文本中的 Markdown 符号和多余的换行符
@@ -5105,20 +5104,19 @@ def api_calendar_events():
# 构建基础查询 - 使用 future_events 表
query = """
SELECT
data_id,
calendar_time,
type,
star,
title,
former,
forecast,
fact,
related_stocks,
concepts,
SELECT data_id, \
calendar_time, \
type, \
star, \
title, \
former, \
forecast, \
fact, \
related_stocks, \
concepts, \
inferred_tag
FROM future_events
WHERE 1=1
WHERE 1 = 1 \
"""
params = {}
@@ -5157,7 +5155,9 @@ def api_calendar_events():
# 总数统计(不包含分页)
count_query = """
SELECT COUNT(*) as count FROM future_events WHERE 1=1
SELECT COUNT(*) as count \
FROM future_events \
WHERE 1=1 \
"""
count_params = params.copy()
count_params.pop('limit', None)
@@ -5229,8 +5229,7 @@ def api_calendar_events():
SELECT F007N as close_price, F010N as change_pct, TRADEDATE
FROM ea_trade
WHERE SECCODE LIKE :stock_code_pattern
ORDER BY TRADEDATE DESC
LIMIT 7
ORDER BY TRADEDATE DESC LIMIT 7 \
"""
trade_result = db.session.execute(text(trade_query),
{'stock_code_pattern': f'{clean_code}%'})
@@ -5293,10 +5292,10 @@ 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)
# 将转义的换行符转换为真正的换行符,保留 Markdown 格式
cleaned_former = unescape_markdown_text(event.former)
cleaned_forecast = unescape_markdown_text(event.forecast)
cleaned_fact = unescape_markdown_text(event.fact)
event_dict = {
'id': event.data_id,
@@ -5351,8 +5350,6 @@ def api_calendar_events():
# 11. 投资日历-数据接口
@app.route('/api/calendar/data', methods=['GET'])
def api_calendar_data():
"""投资日历数据接口"""
try:
@@ -5382,17 +5379,16 @@ def api_calendar_data():
data_list1 = query1.order_by(RelatedData.created_at.desc()).all()
query2_sql = """
SELECT
data_id as id,
title,
type as data_type,
former,
forecast,
fact,
star,
SELECT data_id as id, \
title, \
type as data_type, \
former, \
forecast, \
fact, \
star, \
calendar_time as created_at
FROM future_events
WHERE type = 'data'
WHERE type = 'data' \
"""
# 添加时间筛选条件
@@ -5498,6 +5494,7 @@ def api_calendar_data():
'data': None
}), 500
# 12. 投资日历-详情接口
def extract_concepts_from_concepts_field(concepts_text):
"""从concepts字段中提取概念信息"""
@@ -5539,26 +5536,23 @@ def extract_concepts_from_concepts_field(concepts_text):
@app.route('/api/calendar/detail/<int:item_id>', methods=['GET'])
def api_future_event_detail(item_id):
"""未来事件详情接口 - 连接 future_events 表 (修正数据解析) - 仅限 Pro/Max 会员"""
try:
# 从 future_events 表查询事件详情
query = """
SELECT
data_id,
calendar_time,
type,
star,
title,
former,
forecast,
fact,
related_stocks,
SELECT data_id, \
calendar_time, \
type, \
star, \
title, \
former, \
forecast, \
fact, \
related_stocks, \
concepts
FROM future_events
WHERE data_id = :item_id
WHERE data_id = :item_id \
"""
result = db.session.execute(text(query), {'item_id': item_id})
@@ -5662,8 +5656,7 @@ def api_future_event_detail(item_id):
SELECT F004V as sw_primary_sector
FROM ea_sector
WHERE SECCODE LIKE :stock_code_pattern
AND F002V = '申银万国行业分类'
LIMIT 1
AND F002V = '申银万国行业分类' LIMIT 1 \
"""
sector_result = db.session.execute(text(sector_query),
{'stock_code_pattern': f'{clean_code}%'})
@@ -5681,8 +5674,7 @@ def api_future_event_detail(item_id):
SELECT F007N as close_price, F010N as change_pct, TRADEDATE
FROM ea_trade
WHERE SECCODE LIKE :stock_code_pattern
ORDER BY TRADEDATE DESC
LIMIT 7
ORDER BY TRADEDATE DESC LIMIT 7 \
"""
trade_result = db.session.execute(text(trade_query),
{'stock_code_pattern': f'{clean_code}%'})
@@ -5775,8 +5767,6 @@ def api_future_event_detail(item_id):
# 13-15. 筛选弹窗接口(已有,优化格式)
@app.route('/api/filter/options', methods=['GET'])
def api_filter_options():
"""筛选选项接口"""
try:
@@ -6323,7 +6313,6 @@ class UserFeedback(db.Model):
}
# 通用错误处理
@app.errorhandler(404)
def api_not_found(error):
@@ -6347,8 +6336,10 @@ def api_method_not_allowed(error):
return error
if __name__ == '__main__':
# 初始化申银万国行业分类缓存
with app.app_context():
init_sywg_industry_cache()
app.run(
host='0.0.0.0',