v2.2.1: frontend closed-source + Docker one-click deploy

- Remove frontend source code (now in private repo)
- Add pre-built frontend/dist/ with Nginx serving
- Simplify docker-compose.yml (no Node.js build needed)
- Update README with docs index and Docker deploy guide
- Add admin order list and AI analysis stats tabs
- Add quick trade API routes
- Clean up redundant files (package-lock.json, yarn.lock, .iml)
- Add GitHub Actions workflow for frontend update automation
This commit is contained in:
TIANHE
2026-02-27 19:57:23 +08:00
parent ffdd2ffbae
commit abbad97bdd
250 changed files with 1895 additions and 91621 deletions
+447
View File
@@ -1071,3 +1071,450 @@ def get_system_strategies():
import traceback
logger.error(traceback.format_exc())
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
# ==================== Admin Orders ====================
@user_bp.route('/admin-orders', methods=['GET'])
@login_required
@admin_required
def get_admin_orders():
"""
Get all orders across the system (admin only).
Merges qd_membership_orders and qd_usdt_orders into a unified list.
Query params:
page: int (default 1)
page_size: int (default 20, max 100)
status: str (optional, filter by status: paid/pending/confirmed/expired/all)
search: str (optional, search by username/email)
"""
try:
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
status_filter = request.args.get('status', '', type=str).strip().lower()
search = request.args.get('search', '', type=str).strip()
page_size = min(100, max(1, page_size))
offset = (page - 1) * page_size
with get_db_connection() as db:
cur = db.cursor()
# --- USDT Orders (primary) ---
usdt_conditions = []
usdt_params = []
if status_filter and status_filter != 'all':
usdt_conditions.append("o.status = ?")
usdt_params.append(status_filter)
if search:
usdt_conditions.append("(u.username ILIKE ? OR u.email ILIKE ? OR u.nickname ILIKE ?)")
like_val = f"%{search}%"
usdt_params.extend([like_val, like_val, like_val])
usdt_where = ""
if usdt_conditions:
usdt_where = "WHERE " + " AND ".join(usdt_conditions)
# Count
cur.execute(
f"SELECT COUNT(*) as cnt FROM qd_usdt_orders o LEFT JOIN qd_users u ON u.id = o.user_id {usdt_where}",
tuple(usdt_params)
)
usdt_total = cur.fetchone()['cnt']
# --- Membership Orders (mock) ---
mock_conditions = []
mock_params = []
if status_filter and status_filter != 'all':
mock_conditions.append("m.status = ?")
mock_params.append(status_filter)
if search:
mock_conditions.append("(u.username ILIKE ? OR u.email ILIKE ? OR u.nickname ILIKE ?)")
like_val = f"%{search}%"
mock_params.extend([like_val, like_val, like_val])
mock_where = ""
if mock_conditions:
mock_where = "WHERE " + " AND ".join(mock_conditions)
cur.execute(
f"SELECT COUNT(*) as cnt FROM qd_membership_orders m LEFT JOIN qd_users u ON u.id = m.user_id {mock_where}",
tuple(mock_params)
)
mock_total = cur.fetchone()['cnt']
total = usdt_total + mock_total
# Use UNION ALL to merge both tables into one sorted list
# We select a unified schema
union_sql = f"""
SELECT * FROM (
SELECT
o.id,
'usdt' AS order_type,
o.user_id,
u.username,
u.nickname,
u.email AS user_email,
o.plan,
o.amount_usdt AS amount,
'USDT' AS currency,
o.chain,
o.address,
o.tx_hash,
o.status,
o.created_at,
o.paid_at,
o.confirmed_at,
o.expires_at
FROM qd_usdt_orders o
LEFT JOIN qd_users u ON u.id = o.user_id
{usdt_where}
UNION ALL
SELECT
m.id,
'mock' AS order_type,
m.user_id,
u.username,
u.nickname,
u.email AS user_email,
m.plan,
m.price_usd AS amount,
'USD' AS currency,
'' AS chain,
'' AS address,
'' AS tx_hash,
m.status,
m.created_at,
m.paid_at,
NULL AS confirmed_at,
NULL AS expires_at
FROM qd_membership_orders m
LEFT JOIN qd_users u ON u.id = m.user_id
{mock_where}
) AS combined
ORDER BY combined.created_at DESC
LIMIT ? OFFSET ?
"""
all_params = list(usdt_params) + list(mock_params) + [page_size, offset]
cur.execute(union_sql, tuple(all_params))
rows = cur.fetchall() or []
# Summary stats
cur.execute(
f"""SELECT
COUNT(*) AS total_orders,
COALESCE(SUM(CASE WHEN status IN ('paid','confirmed') THEN 1 ELSE 0 END), 0) AS paid_orders,
COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) AS pending_orders,
COALESCE(SUM(CASE WHEN status IN ('expired','cancelled','failed') THEN 1 ELSE 0 END), 0) AS failed_orders,
COALESCE(SUM(CASE WHEN status IN ('paid','confirmed') THEN amount_usdt ELSE 0 END), 0) AS total_revenue
FROM qd_usdt_orders"""
)
summary_row = cur.fetchone() or {}
cur.close()
items = []
for row in rows:
created_at = row.get('created_at')
paid_at = row.get('paid_at')
confirmed_at = row.get('confirmed_at')
expires_at = row.get('expires_at')
if hasattr(created_at, 'isoformat'):
created_at = created_at.isoformat()
if hasattr(paid_at, 'isoformat'):
paid_at = paid_at.isoformat()
if hasattr(confirmed_at, 'isoformat'):
confirmed_at = confirmed_at.isoformat()
if hasattr(expires_at, 'isoformat'):
expires_at = expires_at.isoformat()
items.append({
'id': row['id'],
'order_type': row.get('order_type') or '',
'user_id': row.get('user_id'),
'username': row.get('username') or '',
'nickname': row.get('nickname') or '',
'user_email': row.get('user_email') or '',
'plan': row.get('plan') or '',
'amount': float(row.get('amount') or 0),
'currency': row.get('currency') or '',
'chain': row.get('chain') or '',
'address': row.get('address') or '',
'tx_hash': row.get('tx_hash') or '',
'status': row.get('status') or '',
'created_at': created_at,
'paid_at': paid_at,
'confirmed_at': confirmed_at,
'expires_at': expires_at
})
return jsonify({
'code': 1,
'msg': 'success',
'data': {
'items': items,
'total': total,
'page': page,
'page_size': page_size,
'summary': {
'total_orders': int(summary_row.get('total_orders') or 0),
'paid_orders': int(summary_row.get('paid_orders') or 0),
'pending_orders': int(summary_row.get('pending_orders') or 0),
'failed_orders': int(summary_row.get('failed_orders') or 0),
'total_revenue': round(float(summary_row.get('total_revenue') or 0), 2)
}
}
})
except Exception as e:
logger.error(f"get_admin_orders failed: {e}")
import traceback
logger.error(traceback.format_exc())
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
# ==================== Admin AI Analysis Stats ====================
@user_bp.route('/admin-ai-stats', methods=['GET'])
@login_required
@admin_required
def get_admin_ai_stats():
"""
Get AI analysis usage statistics across the system (admin only).
Does NOT expose analysis results, only aggregated counts/stats.
Query params:
page: int (default 1)
page_size: int (default 20, max 100)
search: str (optional, search by username)
"""
try:
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
search = request.args.get('search', '', type=str).strip()
page_size = min(100, max(1, page_size))
offset = (page - 1) * page_size
with get_db_connection() as db:
cur = db.cursor()
# --- Overall summary (from qd_analysis_tasks + qd_analysis_memory) ---
cur.execute("""
SELECT
COUNT(*) AS total_tasks,
COUNT(DISTINCT user_id) AS unique_users,
COUNT(DISTINCT symbol) AS unique_symbols,
COUNT(DISTINCT market) AS unique_markets
FROM qd_analysis_tasks
""")
task_summary = cur.fetchone() or {}
memory_summary = {}
try:
cur.execute("""
SELECT
COUNT(*) AS total_memory,
COALESCE(SUM(CASE WHEN was_correct = true THEN 1 ELSE 0 END), 0) AS correct_count,
COALESCE(SUM(CASE WHEN was_correct = false THEN 1 ELSE 0 END), 0) AS incorrect_count,
COALESCE(SUM(CASE WHEN user_feedback = 'helpful' THEN 1 ELSE 0 END), 0) AS helpful_count,
COALESCE(SUM(CASE WHEN user_feedback = 'not_helpful' THEN 1 ELSE 0 END), 0) AS not_helpful_count
FROM qd_analysis_memory
""")
memory_summary = cur.fetchone() or {}
except Exception as mem_err:
logger.warning(f"qd_analysis_memory query failed (table/column may not exist): {mem_err}")
db.rollback()
cur = db.cursor() # re-create cursor after rollback
memory_summary = {}
# --- Per-user stats ---
user_conditions = []
user_params = []
if search:
user_conditions.append("(u.username ILIKE ? OR u.nickname ILIKE ? OR u.email ILIKE ?)")
like_val = f"%{search}%"
user_params.extend([like_val, like_val, like_val])
user_where = ""
if user_conditions:
user_where = "WHERE " + " AND ".join(user_conditions)
# Count distinct users who have analysis records
cur.execute(
f"""
SELECT COUNT(DISTINCT t.user_id) AS cnt
FROM qd_analysis_tasks t
LEFT JOIN qd_users u ON u.id = t.user_id
{user_where}
""",
tuple(user_params)
)
user_total = cur.fetchone()['cnt']
# Get per-user aggregated stats
cur.execute(
f"""
SELECT
t.user_id,
u.username,
u.nickname,
u.email,
COUNT(*) AS analysis_count,
COUNT(DISTINCT t.symbol) AS symbol_count,
COUNT(DISTINCT t.market) AS market_count,
MAX(t.created_at) AS last_analysis_at,
MIN(t.created_at) AS first_analysis_at
FROM qd_analysis_tasks t
LEFT JOIN qd_users u ON u.id = t.user_id
{user_where}
GROUP BY t.user_id, u.username, u.nickname, u.email
ORDER BY analysis_count DESC
LIMIT ? OFFSET ?
""",
tuple(user_params) + (page_size, offset)
)
user_rows = cur.fetchall() or []
# Get per-user analysis_memory stats (correct/helpful counts)
user_ids = [r['user_id'] for r in user_rows if r.get('user_id')]
memory_stats_map = {}
if user_ids:
try:
placeholders = ','.join(['?'] * len(user_ids))
cur.execute(
f"""
SELECT
user_id,
COUNT(*) AS memory_count,
COALESCE(SUM(CASE WHEN was_correct = true THEN 1 ELSE 0 END), 0) AS correct,
COALESCE(SUM(CASE WHEN was_correct = false THEN 1 ELSE 0 END), 0) AS incorrect,
COALESCE(SUM(CASE WHEN user_feedback = 'helpful' THEN 1 ELSE 0 END), 0) AS helpful,
COALESCE(SUM(CASE WHEN user_feedback = 'not_helpful' THEN 1 ELSE 0 END), 0) AS not_helpful
FROM qd_analysis_memory
WHERE user_id IN ({placeholders})
GROUP BY user_id
""",
tuple(user_ids)
)
for row in (cur.fetchall() or []):
memory_stats_map[row['user_id']] = {
'memory_count': row['memory_count'],
'correct': row['correct'],
'incorrect': row['incorrect'],
'helpful': row['helpful'],
'not_helpful': row['not_helpful']
}
except Exception as mem_err:
logger.warning(f"qd_analysis_memory per-user query failed: {mem_err}")
db.rollback()
cur = db.cursor() # re-create cursor after rollback
memory_stats_map = {}
# Get recent analysis records (last 50)
cur.execute(
"""
SELECT
t.id,
t.user_id,
u.username,
u.nickname,
t.market,
t.symbol,
t.model,
t.status,
t.created_at,
t.completed_at
FROM qd_analysis_tasks t
LEFT JOIN qd_users u ON u.id = t.user_id
ORDER BY t.created_at DESC
LIMIT 50
"""
)
recent_rows = cur.fetchall() or []
cur.close()
# Build per-user items
user_items = []
for row in user_rows:
uid = row.get('user_id')
ms = memory_stats_map.get(uid, {})
last_at = row.get('last_analysis_at')
first_at = row.get('first_analysis_at')
if hasattr(last_at, 'isoformat'):
last_at = last_at.isoformat()
if hasattr(first_at, 'isoformat'):
first_at = first_at.isoformat()
user_items.append({
'user_id': uid,
'username': row.get('username') or '',
'nickname': row.get('nickname') or '',
'email': row.get('email') or '',
'analysis_count': row.get('analysis_count') or 0,
'symbol_count': row.get('symbol_count') or 0,
'market_count': row.get('market_count') or 0,
'correct': ms.get('correct', 0),
'incorrect': ms.get('incorrect', 0),
'helpful': ms.get('helpful', 0),
'not_helpful': ms.get('not_helpful', 0),
'last_analysis_at': last_at,
'first_analysis_at': first_at
})
# Build recent records
recent_items = []
for row in recent_rows:
created_at = row.get('created_at')
completed_at = row.get('completed_at')
if hasattr(created_at, 'isoformat'):
created_at = created_at.isoformat()
if hasattr(completed_at, 'isoformat'):
completed_at = completed_at.isoformat()
recent_items.append({
'id': row['id'],
'user_id': row.get('user_id'),
'username': row.get('username') or '',
'nickname': row.get('nickname') or '',
'market': row.get('market') or '',
'symbol': row.get('symbol') or '',
'model': row.get('model') or '',
'status': row.get('status') or '',
'created_at': created_at,
'completed_at': completed_at
})
return jsonify({
'code': 1,
'msg': 'success',
'data': {
'user_stats': user_items,
'user_total': user_total,
'page': page,
'page_size': page_size,
'recent': recent_items,
'summary': {
'total_analyses': int(task_summary.get('total_tasks') or 0),
'unique_users': int(task_summary.get('unique_users') or 0),
'unique_symbols': int(task_summary.get('unique_symbols') or 0),
'unique_markets': int(task_summary.get('unique_markets') or 0),
'total_memory': int(memory_summary.get('total_memory') or 0),
'correct_count': int(memory_summary.get('correct_count') or 0),
'incorrect_count': int(memory_summary.get('incorrect_count') or 0),
'helpful_count': int(memory_summary.get('helpful_count') or 0),
'not_helpful_count': int(memory_summary.get('not_helpful_count') or 0)
}
}
})
except Exception as e:
logger.error(f"get_admin_ai_stats failed: {e}")
import traceback
logger.error(traceback.format_exc())
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500