个股论坛重做

This commit is contained in:
2026-01-06 14:17:26 +08:00
parent eb50b14b7b
commit 961d6482c2
3 changed files with 189 additions and 14 deletions

51
app.py
View File

@@ -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'])

View File

@@ -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
# ============================================================

View File

@@ -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 || [];
};