个股论坛重做
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
|
||||
|
||||
|
||||
# ==================== ClickHouse 连接池(单例模式) ====================
|
||||
# ==================== ClickHouse 连接池(带健康检查) ====================
|
||||
_clickhouse_client = None
|
||||
_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():
|
||||
"""获取 ClickHouse 客户端(单例模式,避免重复创建连接)"""
|
||||
"""获取 ClickHouse 客户端(带健康检查和自动重连)"""
|
||||
global _clickhouse_client
|
||||
if _clickhouse_client is None:
|
||||
with _clickhouse_client_lock:
|
||||
if _clickhouse_client is None:
|
||||
_clickhouse_client = Cclient(
|
||||
host='127.0.0.1',
|
||||
port=9000,
|
||||
user='default',
|
||||
password='Zzl33818!',
|
||||
database='stock'
|
||||
)
|
||||
print("[ClickHouse] 创建新连接(单例)")
|
||||
return _clickhouse_client
|
||||
|
||||
with _clickhouse_client_lock:
|
||||
# 如果客户端不存在,创建新连接
|
||||
if _clickhouse_client is None:
|
||||
_clickhouse_client = _create_clickhouse_client()
|
||||
print("[ClickHouse] 创建新连接")
|
||||
return _clickhouse_client
|
||||
|
||||
# 健康检查:尝试执行简单查询
|
||||
try:
|
||||
_clickhouse_client.execute("SELECT 1")
|
||||
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'])
|
||||
|
||||
@@ -1192,6 +1192,84 @@ def create_reply(post_id):
|
||||
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)
|
||||
# ============================================================
|
||||
|
||||
@@ -607,3 +607,77 @@ export const togglePinMessage = async (messageId: string, isPinned: boolean): Pr
|
||||
});
|
||||
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