update app_vx
This commit is contained in:
465
app_vx.py
465
app_vx.py
@@ -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
|
||||
@@ -108,7 +111,7 @@ SMS_TEMPLATE_REGISTER = "2386557" # 注册模板
|
||||
SMS_TEMPLATE_LOGIN = "2386540" # 登录模板
|
||||
verification_codes = {}
|
||||
|
||||
#微信小程序
|
||||
# 微信小程序
|
||||
app.config['WECHAT_APP_ID'] = 'wx0edeaab76d4fa414'
|
||||
app.config['WECHAT_APP_SECRET'] = '0d0c70084f05a8c1411f6b89da7e815d'
|
||||
app.config['BASE_URL'] = 'http://43.143.189.195:5002'
|
||||
@@ -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)
|
||||
@@ -172,7 +184,7 @@ def token_required(f):
|
||||
|
||||
token_data = user_tokens.get(token)
|
||||
if not token_data:
|
||||
return jsonify({'message': 'Token无效','code':401}), 401
|
||||
return jsonify({'message': 'Token无效', 'code': 401}), 401
|
||||
|
||||
# 检查是否过期
|
||||
if token_data['expires'] < datetime.now():
|
||||
@@ -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:
|
||||
@@ -2924,11 +2964,12 @@ def get_calendar_event_counts():
|
||||
|
||||
# 修改查询以仅统计type为event的事件数量
|
||||
query = """
|
||||
SELECT DATE(calendar_time) as date, COUNT(*) as count
|
||||
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():
|
||||
"""统一的手机号登录/注册接口"""
|
||||
@@ -3345,7 +3388,7 @@ def verify_token():
|
||||
token_data = user_tokens.get(token)
|
||||
|
||||
if not token_data:
|
||||
return jsonify({'valid': False, 'message': 'Token无效','code':401}), 401
|
||||
return jsonify({'valid': False, 'message': 'Token无效', 'code': 401}), 401
|
||||
|
||||
# 检查是否过期
|
||||
if token_data['expires'] < datetime.now():
|
||||
@@ -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,17 +3856,11 @@ 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 >= (
|
||||
SELECT MAX(timestamp) - INTERVAL 1 DAY
|
||||
SELECT MAX (timestamp) - INTERVAL 1 DAY
|
||||
FROM stock_minute
|
||||
WHERE code = %(code)s
|
||||
)
|
||||
@@ -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,17 +4190,11 @@ 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 >= (
|
||||
SELECT MAX(timestamp) - INTERVAL 1 DAY
|
||||
SELECT MAX (timestamp) - INTERVAL 1 DAY
|
||||
FROM stock_minute
|
||||
WHERE code = %(code)s
|
||||
)
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user