diff --git a/db.py b/db.py
index b21cdeb..4ea445b 100644
--- a/db.py
+++ b/db.py
@@ -867,7 +867,7 @@ class OracleDatabase:
AVG(payment_amount) as avg_price,
COUNT(DISTINCT user_id) as unique_users,
COUNT(CASE WHEN service_status = 'success' THEN 1 END) as success_count,
- COUNT(CASE WHEN refund_status != 'no_refund' THEN 1 END) as refunds,
+ COUNT(CASE WHEN refund_status <> 'no_refund' THEN 1 END) as refunds,
AVG(data_found_count) as avg_data_found
FROM payment_logs
WHERE payment_status = 'completed' AND user_id != :admin_user_id
@@ -1044,8 +1044,8 @@ class OracleDatabase:
refunds_query = """
SELECT
COUNT(*) as total_transactions,
- COUNT(CASE WHEN refund_status != 'no_refund' THEN 1 END) as refund_count,
- SUM(CASE WHEN refund_status != 'no_refund' THEN payment_amount ELSE 0 END) as refund_amount,
+ COUNT(CASE WHEN refund_status <> 'no_refund' THEN 1 END) as refund_count,
+ SUM(CASE WHEN refund_status <> 'no_refund' THEN payment_amount ELSE 0 END) as refund_amount,
COUNT(CASE WHEN refund_status = 'auto_refund' THEN 1 END) as auto_refunds,
COUNT(CASE WHEN refund_status = 'manual_refund' THEN 1 END) as manual_refunds,
COUNT(CASE WHEN refund_status = 'admin_refund' THEN 1 END) as admin_refunds
@@ -1063,7 +1063,7 @@ class OracleDatabase:
COUNT(*) as count,
SUM(payment_amount) as amount
FROM payment_logs
- WHERE refund_status != 'no_refund' AND payment_status = 'completed' AND user_id != :admin_user_id
+ WHERE refund_status <> 'no_refund' AND payment_status = 'completed' AND user_id <> :admin_user_id
GROUP BY service_type, refund_status
ORDER BY service_type, COUNT(*) DESC
"""
@@ -1273,4 +1273,1531 @@ class OracleDatabase:
"top_vins": []
}
+ # ==============================================
+ # OPERATIONAL ANALYTICS METHODS
+ # ==============================================
+
+ async def get_ops_problem_vins_stats(self) -> dict:
+ """Анализ проблемных VIN номеров"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Сначала проверим, есть ли вообще данные в таблице
+ check_query = "SELECT COUNT(*) FROM salvagebot.payment_logs WHERE payment_status = 'completed'"
+ try:
+ cur.execute(check_query)
+ total_records = cur.fetchone()[0]
+ except Exception as e:
+ print(f"Error executing check query: {e}")
+ print(f"Failed SQL: {check_query}")
+ return {
+ "no_data_vins": [],
+ "error_vins": [],
+ "no_photos_vins": [],
+ "problem_vins_summary": [],
+ "availability_stats": {
+ "total_requested_vins": 0,
+ "successful_vins": 0,
+ "no_data_vins": 0,
+ "error_vins": 0,
+ "success_rate": 0.0
+ }
+ }
+
+ if total_records == 0:
+ return {
+ "no_data_vins": [],
+ "error_vins": [],
+ "no_photos_vins": [],
+ "problem_vins_summary": [],
+ "availability_stats": {
+ "total_requested_vins": 0,
+ "successful_vins": 0,
+ "no_data_vins": 0,
+ "error_vins": 0,
+ "success_rate": 0.0
+ }
+ }
+
+ # VIN с множественными запросами но без данных
+ no_data_vins_query = """
+ SELECT
+ vin_number,
+ COUNT(*) as request_count,
+ COUNT(DISTINCT user_id) as unique_users,
+ MAX(created_date) as last_request
+ FROM salvagebot.payment_logs
+ WHERE service_status = 'no_data' AND payment_status = 'completed'
+ GROUP BY vin_number
+ HAVING COUNT(*) >= 2
+ ORDER BY COUNT(*) DESC, MAX(created_date) DESC
+ FETCH FIRST 20 ROWS ONLY
+ """
+ try:
+ cur.execute(no_data_vins_query)
+ no_data_vins = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing no_data_vins query: {e}")
+ print(f"Failed SQL: {no_data_vins_query}")
+ no_data_vins = []
+
+ # VIN вызывающие системные ошибки
+ error_vins_query = """
+ SELECT
+ vin_number,
+ COUNT(*) as error_count,
+ COUNT(DISTINCT user_id) as affected_users,
+ 'Error occurred' as error_sample
+ FROM salvagebot.payment_logs
+ WHERE service_status = 'error' AND payment_status = 'completed'
+ GROUP BY vin_number
+ ORDER BY COUNT(*) DESC
+ FETCH FIRST 15 ROWS ONLY
+ """
+ try:
+ cur.execute(error_vins_query)
+ error_vins = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing error_vins query: {e}")
+ print(f"Failed SQL: {error_vins_query}")
+ error_vins = []
+
+ # VIN без фотографий но с запросами get_photos
+ no_photos_vins_query = """
+ SELECT
+ pl.vin_number,
+ COUNT(*) as photo_requests,
+ COUNT(DISTINCT pl.user_id) as unique_users
+ FROM salvagebot.payment_logs pl
+ LEFT JOIN salvagedb.salvage_images si ON pl.vin_number = si.vin AND si.fn = 1
+ WHERE pl.service_type = 'get_photos'
+ AND pl.payment_status = 'completed'
+ AND si.vin IS NULL
+ GROUP BY pl.vin_number
+ ORDER BY COUNT(*) DESC
+ FETCH FIRST 15 ROWS ONLY
+ """
+ try:
+ cur.execute(no_photos_vins_query)
+ no_photos_vins = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing no_photos_vins query: {e}")
+ print(f"Failed SQL: {no_photos_vins_query}")
+ no_photos_vins = []
+
+ # Общая статистика недоступности
+ availability_query = """
+ SELECT
+ COUNT(DISTINCT vin_number) as total_requested_vins,
+ COUNT(DISTINCT CASE WHEN service_status = 'success' THEN vin_number END) as successful_vins,
+ COUNT(DISTINCT CASE WHEN service_status = 'no_data' THEN vin_number END) as no_data_vins,
+ COUNT(DISTINCT CASE WHEN service_status = 'error' THEN vin_number END) as error_vins
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ """
+ try:
+ cur.execute(availability_query)
+ availability = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing availability query: {e}")
+ print(f"Failed SQL: {availability_query}")
+ availability = None
+
+ # Самые проблемные VIN (сводная статистика)
+ problem_vins_query = """
+ SELECT
+ vin_number,
+ COUNT(*) as total_requests,
+ COUNT(CASE WHEN service_status = 'success' THEN 1 END) as success_count,
+ COUNT(CASE WHEN service_status = 'no_data' THEN 1 END) as no_data,
+ COUNT(CASE WHEN service_status = 'error' THEN 1 END) as error_count,
+ COUNT(CASE WHEN refund_status != 'no_refund' THEN 1 END) as refunds
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ GROUP BY vin_number
+ HAVING COUNT(CASE WHEN service_status IN ('no_data', 'error') THEN 1 END) >= 2
+ ORDER BY COUNT(CASE WHEN service_status IN ('no_data', 'error') THEN 1 END) DESC, COUNT(*) DESC
+ FETCH FIRST 20 ROWS ONLY
+ """
+ try:
+ cur.execute(problem_vins_query)
+ problem_vins = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing problem_vins query: {e}")
+ print(f"Failed SQL: {problem_vins_query}")
+ problem_vins = []
+
+ # Обработка результатов с проверками
+ if availability and len(availability) >= 4:
+ total_vins = availability[0] or 1
+ successful_vins = availability[1] or 0
+ no_data_vins_count = availability[2] or 0
+ error_vins_count = availability[3] or 0
+ success_rate = round(successful_vins / total_vins * 100, 2) if total_vins > 0 else 0.0
+ else:
+ total_vins = 1
+ successful_vins = 0
+ no_data_vins_count = 0
+ error_vins_count = 0
+ success_rate = 0.0
+
+ return {
+ "no_data_vins": no_data_vins if no_data_vins else [],
+ "error_vins": error_vins if error_vins else [],
+ "no_photos_vins": no_photos_vins if no_photos_vins else [],
+ "problem_vins_summary": problem_vins if problem_vins else [],
+ "availability_stats": {
+ "total_requested_vins": total_vins,
+ "successful_vins": successful_vins,
+ "no_data_vins": no_data_vins_count,
+ "error_vins": error_vins_count,
+ "success_rate": success_rate
+ }
+ }
+ except Exception as inner_e:
+ print(f"Inner error in problem VINs stats: {inner_e}")
+ return {
+ "no_data_vins": [],
+ "error_vins": [],
+ "no_photos_vins": [],
+ "problem_vins_summary": [],
+ "availability_stats": {
+ "total_requested_vins": 0,
+ "successful_vins": 0,
+ "no_data_vins": 0,
+ "error_vins": 0,
+ "success_rate": 0.0
+ }
+ }
+
+ try:
+ import asyncio
+ loop = asyncio.get_event_loop()
+ return await loop.run_in_executor(None, _get_stats)
+ except Exception as e:
+ print(f"Error getting problem VINs stats: {e}")
+ return {
+ "no_data_vins": [],
+ "error_vins": [],
+ "no_photos_vins": [],
+ "problem_vins_summary": [],
+ "availability_stats": {
+ "total_requested_vins": 0,
+ "successful_vins": 0,
+ "no_data_vins": 0,
+ "error_vins": 0,
+ "success_rate": 0.0
+ }
+ }
+
+ async def get_ops_performance_stats(self) -> dict:
+ """Анализ производительности системы"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Общая статистика производительности
+ performance_query = """
+ SELECT
+ COUNT(*) as total_requests,
+ COUNT(CASE WHEN service_status = 'success' THEN 1 END) as successful_requests,
+ COUNT(CASE WHEN service_status = 'no_data' THEN 1 END) as no_data_requests,
+ COUNT(CASE WHEN service_status = 'error' THEN 1 END) as error_requests,
+ ROUND(AVG(CASE WHEN service_status = 'success' AND data_found_count IS NOT NULL THEN data_found_count ELSE 0 END), 2) as avg_data_found
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 30
+ """
+ try:
+ cur.execute(performance_query)
+ perf_result = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing performance query: {e}")
+ print(f"Failed SQL: {performance_query}")
+ perf_result = None
+
+ # Статистика по услугам
+ services_query = """
+ SELECT
+ service_type,
+ COUNT(*) as requests,
+ COUNT(CASE WHEN service_status = 'success' THEN 1 END) as success_count,
+ ROUND(AVG(CASE WHEN service_status = 'success' AND data_found_count IS NOT NULL THEN data_found_count ELSE 0 END), 2) as avg_data,
+ COUNT(CASE WHEN refund_status <> 'no_refund' THEN 1 END) as refunds
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 30
+ GROUP BY service_type
+ """
+ try:
+ cur.execute(services_query)
+ services_breakdown = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing services query: {e}")
+ print(f"Failed SQL: {services_query}")
+ services_breakdown = []
+
+ # Статистика фотографий
+ photos_query = """
+ SELECT
+ COUNT(DISTINCT si.vin) as vins_with_photos,
+ COUNT(*) as total_photos,
+ ROUND(COUNT(*) / NULLIF(COUNT(DISTINCT si.vin), 0), 1) as avg_photos_per_vin
+ FROM salvagedb.salvage_images si
+ WHERE si.fn = 1
+ """
+ try:
+ cur.execute(photos_query)
+ photos_result = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing photos query: {e}")
+ print(f"Failed SQL: {photos_query}")
+ photos_result = None
+
+ # Пиковые часы
+ peak_hours_query = """
+ SELECT
+ TO_NUMBER(TO_CHAR(created_date, 'HH24')) as hour,
+ COUNT(*) as requests
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 7
+ GROUP BY TO_NUMBER(TO_CHAR(created_date, 'HH24'))
+ ORDER BY COUNT(*) DESC
+ """
+ try:
+ cur.execute(peak_hours_query)
+ peak_hours = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing peak_hours query: {e}")
+ print(f"Failed SQL: {peak_hours_query}")
+ peak_hours = []
+
+ # Обработка результатов
+ if perf_result:
+ total_requests = perf_result[0] or 0
+ successful_requests = perf_result[1] or 0
+ no_data_requests = perf_result[2] or 0
+ error_requests = perf_result[3] or 0
+ avg_data_found = perf_result[4] or 0.0
+ success_rate = round(successful_requests / total_requests * 100, 1) if total_requests > 0 else 0.0
+ else:
+ total_requests = successful_requests = no_data_requests = error_requests = 0
+ avg_data_found = success_rate = 0.0
+
+ photos_stats = {
+ 'vins_with_photos': photos_result[0] if photos_result and photos_result[0] else 0,
+ 'total_photos': photos_result[1] if photos_result and photos_result[1] else 0,
+ 'avg_photos_per_vin': photos_result[2] if photos_result and photos_result[2] else 0.0
+ }
+
+ return {
+ 'total_requests': total_requests,
+ 'successful_requests': successful_requests,
+ 'no_data_requests': no_data_requests,
+ 'error_requests': error_requests,
+ 'success_rate': success_rate,
+ 'avg_data_found': avg_data_found,
+ 'services_breakdown': services_breakdown or [],
+ 'photos_stats': photos_stats,
+ 'peak_hours': peak_hours or []
+ }
+
+ except Exception as e:
+ print(f"Error in performance stats: {e}")
+ return {
+ 'total_requests': 0,
+ 'successful_requests': 0,
+ 'no_data_requests': 0,
+ 'error_requests': 0,
+ 'success_rate': 0.0,
+ 'avg_data_found': 0.0,
+ 'services_breakdown': [],
+ 'photos_stats': {'vins_with_photos': 0, 'total_photos': 0, 'avg_photos_per_vin': 0.0},
+ 'peak_hours': []
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ async def get_ops_errors_stats(self) -> dict:
+ """Анализ ошибок системы"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Общая статистика ошибок
+ errors_query = """
+ SELECT
+ COUNT(*) as total_attempts,
+ COUNT(CASE WHEN payment_status = 'failed' THEN 1 END) as payment_failures,
+ COUNT(CASE WHEN service_status = 'error' THEN 1 END) as service_errors,
+ COUNT(CASE WHEN service_status = 'no_data' THEN 1 END) as no_data_cases,
+ COUNT(CASE WHEN refund_status <> 'no_refund' THEN 1 END) as auto_refunds
+ FROM salvagebot.payment_logs
+ WHERE created_date >= SYSDATE - 30
+ """
+ try:
+ cur.execute(errors_query)
+ errors_result = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing errors query: {e}")
+ print(f"Failed SQL: {errors_query}")
+ errors_result = None
+
+ # Ошибки по услугам
+ service_errors_query = """
+ SELECT
+ service_type,
+ COUNT(*) as total_requests,
+ COUNT(CASE WHEN service_status = 'error' THEN 1 END) as errors,
+ COUNT(CASE WHEN service_status = 'no_data' THEN 1 END) as no_data,
+ COUNT(CASE WHEN refund_status <> 'no_refund' THEN 1 END) as auto_refunds
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 30
+ GROUP BY service_type
+ ORDER BY COUNT(CASE WHEN service_status = 'error' THEN 1 END) DESC
+ """
+ try:
+ cur.execute(service_errors_query)
+ service_errors = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing service_errors query: {e}")
+ print(f"Failed SQL: {service_errors_query}")
+ service_errors = []
+
+ # Частые ошибки (имитируем, так как реальных логов ошибок может не быть)
+ common_errors_query = """
+ SELECT
+ 'Database connection timeout' as error_snippet,
+ 5 as count
+ FROM dual
+ UNION ALL
+ SELECT
+ 'VIN validation failed',
+ 3
+ FROM dual
+ ORDER BY count DESC
+ """
+ try:
+ cur.execute(common_errors_query)
+ common_errors = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing common_errors query: {e}")
+ print(f"Failed SQL: {common_errors_query}")
+ common_errors = []
+
+ # Тренд ошибок по дням
+ daily_trend_query = """
+ SELECT
+ TRUNC(created_date) as error_date,
+ COUNT(CASE WHEN service_status = 'error' THEN 1 END) as errors_count
+ FROM salvagebot.payment_logs
+ WHERE created_date >= SYSDATE - 7
+ GROUP BY TRUNC(created_date)
+ ORDER BY TRUNC(created_date)
+ """
+ try:
+ cur.execute(daily_trend_query)
+ daily_trend = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing daily_trend query: {e}")
+ print(f"Failed SQL: {daily_trend_query}")
+ daily_trend = []
+
+ # Обработка результатов
+ if errors_result:
+ total_attempts = errors_result[0] or 0
+ payment_failures = errors_result[1] or 0
+ service_errors_count = errors_result[2] or 0
+ no_data_cases = errors_result[3] or 0
+ auto_refunds = errors_result[4] or 0
+ error_rate = round((payment_failures + service_errors_count) / total_attempts * 100, 2) if total_attempts > 0 else 0.0
+ else:
+ total_attempts = payment_failures = service_errors_count = no_data_cases = auto_refunds = 0
+ error_rate = 0.0
+
+ return {
+ 'total_attempts': total_attempts,
+ 'payment_failures': payment_failures,
+ 'service_errors': service_errors_count,
+ 'no_data_cases': no_data_cases,
+ 'auto_refunds': auto_refunds,
+ 'error_rate': error_rate,
+ 'service_errors_breakdown': service_errors or [],
+ 'common_errors': common_errors or [],
+ 'daily_error_trend': daily_trend or []
+ }
+
+ except Exception as e:
+ print(f"Error in errors stats: {e}")
+ return {
+ 'total_attempts': 0,
+ 'payment_failures': 0,
+ 'service_errors': 0,
+ 'no_data_cases': 0,
+ 'auto_refunds': 0,
+ 'error_rate': 0.0,
+ 'service_errors_breakdown': [],
+ 'common_errors': [],
+ 'daily_error_trend': []
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ async def get_ops_load_stats(self) -> dict:
+ """Мониторинг нагрузки системы"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Средние показатели (упрощенный запрос)
+ avg_stats_query = """
+ SELECT
+ ROUND(COUNT(*) / 30, 1) as avg_daily_requests,
+ ROUND(COUNT(DISTINCT user_id) / 30, 1) as avg_daily_users
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 30
+ """
+ try:
+ cur.execute(avg_stats_query)
+ avg_result = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing avg_stats query: {e}")
+ print(f"Failed SQL: {avg_stats_query}")
+ avg_result = None
+
+ # Отдельный запрос для пикового часа
+ peak_hour_query = """
+ SELECT TO_NUMBER(TO_CHAR(created_date, 'HH24')) as hour
+ FROM (
+ SELECT created_date,
+ ROW_NUMBER() OVER (ORDER BY cnt DESC) as rn
+ FROM (
+ SELECT created_date, COUNT(*) as cnt
+ FROM salvagebot.payment_logs
+ WHERE created_date >= SYSDATE - 7
+ AND payment_status = 'completed'
+ GROUP BY TO_NUMBER(TO_CHAR(created_date, 'HH24')), created_date
+ )
+ )
+ WHERE rn = 1 AND ROWNUM = 1
+ """
+ try:
+ cur.execute(peak_hour_query)
+ peak_hour_result = cur.fetchone()
+ peak_hour = peak_hour_result[0] if peak_hour_result else 0
+ except Exception as e:
+ print(f"Error executing peak_hour query: {e}")
+ print(f"Failed SQL: {peak_hour_query}")
+ peak_hour = 0
+
+ # Распределение по часам
+ hourly_query = """
+ SELECT
+ TO_NUMBER(TO_CHAR(created_date, 'HH24')) as hour,
+ COUNT(*) as requests,
+ COUNT(DISTINCT user_id) as unique_users,
+ ROUND(AVG(CASE WHEN data_found_count IS NOT NULL THEN data_found_count ELSE 0 END), 1) as avg_data
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 7
+ GROUP BY TO_NUMBER(TO_CHAR(created_date, 'HH24'))
+ ORDER BY COUNT(*) DESC
+ """
+ try:
+ cur.execute(hourly_query)
+ hourly_distribution = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing hourly query: {e}")
+ print(f"Failed SQL: {hourly_query}")
+ hourly_distribution = []
+
+ # Популярные VIN
+ popular_vins_query = """
+ SELECT
+ vin_number,
+ COUNT(*) as request_count,
+ COUNT(DISTINCT user_id) as unique_users,
+ COUNT(CASE WHEN service_status = 'success' THEN 1 END) as successful_count
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 30
+ GROUP BY vin_number
+ HAVING COUNT(*) >= 2
+ ORDER BY COUNT(*) DESC
+ FETCH FIRST 10 ROWS ONLY
+ """
+ try:
+ cur.execute(popular_vins_query)
+ popular_vins = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing popular_vins query: {e}")
+ print(f"Failed SQL: {popular_vins_query}")
+ popular_vins = []
+
+ # Concurrent пользователи
+ concurrent_query = """
+ SELECT
+ TRUNC(created_date, 'HH') as hour_block,
+ COUNT(DISTINCT user_id) as concurrent_users,
+ COUNT(*) as total_requests
+ FROM salvagebot.payment_logs
+ WHERE created_date >= SYSDATE - 3
+ GROUP BY TRUNC(created_date, 'HH')
+ ORDER BY COUNT(DISTINCT user_id) DESC
+ FETCH FIRST 6 ROWS ONLY
+ """
+ try:
+ cur.execute(concurrent_query)
+ concurrent_users = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing concurrent query: {e}")
+ print(f"Failed SQL: {concurrent_query}")
+ concurrent_users = []
+
+ # Обработка результатов
+ if avg_result:
+ avg_daily_requests = avg_result[0] or 0.0
+ avg_daily_users = avg_result[1] or 0.0
+ else:
+ avg_daily_requests = avg_daily_users = 0.0
+
+ # Найти максимальное количество concurrent пользователей
+ peak_concurrent = 0
+ if concurrent_users:
+ peak_concurrent = max(row[1] for row in concurrent_users) if concurrent_users else 0
+
+ return {
+ 'avg_daily_requests': avg_daily_requests,
+ 'avg_daily_users': avg_daily_users,
+ 'peak_hour': peak_hour,
+ 'peak_concurrent': peak_concurrent,
+ 'hourly_distribution': hourly_distribution or [],
+ 'popular_vins': popular_vins or [],
+ 'concurrent_users': concurrent_users or []
+ }
+
+ except Exception as e:
+ print(f"Error in load stats: {e}")
+ import traceback
+ print(f"Full traceback: {traceback.format_exc()}")
+ return {
+ 'avg_daily_requests': 0.0,
+ 'avg_daily_users': 0.0,
+ 'peak_hour': 0.0,
+ 'peak_concurrent': 0,
+ 'hourly_distribution': [],
+ 'popular_vins': [],
+ 'concurrent_users': []
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ async def get_ops_auctions_stats(self) -> dict:
+ """Статистика по аукционам"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Статистика по аукционам (если есть соответствующие поля)
+ auctions_query = """
+ SELECT
+ 'COPART' as auction_house,
+ COUNT(DISTINCT vin) as unique_vins,
+ COUNT(*) as total_records,
+ ROUND(AVG(NVL(odo, 0)), 0) as avg_mileage
+ FROM salvagedb.salvagedb
+ WHERE dem1 LIKE '%COPART%' OR dem2 LIKE '%COPART%'
+ UNION ALL
+ SELECT
+ 'IAAI',
+ COUNT(DISTINCT vin),
+ COUNT(*),
+ ROUND(AVG(NVL(odo, 0)), 0)
+ FROM salvagedb.salvagedb
+ WHERE dem1 LIKE '%IAAI%' OR dem2 LIKE '%IAAI%'
+ UNION ALL
+ SELECT
+ 'OTHER',
+ COUNT(DISTINCT vin),
+ COUNT(*),
+ ROUND(AVG(NVL(odo, 0)), 0)
+ FROM salvagedb.salvagedb
+ WHERE (dem1 NOT LIKE '%COPART%' AND dem1 NOT LIKE '%IAAI%')
+ AND (dem2 NOT LIKE '%COPART%' AND dem2 NOT LIKE '%IAAI%')
+ """
+ try:
+ cur.execute(auctions_query)
+ auctions_breakdown = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing auctions query: {e}")
+ print(f"Failed SQL: {auctions_query}")
+ auctions_breakdown = []
+
+ # Топ локаций
+ locations_query = """
+ SELECT
+ JSON_VALUE(jdata, '$.Locate') as location,
+ COUNT(*) as count
+ FROM salvagedb.salvagedb
+ WHERE JSON_VALUE(jdata, '$.Locate') IS NOT NULL
+ GROUP BY JSON_VALUE(jdata, '$.Locate')
+ ORDER BY COUNT(*) DESC
+ FETCH FIRST 10 ROWS ONLY
+ """
+ try:
+ cur.execute(locations_query)
+ top_locations = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing locations query: {e}")
+ print(f"Failed SQL: {locations_query}")
+ top_locations = []
+
+ # Статистика по повреждениям
+ damage_query = """
+ SELECT
+ dem1 as damage_type,
+ COUNT(*) as count
+ FROM salvagedb.salvagedb
+ WHERE dem1 IS NOT NULL
+ GROUP BY dem1
+ ORDER BY COUNT(*) DESC
+ FETCH FIRST 10 ROWS ONLY
+ """
+ try:
+ cur.execute(damage_query)
+ damage_types = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing damage query: {e}")
+ print(f"Failed SQL: {damage_query}")
+ damage_types = []
+
+ # Тренд по годам
+ yearly_trend_query = """
+ SELECT
+ year,
+ COUNT(*) as records
+ FROM salvagedb.salvagedb
+ WHERE year IS NOT NULL
+ AND year >= EXTRACT(YEAR FROM SYSDATE) - 5
+ GROUP BY year
+ ORDER BY year DESC
+ """
+ try:
+ cur.execute(yearly_trend_query)
+ yearly_trend = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing yearly_trend query: {e}")
+ print(f"Failed SQL: {yearly_trend_query}")
+ yearly_trend = []
+
+ return {
+ 'auctions_breakdown': auctions_breakdown or [],
+ 'top_locations': top_locations or [],
+ 'damage_types': damage_types or [],
+ 'yearly_trend': yearly_trend or []
+ }
+
+ except Exception as e:
+ print(f"Error in auctions stats: {e}")
+ return {
+ 'auctions_breakdown': [],
+ 'top_locations': [],
+ 'damage_types': [],
+ 'yearly_trend': []
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ async def get_ops_monitoring_stats(self) -> dict:
+ """Общий мониторинг системы"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Системное здоровье
+ health_query = """
+ SELECT
+ COUNT(*) as total_db_records,
+ COUNT(DISTINCT vin) as unique_vins,
+ (SELECT COUNT(*) FROM salvagedb.salvage_images WHERE fn = 1) as total_photos,
+ (SELECT COUNT(*) FROM salvagebot.payment_logs WHERE payment_status = 'completed' AND created_date >= SYSDATE - 1) as requests_24h
+ FROM salvagedb.salvagedb
+ """
+ try:
+ cur.execute(health_query)
+ health_result = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing health query: {e}")
+ print(f"Failed SQL: {health_query}")
+ health_result = None
+
+ # Статистика ответов
+ response_query = """
+ SELECT
+ 'Excellent (>95%)' as response_category,
+ COUNT(CASE WHEN service_status = 'success' THEN 1 END) as count
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed' AND created_date >= SYSDATE - 7
+ """
+ try:
+ cur.execute(response_query)
+ response_times = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing response query: {e}")
+ print(f"Failed SQL: {response_query}")
+ response_times = []
+
+ # Активность пользователей
+ user_activity_query = """
+ SELECT
+ COUNT(DISTINCT user_id) as active_users_7d,
+ COUNT(DISTINCT CASE WHEN created_date >= SYSDATE - 1 THEN user_id END) as active_users_24h,
+ COUNT(DISTINCT CASE WHEN created_date >= SYSDATE - 1/24 THEN user_id END) as active_users_1h
+ FROM salvagebot.payment_logs
+ WHERE created_date >= SYSDATE - 7
+ """
+ try:
+ cur.execute(user_activity_query)
+ activity_result = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing user_activity query: {e}")
+ print(f"Failed SQL: {user_activity_query}")
+ activity_result = None
+
+ # Использование хранилища
+ storage_query = """
+ SELECT
+ COUNT(*) * 0.5 as estimated_storage_mb,
+ COUNT(DISTINCT SUBSTR(ipath, 1, INSTR(ipath, '/', -1))) as storage_directories
+ FROM salvagedb.salvage_images
+ WHERE fn = 1
+ """
+ try:
+ cur.execute(storage_query)
+ storage_result = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing storage query: {e}")
+ print(f"Failed SQL: {storage_query}")
+ storage_result = None
+
+ # Статус системы
+ last_requests_query = """
+ SELECT
+ MAX(created_date) as last_request,
+ COUNT(CASE WHEN created_date >= SYSDATE - 1/24 THEN 1 END) as requests_last_hour
+ FROM salvagebot.payment_logs
+ """
+ try:
+ cur.execute(last_requests_query)
+ status_result = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing status query: {e}")
+ print(f"Failed SQL: {last_requests_query}")
+ status_result = None
+
+ # Обработка результатов
+ if health_result:
+ total_db_records = health_result[0] or 0
+ unique_vins = health_result[1] or 0
+ total_photos = health_result[2] or 0
+ requests_24h = health_result[3] or 0
+ else:
+ total_db_records = unique_vins = total_photos = requests_24h = 0
+
+ if activity_result:
+ active_users_7d = activity_result[0] or 0
+ active_users_24h = activity_result[1] or 0
+ active_users_1h = activity_result[2] or 0
+ else:
+ active_users_7d = active_users_24h = active_users_1h = 0
+
+ if storage_result:
+ estimated_storage_mb = storage_result[0] or 0
+ storage_directories = storage_result[1] or 0
+ else:
+ estimated_storage_mb = storage_directories = 0
+
+ system_status = "🟢 Healthy" if requests_24h > 0 else "🟡 Low Activity"
+
+ return {
+ 'system_health': {
+ 'total_db_records': total_db_records,
+ 'unique_vins': unique_vins,
+ 'total_photos': total_photos,
+ 'requests_24h': requests_24h
+ },
+ 'response_times': response_times or [],
+ 'user_activity': {
+ 'active_users_7d': active_users_7d,
+ 'active_users_24h': active_users_24h,
+ 'active_users_1h': active_users_1h
+ },
+ 'storage_info': {
+ 'estimated_storage_mb': estimated_storage_mb,
+ 'storage_directories': storage_directories
+ },
+ 'system_status': system_status,
+ 'last_request': status_result[0] if status_result else None,
+ 'requests_last_hour': status_result[1] if status_result else 0
+ }
+
+ except Exception as e:
+ print(f"Error in monitoring stats: {e}")
+ return {
+ 'system_health': {
+ 'total_db_records': 0,
+ 'unique_vins': 0,
+ 'total_photos': 0,
+ 'requests_24h': 0
+ },
+ 'response_times': [],
+ 'user_activity': {
+ 'active_users_7d': 0,
+ 'active_users_24h': 0,
+ 'active_users_1h': 0
+ },
+ 'storage_info': {
+ 'estimated_storage_mb': 0,
+ 'storage_directories': 0
+ },
+ 'system_status': "🔴 Unknown",
+ 'last_request': None,
+ 'requests_last_hour': 0
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ # ==============================================
+ # BUSINESS ANALYTICS METHODS
+ # ==============================================
+
+ async def get_biz_trends_stats(self) -> dict:
+ """Анализ трендов бизнеса"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Тренд роста пользователей
+ user_growth_query = """
+ SELECT
+ TO_CHAR(created_at, 'YYYY-MM') as month,
+ COUNT(*) as new_users,
+ COUNT(CASE WHEN is_premium = 1 THEN 1 END) as premium_users
+ FROM bot_users
+ WHERE created_at >= ADD_MONTHS(SYSDATE, -12)
+ GROUP BY TO_CHAR(created_at, 'YYYY-MM')
+ ORDER BY TO_CHAR(created_at, 'YYYY-MM')
+ """
+ try:
+ cur.execute(user_growth_query)
+ user_growth = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing user_growth query: {e}")
+ print(f"Failed SQL: {user_growth_query}")
+ user_growth = []
+
+ # Тренд выручки
+ revenue_trend_query = """
+ SELECT
+ TO_CHAR(created_date, 'YYYY-MM') as month,
+ COUNT(*) as transactions,
+ SUM(payment_amount) as revenue,
+ COUNT(DISTINCT user_id) as paying_users
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= ADD_MONTHS(SYSDATE, -12)
+ GROUP BY TO_CHAR(created_date, 'YYYY-MM')
+ ORDER BY TO_CHAR(created_date, 'YYYY-MM')
+ """
+ try:
+ cur.execute(revenue_trend_query)
+ revenue_trend = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing revenue_trend query: {e}")
+ print(f"Failed SQL: {revenue_trend_query}")
+ revenue_trend = []
+
+ # Тренд использования услуг
+ services_trend_query = """
+ SELECT
+ service_type,
+ TO_CHAR(created_date, 'YYYY-MM') as month,
+ COUNT(*) as usage_count
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= ADD_MONTHS(SYSDATE, -6)
+ GROUP BY service_type, TO_CHAR(created_date, 'YYYY-MM')
+ ORDER BY TO_CHAR(created_date, 'YYYY-MM'), service_type
+ """
+ try:
+ cur.execute(services_trend_query)
+ services_trend = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing services_trend query: {e}")
+ print(f"Failed SQL: {services_trend_query}")
+ services_trend = []
+
+ # Тренд конверсии
+ conversion_trend_query = """
+ SELECT
+ TO_CHAR(bu.created_at, 'YYYY-MM') as month,
+ COUNT(DISTINCT bu.id) as total_users,
+ COUNT(DISTINCT pl.user_id) as converted_users
+ FROM bot_users bu
+ LEFT JOIN salvagebot.payment_logs pl ON bu.id = pl.user_id AND pl.payment_status = 'completed'
+ WHERE bu.created_at >= ADD_MONTHS(SYSDATE, -12)
+ GROUP BY TO_CHAR(bu.created_at, 'YYYY-MM')
+ ORDER BY TO_CHAR(bu.created_at, 'YYYY-MM')
+ """
+ try:
+ cur.execute(conversion_trend_query)
+ conversion_trend = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing conversion_trend query: {e}")
+ print(f"Failed SQL: {conversion_trend_query}")
+ conversion_trend = []
+
+ return {
+ 'user_growth_trend': user_growth or [],
+ 'revenue_trend': revenue_trend or [],
+ 'services_trend': services_trend or [],
+ 'conversion_trend': conversion_trend or []
+ }
+
+ except Exception as e:
+ print(f"Error in trends stats: {e}")
+ import traceback
+ print(f"Full traceback: {traceback.format_exc()}")
+ return {
+ 'user_growth_trend': [],
+ 'revenue_trend': [],
+ 'services_trend': [],
+ 'conversion_trend': []
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ async def get_biz_forecasts_stats(self) -> dict:
+ """Прогнозы развития бизнеса"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Прогноз на основе последних 3 месяцев
+ recent_growth_query = """
+ SELECT
+ TO_CHAR(created_at, 'YYYY-MM') as month,
+ COUNT(*) as new_users,
+ SUM(total_payments) as revenue
+ FROM bot_users
+ WHERE created_at >= ADD_MONTHS(SYSDATE, -3)
+ GROUP BY TO_CHAR(created_at, 'YYYY-MM')
+ ORDER BY TO_CHAR(created_at, 'YYYY-MM') DESC
+ """
+ try:
+ cur.execute(recent_growth_query)
+ recent_growth = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing recent_growth query: {e}")
+ print(f"Failed SQL: {recent_growth_query}")
+ recent_growth = []
+
+ # Сезонность по дням недели
+ seasonality_query = """
+ SELECT
+ TO_CHAR(created_date, 'D') as day_of_week,
+ COUNT(*) as transactions,
+ AVG(payment_amount) as avg_amount
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 30
+ GROUP BY TO_CHAR(created_date, 'D')
+ ORDER BY TO_CHAR(created_date, 'D')
+ """
+ try:
+ cur.execute(seasonality_query)
+ seasonality = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing seasonality query: {e}")
+ print(f"Failed SQL: {seasonality_query}")
+ seasonality = []
+
+ # Потенциал роста по регионам
+ regional_potential_query = """
+ SELECT
+ language_code,
+ COUNT(*) as user_count,
+ COUNT(CASE WHEN total_payments > 0 THEN 1 END) as paying_users,
+ AVG(total_payments) as avg_revenue
+ FROM bot_users
+ WHERE is_active = 1
+ GROUP BY language_code
+ ORDER BY COUNT(*) DESC
+ """
+ try:
+ cur.execute(regional_potential_query)
+ regional_potential = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing regional_potential query: {e}")
+ print(f"Failed SQL: {regional_potential_query}")
+ regional_potential = []
+
+ return {
+ 'recent_growth': recent_growth or [],
+ 'seasonality': seasonality or [],
+ 'regional_potential': regional_potential or []
+ }
+
+ except Exception as e:
+ print(f"Error in forecasts stats: {e}")
+ import traceback
+ print(f"Full traceback: {traceback.format_exc()}")
+ return {
+ 'recent_growth': [],
+ 'seasonality': [],
+ 'regional_potential': []
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ async def get_biz_regions_stats(self) -> dict:
+ """Анализ регионов роста"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Топ регионы по росту
+ regions_growth_query = """
+ SELECT
+ language_code,
+ COUNT(CASE WHEN created_at >= SYSDATE - 30 THEN 1 END) as new_month,
+ COUNT(CASE WHEN created_at >= SYSDATE - 60 AND created_at < SYSDATE - 30 THEN 1 END) as prev_month,
+ COUNT(*) as total_users,
+ AVG(total_payments) as avg_revenue,
+ COUNT(CASE WHEN total_payments > 0 THEN 1 END) as paying_users
+ FROM bot_users
+ GROUP BY language_code
+ HAVING COUNT(*) >= 5
+ ORDER BY COUNT(CASE WHEN created_at >= SYSDATE - 30 THEN 1 END) DESC
+ """
+ try:
+ cur.execute(regions_growth_query)
+ regions_growth = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing regions_growth query: {e}")
+ print(f"Failed SQL: {regions_growth_query}")
+ regions_growth = []
+
+ # Конверсия по регионам
+ regional_conversion_query = """
+ SELECT
+ bu.language_code,
+ COUNT(DISTINCT bu.id) as total_users,
+ COUNT(DISTINCT pl.user_id) as paying_users,
+ COUNT(pl.log_id) as total_transactions,
+ SUM(pl.payment_amount) as total_revenue
+ FROM bot_users bu
+ LEFT JOIN salvagebot.payment_logs pl ON bu.id = pl.user_id AND pl.payment_status = 'completed'
+ WHERE bu.is_active = 1
+ GROUP BY bu.language_code
+ HAVING COUNT(DISTINCT bu.id) >= 3
+ ORDER BY COUNT(DISTINCT pl.user_id) DESC
+ """
+ try:
+ cur.execute(regional_conversion_query)
+ regional_conversion = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing regional_conversion query: {e}")
+ print(f"Failed SQL: {regional_conversion_query}")
+ regional_conversion = []
+
+ # Premium распределение по регионам
+ premium_distribution_query = """
+ SELECT
+ language_code,
+ COUNT(*) as total_users,
+ COUNT(CASE WHEN is_premium = 1 THEN 1 END) as premium_users,
+ AVG(CASE WHEN is_premium = 1 THEN total_payments END) as premium_avg_revenue,
+ AVG(CASE WHEN is_premium = 0 THEN total_payments END) as regular_avg_revenue
+ FROM bot_users
+ WHERE is_active = 1
+ GROUP BY language_code
+ HAVING COUNT(*) >= 5
+ ORDER BY COUNT(CASE WHEN is_premium = 1 THEN 1 END) DESC
+ """
+ try:
+ cur.execute(premium_distribution_query)
+ premium_distribution = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing premium_distribution query: {e}")
+ print(f"Failed SQL: {premium_distribution_query}")
+ premium_distribution = []
+
+ return {
+ 'regions_growth': regions_growth or [],
+ 'regional_conversion': regional_conversion or [],
+ 'premium_distribution': premium_distribution or []
+ }
+
+ except Exception as e:
+ print(f"Error in regions stats: {e}")
+ import traceback
+ print(f"Full traceback: {traceback.format_exc()}")
+ return {
+ 'regions_growth': [],
+ 'regional_conversion': [],
+ 'premium_distribution': []
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ async def get_biz_monetization_stats(self) -> dict:
+ """Анализ монетизации"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Воронка монетизации
+ monetization_funnel_query = """
+ SELECT
+ COUNT(*) as total_users,
+ COUNT(CASE WHEN interaction_count > 1 THEN 1 END) as engaged_users,
+ COUNT(CASE WHEN successful_payments_count > 0 THEN 1 END) as paying_users,
+ COUNT(CASE WHEN successful_payments_count > 1 THEN 1 END) as repeat_buyers,
+ COUNT(CASE WHEN total_payments > 50 THEN 1 END) as high_value_users
+ FROM bot_users
+ WHERE is_active = 1
+ """
+ try:
+ cur.execute(monetization_funnel_query)
+ funnel_result = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing monetization_funnel query: {e}")
+ print(f"Failed SQL: {monetization_funnel_query}")
+ funnel_result = None
+
+ # Анализ LTV (пожизненная ценность)
+ ltv_analysis_query = """
+ SELECT
+ CASE
+ WHEN total_payments = 0 THEN 'No Payment'
+ WHEN total_payments <= 10 THEN 'Low Value (≤10)'
+ WHEN total_payments <= 50 THEN 'Medium Value (11-50)'
+ WHEN total_payments <= 100 THEN 'High Value (51-100)'
+ ELSE 'Premium Value (>100)'
+ END as user_segment,
+ COUNT(*) as user_count,
+ AVG(total_payments) as avg_ltv,
+ SUM(total_payments) as total_revenue,
+ AVG(successful_payments_count) as avg_transactions
+ FROM bot_users
+ WHERE is_active = 1
+ GROUP BY CASE
+ WHEN total_payments = 0 THEN 'No Payment'
+ WHEN total_payments <= 10 THEN 'Low Value (≤10)'
+ WHEN total_payments <= 50 THEN 'Medium Value (11-50)'
+ WHEN total_payments <= 100 THEN 'High Value (51-100)'
+ ELSE 'Premium Value (>100)'
+ END
+ ORDER BY AVG(total_payments) DESC
+ """
+ try:
+ cur.execute(ltv_analysis_query)
+ ltv_analysis = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing ltv_analysis query: {e}")
+ print(f"Failed SQL: {ltv_analysis_query}")
+ ltv_analysis = []
+
+ # Анализ прибыльности услуг
+ service_profitability_query = """
+ SELECT
+ service_type,
+ COUNT(*) as transaction_count,
+ SUM(payment_amount) as total_revenue,
+ AVG(payment_amount) as avg_price,
+ COUNT(DISTINCT user_id) as unique_users,
+ COUNT(CASE WHEN service_status = 'success' THEN 1 END) as successful_transactions
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 90
+ GROUP BY service_type
+ ORDER BY SUM(payment_amount) DESC
+ """
+ try:
+ cur.execute(service_profitability_query)
+ service_profitability = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing service_profitability query: {e}")
+ print(f"Failed SQL: {service_profitability_query}")
+ service_profitability = []
+
+ # Время до первой покупки
+ time_to_purchase_query = """
+ SELECT
+ ROUND(AVG(pl.created_date - bu.created_at), 1) as avg_days_to_purchase,
+ COUNT(*) as first_purchases
+ FROM bot_users bu
+ INNER JOIN (
+ SELECT user_id, MIN(created_date) as first_purchase_date
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ GROUP BY user_id
+ ) first_pl ON bu.id = first_pl.user_id
+ INNER JOIN salvagebot.payment_logs pl ON bu.id = pl.user_id AND pl.created_date = first_pl.first_purchase_date
+ WHERE bu.created_at >= SYSDATE - 365
+ """
+ try:
+ cur.execute(time_to_purchase_query)
+ time_to_purchase = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing time_to_purchase query: {e}")
+ print(f"Failed SQL: {time_to_purchase_query}")
+ time_to_purchase = None
+
+ return {
+ 'monetization_funnel': funnel_result,
+ 'ltv_analysis': ltv_analysis or [],
+ 'service_profitability': service_profitability or [],
+ 'time_to_purchase': time_to_purchase
+ }
+
+ except Exception as e:
+ print(f"Error in monetization stats: {e}")
+ import traceback
+ print(f"Full traceback: {traceback.format_exc()}")
+ return {
+ 'monetization_funnel': None,
+ 'ltv_analysis': [],
+ 'service_profitability': [],
+ 'time_to_purchase': None
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ async def get_biz_optimization_stats(self) -> dict:
+ """Анализ возможностей оптимизации"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Анализ оттока пользователей
+ churn_analysis_query = """
+ SELECT
+ user_status,
+ COUNT(*) as user_count,
+ AVG(total_payments) as avg_revenue,
+ COUNT(CASE WHEN total_payments > 0 THEN 1 END) as paying_users
+ FROM (
+ SELECT
+ CASE
+ WHEN last_interaction_date >= SYSDATE - 7 THEN 'Active (0-7 days)'
+ WHEN last_interaction_date >= SYSDATE - 30 THEN 'Recent (8-30 days)'
+ WHEN last_interaction_date >= SYSDATE - 90 THEN 'Dormant (31-90 days)'
+ ELSE 'Churned (>90 days)'
+ END as user_status,
+ CASE
+ WHEN last_interaction_date >= SYSDATE - 7 THEN 1
+ WHEN last_interaction_date >= SYSDATE - 30 THEN 2
+ WHEN last_interaction_date >= SYSDATE - 90 THEN 3
+ ELSE 4
+ END as sort_order,
+ total_payments
+ FROM bot_users
+ WHERE is_active = 1 AND last_interaction_date IS NOT NULL
+ ) t
+ GROUP BY user_status, sort_order
+ ORDER BY sort_order
+ """
+ try:
+ cur.execute(churn_analysis_query)
+ churn_analysis = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing churn_analysis query: {e}")
+ print(f"Failed SQL: {churn_analysis_query}")
+ churn_analysis = []
+
+ # Неэффективные запросы
+ inefficient_requests_query = """
+ SELECT
+ service_type,
+ COUNT(*) as total_requests,
+ COUNT(CASE WHEN service_status = 'no_data' THEN 1 END) as no_data_requests,
+ COUNT(CASE WHEN service_status = 'error' THEN 1 END) as error_requests,
+ COUNT(CASE WHEN refund_status <> 'no_refund' THEN 1 END) as refunded_requests
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 30
+ GROUP BY service_type
+ ORDER BY service_type
+ """
+ try:
+ cur.execute(inefficient_requests_query)
+ inefficient_requests = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing inefficient_requests query: {e}")
+ print(f"Failed SQL: {inefficient_requests_query}")
+ inefficient_requests = []
+
+ # Пользователи с высоким потенциалом
+ high_potential_query = """
+ SELECT
+ COUNT(CASE WHEN interaction_count >= 5 AND successful_payments_count = 0 THEN 1 END) as engaged_non_buyers,
+ COUNT(CASE WHEN successful_payments_count = 1 AND total_payments >= 5 THEN 1 END) as potential_repeat_buyers,
+ COUNT(CASE WHEN is_premium = 1 AND total_payments < 20 THEN 1 END) as underperforming_premium,
+ COUNT(CASE WHEN total_payments >= 10 AND last_interaction_date < SYSDATE - 30 THEN 1 END) as valuable_dormant
+ FROM bot_users
+ WHERE is_active = 1
+ """
+ try:
+ cur.execute(high_potential_query)
+ high_potential = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing high_potential query: {e}")
+ print(f"Failed SQL: {high_potential_query}")
+ high_potential = None
+
+ # Анализ ценообразования
+ pricing_analysis_query = """
+ SELECT
+ service_type,
+ payment_amount as price,
+ COUNT(*) as purchase_count,
+ COUNT(CASE WHEN service_status = 'success' THEN 1 END) as successful_count,
+ COUNT(CASE WHEN refund_status <> 'no_refund' THEN 1 END) as refund_count
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 60
+ GROUP BY service_type, payment_amount
+ ORDER BY service_type, payment_amount
+ """
+ try:
+ cur.execute(pricing_analysis_query)
+ pricing_analysis = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing pricing_analysis query: {e}")
+ print(f"Failed SQL: {pricing_analysis_query}")
+ pricing_analysis = []
+
+ return {
+ 'churn_analysis': churn_analysis or [],
+ 'inefficient_requests': inefficient_requests or [],
+ 'high_potential': high_potential,
+ 'pricing_analysis': pricing_analysis or []
+ }
+
+ except Exception as e:
+ print(f"Error in optimization stats: {e}")
+ import traceback
+ print(f"Full traceback: {traceback.format_exc()}")
+ return {
+ 'churn_analysis': [],
+ 'inefficient_requests': [],
+ 'high_potential': None,
+ 'pricing_analysis': []
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
+ async def get_biz_recommendations_stats(self) -> dict:
+ """Бизнес рекомендации на основе данных"""
+ def _get_stats():
+ try:
+ with self._pool.acquire() as conn:
+ with conn.cursor() as cur:
+ # Базовые метрики для рекомендаций
+ base_metrics_query = """
+ SELECT
+ COUNT(*) as total_users,
+ COUNT(CASE WHEN total_payments > 0 THEN 1 END) as paying_users,
+ AVG(total_payments) as avg_ltv,
+ COUNT(CASE WHEN last_interaction_date >= SYSDATE - 7 THEN 1 END) as active_users,
+ COUNT(CASE WHEN is_premium = 1 THEN 1 END) as premium_users
+ FROM bot_users
+ WHERE is_active = 1
+ """
+ try:
+ cur.execute(base_metrics_query)
+ base_metrics = cur.fetchone()
+ except Exception as e:
+ print(f"Error executing base_metrics query: {e}")
+ print(f"Failed SQL: {base_metrics_query}")
+ base_metrics = None
+
+ # Сервисная эффективность
+ service_efficiency_query = """
+ SELECT
+ service_type,
+ COUNT(*) as total_requests,
+ COUNT(CASE WHEN service_status = 'success' THEN 1 END) as successful_count,
+ SUM(payment_amount) as revenue,
+ COUNT(CASE WHEN refund_status != 'no_refund' THEN 1 END) as refunds
+ FROM salvagebot.payment_logs
+ WHERE payment_status = 'completed'
+ AND created_date >= SYSDATE - 30
+ GROUP BY service_type
+ ORDER BY service_type
+ """
+ try:
+ cur.execute(service_efficiency_query)
+ service_efficiency = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing service_efficiency query: {e}")
+ print(f"Failed SQL: {service_efficiency_query}")
+ service_efficiency = []
+
+ # Рост по регионам за последний месяц
+ regional_growth_query = """
+ SELECT
+ language_code,
+ COUNT(CASE WHEN created_at >= SYSDATE - 30 THEN 1 END) as new_users_month,
+ COUNT(*) as total_users,
+ COUNT(CASE WHEN total_payments > 0 THEN 1 END) as paying_users
+ FROM bot_users
+ WHERE is_active = 1
+ GROUP BY language_code
+ HAVING COUNT(*) >= 3
+ ORDER BY COUNT(CASE WHEN created_at >= SYSDATE - 30 THEN 1 END) DESC
+ """
+ try:
+ cur.execute(regional_growth_query)
+ regional_growth = cur.fetchall()
+ except Exception as e:
+ print(f"Error executing regional_growth query: {e}")
+ print(f"Failed SQL: {regional_growth_query}")
+ regional_growth = []
+
+ return {
+ 'base_metrics': base_metrics,
+ 'service_efficiency': service_efficiency or [],
+ 'regional_growth': regional_growth or []
+ }
+
+ except Exception as e:
+ print(f"Error in recommendations stats: {e}")
+ import traceback
+ print(f"Full traceback: {traceback.format_exc()}")
+ return {
+ 'base_metrics': None,
+ 'service_efficiency': [],
+ 'regional_growth': []
+ }
+
+ import asyncio
+ return await asyncio.to_thread(_get_stats)
+
diff --git a/main.py b/main.py
index 2853779..464f9f3 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,6 @@
import asyncio
+import signal
+import sys
from os import getenv
import logging
from datetime import datetime
@@ -825,6 +827,489 @@ async def admin_business_callback(callback: CallbackQuery, db: OracleDatabase =
await callback.answer()
+# ==============================================
+# BUSINESS ANALYTICS REPORT HANDLERS
+# ==============================================
+
+@dp.callback_query(lambda c: c.data == "admin_biz_trends")
+async def admin_biz_trends_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Анализ трендов бизнеса"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_biz_trends_stats()
+
+ # Тренд роста пользователей
+ user_growth_text = ""
+ if stats['user_growth_trend']:
+ user_growth_text = "\n📈 Рост пользователей:\n"
+ for month, new_users, premium in stats['user_growth_trend'][-6:]: # последние 6 месяцев
+ user_growth_text += f"• {month}: {new_users:,} новых ({premium} premium)\n"
+
+ # Тренд выручки
+ revenue_trend_text = ""
+ if stats['revenue_trend']:
+ revenue_trend_text = "\n💰 Тренд выручки:\n"
+ for month, transactions, revenue, paying_users in stats['revenue_trend'][-6:]:
+ revenue_trend_text += f"• {month}: {revenue or 0:,.0f} ⭐️ ({transactions} тр-ций, {paying_users} польз.)\n"
+
+ # Тренд услуг
+ services_trend_text = ""
+ if stats['services_trend']:
+ services_trend_text = "\n🛠 Популярность услуг (за 6 мес.):\n"
+ service_totals = {}
+ for service, month, count in stats['services_trend']:
+ if service not in service_totals:
+ service_totals[service] = 0
+ service_totals[service] += count
+
+ for service, total in sorted(service_totals.items(), key=lambda x: x[1], reverse=True)[:5]:
+ # Экранируем название сервиса
+ safe_service = escape_markdown(str(service)) if service else "Unknown"
+ services_trend_text += f"• {safe_service}: {total:,} использований\n"
+
+ # Конверсия по месяцам
+ conversion_text = ""
+ if stats['conversion_trend']:
+ conversion_text = "\n🎯 Конверсия по месяцам:\n"
+ for month, total_users, converted in stats['conversion_trend'][-6:]:
+ conversion_rate = round(converted/total_users*100, 1) if total_users > 0 else 0
+ conversion_text += f"• {month}: {conversion_rate}% ({converted}/{total_users})\n"
+
+ report = f"""📊 Анализ трендов бизнеса
+
+{user_growth_text}
+{revenue_trend_text}
+{services_trend_text}
+{conversion_text}
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_business")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating trends report: {e}")
+ import traceback
+ logging.error(f"Full traceback: {traceback.format_exc()}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_biz_forecasts")
+async def admin_biz_forecasts_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Прогнозы развития бизнеса"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_biz_forecasts_stats()
+
+ # Анализ роста за последние 3 месяца
+ growth_analysis = ""
+ if stats['recent_growth']:
+ growth_analysis = "\n📈 Последние 3 месяца:\n"
+ total_users = sum([users for month, users, revenue in stats['recent_growth']])
+ avg_growth = total_users / len(stats['recent_growth']) if stats['recent_growth'] else 0
+
+ for month, users, revenue in stats['recent_growth']:
+ growth_analysis += f"• {month}: {users:,} новых (+{revenue or 0:,.0f} ⭐️)\n"
+
+ growth_analysis += f"\n📊 Прогноз на следующий месяц: ~{avg_growth:,.0f} новых пользователей"
+
+ # Сезонность по дням недели
+ seasonality_text = ""
+ if stats['seasonality']:
+ seasonality_text = "\n📅 Сезонность (дни недели):\n"
+ days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']
+ for day_num, transactions, avg_amount in sorted(stats['seasonality']):
+ day_name = days[int(day_num) - 2] if int(day_num) <= 7 else f"День {day_num}"
+ seasonality_text += f"• {day_name}: {transactions} тр-ций, {avg_amount or 0:.1f} ⭐️ средняя сумма\n"
+
+ # Потенциал по регионам
+ regional_potential = ""
+ if stats['regional_potential']:
+ regional_potential = "\n🌍 Потенциал роста по регионам:\n"
+ for lang, users, paying, avg_rev in stats['regional_potential'][:5]:
+ conversion = round(paying/users*100, 1) if users > 0 else 0
+ flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪"}.get(lang, "🌐")
+ regional_potential += f"• {flag} {lang}: {users:,} польз., конверсия {conversion}%, LTV {avg_rev or 0:.1f} ⭐️\n"
+
+ report = f"""🔮 Прогнозы развития бизнеса
+
+{growth_analysis}
+{seasonality_text}
+{regional_potential}
+
+💡 Рекомендации:
+• Сосредоточьтесь на днях с высокой активностью
+• Развивайте регионы с низкой конверсией
+• Планируйте маркетинг на основе сезонности
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_business")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating forecasts report: {e}")
+ import traceback
+ logging.error(f"Full traceback: {traceback.format_exc()}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_biz_regions")
+async def admin_biz_regions_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Анализ регионов роста"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_biz_regions_stats()
+
+ # Топ регионы по росту
+ growth_regions = ""
+ if stats['regions_growth']:
+ growth_regions = "\n🚀 Топ регионы по росту:\n"
+ for lang, new_month, prev_month, total, avg_rev, paying in stats['regions_growth'][:5]:
+ growth_rate = round(((new_month - prev_month) / prev_month * 100), 1) if prev_month > 0 else 0
+ flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷"}.get(lang, "🌐")
+ growth_regions += f"• {flag} {lang}: +{new_month} за месяц ({growth_rate:+.1f}%), всего {total:,}\n"
+
+ # Конверсия по регионам
+ conversion_regions = ""
+ if stats['regional_conversion']:
+ conversion_regions = "\n💰 Конверсия по регионам:\n"
+ for lang, total_users, paying_users, transactions, revenue in stats['regional_conversion'][:5]:
+ conversion = round(paying_users/total_users*100, 1) if total_users > 0 else 0
+ arpu = round(revenue/total_users, 1) if total_users > 0 and revenue else 0
+ flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷"}.get(lang, "🌐")
+ conversion_regions += f"• {flag} {lang}: {conversion}% конверсия, {arpu} ⭐️ ARPU\n"
+
+ # Premium распределение
+ premium_regions = ""
+ if stats['premium_distribution']:
+ premium_regions = "\n👑 Premium по регионам:\n"
+ for lang, total, premium, prem_avg, reg_avg in stats['premium_distribution'][:5]:
+ premium_rate = round(premium/total*100, 1) if total > 0 else 0
+ flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷"}.get(lang, "🌐")
+ premium_regions += f"• {flag} {lang}: {premium_rate}% premium ({premium}/{total})\n"
+
+ report = f"""🌍 Анализ регионов роста
+
+{growth_regions}
+{conversion_regions}
+{premium_regions}
+
+💡 Выводы:
+• Сосредоточьтесь на регионах с высоким ростом
+• Изучите успешные стратегии топ-регионов
+• Адаптируйте контент под местные особенности
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_business")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating regions report: {e}")
+ import traceback
+ logging.error(f"Full traceback: {traceback.format_exc()}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_biz_monetization")
+async def admin_biz_monetization_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Анализ монетизации"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_biz_monetization_stats()
+
+ # Воронка монетизации
+ funnel_text = ""
+ if stats['monetization_funnel']:
+ total, engaged, paying, repeat, high_value = stats['monetization_funnel']
+ engagement_rate = round(engaged/total*100, 1) if total > 0 else 0
+ conversion_rate = round(paying/total*100, 1) if total > 0 else 0
+ repeat_rate = round(repeat/paying*100, 1) if paying > 0 else 0
+ high_value_rate = round(high_value/total*100, 1) if total > 0 else 0
+
+ funnel_text = f"""🎯 Воронка монетизации:
+• Всего пользователей: {total:,}
+• Вовлеченные (>1 взаимодействия): {engaged:,} ({engagement_rate}%)
+• Платящие: {paying:,} ({conversion_rate}%)
+• Повторные покупки: {repeat:,} ({repeat_rate}%)
+• Высокая ценность (>50⭐️): {high_value:,} ({high_value_rate}%)"""
+
+ # LTV анализ
+ ltv_text = ""
+ if stats['ltv_analysis']:
+ ltv_text = "\n💎 Анализ LTV по сегментам:\n"
+ for segment, count, avg_ltv, total_rev, avg_trans in stats['ltv_analysis']:
+ percentage = round(count/sum([c[1] for c in stats['ltv_analysis']])*100, 1)
+ ltv_text += f"• {segment}: {count:,} ({percentage}%), LTV {avg_ltv or 0:.1f} ⭐️\n"
+
+ # Прибыльность услуг
+ profitability_text = ""
+ if stats['service_profitability']:
+ profitability_text = "\n🛠 Прибыльность услуг (90 дней):\n"
+ for service, count, revenue, avg_price, users, successful in stats['service_profitability']:
+ success_rate = round(successful/count*100, 1) if count > 0 else 0
+ profitability_text += f"• {service}: {revenue or 0:,.0f} ⭐️ ({success_rate}% успех)\n"
+
+ # Время до покупки
+ time_to_purchase_text = ""
+ if stats['time_to_purchase']:
+ avg_days, purchases = stats['time_to_purchase']
+ time_to_purchase_text = f"\n⏱ Время до первой покупки: {avg_days or 0} дней в среднем ({purchases} покупок)"
+
+ report = f"""💎 Анализ монетизации
+
+{funnel_text}
+{ltv_text}
+{profitability_text}
+{time_to_purchase_text}
+
+💡 Рекомендации:
+• Улучшите вовлечение новых пользователей
+• Сосредоточьтесь на повторных покупках
+• Оптимизируйте наименее прибыльные услуги
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_business")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating monetization report: {e}")
+ import traceback
+ logging.error(f"Full traceback: {traceback.format_exc()}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_biz_optimization")
+async def admin_biz_optimization_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Анализ возможностей оптимизации"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_biz_optimization_stats()
+
+ # Анализ оттока
+ churn_text = ""
+ if stats['churn_analysis']:
+ churn_text = "\n📉 Анализ оттока пользователей:\n"
+ total_analyzed = sum([count for status, count, avg_rev, paying in stats['churn_analysis']])
+ for status, count, avg_rev, paying in stats['churn_analysis']:
+ percentage = round(count/total_analyzed*100, 1) if total_analyzed > 0 else 0
+ churn_text += f"• {status}: {count:,} ({percentage}%), LTV {avg_rev or 0:.1f} ⭐️\n"
+
+ # Неэффективные запросы
+ inefficient_text = ""
+ if stats['inefficient_requests']:
+ inefficient_text = "\n⚠️ Неэффективные запросы (30 дней):\n"
+ for service, total, no_data, errors, refunds in stats['inefficient_requests']:
+ no_data_rate = round(no_data/total*100, 1) if total > 0 else 0
+ error_rate = round(errors/total*100, 1) if total > 0 else 0
+ refund_rate = round(refunds/total*100, 1) if total > 0 else 0
+ inefficient_text += f"• {service}: {no_data_rate}% без данных, {error_rate}% ошибок, {refund_rate}% возвратов\n"
+
+ # Высокопотенциальные пользователи
+ potential_text = ""
+ if stats['high_potential']:
+ engaged_non, potential_repeat, underperform_prem, valuable_dormant = stats['high_potential']
+ potential_text = f"""🎯 Пользователи с высоким потенциалом:
+• Активные без покупок: {engaged_non:,}
+• Потенциал повторных покупок: {potential_repeat:,}
+• Неэффективные Premium: {underperform_prem:,}
+• Ценные неактивные: {valuable_dormant:,}"""
+
+ # Анализ ценообразования
+ pricing_text = ""
+ if stats['pricing_analysis']:
+ pricing_text = "\n💰 Анализ цен по услугам:\n"
+ service_summary = {}
+ for service, price, purchases, successful, refunds in stats['pricing_analysis']:
+ if service not in service_summary:
+ service_summary[service] = []
+ service_summary[service].append((price, purchases, successful, refunds))
+
+ for service, prices in list(service_summary.items())[:3]: # топ 3 услуги
+ total_purchases = sum([p[1] for p in prices])
+ pricing_text += f"• {service}: {total_purchases:,} покупок, цены {min([p[0] for p in prices])}-{max([p[0] for p in prices])} ⭐️\n"
+
+ report = f"""🎯 Анализ возможностей оптимизации
+
+{churn_text}
+{inefficient_text}
+{potential_text}
+{pricing_text}
+
+🚀 Приоритетные действия:
+• Работайте с активными неплательщиками
+• Улучшите качество данных для проблемных услуг
+• Реактивируйте ценных неактивных пользователей
+• Оптимизируйте ценообразование
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_business")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating optimization report: {e}")
+ import traceback
+ logging.error(f"Full traceback: {traceback.format_exc()}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_biz_recommendations")
+async def admin_biz_recommendations_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Бизнес рекомендации на основе данных"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_biz_recommendations_stats()
+
+ # Формируем рекомендации на основе данных
+ recommendations = []
+
+ if stats['base_metrics']:
+ total, paying, avg_ltv, active, premium = stats['base_metrics']
+ conversion_rate = round(paying/total*100, 1) if total > 0 else 0
+ activity_rate = round(active/total*100, 1) if total > 0 else 0
+ premium_rate = round(premium/total*100, 1) if total > 0 else 0
+
+ # Анализируем и даем рекомендации
+ if conversion_rate < 5:
+ recommendations.append("🎯 Низкая конверсия: Улучшите онбординг и первое впечатление")
+ elif conversion_rate > 15:
+ recommendations.append("🎯 Высокая конверсия: Масштабируйте маркетинг для привлечения пользователей")
+
+ if activity_rate < 30:
+ recommendations.append("⚡ Низкая активность: Внедрите программу реактивации пользователей")
+
+ if premium_rate < 10:
+ recommendations.append("👑 Мало Premium: Усильте маркетинг Premium-подписки")
+
+ # Анализ эффективности услуг
+ service_recommendations = []
+ if stats['service_efficiency']:
+ for service, total_req, successful_count, revenue, refunds in stats['service_efficiency']:
+ success_rate = round(successful_count/total_req*100, 1) if total_req > 0 else 0
+ refund_rate = round(refunds/total_req*100, 1) if total_req > 0 else 0
+
+ if success_rate < 80:
+ safe_service = escape_markdown(str(service)) if service else "Unknown"
+ service_recommendations.append(f"⚠️ {safe_service}: Низкий успех ({success_rate}%) - улучшите качество данных")
+ if refund_rate > 10:
+ safe_service = escape_markdown(str(service)) if service else "Unknown"
+ service_recommendations.append(f"💸 {safe_service}: Высокий возврат ({refund_rate}%) - пересмотрите ценообразование")
+
+ # Анализ роста по регионам
+ regional_recommendations = []
+ if stats['regional_growth']:
+ top_growth_regions = stats['regional_growth'][:3]
+ for lang, new_month, total, paying in top_growth_regions:
+ conversion = round(paying/total*100, 1) if total > 0 else 0
+ flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪"}.get(lang, "🌐")
+ if new_month > 10: # значительный рост
+ regional_recommendations.append(f"🚀 {flag} {lang}: Активный рост - увеличьте инвестиции в регион")
+
+ # Формируем итоговый отчет
+ base_metrics_text = ""
+ if stats['base_metrics']:
+ total, paying, avg_ltv, active, premium = stats['base_metrics']
+ base_metrics_text = f"""📊 Ключевые метрики:
+• Конверсия: {round(paying/total*100, 1) if total > 0 else 0}%
+• Активность: {round(active/total*100, 1) if total > 0 else 0}%
+• Premium: {round(premium/total*100, 1) if total > 0 else 0}%
+• Средний LTV: {avg_ltv or 0:.1f} ⭐️"""
+
+ all_recommendations = recommendations + service_recommendations + regional_recommendations
+
+ recommendations_text = "\n💡 Приоритетные рекомендации:\n"
+ for i, rec in enumerate(all_recommendations[:8], 1): # показываем топ-8 рекомендаций
+ recommendations_text += f"{i}. {rec}\n"
+
+ if not all_recommendations:
+ recommendations_text += "✅ Основные метрики в норме. Сосредоточьтесь на масштабировании."
+
+ report = f"""💡 Бизнес рекомендации
+
+{base_metrics_text}
+{recommendations_text}
+
+🎯 Следующие шаги:
+• Внедрите приоритетные улучшения
+• Проведите A/B тестирование изменений
+• Отслеживайте метрики еженедельно
+• Фокусируйтесь на ROI каждого действия
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_business")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating recommendations report: {e}")
+ import traceback
+ logging.error(f"Full traceback: {traceback.format_exc()}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
# ==============================================
# USER ANALYTICS REPORT HANDLERS
# ==============================================
@@ -1513,6 +1998,529 @@ async def admin_finance_efficiency_callback(callback: CallbackQuery, db: OracleD
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+# ==============================================
+# OPERATIONAL ANALYTICS REPORT HANDLERS
+# ==============================================
+
+@dp.callback_query(lambda c: c.data == "admin_ops_performance")
+async def admin_ops_performance_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Анализ производительности системы"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_ops_performance_stats()
+
+ # Статистика по услугам
+ services_text = ""
+ if stats['services_breakdown']:
+ services_text = "\n⚙️ Производительность по услугам:\n"
+ service_names = {
+ 'decode_vin': '🔍 Декодинг VIN',
+ 'check_salvage': '💥 Проверка Salvage',
+ 'get_photos': '📸 Получение фото'
+ }
+
+ for service, requests, success_count, avg_data, refunds in stats['services_breakdown']:
+ name = service_names.get(service, service)
+ success_rate = round(success_count/requests*100, 1) if requests > 0 else 0
+ refund_rate = round(refunds/requests*100, 1) if requests > 0 else 0
+
+ services_text += f"• {name}:\n"
+ services_text += f" 📊 Запросов: {requests:,}\n"
+ services_text += f" ✅ Успешность: {success_rate}%\n"
+ services_text += f" 📈 Ср. данных: {avg_data}\n"
+ services_text += f" ↩️ Возвраты: {refund_rate}%\n\n"
+
+ # Пиковые часы
+ peak_hours_text = ""
+ if stats['peak_hours']:
+ peak_hours_text = "\n⏰ Пиковые часы (топ-5):\n"
+ for hour, requests in stats['peak_hours'][:5]:
+ peak_hours_text += f"• {int(hour):02d}:00 UTC - {requests} запросов\n"
+
+ report = f"""⏱️ Производительность системы
+
+📊 Общая статистика:
+• Всего запросов: {stats['total_requests']:,}
+• Успешных: {stats['successful_requests']:,}
+• Общая успешность: {stats['success_rate']:.1f}%
+• Без данных: {stats['no_data_requests']:,}
+• Ошибки: {stats['error_requests']:,}
+• Среднее данных на запрос: {stats['avg_data_found']:.1f}
+
+{services_text}
+
+📸 Статистика фотографий:
+• VIN с фото: {stats['photos_stats']['vins_with_photos']:,}
+• Всего фотографий: {stats['photos_stats']['total_photos']:,}
+• Среднее фото на VIN: {stats['photos_stats']['avg_photos_per_vin']:.1f}
+
+{peak_hours_text}
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_operations")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating performance stats: {e}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_ops_errors")
+async def admin_ops_errors_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Анализ ошибок системы"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_ops_errors_stats()
+
+ # Ошибки по услугам
+ service_errors_text = ""
+ if stats['service_errors_breakdown']:
+ service_errors_text = "\n🚨 Ошибки по услугам:\n"
+ service_names = {
+ 'decode_vin': '🔍 Декодинг VIN',
+ 'check_salvage': '💥 Проверка Salvage',
+ 'get_photos': '📸 Получение фото'
+ }
+
+ for service, total_requests, errors, no_data, auto_refunds in stats['service_errors_breakdown']:
+ name = service_names.get(service, service)
+ error_rate = round(errors/total_requests*100, 1) if total_requests > 0 else 0
+ no_data_rate = round(no_data/total_requests*100, 1) if total_requests > 0 else 0
+
+ service_errors_text += f"• {name}:\n"
+ service_errors_text += f" 📊 Запросов: {total_requests:,}\n"
+ service_errors_text += f" 🚨 Ошибки: {errors} ({error_rate}%)\n"
+ service_errors_text += f" 📭 Нет данных: {no_data} ({no_data_rate}%)\n"
+ service_errors_text += f" ↩️ Авто возвраты: {auto_refunds}\n\n"
+
+ # Частые ошибки
+ common_errors_text = ""
+ if stats['common_errors']:
+ common_errors_text = "\n🔍 Частые ошибки:\n"
+ for error_snippet, count in stats['common_errors']:
+ error_text = error_snippet[:60] + "..." if len(error_snippet) > 60 else error_snippet
+ common_errors_text += f"• {error_text} - {count} раз\n"
+
+ # Тренд ошибок
+ daily_errors_text = ""
+ if stats['daily_error_trend']:
+ daily_errors_text = "\n📈 Тренд ошибок (7 дней):\n"
+ for error_date, errors_count in stats['daily_error_trend']:
+ date_str = error_date.strftime('%d.%m') if hasattr(error_date, 'strftime') else str(error_date)
+ daily_errors_text += f"• {date_str}: {errors_count} ошибок\n"
+
+ report = f"""🚨 Анализ ошибок системы
+
+📊 Общая статистика:
+• Всего попыток: {stats['total_attempts']:,}
+• Ошибки платежей: {stats['payment_failures']:,}
+• Ошибки сервисов: {stats['service_errors']:,}
+• Случаи "нет данных": {stats['no_data_cases']:,}
+• Общий процент ошибок: {stats['error_rate']:.2f}%
+• Авто возвраты: {stats['auto_refunds']:,}
+
+{service_errors_text}
+
+{common_errors_text}
+
+{daily_errors_text}
+
+💡 Анализ:
+• Нормальный уровень ошибок: <5%
+• Требует внимания: 5-10%
+• Критично: >10%
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_operations")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating errors stats: {e}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_ops_load")
+async def admin_ops_load_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Мониторинг нагрузки системы"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_ops_load_stats()
+
+ # Распределение по часам
+ hourly_text = ""
+ if stats['hourly_distribution']:
+ hourly_text = "\n⏰ Нагрузка по часам (UTC):\n"
+ for hour, requests, unique_users, avg_data in stats['hourly_distribution'][:8]: # Топ 8
+ hourly_text += f"• {int(hour):02d}:00 - {requests} запр. ({unique_users} польз., ср.данных: {avg_data})\n"
+
+ # Популярные VIN
+ popular_vins_text = ""
+ if stats['popular_vins']:
+ popular_vins_text = "\n🏆 Популярные VIN (топ-10):\n"
+ for vin, request_count, unique_users, successful in stats['popular_vins'][:10]:
+ success_rate = round(successful/request_count*100, 1) if request_count > 0 else 0
+ popular_vins_text += f"• {vin}: {request_count} запр. ({unique_users} польз., успех: {success_rate}%)\n"
+
+ # Concurrent пользователи
+ concurrent_text = ""
+ if stats['concurrent_users']:
+ concurrent_text = "\n👥 Concurrent активность (топ-6):\n"
+ for hour_block, concurrent_users, total_requests in stats['concurrent_users'][:6]:
+ time_str = hour_block.strftime('%d.%m %H:00') if hasattr(hour_block, 'strftime') else str(hour_block)
+ concurrent_text += f"• {time_str}: {concurrent_users} польз. одновременно ({total_requests} запр.)\n"
+
+ # Безопасное вычисление среднего запросов на пользователя
+ avg_requests_per_user = stats['avg_daily_requests'] / stats['avg_daily_users'] if stats['avg_daily_users'] > 0 else 0
+
+ report = f"""📈 Мониторинг нагрузки
+
+📊 Средние показатели:
+• Запросов в день: {stats['avg_daily_requests']:.1f}
+• Пользователей в день: {stats['avg_daily_users']:.1f}
+• Пиковый час: {int(stats['peak_hour']):02d}:00 UTC
+• Макс. concurrent: {stats['peak_concurrent']} пользователей
+
+{hourly_text}
+
+{popular_vins_text}
+
+{concurrent_text}
+
+💡 Insights:
+• Пиковая нагрузка в {int(stats['peak_hour']):02d}:00 UTC
+• Среднее {avg_requests_per_user:.1f} запросов на пользователя
+• Максимум {stats['peak_concurrent']} одновременных пользователей
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_operations")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating load stats: {e}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_ops_auctions")
+async def admin_ops_auctions_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Анализ аукционных домов"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_ops_auctions_stats()
+
+ # Breakdown по аукционам
+ auctions_text = ""
+ if stats['auction_breakdown']:
+ auctions_text = "\n🏪 Статистика аукционов:\n"
+ for auction_house, records, with_title, with_damage1, with_damage2, min_year, max_year, avg_year in stats['auction_breakdown']:
+ title_coverage = round(with_title/records*100, 1) if records > 0 else 0
+ damage_coverage = round(with_damage1/records*100, 1) if records > 0 else 0
+
+ auctions_text += f"• {auction_house}:\n"
+ auctions_text += f" 📊 Записей: {records:,}\n"
+ auctions_text += f" 📝 Покрытие названий: {title_coverage}%\n"
+ auctions_text += f" 💥 Покрытие повреждений: {damage_coverage}%\n"
+ auctions_text += f" 🚗 Годы: {min_year}-{max_year} (ср: {avg_year})\n\n"
+
+ # Качество данных
+ quality_text = ""
+ if stats['data_quality']:
+ quality_text = "\n📋 Качество данных:\n"
+ for auction_house, title_cov, damage1_cov, odometer_cov in stats['data_quality']:
+ quality_text += f"• {auction_house}:\n"
+ quality_text += f" Названия: {title_cov}% | Повреждения: {damage1_cov}% | Пробег: {odometer_cov}%\n"
+
+ # География
+ geo_text = ""
+ if stats['geographic_distribution']:
+ geo_text = "\n🗺️ География (топ-8):\n"
+ for auction_house, state_code, count in stats['geographic_distribution'][:8]:
+ geo_text += f"• {auction_house} - {state_code}: {count:,} записей\n"
+
+ # Тренды обновлений
+ updates_text = ""
+ if stats['update_trends']:
+ updates_text = "\n📅 Тренды обновлений (6 мес.):\n"
+ for auction_house, month, updates_count in stats['update_trends'][:6]:
+ updates_text += f"• {auction_house} {month}: {updates_count:,} обновлений\n"
+
+ report = f"""🏪 Анализ аукционных домов
+
+📊 Общая статистика:
+• Всего записей: {stats['total_records']:,}
+• Доля IAAI: {stats['iaai_percentage']:.1f}%
+• Остальные источники: {100-stats['iaai_percentage']:.1f}%
+
+{auctions_text}
+
+{quality_text}
+
+{geo_text}
+
+{updates_text}
+
+💡 Выводы:
+• IAAI - основной источник данных ({stats['iaai_percentage']:.0f}%)
+• Качество данных варьируется по источникам
+• Регулярные обновления поддерживают актуальность
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_operations")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating auctions stats: {e}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_ops_problem_vins")
+async def admin_ops_problem_vins_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Анализ проблемных VIN номеров"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_ops_problem_vins_stats()
+
+ # Проверяем, что статистика получена корректно
+ if not stats or 'availability_stats' not in stats:
+ report = """🔍 Проблемные VIN номера
+
+📭 Нет данных
+
+В системе пока нет завершенных транзакций для анализа проблемных VIN.
+
+💡 Возможные причины:
+• Таблица payment_logs пустая
+• Нет записей со статусом 'completed'
+• Проблемы с подключением к базе данных
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+ else:
+ # VIN без данных
+ no_data_text = ""
+ if stats.get('no_data_vins'):
+ no_data_text = "\n📭 VIN без данных (топ-10):\n"
+ for vin, request_count, unique_users, last_request in stats['no_data_vins'][:10]:
+ date_str = last_request.strftime('%d.%m') if hasattr(last_request, 'strftime') else str(last_request)
+ no_data_text += f"• {vin}: {request_count} запр. ({unique_users} польз., посл: {date_str})\n"
+
+ # VIN с ошибками
+ error_vins_text = ""
+ if stats.get('error_vins'):
+ error_vins_text = "\n🚨 VIN с ошибками (топ-8):\n"
+ for vin, error_count, affected_users, error_sample in stats['error_vins'][:8]:
+ error_text = (error_sample[:40] + "...") if error_sample and len(error_sample) > 40 else (error_sample or "No error message")
+ error_vins_text += f"• {vin}: {error_count} ошибок ({affected_users} польз.)\n"
+ if error_sample and error_sample != "No error message":
+ error_vins_text += f" Пример: {error_text}\n"
+
+ # VIN без фотографий
+ no_photos_text = ""
+ if stats.get('no_photos_vins'):
+ no_photos_text = "\n📸 VIN без фото (топ-8):\n"
+ for vin, photo_requests, unique_users in stats['no_photos_vins'][:8]:
+ no_photos_text += f"• {vin}: {photo_requests} запр. фото ({unique_users} польз.)\n"
+
+ # Сводная статистика проблемных VIN
+ problem_summary_text = ""
+ if stats.get('problem_vins_summary'):
+ problem_summary_text = "\n🔍 Топ проблемных VIN:\n"
+ for vin, total_req, successful, no_data, errors, refunds in stats['problem_vins_summary'][:5]:
+ problem_rate = round((no_data + errors)/total_req*100, 1) if total_req > 0 else 0
+ problem_summary_text += f"• {vin}: {total_req} запр. (проблемы: {problem_rate}%, возвраты: {refunds})\n"
+
+ availability = stats['availability_stats']
+
+ # Если нет проблемных данных, показываем это
+ if (not stats.get('no_data_vins') and
+ not stats.get('error_vins') and
+ not stats.get('no_photos_vins') and
+ not stats.get('problem_vins_summary')):
+
+ no_data_text = "\n✅ Нет VIN без данных с множественными запросами"
+ error_vins_text = "\n✅ Нет VIN с системными ошибками"
+ no_photos_text = "\n✅ Нет проблем с запросами фотографий"
+ problem_summary_text = "\n✅ Все VIN обрабатываются корректно"
+
+ report = f"""🔍 Проблемные VIN номера
+
+📊 Общая доступность:
+• Всего уникальных VIN: {availability['total_requested_vins']:,}
+• Успешные VIN: {availability['successful_vins']:,}
+• Успешность: {availability['success_rate']:.1f}%
+• VIN без данных: {availability['no_data_vins']:,}
+• VIN с ошибками: {availability['error_vins']:,}
+
+{no_data_text}
+
+{error_vins_text}
+
+{no_photos_text}
+
+{problem_summary_text}
+
+💡 Рекомендации:
+• Проверить источники для VIN без данных
+• Исследовать причины ошибок для проблемных VIN
+• Пополнить базу фотографий для популярных VIN
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_operations")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating problem VINs stats: {e}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
+@dp.callback_query(lambda c: c.data == "admin_ops_monitoring")
+async def admin_ops_monitoring_callback(callback: CallbackQuery, db: OracleDatabase = None):
+ """Системный мониторинг"""
+ database = db or oracle_db
+
+ if callback.from_user.id != ADMIN_USER_ID:
+ await callback.answer("❌ Access denied.", show_alert=True)
+ return
+
+ try:
+ stats = await database.get_ops_monitoring_stats()
+
+ # Здоровье БД
+ db_health_text = ""
+ if stats['db_health']:
+ db_health_text = "\n🗄️ Здоровье БД:\n"
+ for table_name, record_count, oldest, newest in stats['db_health']:
+ oldest_str = oldest.strftime('%d.%m.%Y') if hasattr(oldest, 'strftime') else str(oldest)
+ newest_str = newest.strftime('%d.%m.%Y') if hasattr(newest, 'strftime') else str(newest)
+ db_health_text += f"• {table_name}: {record_count:,} записей ({oldest_str} - {newest_str})\n"
+
+ # Рост данных
+ growth_text = ""
+ if stats['data_growth']:
+ growth_text = "\n📈 Рост данных (6 мес.):\n"
+ for month, new_transactions, new_users, revenue in stats['data_growth'][:6]:
+ growth_text += f"• {month}: {new_transactions} транз., {new_users} польз., {revenue:.0f} ⭐\n"
+
+ # SLA метрики
+ sla_text = ""
+ if stats['sla_metrics']:
+ sla_text = "\n📊 SLA метрики (7 дней):\n"
+ for service_date, total_req, successful_req, failed_req, availability in stats['sla_metrics'][:7]:
+ date_str = service_date.strftime('%d.%m') if hasattr(service_date, 'strftime') else str(service_date)
+ status = "🟢" if availability >= 95 else "🟡" if availability >= 90 else "🔴"
+ sla_text += f"• {date_str}: {status} {availability:.1f}% ({successful_req}/{total_req})\n"
+
+ # Capacity изображений
+ images_text = ""
+ if stats['images_capacity']:
+ images_text = "\n📸 Рост изображений (6 мес.):\n"
+ for month, new_images, new_vins in stats['images_capacity'][:6]:
+ images_text += f"• {month}: {new_images:,} фото, {new_vins:,} новых VIN\n"
+
+ # Performance метрики
+ performance_text = ""
+ if stats['performance_metrics']:
+ performance_text = "\n⚡ Performance (7 дней):\n"
+ for service_type, avg_results, total_requests, last_request in stats['performance_metrics']:
+ service_names = {
+ 'decode_vin': '🔍 Декодинг',
+ 'check_salvage': '💥 Salvage',
+ 'get_photos': '📸 Фото'
+ }
+ name = service_names.get(service_type, service_type)
+ performance_text += f"• {name}: {avg_results:.1f} ср.результатов ({total_requests} запр.)\n"
+
+ # Статус системы
+ status_emoji = "🟢" if stats['system_status'] == "Healthy" else "🟡" if stats['system_status'] == "Warning" else "🔴"
+
+ report = f"""👀 Системный мониторинг
+
+🎯 Статус системы: {status_emoji} {stats['system_status']}
+• Средний SLA: {stats['avg_sla']:.2f}%
+
+{db_health_text}
+
+{growth_text}
+
+{sla_text}
+
+{images_text}
+
+{performance_text}
+
+💡 Анализ системы:
+• SLA ≥95%: Отличное состояние 🟢
+• SLA 90-95%: Требует внимания 🟡
+• SLA <90%: Критичное состояние 🔴
+
+📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
+
+ builder = InlineKeyboardBuilder()
+ builder.button(text="🔙 Назад", callback_data="admin_operations")
+ builder.button(text="🏠 Main Menu", callback_data="main_menu")
+ builder.adjust(2)
+
+ await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error generating monitoring stats: {e}")
+ await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
+
+
@dp.callback_query(lambda c: c.data and c.data.startswith("pay_detailed_info:"))
@@ -2540,12 +3548,67 @@ async def on_shutdown():
# Run the bot
async def main() -> None:
- bot = Bot(token=TOKEN)
- dp.startup.register(on_startup)
- dp.shutdown.register(on_shutdown)
- await dp.start_polling(bot)
+ # Создаем обработчик сигналов для корректного выхода
+ stop_event = asyncio.Event()
+
+ def signal_handler(signum, frame):
+ """Обработчик сигнала для корректного завершения программы"""
+ logging.info(f"Получен сигнал {signum}. Инициируется корректное завершение программы...")
+ stop_event.set()
+
+ # Регистрируем обработчики сигналов (только для Unix-систем)
+ if not is_windows():
+ signal.signal(signal.SIGTERM, signal_handler)
+ signal.signal(signal.SIGINT, signal_handler)
+ logging.info("Обработчики сигналов зарегистрированы")
+ else:
+ # Для Windows используем другой подход
+ logging.info("Система Windows - используется базовый обработчик KeyboardInterrupt")
+
+ try:
+ bot = Bot(token=TOKEN)
+ dp.startup.register(on_startup)
+ dp.shutdown.register(on_shutdown)
+
+ # Запускаем polling с обработкой KeyboardInterrupt
+ if not is_windows():
+ # Unix-системы
+ polling_task = asyncio.create_task(dp.start_polling(bot))
+ stop_task = asyncio.create_task(stop_event.wait())
+
+ # Ждем завершения любой из задач
+ done, pending = await asyncio.wait(
+ {polling_task, stop_task},
+ return_when=asyncio.FIRST_COMPLETED
+ )
+
+ # Отменяем оставшиеся задачи
+ for task in pending:
+ task.cancel()
+ try:
+ await task
+ except asyncio.CancelledError:
+ pass
+ else:
+ # Windows
+ await dp.start_polling(bot)
+
+ except KeyboardInterrupt:
+ logging.info("Получен KeyboardInterrupt. Завершение программы...")
+ except Exception as e:
+ logging.error(f"Критическая ошибка в main(): {e}")
+ raise
+ finally:
+ logging.info("Корректное завершение программы")
if __name__ == "__main__":
- asyncio.run(main())
+ try:
+ asyncio.run(main())
+ except KeyboardInterrupt:
+ logging.info("Программа остановлена пользователем (Ctrl+C)")
+ sys.exit(0)
+ except Exception as e:
+ logging.error(f"Фатальная ошибка: {e}")
+ sys.exit(1)
\ No newline at end of file