feat: Multi-user system with PostgreSQL - WIP temporary save

This commit is contained in:
TIANHE
2026-01-14 05:29:55 +08:00
parent 996e3b38fe
commit 61a5e5e6aa
68 changed files with 91057 additions and 1920 deletions
+4 -1
View File
@@ -21,9 +21,12 @@ def register_routes(app: Flask):
from app.routes.portfolio import portfolio_bp
from app.routes.ibkr import ibkr_bp
from app.routes.mt5 import mt5_bp
from app.routes.user import user_bp
from app.routes.strategy_code import strategy_code_bp
app.register_blueprint(health_bp)
app.register_blueprint(auth_bp, url_prefix='/api/user')
app.register_blueprint(user_bp, url_prefix='/api/users') # User management
app.register_blueprint(kline_bp, url_prefix='/api/indicator')
app.register_blueprint(analysis_bp, url_prefix='/api/analysis')
app.register_blueprint(backtest_bp, url_prefix='/api/indicator')
@@ -37,4 +40,4 @@ def register_routes(app: Flask):
app.register_blueprint(portfolio_bp, url_prefix='/api/portfolio')
app.register_blueprint(ibkr_bp, url_prefix='/api/ibkr')
app.register_blueprint(mt5_bp, url_prefix='/api/mt5')
app.register_blueprint(strategy_code_bp, url_prefix='/api/strategy-code')
+1 -1
View File
@@ -32,7 +32,7 @@ def chat_message():
})
@ai_chat_bp.route('/chat/history', methods=['POST'])
@ai_chat_bp.route('/chat/history', methods=['GET'])
def get_chat_history():
"""Return empty history (compatibility stub)."""
return jsonify({'code': 1, 'msg': 'success', 'data': []})
+143 -88
View File
@@ -2,7 +2,7 @@
Analysis API routes (local-only).
Implements multi-dimensional analysis plus lightweight task/history APIs for the frontend.
"""
from flask import Blueprint, request, jsonify, Response
from flask import Blueprint, request, jsonify, Response, g
import json
import traceback
import time
@@ -11,11 +11,11 @@ from app.services.analysis import AnalysisService, reflect_analysis
from app.utils.logger import get_logger
from app.utils.db import get_db_connection
from app.utils.language import detect_request_language
from app.utils.auth import login_required
logger = get_logger(__name__)
analysis_bp = Blueprint('analysis', __name__)
DEFAULT_USER_ID = 1
def _now_ts() -> int:
return int(time.time())
@@ -23,27 +23,58 @@ def _now_ts() -> int:
def _normalize_symbol(symbol: str) -> str:
return (symbol or '').strip().upper()
def _store_task(market: str, symbol: str, model: str, language: str, status: str, result: dict = None, error_message: str = "") -> int:
now = _now_ts()
def _store_task(user_id: int, market: str, symbol: str, model: str, language: str, status: str, result: dict = None, error_message: str = "") -> int:
"""Create a new task record. For pending tasks, completed_at is NULL."""
result_json = json.dumps(result or {}, ensure_ascii=False)
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
INSERT INTO qd_analysis_tasks (user_id, market, symbol, model, language, status, result_json, error_message, created_at, completed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(DEFAULT_USER_ID, market, symbol, model or '', language or 'en-US', status, result_json, error_message or '', now, now if status in ['completed', 'failed'] else None)
)
if status in ['completed', 'failed']:
cur.execute(
"""
INSERT INTO qd_analysis_tasks (user_id, market, symbol, model, language, status, result_json, error_message, created_at, completed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
""",
(user_id, market, symbol, model or '', language or 'en-US', status, result_json, error_message or '')
)
else:
cur.execute(
"""
INSERT INTO qd_analysis_tasks (user_id, market, symbol, model, language, status, result_json, error_message, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
""",
(user_id, market, symbol, model or '', language or 'en-US', status, result_json, error_message or '')
)
task_id = cur.lastrowid
db.commit()
cur.close()
return int(task_id)
def _get_task(task_id: int) -> dict:
def _update_task(task_id: int, status: str, result: dict = None, error_message: str = "") -> bool:
"""Update an existing task with result and status."""
try:
result_json = json.dumps(result or {}, ensure_ascii=False)
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
UPDATE qd_analysis_tasks
SET status = ?, result_json = ?, error_message = ?, completed_at = NOW()
WHERE id = ?
""",
(status, result_json, error_message or '', task_id)
)
db.commit()
cur.close()
return True
except Exception as e:
logger.error(f"_update_task failed: {e}")
return False
def _get_task(task_id: int, user_id: int) -> dict:
with get_db_connection() as db:
cur = db.cursor()
cur.execute("SELECT * FROM qd_analysis_tasks WHERE id = ? AND user_id = ?", (task_id, DEFAULT_USER_ID))
cur.execute("SELECT * FROM qd_analysis_tasks WHERE id = ? AND user_id = ?", (task_id, user_id))
row = cur.fetchone()
cur.close()
return row or None
@@ -60,16 +91,25 @@ def _parse_result_json(row: dict) -> dict:
@analysis_bp.route('/multi', methods=['POST'])
@analysis_bp.route('/multiAnalysis', methods=['POST']) # compatibility with legacy naming
@login_required
def multi_analysis():
"""
Multi-dimensional analysis.
Multi-dimensional analysis for the current user.
Request body:
market: Market (AShare, USStock, HShare, Crypto, Forex, Futures)
symbol: Symbol
language: Optional; if omitted we will detect from request headers (X-App-Lang / Accept-Language)
"""
task_id = None
user_id = None
market = ''
symbol = ''
model = None
language = 'en-US'
try:
user_id = g.user_id
data = request.get_json()
if not data:
return jsonify({
@@ -100,49 +140,64 @@ def multi_analysis():
logger.info(f"Analyze request: {market}:{symbol}, use_multi_agent={use_multi_agent}, model={model}")
# Create analysis service instance (local-only; no paid credits)
service = AnalysisService(use_multi_agent=use_multi_agent)
result = service.analyze(market, symbol, language, model=model, timeframe=timeframe)
# Step 1: Create a "pending" task record first (so user can see progress in history)
task_id = _store_task(user_id, market, symbol, model or '', language, 'pending', result={}, error_message='')
# Step 2: Run analysis in background thread (so user can navigate away)
import threading
def run_analysis_background(task_id_inner, market_inner, symbol_inner, language_inner, model_inner, timeframe_inner, use_multi_agent_inner):
"""Execute analysis in background and update task when done."""
try:
service = AnalysisService(use_multi_agent=use_multi_agent_inner)
result = service.analyze(market_inner, symbol_inner, language_inner, model=model_inner, timeframe=timeframe_inner)
_update_task(task_id_inner, 'completed', result=result, error_message='')
logger.info(f"Background analysis completed for task {task_id_inner}")
except Exception as e:
logger.error(f"Background analysis failed for task {task_id_inner}: {e}")
_update_task(task_id_inner, 'failed', result={}, error_message=str(e))
analysis_thread = threading.Thread(
target=run_analysis_background,
args=(task_id, market, symbol, language, model, timeframe, use_multi_agent),
daemon=False # Keep running even if main request thread ends
)
analysis_thread.start()
# Persist as "completed" history (no paid credits in local mode).
task_id = _store_task(market, symbol, model or '', language, 'completed', result=result, error_message='')
# Keep frontend compatible: if it expects task polling, it can still use the id.
result_payload = dict(result or {})
result_payload['task_id'] = task_id
return jsonify({'code': 1, 'msg': 'success', 'data': result_payload})
# Step 3: Return immediately with task_id (frontend will poll for results)
return jsonify({'code': 1, 'msg': 'success', 'data': {'task_id': task_id, 'status': 'pending'}})
except Exception as e:
logger.error(f"Analysis failed: {str(e)}")
logger.error(traceback.format_exc())
# Update existing task as "failed", or create a new failed record if task_id doesn't exist
try:
market = (data or {}).get('market', '') if 'data' in locals() else ''
symbol = (data or {}).get('symbol', '') if 'data' in locals() else ''
language = detect_request_language(request, body=(data or {}), default='en-US')
model = (data or {}).get('model', '') if 'data' in locals() else ''
market = str(market).strip()
symbol = _normalize_symbol(symbol)
_store_task(market, symbol, model, language, 'failed', result={}, error_message=str(e))
if task_id:
_update_task(task_id, 'failed', result={}, error_message=str(e))
elif user_id:
_store_task(user_id, market, symbol, model or '', language, 'failed', result={}, error_message=str(e))
except Exception:
pass
return jsonify({
'code': 0,
'msg': f'Analysis failed: {str(e)}',
'data': None
'data': {'task_id': task_id} if task_id else None
}), 500
@analysis_bp.route('/getTaskStatus', methods=['POST'])
@analysis_bp.route('/getTaskStatus', methods=['GET'])
@login_required
def get_task_status():
"""Frontend compatibility: return task status + result by task_id."""
"""Frontend compatibility: return task status + result by task_id for the current user."""
try:
data = request.get_json() or {}
task_id = int(data.get('task_id') or 0)
user_id = g.user_id
task_id = int(request.args.get('task_id') or 0)
if not task_id:
return jsonify({'code': 0, 'msg': 'Missing task_id', 'data': None}), 400
row = _get_task(task_id)
row = _get_task(task_id, user_id)
if not row:
return jsonify({'code': 0, 'msg': 'Task not found', 'data': None}), 404
@@ -161,20 +216,21 @@ def get_task_status():
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@analysis_bp.route('/getHistoryList', methods=['POST'])
@analysis_bp.route('/getHistoryList', methods=['GET'])
@login_required
def get_history_list():
"""Frontend compatibility: paginated analysis history for the single user."""
"""Frontend compatibility: paginated analysis history for the current user."""
try:
data = request.get_json() or {}
page = int(data.get('page') or 1)
pagesize = int(data.get('pagesize') or 20)
user_id = g.user_id
page = int(request.args.get('page') or 1)
pagesize = int(request.args.get('pagesize') or 20)
page = max(page, 1)
pagesize = min(max(pagesize, 1), 100)
offset = (page - 1) * pagesize
with get_db_connection() as db:
cur = db.cursor()
cur.execute("SELECT COUNT(1) as cnt FROM qd_analysis_tasks WHERE user_id = ?", (DEFAULT_USER_ID,))
cur.execute("SELECT COUNT(1) as cnt FROM qd_analysis_tasks WHERE user_id = ?", (user_id,))
total = int((cur.fetchone() or {}).get('cnt') or 0)
cur.execute(
"""
@@ -184,7 +240,7 @@ def get_history_list():
ORDER BY id DESC
LIMIT ? OFFSET ?
""",
(DEFAULT_USER_ID, pagesize, offset)
(user_id, pagesize, offset)
)
rows = cur.fetchall() or []
cur.close()
@@ -192,6 +248,11 @@ def get_history_list():
out = []
for r in rows:
has_result = bool((r.get('result_json') or '').strip())
# Convert datetime to Unix timestamp for frontend compatibility
created_at = r.get('created_at')
completed_at = r.get('completed_at')
createtime = int(created_at.timestamp()) if created_at else 0
completetime = int(completed_at.timestamp()) if completed_at else None
out.append({
'id': r.get('id'),
'market': r.get('market'),
@@ -200,8 +261,8 @@ def get_history_list():
'status': r.get('status'),
'has_result': has_result,
'error_message': r.get('error_message') or '',
'createtime': int(r.get('created_at') or 0),
'completetime': int(r.get('completed_at') or 0) if r.get('completed_at') else None
'createtime': createtime,
'completetime': completetime
})
return jsonify({'code': 1, 'msg': 'success', 'data': {'list': out, 'total': total}})
@@ -211,13 +272,46 @@ def get_history_list():
return jsonify({'code': 0, 'msg': str(e), 'data': {'list': [], 'total': 0}}), 500
@analysis_bp.route('/deleteTask', methods=['POST'])
@login_required
def delete_task():
"""Delete an analysis task by task_id for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
task_id = int(data.get('task_id') or 0)
if not task_id:
return jsonify({'code': 0, 'msg': 'Missing task_id', 'data': None}), 400
# Verify task belongs to user
row = _get_task(task_id, user_id)
if not row:
return jsonify({'code': 0, 'msg': 'Task not found', 'data': None}), 404
# Delete the task
with get_db_connection() as db:
cur = db.cursor()
cur.execute("DELETE FROM qd_analysis_tasks WHERE id = ? AND user_id = ?", (task_id, user_id))
db.commit()
cur.close()
return jsonify({'code': 1, 'msg': 'success', 'data': {'deleted_id': task_id}})
except Exception as e:
logger.error(f"delete_task failed: {str(e)}")
logger.error(traceback.format_exc())
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@analysis_bp.route('/createTask', methods=['POST'])
@login_required
def create_task():
"""
Compatibility endpoint for legacy frontend.
In local-only mode we do not run a separate async worker; we create a completed task record immediately.
"""
try:
user_id = g.user_id
data = request.get_json() or {}
market = str((data.get('market') or '')).strip()
symbol = _normalize_symbol(data.get('symbol'))
@@ -228,7 +322,7 @@ def create_task():
return jsonify({'code': 0, 'msg': 'Missing market or symbol', 'data': None}), 400
# Create a placeholder "pending" task so frontend can show task_id if it needs it.
task_id = _store_task(market, symbol, str(model), language, 'pending', result={}, error_message='')
task_id = _store_task(user_id, market, symbol, str(model), language, 'pending', result={}, error_message='')
return jsonify({'code': 1, 'msg': 'success', 'data': {'task_id': task_id, 'status': 'pending'}})
except Exception as e:
logger.error(f"create_task failed: {str(e)}")
@@ -236,47 +330,8 @@ def create_task():
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@analysis_bp.route('/stream', methods=['POST'])
def stream_analysis():
"""Streaming analysis (SSE)."""
try:
data = request.get_json()
if not data:
return jsonify({'code': 0, 'msg': 'Request body is required'}), 400
market = data.get('market', '')
symbol = data.get('symbol', '')
language = detect_request_language(request, body=data, default='en-US')
use_multi_agent = data.get('use_multi_agent', None)
timeframe = data.get('timeframe', '1D')
def generate():
try:
yield f"data: {json.dumps({'status': 'started', 'message': 'Analysis started'})}\n\n"
service = AnalysisService(use_multi_agent=use_multi_agent)
result = service.analyze(market, symbol, language, timeframe=timeframe)
yield f"data: {json.dumps({'status': 'completed', 'data': result})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n\n"
return Response(
generate(),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no'
}
)
except Exception as e:
logger.error(f"Streaming analysis failed: {str(e)}")
return jsonify({'code': 0, 'msg': str(e)}), 500
@analysis_bp.route('/reflect', methods=['POST'])
@login_required
def reflect():
"""
Reflection API.
+147 -38
View File
@@ -1,15 +1,38 @@
"""
Authentication API Routes
from flask import Blueprint, request, jsonify
Handles login, logout, and user info retrieval.
Supports both multi-user (database) and single-user (legacy) modes.
"""
import os
from flask import Blueprint, request, jsonify, g
from app.config.settings import Config
from app.utils.auth import generate_token
from app.utils.auth import generate_token, login_required, authenticate_legacy
from app.utils.logger import get_logger
auth_bp = Blueprint('auth', __name__)
logger = get_logger(__name__)
auth_bp = Blueprint('auth', __name__)
def _is_single_user_mode() -> bool:
"""Check if system is in single-user (legacy) mode"""
return os.getenv('SINGLE_USER_MODE', 'false').lower() == 'true'
@auth_bp.route('/login', methods=['POST'])
def login():
"""Login (single-user, env-configured credentials)."""
"""
User login endpoint.
Request body:
username: str
password: str
Returns:
token: JWT token
userinfo: User information
"""
try:
data = request.get_json()
if not data:
@@ -20,49 +43,135 @@ def login():
if not username or not password:
return jsonify({'code': 400, 'msg': 'Missing username or password', 'data': None}), 400
# Validate credentials from environment / settings
if username == Config.ADMIN_USER and password == Config.ADMIN_PASSWORD:
token = generate_token(username)
if token:
return jsonify({
'code': 1,
'msg': 'Login successful',
'data': {
'token': token,
'userinfo': {
'username': username,
'nickname': 'Admin',
'avatar': ''
}
}
})
else:
return jsonify({'code': 500, 'msg': 'Token generation error', 'data': None}), 500
else:
is_demo = os.getenv('IS_DEMO_MODE', 'false').lower() == 'true'
user = None
# Try multi-user authentication first
if not _is_single_user_mode():
try:
from app.services.user_service import get_user_service
user = get_user_service().authenticate(username, password)
except Exception as e:
logger.warning(f"Multi-user auth failed, trying legacy: {e}")
# Fallback to legacy single-user mode
if not user:
user = authenticate_legacy(username, password)
if not user:
return jsonify({'code': 0, 'msg': 'Invalid credentials', 'data': None}), 401
# Generate token
token = generate_token(
user_id=user.get('id') or user.get('user_id', 1),
username=user.get('username', username),
role=user.get('role', 'admin')
)
if not token:
return jsonify({'code': 500, 'msg': 'Token generation error', 'data': None}), 500
# Build user info for frontend
userinfo = {
'id': user.get('id') or user.get('user_id', 1),
'username': user.get('username', username),
'nickname': user.get('nickname', 'User') + (' (Demo)' if is_demo else ''),
'avatar': user.get('avatar', '/avatar2.jpg'),
'is_demo': is_demo,
'role': {
'id': user.get('role', 'admin'),
'permissions': _get_permissions(user.get('role', 'admin'))
}
}
return jsonify({
'code': 1,
'msg': 'Login successful',
'data': {
'token': token,
'userinfo': userinfo
}
})
except Exception as e:
logger.error(f"Login error: {e}")
return jsonify({'code': 500, 'msg': str(e), 'data': None}), 500
@auth_bp.route('/logout', methods=['POST'])
def logout():
"""Logout (client removes token; server is stateless)."""
return jsonify({'code': 1, 'msg': 'Logout successful', 'data': None})
@auth_bp.route('/info', methods=['GET'])
def get_user_info():
"""Get user info (single-user mock)."""
return jsonify({
'code': 1,
'msg': 'Success',
'data': {
'id': 1,
'username': Config.ADMIN_USER,
'nickname': 'Admin',
'avatar': '/avatar2.jpg',
'role': {'id': 'admin', 'permissions': ['dashboard', 'exception', 'account']}
}
})
@auth_bp.route('/info', methods=['GET'])
@login_required
def get_user_info():
"""Get current user info."""
try:
is_demo = os.getenv('IS_DEMO_MODE', 'false').lower() == 'true'
user_id = getattr(g, 'user_id', 1)
username = getattr(g, 'user', Config.ADMIN_USER)
role = getattr(g, 'user_role', 'admin')
# Try to get full user info from database
user_data = None
if not _is_single_user_mode():
try:
from app.services.user_service import get_user_service
user_data = get_user_service().get_user_by_id(user_id)
except Exception as e:
logger.warning(f"Failed to get user from database: {e}")
if user_data:
return jsonify({
'code': 1,
'msg': 'Success',
'data': {
'id': user_data.get('id'),
'username': user_data.get('username'),
'nickname': user_data.get('nickname', 'User') + (' (Demo)' if is_demo else ''),
'email': user_data.get('email'),
'avatar': user_data.get('avatar', '/avatar2.jpg'),
'is_demo': is_demo,
'role': {
'id': user_data.get('role', 'user'),
'permissions': _get_permissions(user_data.get('role', 'user'))
}
}
})
# Fallback for legacy mode
return jsonify({
'code': 1,
'msg': 'Success',
'data': {
'id': user_id,
'username': username,
'nickname': 'Admin' + (' (Demo)' if is_demo else ''),
'avatar': '/avatar2.jpg',
'is_demo': is_demo,
'role': {
'id': role,
'permissions': _get_permissions(role)
}
}
})
except Exception as e:
logger.error(f"get_user_info error: {e}")
return jsonify({'code': 500, 'msg': str(e), 'data': None}), 500
def _get_permissions(role: str) -> list:
"""Get permissions list for a role"""
try:
from app.services.user_service import get_user_service
return get_user_service().get_user_permissions(role)
except Exception:
# Default permissions for admin
if role == 'admin':
return ['dashboard', 'view', 'indicator', 'backtest', 'strategy',
'portfolio', 'settings', 'user_manage', 'credentials']
return ['dashboard', 'view', 'indicator', 'backtest', 'strategy', 'portfolio']
+39 -44
View File
@@ -1,7 +1,7 @@
"""
Backtest API routes
"""
from flask import Blueprint, request, jsonify
from flask import Blueprint, request, jsonify, g
from datetime import datetime
import traceback
import json
@@ -11,6 +11,7 @@ import os
from app.services.backtest import BacktestService
from app.utils.logger import get_logger
from app.utils.db import get_db_connection
from app.utils.auth import login_required
import requests
logger = get_logger(__name__)
@@ -82,12 +83,12 @@ def _normalize_lang(lang: str | None) -> str:
return l2 if l2 in supported else "zh-CN"
@backtest_bp.route('/backtest/precision-info', methods=['POST'])
@backtest_bp.route('/backtest/precision-info', methods=['GET'])
def get_precision_info():
"""
获取回测精度信息(用于前端提示)
Params:
Params (Query String):
market: 市场类型
startDate: 开始日期 (YYYY-MM-DD)
endDate: 结束日期 (YYYY-MM-DD)
@@ -96,13 +97,10 @@ def get_precision_info():
精度信息,包含推荐的执行时间框架和预估K线数量
"""
try:
data = request.get_json()
if not data:
return jsonify({'code': 0, 'msg': 'Request body is required'}), 400
market = data.get('market', 'crypto')
start_date_str = data.get('startDate', '')
end_date_str = data.get('endDate', '')
# Use request.args for GET params
market = request.args.get('market', 'crypto')
start_date_str = request.args.get('startDate', '')
end_date_str = request.args.get('endDate', '')
if not start_date_str or not end_date_str:
return jsonify({'code': 0, 'msg': 'startDate and endDate are required'}), 400
@@ -123,9 +121,10 @@ def get_precision_info():
@backtest_bp.route('/backtest', methods=['POST'])
@login_required
def run_backtest():
"""
Run indicator backtest
Run indicator backtest for the current user.
Params:
indicatorId: Indicator ID (optional)
@@ -148,8 +147,8 @@ def run_backtest():
'data': None
}), 400
# Extract params
user_id = int(data.get('userid') or data.get('userId') or 1)
# Extract params - use current user's ID
user_id = g.user_id
indicator_code = data.get('indicatorCode', '')
indicator_id = data.get('indicatorId')
symbol = data.get('symbol', '')
@@ -267,7 +266,6 @@ def run_backtest():
# Persist backtest run for AI optimization / history
run_id = None
try:
now_ts = int(time.time())
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -276,7 +274,7 @@ def run_backtest():
(user_id, indicator_id, market, symbol, timeframe, start_date, end_date,
initial_capital, commission, slippage, leverage, trade_direction,
strategy_config, status, error_message, result_json, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
""",
(
user_id,
@@ -294,8 +292,7 @@ def run_backtest():
json.dumps(strategy_config or {}, ensure_ascii=False),
'success',
'',
json.dumps(result or {}, ensure_ascii=False),
now_ts
json.dumps(result or {}, ensure_ascii=False)
)
)
run_id = cur.lastrowid
@@ -327,9 +324,8 @@ def run_backtest():
# Best-effort persist failed run (if we have enough context)
try:
data = data if isinstance(data, dict) else {}
user_id = int(data.get('userid') or data.get('userId') or 1)
user_id = g.user_id
indicator_id = data.get('indicatorId')
now_ts = int(time.time())
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -338,7 +334,7 @@ def run_backtest():
(user_id, indicator_id, market, symbol, timeframe, start_date, end_date,
initial_capital, commission, slippage, leverage, trade_direction,
strategy_config, status, error_message, result_json, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
""",
(
user_id,
@@ -356,8 +352,7 @@ def run_backtest():
json.dumps(data.get('strategyConfig') or {}, ensure_ascii=False),
'failed',
str(e),
'',
now_ts
''
)
)
db.commit()
@@ -371,13 +366,13 @@ def run_backtest():
}), 500
@backtest_bp.route('/backtest/history', methods=['POST'])
@backtest_bp.route('/backtest/history', methods=['GET'])
@login_required
def get_backtest_history():
"""
Get backtest run history (saved in SQLite).
Get backtest run history for the current user.
Params:
userid: User ID (default 1)
Params (Query String):
limit: Page size (default 50, max 200)
offset: Offset (default 0)
indicatorId: Optional indicator id filter
@@ -386,17 +381,17 @@ def get_backtest_history():
timeframe: Optional timeframe filter
"""
try:
data = request.get_json() or {}
user_id = int(data.get('userid') or data.get('userId') or 1)
limit = int(data.get('limit') or 50)
offset = int(data.get('offset') or 0)
# Use current user's ID
user_id = g.user_id
limit = int(request.args.get('limit') or 50)
offset = int(request.args.get('offset') or 0)
limit = max(1, min(limit, 200))
offset = max(0, offset)
indicator_id = data.get('indicatorId')
symbol = (data.get('symbol') or '').strip()
market = (data.get('market') or '').strip()
timeframe = (data.get('timeframe') or '').strip()
indicator_id = request.args.get('indicatorId')
symbol = (request.args.get('symbol') or '').strip()
market = (request.args.get('market') or '').strip()
timeframe = (request.args.get('timeframe') or '').strip()
where = ["user_id = ?"]
params = [user_id]
@@ -449,19 +444,18 @@ def get_backtest_history():
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@backtest_bp.route('/backtest/get', methods=['POST'])
@backtest_bp.route('/backtest/get', methods=['GET'])
@login_required
def get_backtest_run():
"""
Get a backtest run detail by run id (includes result_json).
Get a backtest run detail by run id for the current user.
Params:
userid: User ID (default 1)
Params (Query String):
runId: Backtest run id (required)
"""
try:
data = request.get_json() or {}
user_id = int(data.get('userid') or data.get('userId') or 1)
run_id = int(data.get('runId') or 0)
user_id = g.user_id
run_id = int(request.args.get('runId') or 0)
if not run_id:
return jsonify({'code': 0, 'msg': 'runId is required', 'data': None}), 400
@@ -716,17 +710,18 @@ def _heuristic_ai_advice(runs: list[dict], lang: str) -> str:
@backtest_bp.route('/backtest/aiAnalyze', methods=['POST'])
@login_required
def ai_analyze_backtest_runs():
"""
AI analyze selected backtest runs and provide strategy_config tuning suggestions.
AI analyze selected backtest runs and provide strategy_config tuning suggestions
for the current user.
Params:
userid: User ID (default 1)
runIds: list[int] (required)
"""
try:
data = request.get_json() or {}
user_id = int(data.get('userid') or data.get('userId') or 1)
user_id = g.user_id
lang = _normalize_lang(data.get('lang'))
run_ids = data.get('runIds') or []
if not isinstance(run_ids, list) or not run_ids:
+15 -11
View File
@@ -9,17 +9,16 @@ Local deployment notes:
import time
import traceback
import json
from flask import Blueprint, request, jsonify
from flask import Blueprint, request, jsonify, g
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
from app.utils.auth import login_required
logger = get_logger(__name__)
credentials_bp = Blueprint('credentials', __name__)
DEFAULT_USER_ID = 1
def _api_key_hint(api_key: str) -> str:
if not api_key:
@@ -31,9 +30,11 @@ def _api_key_hint(api_key: str) -> str:
@credentials_bp.route('/list', methods=['GET'])
@login_required
def list_credentials():
"""List all credentials for the current user."""
try:
user_id = request.args.get('user_id', type=int) or DEFAULT_USER_ID
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
@@ -57,10 +58,12 @@ def list_credentials():
@credentials_bp.route('/create', methods=['POST'])
@login_required
def create_credential():
"""Create a new credential for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
user_id = int(data.get('user_id') or DEFAULT_USER_ID)
name = (data.get('name') or '').strip()
exchange_id = (data.get('exchange_id') or '').strip()
api_key = (data.get('api_key') or '').strip()
@@ -78,16 +81,15 @@ def create_credential():
'secret_key': secret_key,
'passphrase': passphrase
}, ensure_ascii=False)
now = int(time.time())
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
INSERT INTO qd_exchange_credentials (user_id, name, exchange_id, api_key_hint, encrypted_config, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, NOW(), NOW())
""",
(user_id, name, exchange_id, _api_key_hint(api_key), plaintext_config, now, now)
(user_id, name, exchange_id, _api_key_hint(api_key), plaintext_config)
)
new_id = cur.lastrowid
db.commit()
@@ -101,10 +103,12 @@ def create_credential():
@credentials_bp.route('/delete', methods=['DELETE'])
@login_required
def delete_credential():
"""Delete a credential for the current user."""
try:
user_id = g.user_id
cred_id = request.args.get('id', type=int)
user_id = request.args.get('user_id', type=int) or DEFAULT_USER_ID
if not cred_id:
return jsonify({'code': 0, 'msg': 'Missing id', 'data': None}), 400
@@ -125,14 +129,14 @@ def delete_credential():
@credentials_bp.route('/get', methods=['GET'])
@login_required
def get_credential():
"""
Return decrypted credential for form auto-fill.
NOTE: In a production system, this must be protected by strong authentication/authorization.
"""
try:
user_id = g.user_id
cred_id = request.args.get('id', type=int)
user_id = request.args.get('user_id', type=int) or DEFAULT_USER_ID
if not cred_id:
return jsonify({'code': 0, 'msg': 'Missing id', 'data': None}), 400
+28 -12
View File
@@ -15,10 +15,11 @@ import json
import time
from typing import Any, Dict, List, Tuple
from flask import Blueprint, jsonify, request
from flask import Blueprint, jsonify, request, g
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
from app.utils.auth import login_required
logger = get_logger(__name__)
@@ -255,19 +256,24 @@ def _compute_strategy_stats(trades: List[Dict[str, Any]], strategies: List[Dict[
@dashboard_bp.route("/summary", methods=["GET"])
@login_required
def summary():
"""
Return dashboard summary used by `quantdinger_vue/src/views/dashboard/index.vue`.
"""
try:
# Strategy counts
user_id = g.user_id
# Strategy counts (filtered by user_id)
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
SELECT id, strategy_name, strategy_type, status, initial_capital, trading_config
FROM qd_strategies_trading
"""
WHERE user_id = ?
""",
(user_id,)
)
strategies = cur.fetchall() or []
cur.close()
@@ -292,7 +298,7 @@ def summary():
if isinstance(tc, dict) and _truthy(tc.get("enable_ai_filter")):
ai_enabled_strategy_count += 1
# Positions (best-effort)
# Positions (best-effort, filtered by user_id)
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -300,8 +306,10 @@ def summary():
SELECT p.*, s.strategy_name, s.initial_capital, s.leverage, s.market_type
FROM qd_strategy_positions p
LEFT JOIN qd_strategies_trading s ON s.id = p.strategy_id
WHERE p.user_id = ?
ORDER BY p.updated_at DESC
"""
""",
(user_id,)
)
rows = cur.fetchall() or []
cur.close()
@@ -332,7 +340,7 @@ def summary():
}
)
# Recent trades (best-effort)
# Recent trades (best-effort, filtered by user_id)
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -340,9 +348,11 @@ def summary():
SELECT t.*, s.strategy_name
FROM qd_strategy_trades t
LEFT JOIN qd_strategies_trading s ON s.id = t.strategy_id
WHERE t.user_id = ?
ORDER BY t.created_at DESC
LIMIT 500
"""
""",
(user_id,)
)
recent_trades = cur.fetchall() or []
cur.close()
@@ -497,18 +507,20 @@ def summary():
@dashboard_bp.route("/pendingOrders", methods=["GET"])
@login_required
def pending_orders():
"""
Return pending orders list for dashboard page.
"""
try:
user_id = g.user_id
page = max(1, _safe_int(request.args.get("page"), 1))
page_size = max(1, min(200, _safe_int(request.args.get("pageSize"), 20)))
offset = (page - 1) * page_size
with get_db_connection() as db:
cur = db.cursor()
cur.execute("SELECT COUNT(1) AS cnt FROM pending_orders")
cur.execute("SELECT COUNT(1) AS cnt FROM pending_orders WHERE user_id = ?", (user_id,))
total = int((cur.fetchone() or {}).get("cnt") or 0)
cur.close()
@@ -525,10 +537,11 @@ def pending_orders():
s.execution_mode AS strategy_execution_mode
FROM pending_orders o
LEFT JOIN qd_strategies_trading s ON s.id = o.strategy_id
WHERE o.user_id = ?
ORDER BY o.id DESC
LIMIT %s OFFSET %s
LIMIT ? OFFSET ?
""",
(int(page_size), int(offset)),
(user_id, int(page_size), int(offset)),
)
rows = cur.fetchall() or []
cur.close()
@@ -607,18 +620,21 @@ def pending_orders():
@dashboard_bp.route("/pendingOrders/<int:order_id>", methods=["DELETE"])
@login_required
def delete_pending_order(order_id: int):
"""
Delete a pending order record (dashboard operation).
"""
try:
user_id = g.user_id
oid = int(order_id or 0)
if oid <= 0:
return jsonify({"code": 0, "msg": "invalid_id", "data": None}), 400
with get_db_connection() as db:
cur = db.cursor()
cur.execute("SELECT id, status FROM pending_orders WHERE id = %s", (oid,))
# Verify the order belongs to current user
cur.execute("SELECT id, status FROM pending_orders WHERE id = ? AND user_id = ?", (oid, user_id))
row = cur.fetchone() or {}
if not row:
cur.close()
@@ -627,7 +643,7 @@ def delete_pending_order(order_id: int):
if st == "processing":
cur.close()
return jsonify({"code": 0, "msg": "cannot_delete_processing", "data": None}), 400
cur.execute("DELETE FROM pending_orders WHERE id = %s", (oid,))
cur.execute("DELETE FROM pending_orders WHERE id = ? AND user_id = ?", (oid, user_id))
db.commit()
cur.close()
+20 -19
View File
@@ -17,12 +17,13 @@ import time
import traceback
from typing import Any, Dict, List
from flask import Blueprint, Response, jsonify, request
from flask import Blueprint, Response, jsonify, request, g
import pandas as pd
import numpy as np
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
from app.utils.auth import login_required
import requests
logger = get_logger(__name__)
@@ -115,24 +116,21 @@ def _generate_mock_df(length=200):
return df
@indicator_bp.route("/getIndicators", methods=["POST"])
@indicator_bp.route("/getIndicators", methods=["GET"])
@login_required
def get_indicators():
"""
Get indicator list for a user.
Request:
{ userid: number }
Get indicator list for the current user.
Response:
{ code: 1, data: [ ... ] }
"""
try:
data = request.get_json() or {}
user_id = int(data.get("userid") or 1)
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
# Local mode: "我的指标" should include both purchased and custom indicators.
# Get user's own indicators (both purchased and custom).
cur.execute(
"""
SELECT
@@ -156,13 +154,13 @@ def get_indicators():
@indicator_bp.route("/saveIndicator", methods=["POST"])
@login_required
def save_indicator():
"""
Create or update an indicator.
Create or update an indicator for the current user.
Request (frontend sends many extra fields; we store only the essentials):
{
userid: number,
id: number (0 for create),
name: string,
code: string,
@@ -172,7 +170,7 @@ def save_indicator():
"""
try:
data = request.get_json() or {}
user_id = int(data.get("userid") or 1)
user_id = g.user_id
indicator_id = int(data.get("id") or 0)
code = data.get("code") or ""
name = (data.get("name") or "").strip()
@@ -199,7 +197,7 @@ def save_indicator():
if not name:
name = "Custom Indicator"
now = _now_ts()
now = _now_ts() # For BIGINT fields (createtime, updatetime)
with get_db_connection() as db:
cur = db.cursor()
@@ -209,10 +207,10 @@ def save_indicator():
UPDATE qd_indicator_codes
SET name = ?, code = ?, description = ?,
publish_to_community = ?, pricing_type = ?, price = ?, preview_image = ?,
updatetime = ?, updated_at = ?
updatetime = ?, updated_at = NOW()
WHERE id = ? AND user_id = ? AND (is_buy IS NULL OR is_buy = 0)
""",
(name, code, description, publish_to_community, pricing_type, price, preview_image, now, now, indicator_id, user_id),
(name, code, description, publish_to_community, pricing_type, price, preview_image, now, indicator_id, user_id),
)
else:
cur.execute(
@@ -221,9 +219,9 @@ def save_indicator():
(user_id, is_buy, end_time, name, code, description,
publish_to_community, pricing_type, price, preview_image,
createtime, updatetime, created_at, updated_at)
VALUES (?, 0, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, 0, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
""",
(user_id, name, code, description, publish_to_community, pricing_type, price, preview_image, now, now, now, now),
(user_id, name, code, description, publish_to_community, pricing_type, price, preview_image, now, now),
)
indicator_id = int(cur.lastrowid or 0)
db.commit()
@@ -236,11 +234,12 @@ def save_indicator():
@indicator_bp.route("/deleteIndicator", methods=["POST"])
@login_required
def delete_indicator():
"""Delete an indicator by id."""
"""Delete an indicator by id for the current user."""
try:
data = request.get_json() or {}
user_id = int(data.get("userid") or 1)
user_id = g.user_id
indicator_id = int(data.get("id") or 0)
if not indicator_id:
return jsonify({"code": 0, "msg": "id is required", "data": None}), 400
@@ -261,6 +260,7 @@ def delete_indicator():
@indicator_bp.route("/verifyCode", methods=["POST"])
@login_required
def verify_code():
"""
Verify/Dry-run indicator code with mock data.
@@ -372,6 +372,7 @@ def verify_code():
@indicator_bp.route("/aiGenerate", methods=["POST"])
@login_required
def ai_generate():
"""
SSE endpoint to generate indicator code.
+7 -12
View File
@@ -14,7 +14,7 @@ kline_bp = Blueprint('kline', __name__)
kline_service = KlineService()
@kline_bp.route('/kline', methods=['GET', 'POST'])
@kline_bp.route('/kline', methods=['GET'])
def get_kline():
"""
获取K线数据
@@ -27,17 +27,12 @@ def get_kline():
before_time: 获取此时间之前的数据 (可选Unix时间戳)
"""
try:
# 支持 GET 和 POST
if request.method == 'POST':
data = request.get_json() or {}
else:
data = request.args
market = data.get('market', 'USStock')
symbol = data.get('symbol', '')
timeframe = data.get('timeframe', '1D')
limit = int(data.get('limit', 300))
before_time = data.get('before_time') or data.get('beforeTime')
# 强制 GET, 使用 request.args
market = request.args.get('market', 'USStock')
symbol = request.args.get('symbol', '')
timeframe = request.args.get('timeframe', '1D')
limit = int(request.args.get('limit', 300))
before_time = request.args.get('before_time') or request.args.get('beforeTime')
if before_time:
before_time = int(before_time)
+54 -104
View File
@@ -2,7 +2,7 @@
Market API routes (local-only).
Provides watchlist, market metadata, symbol search, and pricing helpers for the frontend.
"""
from flask import Blueprint, request, jsonify
from flask import Blueprint, request, jsonify, g
import traceback
import json
import time
@@ -13,6 +13,7 @@ from app.utils.logger import get_logger
from app.utils.cache import CacheManager
from app.utils.db import get_db_connection
from app.utils.config_loader import load_addon_config
from app.utils.auth import login_required
from app.data.market_symbols_seed import (
get_hot_symbols as seed_get_hot_symbols,
search_symbols as seed_search_symbols,
@@ -26,11 +27,9 @@ market_bp = Blueprint('market', __name__)
kline_service = KlineService()
cache = CacheManager()
# 线程池用于并行获取价格
# Thread pool for parallel price fetching
executor = ThreadPoolExecutor(max_workers=10)
DEFAULT_USER_ID = 1
def _now_ts() -> int:
return int(time.time())
@@ -125,7 +124,7 @@ def get_market_types():
return jsonify({'code': 1, 'msg': 'success', 'data': data})
@market_bp.route('/menuFooterConfig', methods=['POST'])
@market_bp.route('/menuFooterConfig', methods=['GET'])
def get_menu_footer_config():
"""
Compatibility stub for old PHP `getMenuFooterConfig`.
@@ -150,17 +149,16 @@ def get_menu_footer_config():
}
return jsonify({'code': 1, 'msg': 'success', 'data': data})
@market_bp.route('/symbols/search', methods=['POST'])
@market_bp.route('/symbols/search', methods=['GET'])
def search_symbols():
"""
Lightweight symbol search.
In local-only mode we keep this simple; frontend allows manual input when no results.
"""
try:
data = request.get_json() or {}
market = (data.get('market') or '').strip()
keyword = (data.get('keyword') or '').strip().upper()
limit = int(data.get('limit') or 20)
market = (request.args.get('market') or '').strip()
keyword = (request.args.get('keyword') or '').strip().upper()
limit = int(request.args.get('limit') or 20)
if not market or not keyword:
return jsonify({'code': 1, 'msg': 'success', 'data': []})
@@ -172,29 +170,30 @@ def search_symbols():
logger.error(traceback.format_exc())
return jsonify({'code': 0, 'msg': str(e), 'data': []}), 500
@market_bp.route('/symbols/hot', methods=['POST'])
@market_bp.route('/symbols/hot', methods=['GET'])
def get_hot_symbols():
"""Return a small curated hot list per market (local-only)."""
try:
data = request.get_json() or {}
market = (data.get('market') or '').strip()
limit = int(data.get('limit') or 10)
market = (request.args.get('market') or '').strip()
limit = int(request.args.get('limit') or 10)
hot = seed_get_hot_symbols(market=market, limit=limit)
return jsonify({'code': 1, 'msg': 'success', 'data': hot})
except Exception as e:
logger.error(f"get_hot_symbols failed: {str(e)}")
return jsonify({'code': 0, 'msg': str(e), 'data': []}), 500
@market_bp.route('/watchlist/get', methods=['POST'])
@market_bp.route('/watchlist/get', methods=['GET'])
@login_required
def get_watchlist():
"""Get local watchlist for the single user."""
"""Get watchlist for the current user."""
try:
user_id = g.user_id
_ensure_watchlist_table()
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"SELECT id, market, symbol, name FROM qd_watchlist WHERE user_id = ? ORDER BY id DESC",
(DEFAULT_USER_ID,)
(user_id,)
)
rows = cur.fetchall() or []
@@ -213,8 +212,8 @@ def get_watchlist():
if resolved and resolved != current_name:
row['name'] = resolved
cur.execute(
"UPDATE qd_watchlist SET name = ?, updated_at = ? WHERE user_id = ? AND market = ? AND symbol = ?",
(resolved, _now_ts(), DEFAULT_USER_ID, market, symbol)
"UPDATE qd_watchlist SET name = ?, updated_at = NOW() WHERE user_id = ? AND market = ? AND symbol = ?",
(resolved, user_id, market, symbol)
)
except Exception:
continue
@@ -227,9 +226,11 @@ def get_watchlist():
return jsonify({'code': 0, 'msg': str(e), 'data': []}), 500
@market_bp.route('/watchlist/add', methods=['POST'])
@login_required
def add_watchlist():
"""Add a symbol to local watchlist (no credits/fees in local mode)."""
"""Add a symbol to watchlist for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
market = (data.get('market') or '').strip()
symbol = _normalize_symbol(data.get('symbol'))
@@ -241,18 +242,18 @@ def add_watchlist():
resolved = resolve_symbol_name(market, symbol) or seed_get_symbol_name(market, symbol)
name = name_in or resolved or symbol
now = _now_ts()
with get_db_connection() as db:
cur = db.cursor()
# Insert or ignore duplicates.
# Insert or update (PostgreSQL UPSERT)
cur.execute(
"INSERT OR IGNORE INTO qd_watchlist (user_id, market, symbol, name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
(DEFAULT_USER_ID, market, symbol, name, now, now)
)
# If already exists, keep it fresh and sync name.
cur.execute(
"UPDATE qd_watchlist SET name = ?, updated_at = ? WHERE user_id = ? AND market = ? AND symbol = ?",
(name, now, DEFAULT_USER_ID, market, symbol)
"""
INSERT INTO qd_watchlist (user_id, market, symbol, name, created_at, updated_at)
VALUES (?, ?, ?, ?, NOW(), NOW())
ON CONFLICT(user_id, market, symbol) DO UPDATE SET
name = excluded.name,
updated_at = NOW()
""",
(user_id, market, symbol, name)
)
db.commit()
cur.close()
@@ -264,9 +265,11 @@ def add_watchlist():
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@market_bp.route('/watchlist/remove', methods=['POST'])
@login_required
def remove_watchlist():
"""Remove a symbol from watchlist. Frontend only passes symbol, so we delete across markets."""
"""Remove a symbol from watchlist for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
symbol = _normalize_symbol(data.get('symbol'))
if not symbol:
@@ -276,7 +279,7 @@ def remove_watchlist():
cur = db.cursor()
cur.execute(
"DELETE FROM qd_watchlist WHERE user_id = ? AND symbol = ?",
(DEFAULT_USER_ID, symbol)
(user_id, symbol)
)
db.commit()
cur.close()
@@ -290,51 +293,17 @@ def remove_watchlist():
def get_single_price(market: str, symbol: str) -> dict:
"""获取单个标的的价格数据"""
try:
# 先尝试从缓存获取(60秒缓存)
cache_key = f"watchlist_price:{market}:{symbol}"
cached_data = cache.get(cache_key)
# 使用 get_realtime_price 获取实时价格(内部已有30秒缓存)
# 相比原先的 '1D' K线逻辑,这能更及时地反映 Crypto 等 24h 市场的变化
price_data = kline_service.get_realtime_price(market, symbol)
if cached_data:
logger.debug(f"Cache hit: {market}:{symbol}")
return {
'market': market,
'symbol': symbol,
'price': cached_data.get('price', 0),
'change': cached_data.get('change', 0),
'changePercent': cached_data.get('changePercent', 0)
}
# 获取最新的一根K线
klines = kline_service.get_kline(market, symbol, '1D', 2)
if klines and len(klines) > 0:
latest = klines[-1]
prev_close = klines[-2]['close'] if len(klines) > 1 else latest.get('open', 0)
current_price = latest.get('close', 0)
change = round(current_price - prev_close, 4) if prev_close else 0
change_percent = round(change / prev_close * 100, 2) if prev_close and prev_close > 0 else 0
result = {
'market': market,
'symbol': symbol,
'price': current_price,
'change': change,
'changePercent': change_percent
}
# 缓存60秒
cache.set(cache_key, result, 60)
return result
else:
return {
'market': market,
'symbol': symbol,
'price': 0,
'change': 0,
'changePercent': 0
}
return {
'market': market,
'symbol': symbol,
'price': price_data.get('price', 0),
'change': price_data.get('change', 0),
'changePercent': price_data.get('changePercent', 0)
}
except Exception as e:
logger.error(f"Failed to fetch price {market}:{symbol} - {str(e)}")
return {
@@ -346,45 +315,26 @@ def get_single_price(market: str, symbol: str) -> dict:
}
@market_bp.route('/watchlist/prices', methods=['POST'])
@market_bp.route('/watchlist/prices', methods=['GET'])
def get_watchlist_prices():
"""
批量获取自选股价格
请求体:
{
"watchlist": [
{"market": "USStock", "symbol": "AAPL"},
{"market": "Crypto", "symbol": "BTC"},
...
]
}
响应:
{
"code": 1,
"msg": "success",
"data": [
{"market": "USStock", "symbol": "AAPL", "price": 150.0, "change": 1.5, "changePercent": 1.0},
{"market": "Crypto", "symbol": "BTC", "price": 95000.0, "change": 1000, "changePercent": 1.05}
]
}
Params (Query String):
watchlist: JSON string of list of {market, symbol} objects
e.g. ?watchlist=[{"market":"USStock","symbol":"AAPL"}]
"""
try:
data = request.get_json()
if not data:
return jsonify({
'code': 0,
'msg': 'Request body is required',
'data': []
}), 400
watchlist = data.get('watchlist', [])
watchlist_str = request.args.get('watchlist', '[]')
try:
watchlist = json.loads(watchlist_str)
except Exception:
watchlist = []
if not watchlist or not isinstance(watchlist, list):
return jsonify({
'code': 0,
'msg': 'Invalid watchlist format',
'msg': 'Invalid watchlist format (expected JSON list in query param)',
'data': []
}), 400
+90 -65
View File
@@ -2,7 +2,7 @@
Portfolio API routes (local-only).
Manages manual positions (user's existing holdings) and AI monitoring tasks.
"""
from flask import Blueprint, request, jsonify
from flask import Blueprint, request, jsonify, g
import json
import traceback
import time
@@ -13,6 +13,7 @@ from app.services.kline import KlineService
from app.utils.logger import get_logger
from app.utils.cache import CacheManager
from app.utils.db import get_db_connection
from app.utils.auth import login_required
from app.services.symbol_name import resolve_symbol_name
from app.data.market_symbols_seed import get_symbol_name as seed_get_symbol_name
@@ -23,18 +24,16 @@ kline_service = KlineService()
cache = CacheManager()
# Thread pool for parallel price fetching
# 降低并发数避免触发API限制(尤其是外汇/美股等有速率限制的API)
# Lower concurrency to avoid triggering API limits (especially for forex/US stocks)
executor = ThreadPoolExecutor(max_workers=3)
# 请求间隔(秒),避免请求过快
# Request interval (seconds) to avoid too frequent requests
REQUEST_INTERVAL = 0.3
# 速率限制相关
# Rate limiting related
_request_lock = threading.Lock()
_last_request_time = {} # {market: timestamp}
DEFAULT_USER_ID = 1
def _now_ts() -> int:
return int(time.time())
@@ -109,10 +108,12 @@ def _get_single_price(market: str, symbol: str, force_refresh: bool = False) ->
# ==================== Position CRUD ====================
@portfolio_bp.route('/positions', methods=['GET'])
@login_required
def get_positions():
"""Get all manual positions with current prices."""
"""Get all manual positions with current prices for the current user."""
try:
# 检查是否强制刷新(跳过缓存)
user_id = g.user_id
# Check if force refresh (skip cache)
force_refresh = request.args.get('refresh', '').lower() in ('1', 'true', 'yes')
with get_db_connection() as db:
@@ -124,7 +125,7 @@ def get_positions():
WHERE user_id = ?
ORDER BY id DESC
""",
(DEFAULT_USER_ID,)
(user_id,)
)
rows = cur.fetchall() or []
cur.close()
@@ -212,9 +213,11 @@ def get_positions():
@portfolio_bp.route('/positions', methods=['POST'])
@login_required
def add_position():
"""Add a new manual position."""
"""Add a new manual position for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
market = (data.get('market') or '').strip()
symbol = _normalize_symbol(data.get('symbol'))
@@ -244,7 +247,6 @@ def add_position():
name = name_in or resolved or symbol
tags_json = json.dumps(tags if isinstance(tags, list) else [], ensure_ascii=False)
now = _now_ts()
with get_db_connection() as db:
cur = db.cursor()
@@ -252,7 +254,7 @@ def add_position():
"""
INSERT INTO qd_manual_positions
(user_id, market, symbol, name, side, quantity, entry_price, entry_time, notes, tags, group_name, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
ON CONFLICT(user_id, market, symbol, side) DO UPDATE SET
name = excluded.name,
quantity = excluded.quantity,
@@ -261,9 +263,9 @@ def add_position():
notes = excluded.notes,
tags = excluded.tags,
group_name = excluded.group_name,
updated_at = excluded.updated_at
updated_at = NOW()
""",
(DEFAULT_USER_ID, market, symbol, name, side, quantity, entry_price, entry_time, notes, tags_json, group_name, now, now)
(user_id, market, symbol, name, side, quantity, entry_price, entry_time, notes, tags_json, group_name)
)
position_id = cur.lastrowid
db.commit()
@@ -277,9 +279,11 @@ def add_position():
@portfolio_bp.route('/positions/<int:position_id>', methods=['PUT'])
@login_required
def update_position(position_id):
"""Update an existing position."""
"""Update an existing position for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
updates = []
@@ -323,10 +327,9 @@ def update_position(position_id):
if not updates:
return jsonify({'code': 0, 'msg': 'No fields to update', 'data': None}), 400
updates.append('updated_at = ?')
params.append(_now_ts())
updates.append('updated_at = NOW()')
params.append(position_id)
params.append(DEFAULT_USER_ID)
params.append(user_id)
with get_db_connection() as db:
cur = db.cursor()
@@ -345,14 +348,16 @@ def update_position(position_id):
@portfolio_bp.route('/positions/<int:position_id>', methods=['DELETE'])
@login_required
def delete_position(position_id):
"""Delete a position."""
"""Delete a position for the current user."""
try:
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"DELETE FROM qd_manual_positions WHERE id = ? AND user_id = ?",
(position_id, DEFAULT_USER_ID)
(position_id, user_id)
)
db.commit()
cur.close()
@@ -365,10 +370,12 @@ def delete_position(position_id):
@portfolio_bp.route('/summary', methods=['GET'])
@login_required
def get_portfolio_summary():
"""Get portfolio summary with total value, PnL, and market distribution."""
"""Get portfolio summary with total value, PnL, and market distribution for the current user."""
try:
# 检查是否强制刷新
user_id = g.user_id
# Check if force refresh
force_refresh = request.args.get('refresh', '').lower() in ('1', 'true', 'yes')
with get_db_connection() as db:
@@ -379,7 +386,7 @@ def get_portfolio_summary():
FROM qd_manual_positions
WHERE user_id = ?
""",
(DEFAULT_USER_ID,)
(user_id,)
)
rows = cur.fetchall() or []
cur.close()
@@ -485,9 +492,11 @@ def get_portfolio_summary():
# ==================== Monitor CRUD ====================
@portfolio_bp.route('/monitors', methods=['GET'])
@login_required
def get_monitors():
"""Get all position monitors."""
"""Get all position monitors for the current user."""
try:
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -498,7 +507,7 @@ def get_monitors():
WHERE user_id = ?
ORDER BY id DESC
""",
(DEFAULT_USER_ID,)
(user_id,)
)
rows = cur.fetchall() or []
cur.close()
@@ -529,9 +538,11 @@ def get_monitors():
@portfolio_bp.route('/monitors', methods=['POST'])
@login_required
def add_monitor():
"""Add a new position monitor."""
"""Add a new position monitor for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
name = (data.get('name') or '').strip()
position_ids = data.get('position_ids') or []
@@ -547,9 +558,7 @@ def add_monitor():
monitor_type = 'ai'
# Calculate next_run_at based on interval
now = _now_ts()
interval_minutes = int(config.get('interval_minutes') or 60)
next_run_at = now + (interval_minutes * 60)
position_ids_json = json.dumps(position_ids if isinstance(position_ids, list) else [], ensure_ascii=False)
config_json = json.dumps(config if isinstance(config, dict) else {}, ensure_ascii=False)
@@ -561,10 +570,10 @@ def add_monitor():
"""
INSERT INTO qd_position_monitors
(user_id, name, position_ids, monitor_type, config, notification_config, is_active, next_run_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW() + INTERVAL '%s minutes', NOW(), NOW())
""",
(DEFAULT_USER_ID, name, position_ids_json, monitor_type, config_json, notification_config_json,
1 if is_active else 0, next_run_at, now, now)
(user_id, name, position_ids_json, monitor_type, config_json, notification_config_json,
1 if is_active else 0, interval_minutes)
)
monitor_id = cur.lastrowid
db.commit()
@@ -578,9 +587,11 @@ def add_monitor():
@portfolio_bp.route('/monitors/<int:monitor_id>', methods=['PUT'])
@login_required
def update_monitor(monitor_id):
"""Update an existing monitor."""
"""Update an existing monitor for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
updates = []
@@ -599,15 +610,14 @@ def update_monitor(monitor_id):
updates.append('monitor_type = ?')
params.append((data.get('monitor_type') or 'ai').strip())
next_run_interval = None # Will store interval for special handling
if 'config' in data:
config = data.get('config') or {}
updates.append('config = ?')
params.append(json.dumps(config if isinstance(config, dict) else {}, ensure_ascii=False))
# Recalculate next_run_at if interval changed
interval_minutes = int(config.get('interval_minutes') or 60)
updates.append('next_run_at = ?')
params.append(_now_ts() + (interval_minutes * 60))
# Recalculate next_run_at if interval changed (handled separately for PostgreSQL)
next_run_interval = int(config.get('interval_minutes') or 60)
if 'notification_config' in data:
notification_config = data.get('notification_config') or {}
@@ -621,10 +631,13 @@ def update_monitor(monitor_id):
if not updates:
return jsonify({'code': 0, 'msg': 'No fields to update', 'data': None}), 400
updates.append('updated_at = ?')
params.append(_now_ts())
# Add next_run_at update if interval was changed
if next_run_interval is not None:
updates.append(f"next_run_at = NOW() + INTERVAL '{next_run_interval} minutes'")
updates.append('updated_at = NOW()')
params.append(monitor_id)
params.append(DEFAULT_USER_ID)
params.append(user_id)
with get_db_connection() as db:
cur = db.cursor()
@@ -643,14 +656,16 @@ def update_monitor(monitor_id):
@portfolio_bp.route('/monitors/<int:monitor_id>', methods=['DELETE'])
@login_required
def delete_monitor(monitor_id):
"""Delete a monitor."""
"""Delete a monitor for the current user."""
try:
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"DELETE FROM qd_position_monitors WHERE id = ? AND user_id = ?",
(monitor_id, DEFAULT_USER_ID)
(monitor_id, user_id)
)
db.commit()
cur.close()
@@ -663,6 +678,7 @@ def delete_monitor(monitor_id):
@portfolio_bp.route('/monitors/<int:monitor_id>/run', methods=['POST'])
@login_required
def run_monitor_now(monitor_id):
"""Manually trigger a monitor to run immediately."""
try:
@@ -692,9 +708,11 @@ def run_monitor_now(monitor_id):
# ==================== Alerts CRUD ====================
@portfolio_bp.route('/alerts', methods=['GET'])
@login_required
def get_alerts():
"""Get all position alerts."""
"""Get all position alerts for the current user."""
try:
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -708,7 +726,7 @@ def get_alerts():
WHERE a.user_id = ?
ORDER BY a.id DESC
""",
(DEFAULT_USER_ID,)
(user_id,)
)
rows = cur.fetchall() or []
cur.close()
@@ -743,9 +761,11 @@ def get_alerts():
@portfolio_bp.route('/alerts', methods=['POST'])
@login_required
def add_alert():
"""Add a new position alert."""
"""Add a new position alert for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
position_id = data.get('position_id') # Can be None for symbol-level alerts
market = (data.get('market') or '').strip()
@@ -768,7 +788,7 @@ def add_alert():
cur = db.cursor()
cur.execute(
"SELECT market, symbol FROM qd_manual_positions WHERE id = ? AND user_id = ?",
(position_id, DEFAULT_USER_ID)
(position_id, user_id)
)
pos = cur.fetchone()
cur.close()
@@ -782,7 +802,6 @@ def add_alert():
if threshold <= 0 and alert_type.startswith('price_'):
return jsonify({'code': 0, 'msg': 'Threshold must be positive for price alerts', 'data': None}), 400
now = _now_ts()
notification_config_json = json.dumps(notification_config if isinstance(notification_config, dict) else {}, ensure_ascii=False)
with get_db_connection() as db:
@@ -793,7 +812,7 @@ def add_alert():
if position_id:
cur.execute(
"SELECT id FROM qd_position_alerts WHERE position_id = ? AND user_id = ?",
(position_id, DEFAULT_USER_ID)
(position_id, user_id)
)
existing = cur.fetchone()
if existing:
@@ -805,11 +824,11 @@ def add_alert():
"""
UPDATE qd_position_alerts
SET alert_type = ?, threshold = ?, notification_config = ?,
is_active = ?, is_triggered = 0, repeat_interval = ?, notes = ?, updated_at = ?
is_active = ?, is_triggered = 0, repeat_interval = ?, notes = ?, updated_at = NOW()
WHERE id = ?
""",
(alert_type, threshold, notification_config_json,
1 if is_active else 0, repeat_interval, notes, now, existing_alert_id)
1 if is_active else 0, repeat_interval, notes, existing_alert_id)
)
alert_id = existing_alert_id
else:
@@ -819,10 +838,10 @@ def add_alert():
INSERT INTO qd_position_alerts
(user_id, position_id, market, symbol, alert_type, threshold, notification_config,
is_active, repeat_interval, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
""",
(DEFAULT_USER_ID, position_id, market, symbol, alert_type, threshold, notification_config_json,
1 if is_active else 0, repeat_interval, notes, now, now)
(user_id, position_id, market, symbol, alert_type, threshold, notification_config_json,
1 if is_active else 0, repeat_interval, notes)
)
alert_id = cur.lastrowid
@@ -837,9 +856,11 @@ def add_alert():
@portfolio_bp.route('/alerts/<int:alert_id>', methods=['PUT'])
@login_required
def update_alert(alert_id):
"""Update an existing alert."""
"""Update an existing alert for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
updates = []
@@ -877,10 +898,9 @@ def update_alert(alert_id):
if not updates:
return jsonify({'code': 0, 'msg': 'No fields to update', 'data': None}), 400
updates.append('updated_at = ?')
params.append(_now_ts())
updates.append('updated_at = NOW()')
params.append(alert_id)
params.append(DEFAULT_USER_ID)
params.append(user_id)
with get_db_connection() as db:
cur = db.cursor()
@@ -899,14 +919,16 @@ def update_alert(alert_id):
@portfolio_bp.route('/alerts/<int:alert_id>', methods=['DELETE'])
@login_required
def delete_alert(alert_id):
"""Delete an alert."""
"""Delete an alert for the current user."""
try:
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"DELETE FROM qd_position_alerts WHERE id = ? AND user_id = ?",
(alert_id, DEFAULT_USER_ID)
(alert_id, user_id)
)
db.commit()
cur.close()
@@ -921,9 +943,11 @@ def delete_alert(alert_id):
# ==================== Groups ====================
@portfolio_bp.route('/groups', methods=['GET'])
@login_required
def get_groups():
"""Get list of all groups with position counts."""
"""Get list of all groups with position counts for the current user."""
try:
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -934,14 +958,14 @@ def get_groups():
GROUP BY group_name
ORDER BY group_name
""",
(DEFAULT_USER_ID,)
(user_id,)
)
rows = cur.fetchall() or []
# Also get count of ungrouped
cur.execute(
"SELECT COUNT(*) as count FROM qd_manual_positions WHERE user_id = ? AND (group_name IS NULL OR group_name = '')",
(DEFAULT_USER_ID,)
(user_id,)
)
ungrouped = cur.fetchone()
cur.close()
@@ -971,9 +995,11 @@ def get_groups():
@portfolio_bp.route('/groups/rename', methods=['POST'])
@login_required
def rename_group():
"""Rename a group."""
"""Rename a group for the current user."""
try:
user_id = g.user_id
data = request.get_json() or {}
old_name = (data.get('old_name') or '').strip()
new_name = (data.get('new_name') or '').strip()
@@ -981,12 +1007,11 @@ def rename_group():
if not old_name:
return jsonify({'code': 0, 'msg': 'old_name is required', 'data': None}), 400
now = _now_ts()
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"UPDATE qd_manual_positions SET group_name = ?, updated_at = ? WHERE user_id = ? AND group_name = ?",
(new_name, now, DEFAULT_USER_ID, old_name)
"UPDATE qd_manual_positions SET group_name = ?, updated_at = NOW() WHERE user_id = ? AND group_name = ?",
(new_name, user_id, old_name)
)
db.commit()
cur.close()
+18 -5
View File
@@ -1,11 +1,14 @@
"""
Settings API - 读取和保存 .env 配置
Admin-only endpoints for system configuration management.
"""
import os
import re
from flask import Blueprint, request, jsonify
from app.utils.logger import get_logger
from app.utils.config_loader import clear_config_cache
from app.utils.auth import login_required, admin_required
logger = get_logger(__name__)
@@ -791,8 +794,10 @@ def write_env_file(env_values):
@settings_bp.route('/schema', methods=['GET'])
@login_required
@admin_required
def get_settings_schema():
"""获取配置项定义"""
"""获取配置项定义 (admin only)"""
return jsonify({
'code': 1,
'msg': 'success',
@@ -801,8 +806,10 @@ def get_settings_schema():
@settings_bp.route('/values', methods=['GET'])
@login_required
@admin_required
def get_settings_values():
"""获取当前配置值 - 包括敏感信息(真实值)"""
"""获取当前配置值 - 包括敏感信息(真实值)(admin only)"""
env_values = read_env_file()
# 构建返回数据,返回真实值
@@ -825,8 +832,10 @@ def get_settings_values():
@settings_bp.route('/save', methods=['POST'])
@login_required
@admin_required
def save_settings():
"""保存配置"""
"""保存配置 (admin only)"""
try:
data = request.get_json()
if not data:
@@ -878,8 +887,10 @@ def save_settings():
@settings_bp.route('/openrouter-balance', methods=['GET'])
@login_required
@admin_required
def get_openrouter_balance():
"""查询 OpenRouter 账户余额"""
"""查询 OpenRouter 账户余额 (admin only)"""
try:
import requests
from app.config.api_keys import APIKeys
@@ -954,8 +965,10 @@ def get_openrouter_balance():
@settings_bp.route('/test-connection', methods=['POST'])
@login_required
@admin_required
def test_connection():
"""测试API连接"""
"""测试API连接 (admin only)"""
try:
data = request.get_json()
service = data.get('service')
+192 -91
View File
@@ -1,7 +1,7 @@
"""
交易策略 API 路由
Trading Strategy API Routes
"""
from flask import Blueprint, request, jsonify
from flask import Blueprint, request, jsonify, g
import traceback
import time
@@ -11,6 +11,7 @@ from app.services.backtest import BacktestService
from app import get_trading_executor
from app.utils.logger import get_logger
from app.utils.db import get_db_connection
from app.utils.auth import login_required
from app.data_sources import DataSourceFactory
logger = get_logger(__name__)
@@ -29,12 +30,13 @@ def get_strategy_service() -> StrategyService:
@strategy_bp.route('/strategies', methods=['GET'])
@login_required
def list_strategies():
"""
策略列表本地版单用户
List strategies for the current user.
"""
try:
user_id = request.args.get('user_id', type=int) or 1
user_id = g.user_id
items = get_strategy_service().list_strategies(user_id=user_id)
return jsonify({'code': 1, 'msg': 'success', 'data': {'strategies': items}})
except Exception as e:
@@ -44,12 +46,14 @@ def list_strategies():
@strategy_bp.route('/strategies/detail', methods=['GET'])
@login_required
def get_strategy_detail():
try:
user_id = g.user_id
strategy_id = request.args.get('id', type=int)
if not strategy_id:
return jsonify({'code': 0, 'msg': 'Missing strategy id parameter', 'data': None}), 400
st = get_strategy_service().get_strategy(strategy_id)
st = get_strategy_service().get_strategy(strategy_id, user_id=user_id)
if not st:
return jsonify({'code': 0, 'msg': 'Strategy not found', 'data': None}), 404
return jsonify({'code': 1, 'msg': 'success', 'data': st})
@@ -60,11 +64,13 @@ def get_strategy_detail():
@strategy_bp.route('/strategies/create', methods=['POST'])
@login_required
def create_strategy():
try:
user_id = g.user_id
payload = request.get_json() or {}
# Local mode default user
payload['user_id'] = int(payload.get('user_id') or 1)
# Use current user's ID
payload['user_id'] = user_id
payload['strategy_type'] = payload.get('strategy_type') or 'IndicatorStrategy'
new_id = get_strategy_service().create_strategy(payload)
return jsonify({'code': 1, 'msg': 'success', 'data': {'id': new_id}})
@@ -75,18 +81,20 @@ def create_strategy():
@strategy_bp.route('/strategies/batch-create', methods=['POST'])
@login_required
def batch_create_strategies():
"""
批量创建策略多币种
Batch create strategies (multiple symbols)
请求体:
strategy_name: 策略基础名称
symbols: 币种数组 ["Crypto:BTC/USDT", "Crypto:ETH/USDT"]
... 其他策略配置
Request body:
strategy_name: Base strategy name
symbols: Array of symbols, e.g. ["Crypto:BTC/USDT", "Crypto:ETH/USDT"]
... other strategy config
"""
try:
user_id = g.user_id
payload = request.get_json() or {}
payload['user_id'] = int(payload.get('user_id') or 1)
payload['user_id'] = user_id
payload['strategy_type'] = payload.get('strategy_type') or 'IndicatorStrategy'
result = get_strategy_service().batch_create_strategies(payload)
@@ -94,13 +102,13 @@ def batch_create_strategies():
if result['success']:
return jsonify({
'code': 1,
'msg': f"成功创建 {result['total_created']} 个策略",
'msg': f"Successfully created {result['total_created']} strategies",
'data': result
})
else:
return jsonify({
'code': 0,
'msg': '批量创建失败',
'msg': 'Batch creation failed',
'data': result
})
except Exception as e:
@@ -110,31 +118,33 @@ def batch_create_strategies():
@strategy_bp.route('/strategies/batch-start', methods=['POST'])
@login_required
def batch_start_strategies():
"""
批量启动策略
Batch start strategies
请求体:
strategy_ids: 策略ID数组
strategy_group_id: 策略组ID
Request body:
strategy_ids: Array of strategy IDs
or
strategy_group_id: Strategy group ID
"""
try:
user_id = g.user_id
payload = request.get_json() or {}
strategy_ids = payload.get('strategy_ids') or []
strategy_group_id = payload.get('strategy_group_id')
# 如果提供了策略组ID,获取组内所有策略
# If strategy_group_id provided, get all strategies in the group
if strategy_group_id and not strategy_ids:
strategy_ids = get_strategy_service().get_strategies_by_group(strategy_group_id)
strategy_ids = get_strategy_service().get_strategies_by_group(strategy_group_id, user_id=user_id)
if not strategy_ids:
return jsonify({'code': 0, 'msg': '请提供策略ID', 'data': None}), 400
return jsonify({'code': 0, 'msg': 'Please provide strategy IDs', 'data': None}), 400
# 先更新数据库状态
result = get_strategy_service().batch_start_strategies(strategy_ids)
# Update database status first
result = get_strategy_service().batch_start_strategies(strategy_ids, user_id=user_id)
# 然后启动执行器
# Then start executor
executor = get_trading_executor()
for sid in result.get('success_ids', []):
try:
@@ -144,7 +154,7 @@ def batch_start_strategies():
return jsonify({
'code': 1 if result['success'] else 0,
'msg': f"成功启动 {len(result.get('success_ids', []))} 个策略",
'msg': f"Successfully started {len(result.get('success_ids', []))} strategies",
'data': result
})
except Exception as e:
@@ -154,27 +164,29 @@ def batch_start_strategies():
@strategy_bp.route('/strategies/batch-stop', methods=['POST'])
@login_required
def batch_stop_strategies():
"""
批量停止策略
Batch stop strategies
请求体:
strategy_ids: 策略ID数组
strategy_group_id: 策略组ID
Request body:
strategy_ids: Array of strategy IDs
or
strategy_group_id: Strategy group ID
"""
try:
user_id = g.user_id
payload = request.get_json() or {}
strategy_ids = payload.get('strategy_ids') or []
strategy_group_id = payload.get('strategy_group_id')
if strategy_group_id and not strategy_ids:
strategy_ids = get_strategy_service().get_strategies_by_group(strategy_group_id)
strategy_ids = get_strategy_service().get_strategies_by_group(strategy_group_id, user_id=user_id)
if not strategy_ids:
return jsonify({'code': 0, 'msg': '请提供策略ID', 'data': None}), 400
return jsonify({'code': 0, 'msg': 'Please provide strategy IDs', 'data': None}), 400
# 先停止执行器
# Stop executor first
executor = get_trading_executor()
for sid in strategy_ids:
try:
@@ -182,12 +194,12 @@ def batch_stop_strategies():
except Exception as e:
logger.error(f"Failed to stop executor for strategy {sid}: {e}")
# 然后更新数据库状态
result = get_strategy_service().batch_stop_strategies(strategy_ids)
# Then update database status
result = get_strategy_service().batch_stop_strategies(strategy_ids, user_id=user_id)
return jsonify({
'code': 1 if result['success'] else 0,
'msg': f"成功停止 {len(result.get('success_ids', []))} 个策略",
'msg': f"Successfully stopped {len(result.get('success_ids', []))} strategies",
'data': result
})
except Exception as e:
@@ -197,40 +209,42 @@ def batch_stop_strategies():
@strategy_bp.route('/strategies/batch-delete', methods=['DELETE'])
@login_required
def batch_delete_strategies():
"""
批量删除策略
Batch delete strategies
请求体:
strategy_ids: 策略ID数组
strategy_group_id: 策略组ID
Request body:
strategy_ids: Array of strategy IDs
or
strategy_group_id: Strategy group ID
"""
try:
user_id = g.user_id
payload = request.get_json() or {}
strategy_ids = payload.get('strategy_ids') or []
strategy_group_id = payload.get('strategy_group_id')
if strategy_group_id and not strategy_ids:
strategy_ids = get_strategy_service().get_strategies_by_group(strategy_group_id)
strategy_ids = get_strategy_service().get_strategies_by_group(strategy_group_id, user_id=user_id)
if not strategy_ids:
return jsonify({'code': 0, 'msg': '请提供策略ID', 'data': None}), 400
return jsonify({'code': 0, 'msg': 'Please provide strategy IDs', 'data': None}), 400
# 先停止执行器
# Stop executor first
executor = get_trading_executor()
for sid in strategy_ids:
try:
executor.stop_strategy(sid)
except Exception as e:
pass # 忽略停止错误
pass # Ignore stop errors
# 然后删除
result = get_strategy_service().batch_delete_strategies(strategy_ids)
# Then delete
result = get_strategy_service().batch_delete_strategies(strategy_ids, user_id=user_id)
return jsonify({
'code': 1 if result['success'] else 0,
'msg': f"成功删除 {len(result.get('success_ids', []))} 个策略",
'msg': f"Successfully deleted {len(result.get('success_ids', []))} strategies",
'data': result
})
except Exception as e:
@@ -240,13 +254,15 @@ def batch_delete_strategies():
@strategy_bp.route('/strategies/update', methods=['PUT'])
@login_required
def update_strategy():
try:
user_id = g.user_id
strategy_id = request.args.get('id', type=int)
if not strategy_id:
return jsonify({'code': 0, 'msg': 'Missing strategy id parameter', 'data': None}), 400
payload = request.get_json() or {}
ok = get_strategy_service().update_strategy(strategy_id, payload)
ok = get_strategy_service().update_strategy(strategy_id, payload, user_id=user_id)
if not ok:
return jsonify({'code': 0, 'msg': 'Strategy not found', 'data': None}), 404
return jsonify({'code': 1, 'msg': 'success', 'data': None})
@@ -257,12 +273,14 @@ def update_strategy():
@strategy_bp.route('/strategies/delete', methods=['DELETE'])
@login_required
def delete_strategy():
try:
user_id = g.user_id
strategy_id = request.args.get('id', type=int)
if not strategy_id:
return jsonify({'code': 0, 'msg': 'Missing strategy id parameter', 'data': None}), 400
ok = get_strategy_service().delete_strategy(strategy_id)
ok = get_strategy_service().delete_strategy(strategy_id, user_id=user_id)
return jsonify({'code': 1 if ok else 0, 'msg': 'success' if ok else 'failed', 'data': None})
except Exception as e:
logger.error(f"delete_strategy failed: {str(e)}")
@@ -271,12 +289,20 @@ def delete_strategy():
@strategy_bp.route('/strategies/trades', methods=['GET'])
@login_required
def get_trades():
"""交易记录(从本地 SQLite 读取)"""
"""Get trade records for the current user's strategy."""
try:
user_id = g.user_id
strategy_id = request.args.get('id', type=int)
if not strategy_id:
return jsonify({'code': 0, 'msg': 'Missing strategy id parameter', 'data': {'trades': [], 'items': []}}), 400
# Verify strategy belongs to user
st = get_strategy_service().get_strategy(strategy_id, user_id=user_id)
if not st:
return jsonify({'code': 0, 'msg': 'Strategy not found', 'data': {'trades': [], 'items': []}}), 404
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -299,12 +325,20 @@ def get_trades():
@strategy_bp.route('/strategies/positions', methods=['GET'])
@login_required
def get_positions():
"""持仓记录(从本地 SQLite 读取)"""
"""Get position records for the current user's strategy."""
try:
user_id = g.user_id
strategy_id = request.args.get('id', type=int)
if not strategy_id:
return jsonify({'code': 0, 'msg': 'Missing strategy id parameter', 'data': {'positions': [], 'items': []}}), 400
# Verify strategy belongs to user
st = get_strategy_service().get_strategy(strategy_id, user_id=user_id)
if not st:
return jsonify({'code': 0, 'msg': 'Strategy not found', 'data': {'positions': [], 'items': []}}), 404
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -382,10 +416,10 @@ def get_positions():
cur.execute(
"""
UPDATE qd_strategy_positions
SET current_price = ?, unrealized_pnl = ?, pnl_percent = ?, updated_at = ?
SET current_price = ?, unrealized_pnl = ?, pnl_percent = ?, updated_at = NOW()
WHERE id = ?
""",
(float(cp or 0.0), float(pnl), float(pct), int(now), int(rr.get("id"))),
(float(cp or 0.0), float(pnl), float(pct), int(rr.get("id"))),
)
except Exception:
pass
@@ -400,14 +434,18 @@ def get_positions():
@strategy_bp.route('/strategies/equityCurve', methods=['GET'])
@login_required
def get_equity_curve():
"""净值曲线(本地简单计算:initial_capital + 累计 profit"""
"""Get equity curve for the current user's strategy."""
try:
user_id = g.user_id
strategy_id = request.args.get('id', type=int)
if not strategy_id:
return jsonify({'code': 0, 'msg': 'Missing strategy id parameter', 'data': []}), 400
st = get_strategy_service().get_strategy(strategy_id) or {}
st = get_strategy_service().get_strategy(strategy_id, user_id=user_id) or {}
if not st:
return jsonify({'code': 0, 'msg': 'Strategy not found', 'data': []}), 404
initial = float(st.get('initial_capital') or (st.get('trading_config') or {}).get('initial_capital') or 0)
if initial <= 0:
initial = 1000.0
@@ -447,14 +485,16 @@ def get_equity_curve():
@strategy_bp.route('/strategies/stop', methods=['POST'])
@login_required
def stop_strategy():
"""
停止策略
Stop a strategy for the current user.
参数:
id: 策略ID
Params:
id: Strategy ID
"""
try:
user_id = g.user_id
strategy_id = request.args.get('id', type=int)
if not strategy_id:
@@ -464,18 +504,23 @@ def stop_strategy():
'data': None
}), 400
# 获取策略类型
# Verify strategy belongs to user
st = get_strategy_service().get_strategy(strategy_id, user_id=user_id)
if not st:
return jsonify({'code': 0, 'msg': 'Strategy not found', 'data': None}), 404
# Get strategy type
strategy_type = get_strategy_service().get_strategy_type(strategy_id)
# Local backend: AI strategy executor was removed. Only indicator strategies are supported.
if strategy_type == 'PromptBasedStrategy':
return jsonify({'code': 0, 'msg': 'AI strategy has been removed; local edition does not support starting/stopping AI strategies', 'data': None}), 400
# 指标策略
# Indicator strategy
get_trading_executor().stop_strategy(strategy_id)
# 更新策略状态
get_strategy_service().update_strategy_status(strategy_id, 'stopped')
# Update strategy status
get_strategy_service().update_strategy_status(strategy_id, 'stopped', user_id=user_id)
return jsonify({
'code': 1,
@@ -494,14 +539,16 @@ def stop_strategy():
@strategy_bp.route('/strategies/start', methods=['POST'])
@login_required
def start_strategy():
"""
启动策略
Start a strategy for the current user.
参数:
id: 策略ID
Params:
id: Strategy ID
"""
try:
user_id = g.user_id
strategy_id = request.args.get('id', type=int)
if not strategy_id:
@@ -511,22 +558,27 @@ def start_strategy():
'data': None
}), 400
# 获取策略类型
# Verify strategy belongs to user
st = get_strategy_service().get_strategy(strategy_id, user_id=user_id)
if not st:
return jsonify({'code': 0, 'msg': 'Strategy not found', 'data': None}), 404
# Get strategy type
strategy_type = get_strategy_service().get_strategy_type(strategy_id)
# 更新策略状态
get_strategy_service().update_strategy_status(strategy_id, 'running')
# Update strategy status
get_strategy_service().update_strategy_status(strategy_id, 'running', user_id=user_id)
# Local backend: AI strategy executor was removed. Only indicator strategies are supported.
if strategy_type == 'PromptBasedStrategy':
return jsonify({'code': 0, 'msg': 'AI strategy has been removed; local edition does not support starting AI strategies', 'data': None}), 400
# 指标策略
# Indicator strategy
success = get_trading_executor().start_strategy(strategy_id)
if not success:
# 如果启动失败,恢复状态
get_strategy_service().update_strategy_status(strategy_id, 'stopped')
# If start failed, restore status
get_strategy_service().update_strategy_status(strategy_id, 'stopped', user_id=user_id)
return jsonify({
'code': 0,
'msg': 'Failed to start strategy executor',
@@ -550,12 +602,13 @@ def start_strategy():
@strategy_bp.route('/strategies/test-connection', methods=['POST'])
@login_required
def test_connection():
"""
测试交易所连接
Test exchange connection.
请求体:
exchange_config: 交易所配置
Request body:
exchange_config: Exchange configuration
"""
try:
data = request.get_json() or {}
@@ -619,12 +672,13 @@ def test_connection():
@strategy_bp.route('/strategies/get-symbols', methods=['POST'])
@login_required
def get_symbols():
"""
获取交易所交易对列表
Get exchange trading pairs list.
请求体:
exchange_config: 交易所配置
Request body:
exchange_config: Exchange configuration
"""
try:
data = request.get_json() or {}
@@ -662,9 +716,10 @@ def get_symbols():
@strategy_bp.route('/strategies/preview-compile', methods=['POST'])
@login_required
def preview_compile():
"""
预览编译后的策略结果
Preview compiled strategy result.
"""
try:
data = request.get_json() or {}
@@ -708,9 +763,10 @@ def preview_compile():
@strategy_bp.route('/strategies/notifications', methods=['GET'])
@login_required
def get_strategy_notifications():
"""
Strategy signal notifications (browser channel persistence).
Strategy signal notifications for the current user.
Query:
- id: strategy id (optional)
@@ -718,16 +774,39 @@ def get_strategy_notifications():
- since_id: return rows with id > since_id (optional)
"""
try:
user_id = g.user_id
strategy_id = request.args.get('id', type=int)
limit = request.args.get('limit', type=int) or 50
limit = max(1, min(200, int(limit)))
since_id = request.args.get('since_id', type=int) or 0
# Get user's strategy IDs for filtering notifications
user_strategy_ids = []
with get_db_connection() as db:
cur = db.cursor()
cur.execute("SELECT id FROM qd_strategies_trading WHERE user_id = ?", (user_id,))
rows = cur.fetchall() or []
user_strategy_ids = [r.get('id') for r in rows if r.get('id')]
cur.close()
if not user_strategy_ids:
return jsonify({'code': 1, 'msg': 'success', 'data': {'items': []}})
where = []
args = []
# Filter by user's strategies
if strategy_id:
where.append("strategy_id = ?")
args.append(int(strategy_id))
if strategy_id in user_strategy_ids:
where.append("strategy_id = ?")
args.append(int(strategy_id))
else:
return jsonify({'code': 1, 'msg': 'success', 'data': {'items': []}})
else:
placeholders = ",".join(["?"] * len(user_strategy_ids))
where.append(f"strategy_id IN ({placeholders})")
args.extend(user_strategy_ids)
if since_id:
where.append("id > ?")
args.append(int(since_id))
@@ -756,19 +835,25 @@ def get_strategy_notifications():
@strategy_bp.route('/strategies/notifications/read', methods=['POST'])
@login_required
def mark_notification_read():
"""Mark a single notification as read."""
"""Mark a single notification as read for the current user."""
try:
user_id = g.user_id
data = request.get_json(force=True, silent=True) or {}
notification_id = data.get('id')
if not notification_id:
return jsonify({'code': 0, 'msg': 'Missing id'}), 400
# Only update notifications for user's strategies
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"UPDATE qd_strategy_notifications SET is_read = 1 WHERE id = ?",
(int(notification_id),)
"""
UPDATE qd_strategy_notifications SET is_read = 1
WHERE id = ? AND strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
""",
(int(notification_id), user_id)
)
db.commit()
cur.close()
@@ -780,12 +865,20 @@ def mark_notification_read():
@strategy_bp.route('/strategies/notifications/read-all', methods=['POST'])
@login_required
def mark_all_notifications_read():
"""Mark all notifications as read."""
"""Mark all notifications as read for the current user."""
try:
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
cur.execute("UPDATE qd_strategy_notifications SET is_read = 1")
cur.execute(
"""
UPDATE qd_strategy_notifications SET is_read = 1
WHERE strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
""",
(user_id,)
)
db.commit()
cur.close()
@@ -796,12 +889,20 @@ def mark_all_notifications_read():
@strategy_bp.route('/strategies/notifications/clear', methods=['DELETE'])
@login_required
def clear_notifications():
"""Clear all notifications (delete from database)."""
"""Clear all notifications for the current user."""
try:
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
cur.execute("DELETE FROM qd_strategy_notifications")
cur.execute(
"""
DELETE FROM qd_strategy_notifications
WHERE strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
""",
(user_id,)
)
db.commit()
cur.close()
@@ -41,11 +41,10 @@ def _extract_meta_from_code(code: str) -> Dict[str, str]:
return {"name": name, "description": description}
@strategy_code_bp.route("/strategy/getStrategies", methods=["POST"])
@strategy_code_bp.route("/strategy/getStrategies", methods=["GET"])
def get_strategies():
try:
data = request.get_json() or {}
user_id = int(data.get("userid") or 1)
user_id = int(request.args.get("userid") or 1)
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
+317
View File
@@ -0,0 +1,317 @@
"""
User Management API Routes
Provides endpoints for user CRUD operations, role management, etc.
Only accessible by admin users.
"""
from flask import Blueprint, request, jsonify, g
from app.services.user_service import get_user_service
from app.utils.auth import login_required, admin_required
from app.utils.logger import get_logger
logger = get_logger(__name__)
user_bp = Blueprint('user_manage', __name__)
@user_bp.route('/list', methods=['GET'])
@login_required
@admin_required
def list_users():
"""
List all users (admin only).
Query params:
page: int (default 1)
page_size: int (default 20, max 100)
"""
try:
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
page_size = min(100, max(1, page_size))
result = get_user_service().list_users(page=page, page_size=page_size)
return jsonify({
'code': 1,
'msg': 'success',
'data': result
})
except Exception as e:
logger.error(f"list_users failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/detail', methods=['GET'])
@login_required
@admin_required
def get_user_detail():
"""Get user detail by ID (admin only)"""
try:
user_id = request.args.get('id', type=int)
if not user_id:
return jsonify({'code': 0, 'msg': 'Missing user id', 'data': None}), 400
user = get_user_service().get_user_by_id(user_id)
if not user:
return jsonify({'code': 0, 'msg': 'User not found', 'data': None}), 404
return jsonify({
'code': 1,
'msg': 'success',
'data': user
})
except Exception as e:
logger.error(f"get_user_detail failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/create', methods=['POST'])
@login_required
@admin_required
def create_user():
"""
Create a new user (admin only).
Request body:
username: str (required)
password: str (required)
email: str (optional)
nickname: str (optional)
role: str (optional, default 'user')
"""
try:
data = request.get_json() or {}
user_id = get_user_service().create_user(data)
return jsonify({
'code': 1,
'msg': 'User created successfully',
'data': {'id': user_id}
})
except ValueError as e:
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 400
except Exception as e:
logger.error(f"create_user failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/update', methods=['PUT'])
@login_required
@admin_required
def update_user():
"""
Update user information (admin only).
Query params:
id: int (required)
Request body:
email: str (optional)
nickname: str (optional)
role: str (optional)
status: str (optional)
"""
try:
user_id = request.args.get('id', type=int)
if not user_id:
return jsonify({'code': 0, 'msg': 'Missing user id', 'data': None}), 400
data = request.get_json() or {}
success = get_user_service().update_user(user_id, data)
if success:
return jsonify({'code': 1, 'msg': 'User updated successfully', 'data': None})
else:
return jsonify({'code': 0, 'msg': 'Update failed', 'data': None}), 400
except Exception as e:
logger.error(f"update_user failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/delete', methods=['DELETE'])
@login_required
@admin_required
def delete_user():
"""Delete a user (admin only)"""
try:
user_id = request.args.get('id', type=int)
if not user_id:
return jsonify({'code': 0, 'msg': 'Missing user id', 'data': None}), 400
# Prevent deleting self
if hasattr(g, 'user_id') and g.user_id == user_id:
return jsonify({'code': 0, 'msg': 'Cannot delete yourself', 'data': None}), 400
success = get_user_service().delete_user(user_id)
if success:
return jsonify({'code': 1, 'msg': 'User deleted successfully', 'data': None})
else:
return jsonify({'code': 0, 'msg': 'Delete failed', 'data': None}), 400
except Exception as e:
logger.error(f"delete_user failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/reset-password', methods=['POST'])
@login_required
@admin_required
def reset_user_password():
"""
Reset a user's password (admin only).
Request body:
user_id: int (required)
new_password: str (required)
"""
try:
data = request.get_json() or {}
user_id = data.get('user_id')
new_password = data.get('new_password', '')
if not user_id:
return jsonify({'code': 0, 'msg': 'Missing user_id', 'data': None}), 400
if len(new_password) < 6:
return jsonify({'code': 0, 'msg': 'Password must be at least 6 characters', 'data': None}), 400
success = get_user_service().reset_password(user_id, new_password)
if success:
return jsonify({'code': 1, 'msg': 'Password reset successfully', 'data': None})
else:
return jsonify({'code': 0, 'msg': 'Reset failed', 'data': None}), 400
except ValueError as e:
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 400
except Exception as e:
logger.error(f"reset_user_password failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/roles', methods=['GET'])
@login_required
@admin_required
def get_roles():
"""Get available roles and their permissions"""
service = get_user_service()
roles = []
for role in service.ROLES:
roles.append({
'id': role,
'name': role.capitalize(),
'permissions': service.get_user_permissions(role)
})
return jsonify({
'code': 1,
'msg': 'success',
'data': {'roles': roles}
})
# Self-service endpoints (accessible by any logged-in user)
@user_bp.route('/profile', methods=['GET'])
@login_required
def get_profile():
"""Get current user's profile"""
try:
user_id = getattr(g, 'user_id', None)
if not user_id:
return jsonify({'code': 0, 'msg': 'Not authenticated', 'data': None}), 401
user = get_user_service().get_user_by_id(user_id)
if not user:
return jsonify({'code': 0, 'msg': 'User not found', 'data': None}), 404
# Add permissions
user['permissions'] = get_user_service().get_user_permissions(user.get('role', 'user'))
return jsonify({
'code': 1,
'msg': 'success',
'data': user
})
except Exception as e:
logger.error(f"get_profile failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/profile/update', methods=['PUT'])
@login_required
def update_profile():
"""
Update current user's profile (limited fields).
Request body:
nickname: str (optional)
email: str (optional)
avatar: str (optional)
"""
try:
user_id = getattr(g, 'user_id', None)
if not user_id:
return jsonify({'code': 0, 'msg': 'Not authenticated', 'data': None}), 401
data = request.get_json() or {}
# Only allow updating certain fields for self-service
allowed = {}
for field in ['nickname', 'email', 'avatar']:
if field in data:
allowed[field] = data[field]
if not allowed:
return jsonify({'code': 0, 'msg': 'No valid fields to update', 'data': None}), 400
success = get_user_service().update_user(user_id, allowed)
if success:
return jsonify({'code': 1, 'msg': 'Profile updated successfully', 'data': None})
else:
return jsonify({'code': 0, 'msg': 'Update failed', 'data': None}), 400
except Exception as e:
logger.error(f"update_profile failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/change-password', methods=['POST'])
@login_required
def change_password():
"""
Change current user's password.
Request body:
old_password: str (required)
new_password: str (required)
"""
try:
user_id = getattr(g, 'user_id', None)
if not user_id:
return jsonify({'code': 0, 'msg': 'Not authenticated', 'data': None}), 401
data = request.get_json() or {}
old_password = data.get('old_password', '')
new_password = data.get('new_password', '')
if not old_password or not new_password:
return jsonify({'code': 0, 'msg': 'Both old and new password required', 'data': None}), 400
if len(new_password) < 6:
return jsonify({'code': 0, 'msg': 'New password must be at least 6 characters', 'data': None}), 400
success = get_user_service().change_password(user_id, old_password, new_password)
if success:
return jsonify({'code': 1, 'msg': 'Password changed successfully', 'data': None})
else:
return jsonify({'code': 0, 'msg': 'Old password incorrect', 'data': None}), 400
except ValueError as e:
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 400
except Exception as e:
logger.error(f"change_password failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500