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