个股论坛重做
This commit is contained in:
51
app.py
51
app.py
@@ -8053,25 +8053,48 @@ def get_stock_quotes():
|
|||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
# ==================== ClickHouse 连接池(单例模式) ====================
|
# ==================== ClickHouse 连接池(带健康检查) ====================
|
||||||
_clickhouse_client = None
|
_clickhouse_client = None
|
||||||
_clickhouse_client_lock = threading.Lock()
|
_clickhouse_client_lock = threading.Lock()
|
||||||
|
|
||||||
|
def _create_clickhouse_client():
|
||||||
|
"""创建新的 ClickHouse 客户端连接"""
|
||||||
|
return Cclient(
|
||||||
|
host='127.0.0.1',
|
||||||
|
port=9000,
|
||||||
|
user='default',
|
||||||
|
password='Zzl33818!',
|
||||||
|
database='stock',
|
||||||
|
settings={
|
||||||
|
'connect_timeout': 10,
|
||||||
|
'send_receive_timeout': 300,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def get_clickhouse_client():
|
def get_clickhouse_client():
|
||||||
"""获取 ClickHouse 客户端(单例模式,避免重复创建连接)"""
|
"""获取 ClickHouse 客户端(带健康检查和自动重连)"""
|
||||||
global _clickhouse_client
|
global _clickhouse_client
|
||||||
if _clickhouse_client is None:
|
|
||||||
with _clickhouse_client_lock:
|
with _clickhouse_client_lock:
|
||||||
if _clickhouse_client is None:
|
# 如果客户端不存在,创建新连接
|
||||||
_clickhouse_client = Cclient(
|
if _clickhouse_client is None:
|
||||||
host='127.0.0.1',
|
_clickhouse_client = _create_clickhouse_client()
|
||||||
port=9000,
|
print("[ClickHouse] 创建新连接")
|
||||||
user='default',
|
return _clickhouse_client
|
||||||
password='Zzl33818!',
|
|
||||||
database='stock'
|
# 健康检查:尝试执行简单查询
|
||||||
)
|
try:
|
||||||
print("[ClickHouse] 创建新连接(单例)")
|
_clickhouse_client.execute("SELECT 1")
|
||||||
return _clickhouse_client
|
except Exception as e:
|
||||||
|
print(f"[ClickHouse] 连接失效,正在重连: {e}")
|
||||||
|
try:
|
||||||
|
_clickhouse_client.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_clickhouse_client = _create_clickhouse_client()
|
||||||
|
print("[ClickHouse] 重连成功")
|
||||||
|
|
||||||
|
return _clickhouse_client
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/account/calendar/events', methods=['GET', 'POST'])
|
@app.route('/api/account/calendar/events', methods=['GET', 'POST'])
|
||||||
|
|||||||
@@ -1192,6 +1192,84 @@ def create_reply(post_id):
|
|||||||
return api_error(f'创建回复失败: {str(e)}', 500)
|
return api_error(f'创建回复失败: {str(e)}', 500)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 文件上传接口
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
import os
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
# 允许的图片扩展名
|
||||||
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||||
|
# 上传目录(相对于 Flask 应用根目录)
|
||||||
|
UPLOAD_FOLDER = 'public/uploads/community'
|
||||||
|
# 最大文件大小 10MB
|
||||||
|
MAX_FILE_SIZE = 10 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_file(filename):
|
||||||
|
"""检查文件扩展名是否允许"""
|
||||||
|
return '.' in filename and \
|
||||||
|
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
@community_bp.route('/upload/image', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def upload_image():
|
||||||
|
"""
|
||||||
|
上传图片
|
||||||
|
返回图片 URL,可用于帖子内容中
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return api_error('没有选择文件', 400)
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
|
|
||||||
|
if file.filename == '':
|
||||||
|
return api_error('没有选择文件', 400)
|
||||||
|
|
||||||
|
if not allowed_file(file.filename):
|
||||||
|
return api_error('不支持的文件格式,仅支持 PNG、JPG、GIF、WebP', 400)
|
||||||
|
|
||||||
|
# 检查文件大小
|
||||||
|
file.seek(0, 2) # 移动到文件末尾
|
||||||
|
size = file.tell()
|
||||||
|
file.seek(0) # 回到开头
|
||||||
|
|
||||||
|
if size > MAX_FILE_SIZE:
|
||||||
|
return api_error('文件大小超过限制(最大 10MB)', 400)
|
||||||
|
|
||||||
|
# 生成唯一文件名
|
||||||
|
ext = file.filename.rsplit('.', 1)[1].lower()
|
||||||
|
filename = f"{generate_id()}.{ext}"
|
||||||
|
|
||||||
|
# 创建日期目录
|
||||||
|
today = datetime.now().strftime('%Y%m%d')
|
||||||
|
upload_dir = os.path.join(current_app.root_path, UPLOAD_FOLDER, today)
|
||||||
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
filepath = os.path.join(upload_dir, filename)
|
||||||
|
file.save(filepath)
|
||||||
|
|
||||||
|
# 返回访问 URL
|
||||||
|
url = f"/uploads/community/{today}/{filename}"
|
||||||
|
|
||||||
|
return api_response({
|
||||||
|
'url': url,
|
||||||
|
'filename': filename,
|
||||||
|
'size': size,
|
||||||
|
'type': f'image/{ext}'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Community API] 上传图片失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return api_error(f'上传失败: {str(e)}', 500)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# ES 代理接口(前端直接查询 ES)
|
# ES 代理接口(前端直接查询 ES)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -607,3 +607,77 @@ export const togglePinMessage = async (messageId: string, isPinned: boolean): Pr
|
|||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(isPinned ? '取消置顶失败' : '置顶失败');
|
if (!response.ok) throw new Error(isPinned ? '取消置顶失败' : '置顶失败');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 文件上传相关
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
export interface UploadedImage {
|
||||||
|
url: string;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片
|
||||||
|
*/
|
||||||
|
export const uploadImage = async (file: File): Promise<UploadedImage> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}/api/community/upload/image`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || '上传图片失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 股票搜索相关
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
export interface StockSearchResult {
|
||||||
|
stock_code: string;
|
||||||
|
stock_name: string;
|
||||||
|
full_name?: string;
|
||||||
|
exchange?: string;
|
||||||
|
isIndex?: boolean;
|
||||||
|
security_type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索股票
|
||||||
|
*/
|
||||||
|
export const searchStocks = async (
|
||||||
|
query: string,
|
||||||
|
limit: number = 10,
|
||||||
|
type: 'all' | 'stock' | 'index' = 'stock'
|
||||||
|
): Promise<StockSearchResult[]> => {
|
||||||
|
if (!query.trim()) return [];
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
limit: String(limit),
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}/api/stocks/search?${params}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('搜索股票失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.results || [];
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user