Compare commits
3 Commits
a4d8862a19
...
e479537d16
| Author | SHA1 | Date | |
|---|---|---|---|
| e479537d16 | |||
| ac94dc27e5 | |||
| bf23b80ad9 |
4
.cursorignore
Normal file
4
.cursorignore
Normal file
@ -0,0 +1,4 @@
|
||||
run.cmd
|
||||
run1.cmd
|
||||
env.example
|
||||
salvagebot*.dockerimg
|
||||
@ -6,10 +6,17 @@ from os import getenv
|
||||
|
||||
|
||||
# Telegram Bot настройки
|
||||
TOKEN = getenv("BOT_TOKEN")
|
||||
BOT_TOKEN = getenv("BOT_TOKEN")
|
||||
BOTNAME = getenv("BOT_NAME")
|
||||
|
||||
# Цены на услуги (в Telegram Stars)
|
||||
# Цены на услуги (в центах для Telegram Stars)
|
||||
PRICES = {
|
||||
"detailed_info": 299, # $2.99
|
||||
"check_detailed": 299, # $2.99
|
||||
"photos": 199, # $1.99
|
||||
}
|
||||
|
||||
# Старые переменные для совместимости
|
||||
DECODE_PRICE = int(getenv("DECODE_PRICE", "1"))
|
||||
CHECK_PRICE = int(getenv("CHECK_PRICE", "10"))
|
||||
IMG_PRICE = int(getenv("IMG_PRICE", "100"))
|
||||
|
||||
@ -9,20 +9,138 @@ from .payment_tracking import PaymentTracker
|
||||
|
||||
# Импорты из аналитики
|
||||
from .analytics.users_stats import UserAnalytics
|
||||
from .analytics.finance_stats import FinanceAnalytics
|
||||
from .analytics.business_stats import BusinessAnalytics
|
||||
|
||||
# Главный класс базы данных - агрегатор всех модулей
|
||||
class DatabaseManager(
|
||||
OracleDatabase,
|
||||
VinQueries,
|
||||
UserManager,
|
||||
PaymentTracker,
|
||||
UserAnalytics
|
||||
):
|
||||
class DatabaseManager(OracleDatabase):
|
||||
"""
|
||||
Главный класс для работы с базой данных
|
||||
Наследует все функциональности от специализированных классов
|
||||
Использует композицию для объединения всех модулей
|
||||
"""
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Инициализируем все модули
|
||||
self._vin_queries = None
|
||||
self._user_manager = None
|
||||
self._payment_tracker = None
|
||||
self._user_analytics = None
|
||||
self._finance_analytics = None
|
||||
self._business_analytics = None
|
||||
|
||||
async def initialize(self):
|
||||
"""Инициализация всех модулей"""
|
||||
await self.connect() # Используем connect() вместо initialize()
|
||||
|
||||
# Создаем экземпляры всех модулей
|
||||
self._vin_queries = VinQueries()
|
||||
self._user_manager = UserManager()
|
||||
self._payment_tracker = PaymentTracker()
|
||||
self._user_analytics = UserAnalytics()
|
||||
self._finance_analytics = FinanceAnalytics()
|
||||
self._business_analytics = BusinessAnalytics()
|
||||
|
||||
# Инициализируем все модули
|
||||
for module in [self._vin_queries, self._user_manager, self._payment_tracker,
|
||||
self._user_analytics, self._finance_analytics, self._business_analytics]:
|
||||
await module.connect() # Используем connect() вместо initialize()
|
||||
|
||||
async def close(self):
|
||||
"""Закрытие всех соединений"""
|
||||
# Закрываем модули
|
||||
for module in [self._vin_queries, self._user_manager, self._payment_tracker,
|
||||
self._user_analytics, self._finance_analytics, self._business_analytics]:
|
||||
if module:
|
||||
await module.close()
|
||||
|
||||
await super().close()
|
||||
|
||||
# Делегируем методы к соответствующим модулям
|
||||
|
||||
# VIN методы
|
||||
async def get_vin_info(self, vin: str):
|
||||
return await self._vin_queries.get_vin_info(vin)
|
||||
|
||||
async def get_salvage_records(self, vin: str):
|
||||
return await self._vin_queries.get_salvage_records(vin)
|
||||
|
||||
async def get_photo_paths(self, vin: str):
|
||||
return await self._vin_queries.get_photo_paths(vin)
|
||||
|
||||
async def get_nhtsa_data(self, vin: str):
|
||||
return await self._vin_queries.get_nhtsa_data(vin)
|
||||
|
||||
# User методы
|
||||
async def add_user_if_not_exists(self, user_id: int, username: str, first_name: str, last_name: str):
|
||||
return await self._user_manager.add_user_if_not_exists(user_id, username, first_name, last_name)
|
||||
|
||||
async def update_user_payment(self, user_id: int, amount: float):
|
||||
return await self._user_manager.update_user_payment(user_id, amount)
|
||||
|
||||
async def get_user_stats(self):
|
||||
return await self._user_manager.get_user_stats()
|
||||
|
||||
# Payment методы
|
||||
async def log_payment(self, user_id: int, vin: str, service_type: str, amount: float, payment_id: str):
|
||||
return await self._payment_tracker.log_payment(user_id, vin, service_type, amount, payment_id)
|
||||
|
||||
# Analytics методы - Users
|
||||
async def get_general_user_stats(self):
|
||||
return await self._user_analytics.get_general_user_stats()
|
||||
|
||||
async def get_user_growth_stats(self):
|
||||
return await self._user_analytics.get_user_growth_stats()
|
||||
|
||||
async def get_premium_user_analysis(self):
|
||||
return await self._user_analytics.get_premium_user_analysis()
|
||||
|
||||
async def get_user_geography_stats(self):
|
||||
return await self._user_analytics.get_user_geography_stats()
|
||||
|
||||
async def get_user_activity_analysis(self):
|
||||
return await self._user_analytics.get_user_activity_analysis()
|
||||
|
||||
async def get_user_acquisition_sources(self):
|
||||
return await self._user_analytics.get_user_acquisition_sources()
|
||||
|
||||
# Analytics методы - Finance
|
||||
async def get_revenue_analysis(self):
|
||||
return await self._finance_analytics.get_revenue_analysis()
|
||||
|
||||
async def get_service_performance_stats(self):
|
||||
return await self._finance_analytics.get_service_performance_stats()
|
||||
|
||||
async def get_conversion_funnel_analysis(self):
|
||||
return await self._finance_analytics.get_conversion_funnel_analysis()
|
||||
|
||||
async def get_refund_analysis(self):
|
||||
return await self._finance_analytics.get_refund_analysis()
|
||||
|
||||
async def get_payment_transaction_analysis(self):
|
||||
return await self._finance_analytics.get_payment_transaction_analysis()
|
||||
|
||||
async def get_monetization_efficiency(self):
|
||||
return await self._finance_analytics.get_monetization_efficiency()
|
||||
|
||||
# Analytics методы - Business
|
||||
async def get_business_trends_analysis(self):
|
||||
return await self._business_analytics.get_business_trends_analysis()
|
||||
|
||||
async def get_demand_forecasting(self):
|
||||
return await self._business_analytics.get_demand_forecasting()
|
||||
|
||||
async def get_regional_market_analysis(self):
|
||||
return await self._business_analytics.get_regional_market_analysis()
|
||||
|
||||
async def get_monetization_strategy_analysis(self):
|
||||
return await self._business_analytics.get_monetization_strategy_analysis()
|
||||
|
||||
async def get_operational_optimization_insights(self):
|
||||
return await self._business_analytics.get_operational_optimization_insights()
|
||||
|
||||
async def get_strategic_recommendations(self):
|
||||
return await self._business_analytics.get_strategic_recommendations()
|
||||
|
||||
# Экспорт для удобного импорта
|
||||
__all__ = [
|
||||
@ -31,5 +149,7 @@ __all__ = [
|
||||
'UserManager',
|
||||
'PaymentTracker',
|
||||
'UserAnalytics',
|
||||
'FinanceAnalytics',
|
||||
'BusinessAnalytics',
|
||||
'DatabaseManager'
|
||||
]
|
||||
504
database/analytics/business_stats.py
Normal file
504
database/analytics/business_stats.py
Normal file
@ -0,0 +1,504 @@
|
||||
"""
|
||||
Бизнес аналитика
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from ..base import OracleDatabase
|
||||
|
||||
|
||||
class BusinessAnalytics(OracleDatabase):
|
||||
"""Класс для бизнес аналитики"""
|
||||
|
||||
async def get_biz_trends_stats(self) -> Dict:
|
||||
"""Анализ трендов бизнеса"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() 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')
|
||||
"""
|
||||
cur.execute(user_growth_query)
|
||||
user_growth = cur.fetchall()
|
||||
|
||||
# Тренд выручки
|
||||
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 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')
|
||||
"""
|
||||
cur.execute(revenue_trend_query)
|
||||
revenue_trend = cur.fetchall()
|
||||
|
||||
# Тренд использования услуг
|
||||
services_trend_query = """
|
||||
SELECT
|
||||
service_type,
|
||||
TO_CHAR(created_date, 'YYYY-MM') as month,
|
||||
COUNT(*) as usage_count
|
||||
FROM 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
|
||||
"""
|
||||
cur.execute(services_trend_query)
|
||||
services_trend = cur.fetchall()
|
||||
|
||||
# Тренд конверсии
|
||||
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 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')
|
||||
"""
|
||||
cur.execute(conversion_trend_query)
|
||||
conversion_trend = cur.fetchall()
|
||||
|
||||
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:
|
||||
logging.error(f"SQL Error in get_biz_trends_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
return {
|
||||
'user_growth_trend': [],
|
||||
'revenue_trend': [],
|
||||
'services_trend': [],
|
||||
'conversion_trend': []
|
||||
}
|
||||
|
||||
return await self._execute_query(_get_stats)
|
||||
|
||||
async def get_biz_forecasts_stats(self) -> Dict:
|
||||
"""Прогнозы развития бизнеса"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() 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
|
||||
"""
|
||||
cur.execute(recent_growth_query)
|
||||
recent_growth = cur.fetchall()
|
||||
|
||||
# Сезонность по дням недели
|
||||
seasonality_query = """
|
||||
SELECT
|
||||
TO_CHAR(created_date, 'D') as day_of_week,
|
||||
COUNT(*) as transactions,
|
||||
AVG(payment_amount) as avg_amount
|
||||
FROM 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')
|
||||
"""
|
||||
cur.execute(seasonality_query)
|
||||
seasonality = cur.fetchall()
|
||||
|
||||
# Потенциал роста по регионам
|
||||
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
|
||||
"""
|
||||
cur.execute(regional_potential_query)
|
||||
regional_potential = cur.fetchall()
|
||||
|
||||
return {
|
||||
'recent_growth': recent_growth or [],
|
||||
'seasonality': seasonality or [],
|
||||
'regional_potential': regional_potential or []
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_biz_forecasts_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
return {
|
||||
'recent_growth': [],
|
||||
'seasonality': [],
|
||||
'regional_potential': []
|
||||
}
|
||||
|
||||
return await self._execute_query(_get_stats)
|
||||
|
||||
async def get_biz_regions_stats(self) -> Dict:
|
||||
"""Анализ регионов роста"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() 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
|
||||
"""
|
||||
cur.execute(regions_growth_query)
|
||||
regions_growth = cur.fetchall()
|
||||
|
||||
# Конверсия по регионам
|
||||
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 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
|
||||
"""
|
||||
cur.execute(regional_conversion_query)
|
||||
regional_conversion = cur.fetchall()
|
||||
|
||||
# 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
|
||||
"""
|
||||
cur.execute(premium_distribution_query)
|
||||
premium_distribution = cur.fetchall()
|
||||
|
||||
return {
|
||||
'regions_growth': regions_growth or [],
|
||||
'regional_conversion': regional_conversion or [],
|
||||
'premium_distribution': premium_distribution or []
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_biz_regions_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
return {
|
||||
'regions_growth': [],
|
||||
'regional_conversion': [],
|
||||
'premium_distribution': []
|
||||
}
|
||||
|
||||
return await self._execute_query(_get_stats)
|
||||
|
||||
async def get_biz_monetization_stats(self) -> Dict:
|
||||
"""Анализ монетизации"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() 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
|
||||
"""
|
||||
cur.execute(monetization_funnel_query)
|
||||
funnel_result = cur.fetchone()
|
||||
|
||||
# Анализ 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
|
||||
"""
|
||||
cur.execute(ltv_analysis_query)
|
||||
ltv_analysis = cur.fetchall()
|
||||
|
||||
# Анализ прибыльности услуг
|
||||
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 payment_logs
|
||||
WHERE payment_status = 'completed'
|
||||
AND created_date >= SYSDATE - 90
|
||||
GROUP BY service_type
|
||||
ORDER BY SUM(payment_amount) DESC
|
||||
"""
|
||||
cur.execute(service_profitability_query)
|
||||
service_profitability = cur.fetchall()
|
||||
|
||||
# Время до первой покупки
|
||||
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 payment_logs
|
||||
WHERE payment_status = 'completed'
|
||||
GROUP BY user_id
|
||||
) first_pl ON bu.id = first_pl.user_id
|
||||
INNER JOIN payment_logs pl ON bu.id = pl.user_id AND pl.created_date = first_pl.first_purchase_date
|
||||
WHERE bu.created_at >= SYSDATE - 365
|
||||
"""
|
||||
cur.execute(time_to_purchase_query)
|
||||
time_to_purchase = cur.fetchone()
|
||||
|
||||
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:
|
||||
logging.error(f"SQL Error in get_biz_monetization_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
return {
|
||||
'monetization_funnel': None,
|
||||
'ltv_analysis': [],
|
||||
'service_profitability': [],
|
||||
'time_to_purchase': None
|
||||
}
|
||||
|
||||
return await self._execute_query(_get_stats)
|
||||
|
||||
async def get_biz_optimization_stats(self) -> Dict:
|
||||
"""Анализ возможностей оптимизации"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() 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
|
||||
"""
|
||||
cur.execute(churn_analysis_query)
|
||||
churn_analysis = cur.fetchall()
|
||||
|
||||
# Неэффективные запросы
|
||||
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 payment_logs
|
||||
WHERE payment_status = 'completed'
|
||||
AND created_date >= SYSDATE - 30
|
||||
GROUP BY service_type
|
||||
ORDER BY service_type
|
||||
"""
|
||||
cur.execute(inefficient_requests_query)
|
||||
inefficient_requests = cur.fetchall()
|
||||
|
||||
# Пользователи с высоким потенциалом
|
||||
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
|
||||
"""
|
||||
cur.execute(high_potential_query)
|
||||
high_potential = cur.fetchone()
|
||||
|
||||
# Анализ ценообразования
|
||||
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 payment_logs
|
||||
WHERE payment_status = 'completed'
|
||||
AND created_date >= SYSDATE - 60
|
||||
GROUP BY service_type, payment_amount
|
||||
ORDER BY service_type, payment_amount
|
||||
"""
|
||||
cur.execute(pricing_analysis_query)
|
||||
pricing_analysis = cur.fetchall()
|
||||
|
||||
return {
|
||||
'churn_analysis': churn_analysis or [],
|
||||
'inefficient_requests': inefficient_requests or [],
|
||||
'high_potential': high_potential,
|
||||
'pricing_analysis': pricing_analysis or []
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_biz_optimization_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
return {
|
||||
'churn_analysis': [],
|
||||
'inefficient_requests': [],
|
||||
'high_potential': None,
|
||||
'pricing_analysis': []
|
||||
}
|
||||
|
||||
return await self._execute_query(_get_stats)
|
||||
|
||||
async def get_biz_recommendations_stats(self) -> Dict:
|
||||
"""Автоматические рекомендации для бизнеса"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# Ключевые метрики для анализа
|
||||
key_metrics_query = """
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN successful_payments_count > 0 THEN 1 END) as paying_users,
|
||||
AVG(total_payments) as avg_ltv,
|
||||
COUNT(CASE WHEN last_interaction_date < SYSDATE - 30 THEN 1 END) as dormant_users,
|
||||
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 THEN 1 END) as one_time_buyers
|
||||
FROM bot_users
|
||||
WHERE is_active = 1
|
||||
"""
|
||||
cur.execute(key_metrics_query)
|
||||
key_metrics = cur.fetchone()
|
||||
|
||||
# Метрики успешности услуг
|
||||
service_success_query = """
|
||||
SELECT
|
||||
service_type,
|
||||
COUNT(*) as total_requests,
|
||||
COUNT(CASE WHEN service_status = 'success' THEN 1 END) as successful_requests,
|
||||
COUNT(CASE WHEN refund_status <> 'no_refund' THEN 1 END) as refunds,
|
||||
AVG(payment_amount) as avg_price
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed'
|
||||
AND created_date >= SYSDATE - 30
|
||||
GROUP BY service_type
|
||||
"""
|
||||
cur.execute(service_success_query)
|
||||
service_success = cur.fetchall()
|
||||
|
||||
# Рост пользователей за последние периоды
|
||||
growth_metrics_query = """
|
||||
SELECT
|
||||
COUNT(CASE WHEN created_at >= SYSDATE - 7 THEN 1 END) as users_last_week,
|
||||
COUNT(CASE WHEN created_at >= SYSDATE - 14 AND created_at < SYSDATE - 7 THEN 1 END) as users_prev_week,
|
||||
COUNT(CASE WHEN created_at >= SYSDATE - 30 THEN 1 END) as users_last_month,
|
||||
COUNT(CASE WHEN created_at >= SYSDATE - 60 AND created_at < SYSDATE - 30 THEN 1 END) as users_prev_month
|
||||
FROM bot_users
|
||||
"""
|
||||
cur.execute(growth_metrics_query)
|
||||
growth_metrics = cur.fetchone()
|
||||
|
||||
return {
|
||||
'key_metrics': key_metrics,
|
||||
'service_success': service_success or [],
|
||||
'growth_metrics': growth_metrics
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_biz_recommendations_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
return {
|
||||
'key_metrics': None,
|
||||
'service_success': [],
|
||||
'growth_metrics': None
|
||||
}
|
||||
|
||||
return await self._execute_query(_get_stats)
|
||||
497
database/analytics/finance_stats.py
Normal file
497
database/analytics/finance_stats.py
Normal file
@ -0,0 +1,497 @@
|
||||
"""
|
||||
Финансовая аналитика
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from ..base import OracleDatabase
|
||||
|
||||
|
||||
class FinanceAnalytics(OracleDatabase):
|
||||
"""Класс для финансовой аналитики"""
|
||||
|
||||
async def get_finance_revenue_stats(self, admin_user_id: int = 0) -> Dict:
|
||||
"""Анализ доходов"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# Общая статистика доходов (исключаем админа)
|
||||
revenue_query = """
|
||||
SELECT
|
||||
SUM(payment_amount) as total_revenue,
|
||||
COUNT(*) as total_transactions,
|
||||
AVG(payment_amount) as avg_transaction,
|
||||
COUNT(DISTINCT user_id) as unique_customers,
|
||||
SUM(CASE WHEN created_date >= SYSDATE - 1 THEN payment_amount ELSE 0 END) as revenue_24h,
|
||||
SUM(CASE WHEN created_date >= SYSDATE - 7 THEN payment_amount ELSE 0 END) as revenue_7d,
|
||||
SUM(CASE WHEN created_date >= SYSDATE - 30 THEN payment_amount ELSE 0 END) as revenue_30d,
|
||||
COUNT(CASE WHEN created_date >= SYSDATE - 1 THEN 1 END) as transactions_24h,
|
||||
COUNT(CASE WHEN created_date >= SYSDATE - 7 THEN 1 END) as transactions_7d,
|
||||
COUNT(CASE WHEN created_date >= SYSDATE - 30 THEN 1 END) as transactions_30d
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
"""
|
||||
cur.execute(revenue_query, {"admin_user_id": admin_user_id})
|
||||
revenue = cur.fetchone()
|
||||
|
||||
# Доходы по дням за последние 10 дней (исключаем админа)
|
||||
daily_query = """
|
||||
SELECT
|
||||
TO_CHAR(created_date, 'DD.MM') as day,
|
||||
SUM(payment_amount) as revenue,
|
||||
COUNT(*) as transactions
|
||||
FROM payment_logs
|
||||
WHERE created_date >= SYSDATE - 10 AND payment_status = 'completed' AND user_id != :admin_user_id
|
||||
GROUP BY TO_CHAR(created_date, 'DD.MM'), TRUNC(created_date)
|
||||
ORDER BY TRUNC(created_date) DESC
|
||||
"""
|
||||
cur.execute(daily_query, {"admin_user_id": admin_user_id})
|
||||
daily_revenue = cur.fetchall()
|
||||
|
||||
return {
|
||||
"total_revenue": float(revenue[0]) if revenue[0] else 0.0,
|
||||
"total_transactions": revenue[1] or 0,
|
||||
"avg_transaction": round(float(revenue[2]), 2) if revenue[2] else 0.0,
|
||||
"unique_customers": revenue[3] or 0,
|
||||
"revenue_24h": float(revenue[4]) if revenue[4] else 0.0,
|
||||
"revenue_7d": float(revenue[5]) if revenue[5] else 0.0,
|
||||
"revenue_30d": float(revenue[6]) if revenue[6] else 0.0,
|
||||
"transactions_24h": revenue[7] or 0,
|
||||
"transactions_7d": revenue[8] or 0,
|
||||
"transactions_30d": revenue[9] or 0,
|
||||
"daily_revenue": daily_revenue[:10] if daily_revenue else []
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_finance_revenue_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
raise
|
||||
|
||||
try:
|
||||
return await self._execute_query(_get_stats)
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting revenue stats: {e}")
|
||||
return {}
|
||||
|
||||
async def get_finance_services_stats(self, admin_user_id: int = 0) -> Dict:
|
||||
"""Анализ доходов по услугам"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# Сначала проверим, есть ли данные в таблице (исключаем админа)
|
||||
check_query = "SELECT COUNT(*) FROM payment_logs WHERE payment_status = 'completed' AND user_id != :admin_user_id"
|
||||
cur.execute(check_query, {"admin_user_id": admin_user_id})
|
||||
total_records = cur.fetchone()[0]
|
||||
|
||||
if total_records == 0:
|
||||
return {
|
||||
"services_breakdown": [],
|
||||
"trends": {}
|
||||
}
|
||||
|
||||
# Статистика по типам услуг (исключаем админа)
|
||||
services_query = """
|
||||
SELECT
|
||||
service_type,
|
||||
COUNT(*) as transactions,
|
||||
SUM(payment_amount) as revenue,
|
||||
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,
|
||||
AVG(data_found_count) as avg_data_found
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
GROUP BY service_type
|
||||
ORDER BY SUM(payment_amount) DESC
|
||||
"""
|
||||
cur.execute(services_query, {"admin_user_id": admin_user_id})
|
||||
services = cur.fetchall()
|
||||
|
||||
# Тренды по услугам за последние 30 дней (исключаем админа)
|
||||
trends_query = """
|
||||
SELECT
|
||||
service_type,
|
||||
SUM(CASE WHEN created_date >= SYSDATE - 7 THEN 1 ELSE 0 END) as week_transactions,
|
||||
SUM(CASE WHEN created_date >= SYSDATE - 30 THEN 1 ELSE 0 END) as month_transactions
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
GROUP BY service_type
|
||||
"""
|
||||
cur.execute(trends_query, {"admin_user_id": admin_user_id})
|
||||
trends_result = cur.fetchall()
|
||||
|
||||
trends = {row[0]: {"week": row[1], "month": row[2]} for row in trends_result} if trends_result else {}
|
||||
|
||||
return {
|
||||
"services_breakdown": services if services else [],
|
||||
"trends": trends
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_finance_services_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
return {
|
||||
"services_breakdown": [],
|
||||
"trends": {}
|
||||
}
|
||||
|
||||
try:
|
||||
return await self._execute_query(_get_stats)
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting services stats: {e}")
|
||||
return {
|
||||
"services_breakdown": [],
|
||||
"trends": {}
|
||||
}
|
||||
|
||||
async def get_finance_conversion_stats(self, admin_user_id: int = 0) -> Dict:
|
||||
"""Анализ конверсии"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# Сначала получаем общее количество пользователей
|
||||
users_query = "SELECT COUNT(DISTINCT id) FROM bot_users WHERE is_active = 1"
|
||||
cur.execute(users_query)
|
||||
total_users = cur.fetchone()[0] or 1
|
||||
|
||||
# Статистика по платежам (исключаем админа)
|
||||
payment_query = """
|
||||
SELECT
|
||||
COUNT(DISTINCT user_id) as paying_users,
|
||||
COUNT(*) as total_transactions,
|
||||
COUNT(DISTINCT CASE WHEN user_is_premium = 1 THEN user_id END) as premium_buyers,
|
||||
COUNT(DISTINCT CASE WHEN user_is_premium = 0 THEN user_id END) as regular_buyers,
|
||||
AVG(payment_amount) as avg_purchase
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
"""
|
||||
cur.execute(payment_query, {"admin_user_id": admin_user_id})
|
||||
payment_result = cur.fetchone()
|
||||
|
||||
if not payment_result:
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"paying_users": 0,
|
||||
"conversion_rate": 0.0,
|
||||
"total_transactions": 0,
|
||||
"premium_buyers": 0,
|
||||
"regular_buyers": 0,
|
||||
"avg_purchase": 0.0,
|
||||
"one_time_buyers": 0,
|
||||
"regular_buyers_repeat": 0,
|
||||
"loyal_buyers": 0,
|
||||
"avg_purchases_per_user": 0.0,
|
||||
"repeat_rate": 0.0
|
||||
}
|
||||
|
||||
paying_users = payment_result[0] or 0
|
||||
|
||||
# Повторные покупки (исключаем админа)
|
||||
repeat_query = """
|
||||
SELECT
|
||||
COUNT(CASE WHEN purchase_count = 1 THEN 1 END) as one_time_buyers,
|
||||
COUNT(CASE WHEN purchase_count BETWEEN 2 AND 5 THEN 1 END) as regular_buyers_count,
|
||||
COUNT(CASE WHEN purchase_count > 5 THEN 1 END) as loyal_buyers,
|
||||
AVG(purchase_count) as avg_purchases_per_user
|
||||
FROM (
|
||||
SELECT user_id, COUNT(*) as purchase_count
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
GROUP BY user_id
|
||||
)
|
||||
"""
|
||||
cur.execute(repeat_query, {"admin_user_id": admin_user_id})
|
||||
repeat = cur.fetchone()
|
||||
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"paying_users": paying_users,
|
||||
"conversion_rate": round(paying_users / total_users * 100, 2) if total_users > 0 else 0,
|
||||
"total_transactions": payment_result[1] or 0,
|
||||
"premium_buyers": payment_result[2] or 0,
|
||||
"regular_buyers": payment_result[3] or 0,
|
||||
"avg_purchase": round(float(payment_result[4]), 2) if payment_result[4] else 0.0,
|
||||
"one_time_buyers": repeat[0] or 0 if repeat else 0,
|
||||
"regular_buyers_repeat": repeat[1] or 0 if repeat else 0,
|
||||
"loyal_buyers": repeat[2] or 0 if repeat else 0,
|
||||
"avg_purchases_per_user": round(float(repeat[3]), 2) if repeat and repeat[3] else 0.0,
|
||||
"repeat_rate": round(((repeat[1] or 0) + (repeat[2] or 0)) / paying_users * 100, 2) if paying_users > 0 and repeat else 0
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_finance_conversion_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
return {
|
||||
"total_users": 0,
|
||||
"paying_users": 0,
|
||||
"conversion_rate": 0.0,
|
||||
"total_transactions": 0,
|
||||
"premium_buyers": 0,
|
||||
"regular_buyers": 0,
|
||||
"avg_purchase": 0.0,
|
||||
"one_time_buyers": 0,
|
||||
"regular_buyers_repeat": 0,
|
||||
"loyal_buyers": 0,
|
||||
"avg_purchases_per_user": 0.0,
|
||||
"repeat_rate": 0.0
|
||||
}
|
||||
|
||||
try:
|
||||
return await self._execute_query(_get_stats)
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting conversion stats: {e}")
|
||||
return {
|
||||
"total_users": 0,
|
||||
"paying_users": 0,
|
||||
"conversion_rate": 0.0,
|
||||
"total_transactions": 0,
|
||||
"premium_buyers": 0,
|
||||
"regular_buyers": 0,
|
||||
"avg_purchase": 0.0,
|
||||
"one_time_buyers": 0,
|
||||
"regular_buyers_repeat": 0,
|
||||
"loyal_buyers": 0,
|
||||
"avg_purchases_per_user": 0.0,
|
||||
"repeat_rate": 0.0
|
||||
}
|
||||
|
||||
async def get_finance_refunds_stats(self, admin_user_id: int = 0) -> Dict:
|
||||
"""Анализ возвратов"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# Статистика возвратов (исключаем админа)
|
||||
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 = '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
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
"""
|
||||
cur.execute(refunds_query, {"admin_user_id": admin_user_id})
|
||||
refunds = cur.fetchone()
|
||||
|
||||
# Причины возвратов по типам услуг (исключаем админа)
|
||||
reasons_query = """
|
||||
SELECT
|
||||
service_type,
|
||||
refund_status,
|
||||
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
|
||||
GROUP BY service_type, refund_status
|
||||
ORDER BY service_type, COUNT(*) DESC
|
||||
"""
|
||||
cur.execute(reasons_query, {"admin_user_id": admin_user_id})
|
||||
reasons = cur.fetchall()
|
||||
|
||||
total_transactions = refunds[0] or 1
|
||||
refund_count = refunds[1] or 0
|
||||
|
||||
return {
|
||||
"total_transactions": total_transactions,
|
||||
"refund_count": refund_count,
|
||||
"refund_rate": round(refund_count / total_transactions * 100, 2),
|
||||
"refund_amount": float(refunds[2]) if refunds[2] else 0.0,
|
||||
"auto_refunds": refunds[3] or 0,
|
||||
"manual_refunds": refunds[4] or 0,
|
||||
"admin_refunds": refunds[5] or 0,
|
||||
"refund_breakdown": reasons if reasons else []
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_finance_refunds_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
raise
|
||||
|
||||
try:
|
||||
return await self._execute_query(_get_stats)
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting refunds stats: {e}")
|
||||
return {}
|
||||
|
||||
async def get_finance_transactions_stats(self, admin_user_id: int = 0) -> Dict:
|
||||
"""Анализ транзакций"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# Статистика транзакций (исключаем админа)
|
||||
transactions_query = """
|
||||
SELECT
|
||||
COUNT(*) as total_attempts,
|
||||
COUNT(CASE WHEN payment_status = 'completed' THEN 1 END) as completed_count,
|
||||
COUNT(CASE WHEN payment_status = 'pending' THEN 1 END) as pending_count,
|
||||
COUNT(CASE WHEN payment_status = 'failed' THEN 1 END) as failed_count,
|
||||
COUNT(CASE WHEN service_status = 'success' THEN 1 END) as service_success_count,
|
||||
COUNT(CASE WHEN service_status = 'no_data' THEN 1 END) as no_data_count,
|
||||
COUNT(CASE WHEN service_status = 'error' THEN 1 END) as service_error_count
|
||||
FROM payment_logs
|
||||
WHERE user_id != :admin_user_id
|
||||
"""
|
||||
cur.execute(transactions_query, {"admin_user_id": admin_user_id})
|
||||
transactions = cur.fetchone()
|
||||
|
||||
# Статистика ошибок (исключаем админа)
|
||||
errors_query = """
|
||||
SELECT
|
||||
service_type,
|
||||
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
|
||||
FROM payment_logs
|
||||
WHERE user_id != :admin_user_id
|
||||
GROUP BY service_type
|
||||
"""
|
||||
cur.execute(errors_query, {"admin_user_id": admin_user_id})
|
||||
errors = cur.fetchall()
|
||||
|
||||
total_attempts = transactions[0] or 1
|
||||
|
||||
return {
|
||||
"total_attempts": total_attempts,
|
||||
"completed": transactions[1] or 0,
|
||||
"pending": transactions[2] or 0,
|
||||
"failed": transactions[3] or 0,
|
||||
"payment_success_rate": round((transactions[1] or 0) / total_attempts * 100, 2),
|
||||
"service_success": transactions[4] or 0,
|
||||
"no_data": transactions[5] or 0,
|
||||
"service_error": transactions[6] or 0,
|
||||
"service_success_rate": round((transactions[4] or 0) / (transactions[1] or 1) * 100, 2),
|
||||
"error_breakdown": errors if errors else []
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_finance_transactions_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
raise
|
||||
|
||||
try:
|
||||
return await self._execute_query(_get_stats)
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting transactions stats: {e}")
|
||||
return {}
|
||||
|
||||
async def get_finance_efficiency_stats(self, admin_user_id: int = 0) -> Dict:
|
||||
"""Анализ эффективности"""
|
||||
def _get_stats():
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
# 1. Средний доход на транзакцию (исключаем админа)
|
||||
avg_revenue_query = """
|
||||
SELECT AVG(payment_amount) as avg_revenue_per_transaction
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
"""
|
||||
cur.execute(avg_revenue_query, {"admin_user_id": admin_user_id})
|
||||
avg_revenue_result = cur.fetchone()
|
||||
avg_revenue_per_transaction = round(float(avg_revenue_result[0]), 2) if avg_revenue_result and avg_revenue_result[0] else 0.0
|
||||
|
||||
# 2. Количество уникальных дней с транзакциями (исключаем админа)
|
||||
days_query = """
|
||||
SELECT COUNT(DISTINCT TRUNC(created_date)) as unique_days
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
"""
|
||||
cur.execute(days_query, {"admin_user_id": admin_user_id})
|
||||
days_result = cur.fetchone()
|
||||
unique_days = days_result[0] if days_result and days_result[0] else 1
|
||||
|
||||
# 3. Общее количество транзакций (исключаем админа)
|
||||
total_transactions_query = """
|
||||
SELECT COUNT(*) as total_transactions
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
"""
|
||||
cur.execute(total_transactions_query, {"admin_user_id": admin_user_id})
|
||||
total_transactions_result = cur.fetchone()
|
||||
total_transactions = total_transactions_result[0] if total_transactions_result else 0
|
||||
|
||||
avg_transactions_per_day = round(total_transactions / unique_days, 2) if unique_days > 0 else 0
|
||||
|
||||
# 4. Доход и транзакции на клиента (исключаем админа)
|
||||
customer_stats_query = """
|
||||
SELECT
|
||||
SUM(payment_amount) as total_revenue,
|
||||
COUNT(*) as total_transactions,
|
||||
COUNT(DISTINCT user_id) as unique_customers
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
"""
|
||||
cur.execute(customer_stats_query, {"admin_user_id": admin_user_id})
|
||||
customer_result = cur.fetchone()
|
||||
|
||||
if customer_result and customer_result[2] and customer_result[2] > 0:
|
||||
revenue_per_customer = round(float(customer_result[0]) / customer_result[2], 2)
|
||||
transactions_per_customer = round(float(customer_result[1]) / customer_result[2], 2)
|
||||
else:
|
||||
revenue_per_customer = 0.0
|
||||
transactions_per_customer = 0.0
|
||||
|
||||
# 5. Эффективность по часам дня (исключаем админа)
|
||||
hourly_query = """
|
||||
SELECT
|
||||
EXTRACT(HOUR FROM created_date) as hour,
|
||||
COUNT(*) as transactions,
|
||||
SUM(payment_amount) as revenue
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND created_date >= SYSDATE - 30 AND user_id != :admin_user_id
|
||||
GROUP BY EXTRACT(HOUR FROM created_date)
|
||||
ORDER BY EXTRACT(HOUR FROM created_date)
|
||||
"""
|
||||
cur.execute(hourly_query, {"admin_user_id": admin_user_id})
|
||||
hourly = cur.fetchall()
|
||||
|
||||
# 6. Топ VIN по доходам (исключаем админа)
|
||||
top_vins_query = """
|
||||
SELECT
|
||||
vin_number,
|
||||
COUNT(*) as requests,
|
||||
SUM(payment_amount) as revenue
|
||||
FROM payment_logs
|
||||
WHERE payment_status = 'completed' AND user_id != :admin_user_id
|
||||
GROUP BY vin_number
|
||||
ORDER BY SUM(payment_amount) DESC
|
||||
FETCH FIRST 10 ROWS ONLY
|
||||
"""
|
||||
cur.execute(top_vins_query, {"admin_user_id": admin_user_id})
|
||||
top_vins = cur.fetchall()
|
||||
|
||||
return {
|
||||
"avg_revenue_per_transaction": avg_revenue_per_transaction,
|
||||
"avg_transactions_per_day": avg_transactions_per_day,
|
||||
"revenue_per_customer": revenue_per_customer,
|
||||
"transactions_per_customer": transactions_per_customer,
|
||||
"hourly_distribution": hourly if hourly else [],
|
||||
"top_vins": top_vins[:10] if top_vins else []
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"SQL Error in get_finance_efficiency_stats:")
|
||||
logging.error(f"Error: {e}")
|
||||
return {
|
||||
"avg_revenue_per_transaction": 0.0,
|
||||
"avg_transactions_per_day": 0.0,
|
||||
"revenue_per_customer": 0.0,
|
||||
"transactions_per_customer": 0.0,
|
||||
"hourly_distribution": [],
|
||||
"top_vins": []
|
||||
}
|
||||
|
||||
try:
|
||||
return await self._execute_query(_get_stats)
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting efficiency stats: {e}")
|
||||
return {
|
||||
"avg_revenue_per_transaction": 0.0,
|
||||
"avg_transactions_per_day": 0.0,
|
||||
"revenue_per_customer": 0.0,
|
||||
"transactions_per_customer": 0.0,
|
||||
"hourly_distribution": [],
|
||||
"top_vins": []
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
# Обработчики админ-панели
|
||||
|
||||
# Админ хэндлеры
|
||||
71
handlers/admin/main_admin.py
Normal file
71
handlers/admin/main_admin.py
Normal file
@ -0,0 +1,71 @@
|
||||
# Основные админ хэндлеры
|
||||
|
||||
import logging
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from database import DatabaseManager
|
||||
from config.settings import ADMIN_USER_ID
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.callback_query(lambda c: c.data == "admin_stats")
|
||||
async def admin_stats_callback(callback: CallbackQuery, db: DatabaseManager = None):
|
||||
"""Главная админ панель"""
|
||||
if callback.from_user.id != ADMIN_USER_ID:
|
||||
await callback.answer("❌ Access denied", show_alert=True)
|
||||
return
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="👥 Users Analytics", callback_data="admin_users")
|
||||
builder.button(text="💰 Finance Analytics", callback_data="admin_finance")
|
||||
builder.button(text="⚙️ Operations Analytics", callback_data="admin_operations")
|
||||
builder.button(text="📈 Business Analytics", callback_data="admin_business")
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
builder.adjust(2, 2, 1)
|
||||
|
||||
admin_text = (
|
||||
"🔧 **Admin Panel**\n\n"
|
||||
"Welcome to the administration dashboard!\n\n"
|
||||
"**Available sections:**\n"
|
||||
"• 👥 **Users Analytics** - User statistics and growth\n"
|
||||
"• 💰 **Finance Analytics** - Revenue and payment data\n"
|
||||
"• ⚙️ **Operations Analytics** - System performance\n"
|
||||
"• 📈 **Business Analytics** - Business insights\n\n"
|
||||
"Select a section to view detailed analytics:"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(admin_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(Command("admin_stats"))
|
||||
async def admin_stats_handler(message: Message, db: DatabaseManager = None):
|
||||
"""Команда доступа к админ панели"""
|
||||
if message.from_user.id != ADMIN_USER_ID:
|
||||
await message.answer("❌ Access denied")
|
||||
return
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="👥 Users Analytics", callback_data="admin_users")
|
||||
builder.button(text="💰 Finance Analytics", callback_data="admin_finance")
|
||||
builder.button(text="⚙️ Operations Analytics", callback_data="admin_operations")
|
||||
builder.button(text="📈 Business Analytics", callback_data="admin_business")
|
||||
builder.adjust(2, 2)
|
||||
|
||||
admin_text = (
|
||||
"🔧 **Admin Panel**\n\n"
|
||||
"Welcome to the administration dashboard!\n\n"
|
||||
"**Available sections:**\n"
|
||||
"• 👥 **Users Analytics** - User statistics and growth\n"
|
||||
"• 💰 **Finance Analytics** - Revenue and payment data\n"
|
||||
"• ⚙️ **Operations Analytics** - System performance\n"
|
||||
"• 📈 **Business Analytics** - Business insights\n\n"
|
||||
"Select a section to view detailed analytics:"
|
||||
)
|
||||
|
||||
await message.answer(admin_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
|
||||
187
handlers/main_handlers.py
Normal file
187
handlers/main_handlers.py
Normal file
@ -0,0 +1,187 @@
|
||||
"""
|
||||
Основные хэндлеры бота: старт, помощь, цены, навигация
|
||||
"""
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from database import DatabaseManager
|
||||
from config.settings import ADMIN_USER_ID
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.message(Command("start"))
|
||||
async def command_start_handler(message: Message, db: DatabaseManager = None) -> None:
|
||||
"""Обработчик команды /start"""
|
||||
user_id = message.from_user.id
|
||||
username = message.from_user.username or "Unknown"
|
||||
first_name = message.from_user.first_name or ""
|
||||
last_name = message.from_user.last_name or ""
|
||||
|
||||
# Проверяем и добавляем пользователя в базу
|
||||
if db:
|
||||
await db.add_user_if_not_exists(user_id, username, first_name, last_name)
|
||||
|
||||
# Создаем клавиатуру
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🔍 Decode VIN", callback_data="decode_vin")
|
||||
builder.button(text="🚗 Check VIN", callback_data="check_vin")
|
||||
builder.button(text="📸 Search Car Photo", callback_data="search_car_photo")
|
||||
builder.button(text="💰 Prices", callback_data="prices")
|
||||
builder.button(text="❓ Help", callback_data="help")
|
||||
|
||||
# Добавляем админ кнопку для администратора
|
||||
if user_id == ADMIN_USER_ID:
|
||||
builder.button(text="📊 Admin Panel", callback_data="admin_stats")
|
||||
|
||||
builder.adjust(1)
|
||||
|
||||
welcome_text = (
|
||||
f"👋 Welcome to **SalvageDB Bot**, {first_name}!\n\n"
|
||||
"🚗 I can help you:\n"
|
||||
"• **Decode VIN** - Get basic vehicle information\n"
|
||||
"• **Check VIN** - Get detailed salvage and auction history\n"
|
||||
"• **Search Car Photos** - Find vehicle photos\n\n"
|
||||
"Choose an option below to get started:"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
welcome_text,
|
||||
reply_markup=builder.as_markup(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(lambda c: c.data == "main_menu")
|
||||
async def main_menu_callback(callback: CallbackQuery, db: DatabaseManager = None):
|
||||
"""Возврат в главное меню"""
|
||||
user_id = callback.from_user.id
|
||||
first_name = callback.from_user.first_name or ""
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🔍 Decode VIN", callback_data="decode_vin")
|
||||
builder.button(text="🚗 Check VIN", callback_data="check_vin")
|
||||
builder.button(text="📸 Search Car Photo", callback_data="search_car_photo")
|
||||
builder.button(text="💰 Prices", callback_data="prices")
|
||||
builder.button(text="❓ Help", callback_data="help")
|
||||
|
||||
if user_id == ADMIN_USER_ID:
|
||||
builder.button(text="📊 Admin Panel", callback_data="admin_stats")
|
||||
|
||||
builder.adjust(1)
|
||||
|
||||
welcome_text = (
|
||||
f"👋 Welcome back, {first_name}!\n\n"
|
||||
"🚗 I can help you:\n"
|
||||
"• **Decode VIN** - Get basic vehicle information\n"
|
||||
"• **Check VIN** - Get detailed salvage and auction history\n"
|
||||
"• **Search Car Photos** - Find vehicle photos\n\n"
|
||||
"Choose an option below:"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
welcome_text,
|
||||
reply_markup=builder.as_markup(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(lambda c: c.data == "help")
|
||||
async def help_callback(callback: CallbackQuery, db: DatabaseManager = None):
|
||||
"""Показ справки"""
|
||||
help_text = (
|
||||
"🆘 **SalvageDB Bot Help**\n\n"
|
||||
|
||||
"**🔍 VIN Decode (Free)**\n"
|
||||
"Get basic vehicle information:\n"
|
||||
"• Make, Model, Year\n"
|
||||
"• Engine specifications\n"
|
||||
"• Basic vehicle data\n\n"
|
||||
|
||||
"**🚗 VIN Check ($2.99)**\n"
|
||||
"Get comprehensive salvage report:\n"
|
||||
"• Salvage/auction history\n"
|
||||
"• Accident details\n"
|
||||
"• Title information\n"
|
||||
"• Sale dates and locations\n"
|
||||
"• Damage descriptions\n\n"
|
||||
|
||||
"**📸 Car Photos ($1.99)**\n"
|
||||
"Find vehicle photos:\n"
|
||||
"• High-quality auction photos\n"
|
||||
"• Multiple angles available\n"
|
||||
"• Before/after damage photos\n\n"
|
||||
|
||||
"**💡 Tips:**\n"
|
||||
"• VIN should be 17 characters\n"
|
||||
"• Use uppercase letters\n"
|
||||
"• No spaces or special characters\n"
|
||||
"• Premium services require payment\n\n"
|
||||
|
||||
"Need more help? Contact support."
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
|
||||
await callback.message.edit_text(
|
||||
help_text,
|
||||
reply_markup=builder.as_markup(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(lambda c: c.data == "prices")
|
||||
async def prices_callback(callback: CallbackQuery, db: DatabaseManager = None):
|
||||
"""Показ прайс-листа"""
|
||||
prices_text = (
|
||||
"💰 **SalvageDB Pricing**\n\n"
|
||||
|
||||
"**🆓 Free Services:**\n"
|
||||
"• VIN Decode - Basic vehicle info\n\n"
|
||||
|
||||
"**💳 Premium Services:**\n"
|
||||
|
||||
"**🚗 VIN Check - $2.99**\n"
|
||||
"Complete salvage and auction history:\n"
|
||||
"• ✅ Salvage/auction records\n"
|
||||
"• ✅ Accident details\n"
|
||||
"• ✅ Title information\n"
|
||||
"• ✅ Sale dates and locations\n"
|
||||
"• ✅ Damage descriptions\n"
|
||||
"• ✅ Market values\n\n"
|
||||
|
||||
"**📸 Car Photos - $1.99**\n"
|
||||
"High-quality vehicle photos:\n"
|
||||
"• ✅ Auction photos\n"
|
||||
"• ✅ Multiple angles\n"
|
||||
"• ✅ Damage documentation\n"
|
||||
"• ✅ Before/after photos\n\n"
|
||||
|
||||
"**💎 Why Premium?**\n"
|
||||
"• Professional data sources\n"
|
||||
"• Real-time updates\n"
|
||||
"• Comprehensive reports\n"
|
||||
"• Fast processing\n\n"
|
||||
|
||||
"💳 **Payment:** We accept all major payment methods"
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🔍 Try VIN Decode (Free)", callback_data="decode_vin")
|
||||
builder.button(text="🚗 Get VIN Check ($2.99)", callback_data="check_vin")
|
||||
builder.button(text="📸 Find Photos ($1.99)", callback_data="search_car_photo")
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
builder.adjust(1)
|
||||
|
||||
await callback.message.edit_text(
|
||||
prices_text,
|
||||
reply_markup=builder.as_markup(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await callback.answer()
|
||||
226
handlers/payment_handlers.py
Normal file
226
handlers/payment_handlers.py
Normal file
@ -0,0 +1,226 @@
|
||||
# Обработчики платежей
|
||||
|
||||
import logging
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message, CallbackQuery, LabeledPrice, PreCheckoutQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from database import DatabaseManager
|
||||
from config.settings import PRICES, BOT_TOKEN
|
||||
from utils.formatting import escape_markdown, format_sale_date, parse_location
|
||||
from handlers.vin_handlers import send_vehicle_photos
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.callback_query(lambda c: c.data and c.data.startswith("pay_detailed_info:"))
|
||||
async def pay_detailed_info_callback(callback: CallbackQuery, db: DatabaseManager = None):
|
||||
"""Оплата детальной информации через платёжную форму"""
|
||||
vin = callback.data.split(":", 1)[1]
|
||||
|
||||
# Создаем инвойс для оплаты
|
||||
prices = [LabeledPrice(label="VIN Detailed Report", amount=PRICES["detailed_info"])] # $2.99 в копейках
|
||||
|
||||
await callback.message.answer_invoice(
|
||||
title="VIN Detailed Report",
|
||||
description=f"Comprehensive salvage report for VIN: {vin}",
|
||||
payload=f"detailed_info:{vin}",
|
||||
provider_token="", # Для Telegram Stars не нужен
|
||||
currency="XTR", # Telegram Stars
|
||||
prices=prices,
|
||||
start_parameter="detailed_info"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(lambda c: c.data and c.data.startswith("pay_check_detailed:"))
|
||||
async def pay_check_detailed_callback(callback: CallbackQuery, db: DatabaseManager = None):
|
||||
"""Оплата детального отчета через платёжную форму"""
|
||||
vin = callback.data.split(":", 1)[1]
|
||||
|
||||
prices = [LabeledPrice(label="VIN Check Report", amount=PRICES["check_detailed"])] # $2.99
|
||||
|
||||
await callback.message.answer_invoice(
|
||||
title="VIN Check - Detailed Report",
|
||||
description=f"Complete salvage and auction history for VIN: {vin}",
|
||||
payload=f"check_detailed:{vin}",
|
||||
provider_token="",
|
||||
currency="XTR",
|
||||
prices=prices,
|
||||
start_parameter="check_detailed"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(lambda c: c.data and c.data.startswith("pay_photos:"))
|
||||
async def pay_photos_callback(callback: CallbackQuery, db: DatabaseManager = None):
|
||||
"""Оплата фотографий через платёжную форму"""
|
||||
vin = callback.data.split(":", 1)[1]
|
||||
|
||||
prices = [LabeledPrice(label="Vehicle Photos", amount=PRICES["photos"])] # $1.99
|
||||
|
||||
await callback.message.answer_invoice(
|
||||
title="Vehicle Photos",
|
||||
description=f"High-quality auction photos for VIN: {vin}",
|
||||
payload=f"photos:{vin}",
|
||||
provider_token="",
|
||||
currency="XTR",
|
||||
prices=prices,
|
||||
start_parameter="photos"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.pre_checkout_query()
|
||||
async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery, db: DatabaseManager = None):
|
||||
"""Подтверждение предварительной проверки платежа"""
|
||||
await pre_checkout_query.answer(ok=True)
|
||||
|
||||
|
||||
@router.message(lambda message: message.successful_payment)
|
||||
async def successful_payment_handler(message: Message, db: DatabaseManager = None):
|
||||
"""Обработка успешного платежа"""
|
||||
payment = message.successful_payment
|
||||
payload_parts = payment.invoice_payload.split(":", 1)
|
||||
|
||||
if len(payload_parts) != 2:
|
||||
logging.error(f"Invalid payment payload: {payment.invoice_payload}")
|
||||
await message.answer("❌ **Payment Error**\n\nInvalid payment data. Please contact support.")
|
||||
return
|
||||
|
||||
service_type, vin = payload_parts
|
||||
user_id = message.from_user.id
|
||||
amount = payment.total_amount / 100 # Конвертируем из копеек в доллары
|
||||
|
||||
try:
|
||||
# Логируем платеж
|
||||
if db:
|
||||
await db.log_payment(user_id, vin, service_type, amount, payment.telegram_payment_charge_id)
|
||||
|
||||
if service_type == "detailed_info" or service_type == "check_detailed":
|
||||
await handle_detailed_report_payment(message, vin, db)
|
||||
elif service_type == "photos":
|
||||
await handle_photos_payment(message, vin, db)
|
||||
else:
|
||||
logging.error(f"Unknown service type: {service_type}")
|
||||
await message.answer("❌ **Payment Error**\n\nUnknown service type. Please contact support.")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in successful_payment_handler: {e}")
|
||||
await message.answer("❌ **Processing Error**\n\nYour payment was successful, but there was an error processing your request. Please contact support.")
|
||||
|
||||
|
||||
async def handle_detailed_report_payment(message: Message, vin: str, db: DatabaseManager):
|
||||
"""Обработка оплаты детального отчета"""
|
||||
try:
|
||||
# Получаем данные VIN
|
||||
vin_info = await db.get_vin_info(vin)
|
||||
if not vin_info:
|
||||
await message.answer(f"❌ **Data Error**\n\nVIN `{escape_markdown(vin)}` not found in database.")
|
||||
return
|
||||
|
||||
year, make, model, engine, body_style, fuel_type = vin_info
|
||||
|
||||
# Получаем детальные данные
|
||||
salvage_records = await db.get_salvage_records(vin)
|
||||
nhtsa_data = await db.get_nhtsa_data(vin)
|
||||
|
||||
# Формируем детальный отчет
|
||||
report_text = f"📊 **Detailed VIN Report**\n\n"
|
||||
report_text += f"**VIN:** `{escape_markdown(vin)}`\n\n"
|
||||
|
||||
# Основная информация
|
||||
report_text += f"**🚗 Vehicle Information:**\n"
|
||||
report_text += f"• **Year:** {escape_markdown(str(year))}\n"
|
||||
report_text += f"• **Make:** {escape_markdown(str(make))}\n"
|
||||
report_text += f"• **Model:** {escape_markdown(str(model))}\n"
|
||||
report_text += f"• **Engine:** {escape_markdown(str(engine))}\n"
|
||||
report_text += f"• **Body Style:** {escape_markdown(str(body_style))}\n"
|
||||
report_text += f"• **Fuel Type:** {escape_markdown(str(fuel_type))}\n\n"
|
||||
|
||||
# Записи о повреждениях
|
||||
if salvage_records:
|
||||
report_text += f"**🔥 Salvage History ({len(salvage_records)} records):**\n"
|
||||
for i, record in enumerate(salvage_records, 1):
|
||||
sale_date, damage, sale_location, odometer, lot_number, auction = record
|
||||
|
||||
report_text += f"\n**Record #{i}:**\n"
|
||||
report_text += f"• **Sale Date:** {format_sale_date(str(sale_date))}\n"
|
||||
report_text += f"• **Damage:** {escape_markdown(str(damage))}\n"
|
||||
report_text += f"• **Location:** {parse_location(str(sale_location))}\n"
|
||||
report_text += f"• **Odometer:** {escape_markdown(str(odometer))} miles\n"
|
||||
report_text += f"• **Lot:** {escape_markdown(str(lot_number))}\n"
|
||||
report_text += f"• **Auction:** {escape_markdown(str(auction))}\n"
|
||||
else:
|
||||
report_text += f"**🔥 Salvage History:** No records found\n"
|
||||
|
||||
# NHTSA данные
|
||||
if nhtsa_data:
|
||||
report_text += f"\n**🛡️ NHTSA Data:**\n"
|
||||
for field, value in nhtsa_data.items():
|
||||
if value and str(value) != 'None':
|
||||
report_text += f"• **{field}:** {escape_markdown(str(value))}\n"
|
||||
|
||||
# Создаем кнопки
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="📸 Get Photos ($1.99)", callback_data=f"pay_photos:{vin}")
|
||||
builder.button(text="🔍 Check Another VIN", callback_data="check_vin")
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
builder.adjust(1)
|
||||
|
||||
# Отправляем отчет (может быть длинным, делим на части если нужно)
|
||||
if len(report_text) > 4000:
|
||||
# Разделяем на части
|
||||
parts = [report_text[i:i+4000] for i in range(0, len(report_text), 4000)]
|
||||
for i, part in enumerate(parts):
|
||||
if i == len(parts) - 1: # Последняя часть с кнопками
|
||||
await message.answer(part, reply_markup=builder.as_markup(), parse_mode="Markdown")
|
||||
else:
|
||||
await message.answer(part, parse_mode="Markdown")
|
||||
else:
|
||||
await message.answer(report_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
|
||||
|
||||
logging.info(f"Detailed report sent for VIN {vin} to user {message.from_user.id}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in handle_detailed_report_payment: {e}")
|
||||
await message.answer("❌ **Report Error**\n\nThere was an error generating your report. Please contact support.")
|
||||
|
||||
|
||||
async def handle_photos_payment(message: Message, vin: str, db: DatabaseManager):
|
||||
"""Обработка оплаты фотографий"""
|
||||
try:
|
||||
# Получаем информацию о VIN
|
||||
vin_info = await db.get_vin_info(vin)
|
||||
if not vin_info:
|
||||
await message.answer(f"❌ **Data Error**\n\nVIN `{escape_markdown(vin)}` not found in database.")
|
||||
return
|
||||
|
||||
year, make, model, engine, body_style, fuel_type = vin_info
|
||||
|
||||
# Получаем пути к фотографиям
|
||||
photo_paths = await db.get_photo_paths(vin)
|
||||
|
||||
if photo_paths:
|
||||
# Отправляем фотографии
|
||||
await send_vehicle_photos(message, vin, photo_paths, str(make), str(model), str(year))
|
||||
logging.info(f"Photos sent for VIN {vin} to user {message.from_user.id}")
|
||||
else:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🚗 Get Detailed Report", callback_data=f"pay_check_detailed:{vin}")
|
||||
builder.button(text="📸 Search Another VIN", callback_data="search_car_photo")
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
builder.adjust(1)
|
||||
|
||||
await message.answer(
|
||||
f"📸 **No Photos Available**\n\n"
|
||||
f"Unfortunately, no photos are available for VIN `{escape_markdown(vin)}`.\n\n"
|
||||
f"Your payment has been processed. Please contact support if you believe this is an error.",
|
||||
reply_markup=builder.as_markup(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in handle_photos_payment: {e}")
|
||||
await message.answer("❌ **Photo Error**\n\nThere was an error retrieving your photos. Please contact support.")
|
||||
146
handlers/vin_handlers.py
Normal file
146
handlers/vin_handlers.py
Normal file
@ -0,0 +1,146 @@
|
||||
# VIN-обработчики: декодирование, проверка, поиск фотографий
|
||||
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message, CallbackQuery, FSInputFile, InputMediaPhoto
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from database import DatabaseManager
|
||||
from utils.formatting import escape_markdown
|
||||
|
||||
router = Router()
|
||||
|
||||
class VinStates(StatesGroup):
|
||||
waiting_for_vin = State()
|
||||
waiting_for_check_vin = State()
|
||||
waiting_for_photo_vin = State()
|
||||
|
||||
|
||||
@router.callback_query(lambda c: c.data == "decode_vin")
|
||||
async def decode_vin_callback(callback: CallbackQuery, state: FSMContext, db: DatabaseManager = None):
|
||||
"""Начало процесса декодирования VIN"""
|
||||
try:
|
||||
await state.set_state(VinStates.waiting_for_vin)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🔍 **VIN Decode Service**\n\nPlease enter the VIN number (17 characters):\n\nExample: `1HGBH41JXMN109186`",
|
||||
reply_markup=builder.as_markup(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in decode_vin_callback: {e}")
|
||||
await callback.answer("Произошла ошибка", show_alert=True)
|
||||
|
||||
|
||||
@router.message(VinStates.waiting_for_vin)
|
||||
async def process_vin(message: Message, state: FSMContext, db: DatabaseManager = None):
|
||||
"""Обработка VIN для декодирования"""
|
||||
try:
|
||||
vin = message.text.strip().upper()
|
||||
|
||||
if len(vin) != 17:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
|
||||
await message.answer(
|
||||
"❌ **Invalid VIN**\n\nVIN number must be exactly 17 characters.\nPlease try again:",
|
||||
reply_markup=builder.as_markup(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
# Заглушка для декодирования VIN
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🔍 Check History", callback_data=f"check_vin:{vin}")
|
||||
builder.button(text="📸 Search Photos", callback_data=f"search_photos:{vin}")
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
builder.adjust(1)
|
||||
|
||||
await message.answer(
|
||||
f"✅ **VIN Decoded Successfully**\n\n"
|
||||
f"**VIN:** `{vin}`\n"
|
||||
f"**Make:** Toyota\n"
|
||||
f"**Model:** Camry\n"
|
||||
f"**Year:** 2015\n"
|
||||
f"**Engine:** 2.5L\n\n"
|
||||
f"Choose an action:",
|
||||
reply_markup=builder.as_markup(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in process_vin: {e}")
|
||||
await message.answer("Произошла ошибка при обработке VIN")
|
||||
|
||||
|
||||
async def send_vehicle_photos(message: Message, vin: str, photo_paths: List[str], make: str, model: str, year: str):
|
||||
"""Отправка фотографий автомобиля пользователю"""
|
||||
try:
|
||||
from utils.photo_utils import prepare_photo_paths
|
||||
|
||||
logging.info(f"Sending {len(photo_paths)} photos for VIN {vin}")
|
||||
prepared_paths = prepare_photo_paths(photo_paths)
|
||||
|
||||
if not prepared_paths:
|
||||
await message.answer(
|
||||
"📸 **No Photos Available**\n\nUnfortunately, the photos for this vehicle are currently unavailable.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return
|
||||
|
||||
# Отправляем фотографии группами по 10
|
||||
photo_batch_size = 10
|
||||
total_batches = (len(prepared_paths) + photo_batch_size - 1) // photo_batch_size
|
||||
|
||||
for batch_num in range(total_batches):
|
||||
start_idx = batch_num * photo_batch_size
|
||||
end_idx = min(start_idx + photo_batch_size, len(prepared_paths))
|
||||
batch_paths = prepared_paths[start_idx:end_idx]
|
||||
|
||||
media_group = []
|
||||
for i, photo_path in enumerate(batch_paths):
|
||||
try:
|
||||
if batch_num == 0 and i == 0:
|
||||
caption = f"📸 **Vehicle Photos**\n**VIN:** {vin}\n**Vehicle:** {year} {make} {model}\n**Photos:** {len(prepared_paths)} total"
|
||||
media_group.append(InputMediaPhoto(media=FSInputFile(photo_path), caption=caption, parse_mode="Markdown"))
|
||||
else:
|
||||
media_group.append(InputMediaPhoto(media=FSInputFile(photo_path)))
|
||||
except Exception as e:
|
||||
logging.error(f"Error preparing photo {photo_path}: {e}")
|
||||
continue
|
||||
|
||||
if media_group:
|
||||
try:
|
||||
await message.answer_media_group(media_group)
|
||||
logging.info(f"Sent batch {batch_num + 1}/{total_batches} with {len(media_group)} photos")
|
||||
except Exception as e:
|
||||
logging.error(f"Error sending photo batch {batch_num + 1}: {e}")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="📸 Search More Photos", callback_data="search_car_photo")
|
||||
builder.button(text="🚗 Get Detailed Report", callback_data=f"pay_check_detailed:{vin}")
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
builder.adjust(1)
|
||||
|
||||
await message.answer(
|
||||
f"✅ **Photos sent successfully!**\n\n**VIN:** `{escape_markdown(vin)}`\n**Photos sent:** {len(prepared_paths)}",
|
||||
reply_markup=builder.as_markup(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in send_vehicle_photos: {e}")
|
||||
await message.answer(
|
||||
f"❌ **Error sending photos**\n\nThere was an error sending the photos for VIN `{escape_markdown(vin)}`.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
@ -1 +1 @@
|
||||
# Клавиатуры Telegram бота
|
||||
# Клавиатуры бота
|
||||
51
keyboards/main_keyboards.py
Normal file
51
keyboards/main_keyboards.py
Normal file
@ -0,0 +1,51 @@
|
||||
# Основные клавиатуры бота
|
||||
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from config.settings import ADMIN_USER_ID
|
||||
|
||||
|
||||
def get_main_menu_keyboard(user_id: int = None):
|
||||
"""Главное меню бота"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🔍 Decode VIN", callback_data="decode_vin")
|
||||
builder.button(text="🚗 Check VIN", callback_data="check_vin")
|
||||
builder.button(text="📸 Search Car Photo", callback_data="search_car_photo")
|
||||
builder.button(text="💰 Prices", callback_data="prices")
|
||||
builder.button(text="❓ Help", callback_data="help")
|
||||
|
||||
# Добавляем админ кнопку для администратора
|
||||
if user_id == ADMIN_USER_ID:
|
||||
builder.button(text="📊 Admin Panel", callback_data="admin_stats")
|
||||
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_back_to_main_keyboard():
|
||||
"""Кнопка возврата в главное меню"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_vin_service_keyboard(vin: str):
|
||||
"""Клавиатура для VIN сервисов"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🚗 Get Detailed Report ($2.99)", callback_data=f"pay_check_detailed:{vin}")
|
||||
builder.button(text="📸 Find Photos ($1.99)", callback_data=f"pay_photos:{vin}")
|
||||
builder.button(text="🔍 Decode Another VIN", callback_data="decode_vin")
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_admin_main_keyboard():
|
||||
"""Главная админ панель"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="👥 Users Analytics", callback_data="admin_users")
|
||||
builder.button(text="💰 Finance Analytics", callback_data="admin_finance")
|
||||
builder.button(text="⚙️ Operations Analytics", callback_data="admin_operations")
|
||||
builder.button(text="📈 Business Analytics", callback_data="admin_business")
|
||||
builder.button(text="🏠 Main Menu", callback_data="main_menu")
|
||||
builder.adjust(2, 2, 1)
|
||||
return builder.as_markup()
|
||||
187
main_new.py
Normal file
187
main_new.py
Normal file
@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SalvageDB Bot - Главный файл запуска
|
||||
Модульная архитектура с разделением хэндлеров
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import signal
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
|
||||
from config.settings import BOT_TOKEN
|
||||
from database import DatabaseManager
|
||||
from middlewares.db import DbSessionMiddleware
|
||||
from utils.logging_config import setup_logging
|
||||
from utils.system_utils import get_operating_system, log_system_info
|
||||
|
||||
# Импорт всех роутеров
|
||||
from handlers.main_handlers import router as main_router
|
||||
from handlers.vin_handlers import router as vin_router
|
||||
from handlers.payment_handlers import router as payment_router
|
||||
from handlers.admin.main_admin import router as admin_main_router
|
||||
|
||||
# Глобальные переменные
|
||||
bot = None
|
||||
dp = None
|
||||
database_manager = None
|
||||
|
||||
|
||||
async def on_startup():
|
||||
"""Инициализация при запуске"""
|
||||
global database_manager
|
||||
|
||||
logging.info("=== BOT STARTUP ===")
|
||||
log_system_info()
|
||||
|
||||
# Инициализируем базу данных
|
||||
try:
|
||||
database_manager = DatabaseManager()
|
||||
await database_manager.initialize()
|
||||
logging.info("Database manager initialized successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to initialize database: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
logging.info("Bot startup completed successfully")
|
||||
|
||||
|
||||
async def on_shutdown():
|
||||
"""Очистка при завершении"""
|
||||
global database_manager
|
||||
|
||||
logging.info("=== BOT SHUTDOWN ===")
|
||||
|
||||
if database_manager:
|
||||
await database_manager.close()
|
||||
logging.info("Database connections closed")
|
||||
|
||||
logging.info("Bot shutdown completed")
|
||||
|
||||
|
||||
def setup_signal_handlers():
|
||||
"""Настройка обработчиков сигналов для корректного завершения"""
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
logging.info(f"Received signal {signum}, initiating graceful shutdown...")
|
||||
|
||||
# Получаем текущий event loop
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
# Создаем задачу для завершения
|
||||
loop.create_task(shutdown_bot())
|
||||
except RuntimeError:
|
||||
# Если loop не найден, завершаем принудительно
|
||||
logging.warning("No running event loop found, forcing exit...")
|
||||
sys.exit(0)
|
||||
|
||||
# Регистрируем обработчики для разных сигналов
|
||||
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
|
||||
signal.signal(signal.SIGTERM, signal_handler) # Команда завершения
|
||||
|
||||
# Для Windows добавляем обработку SIGBREAK
|
||||
if get_operating_system() == 'Windows':
|
||||
try:
|
||||
signal.signal(signal.SIGBREAK, signal_handler)
|
||||
except AttributeError:
|
||||
pass # SIGBREAK может быть недоступен в некоторых версиях
|
||||
|
||||
|
||||
async def shutdown_bot():
|
||||
"""Корректное завершение работы бота"""
|
||||
logging.info("Shutting down bot...")
|
||||
|
||||
# Останавливаем polling
|
||||
if dp:
|
||||
await dp.stop_polling()
|
||||
|
||||
# Закрываем сессию бота
|
||||
if bot:
|
||||
await bot.session.close()
|
||||
|
||||
# Вызываем on_shutdown
|
||||
await on_shutdown()
|
||||
|
||||
logging.info("Bot shutdown complete, exiting...")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def setup_routers(dispatcher: Dispatcher):
|
||||
"""Настройка всех роутеров"""
|
||||
# Порядок важен - более специфичные роутеры должны быть первыми
|
||||
|
||||
# Админ роутеры (самый высокий приоритет)
|
||||
dispatcher.include_router(admin_main_router)
|
||||
|
||||
# Платежные роутеры
|
||||
dispatcher.include_router(payment_router)
|
||||
|
||||
# VIN роутеры
|
||||
dispatcher.include_router(vin_router)
|
||||
|
||||
# Основные роутеры (самый низкий приоритет)
|
||||
dispatcher.include_router(main_router)
|
||||
|
||||
logging.info("All routers configured successfully")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Главная функция запуска бота"""
|
||||
global bot, dp
|
||||
|
||||
# Настройка логирования
|
||||
setup_logging()
|
||||
|
||||
# Настройка обработчиков сигналов
|
||||
setup_signal_handlers()
|
||||
|
||||
logging.info("Starting SalvageDB Bot...")
|
||||
logging.info(f"Python version: {sys.version}")
|
||||
logging.info(f"Operating System: {get_operating_system()}")
|
||||
|
||||
try:
|
||||
# Инициализация бота и диспетчера
|
||||
bot = Bot(token=BOT_TOKEN)
|
||||
dp = Dispatcher(storage=MemoryStorage())
|
||||
|
||||
# Настройка middleware для работы с базой данных
|
||||
dp.middleware.setup(DbSessionMiddleware())
|
||||
|
||||
# Настройка всех роутеров
|
||||
setup_routers(dp)
|
||||
|
||||
# Регистрация событий запуска и завершения
|
||||
dp.startup.register(on_startup)
|
||||
dp.shutdown.register(on_shutdown)
|
||||
|
||||
# Запуск polling
|
||||
logging.info("Starting bot polling...")
|
||||
await dp.start_polling(
|
||||
bot,
|
||||
allowed_updates=['message', 'callback_query', 'pre_checkout_query'],
|
||||
drop_pending_updates=True
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Received KeyboardInterrupt, shutting down...")
|
||||
except Exception as e:
|
||||
logging.error(f"Fatal error in main: {e}")
|
||||
raise
|
||||
finally:
|
||||
# Финальная очистка
|
||||
if bot:
|
||||
await bot.session.close()
|
||||
logging.info("Bot stopped")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Bot interrupted by user")
|
||||
except Exception as e:
|
||||
logging.error(f"Fatal error: {e}")
|
||||
sys.exit(1)
|
||||
3614
main_old.py
Normal file
3614
main_old.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,10 @@
|
||||
# middlewares/db.py
|
||||
from aiogram import BaseMiddleware
|
||||
from typing import Callable, Dict, Any, Awaitable
|
||||
from db import OracleDatabase
|
||||
from database import DatabaseManager
|
||||
|
||||
class DbSessionMiddleware(BaseMiddleware):
|
||||
def __init__(self, db: OracleDatabase):
|
||||
def __init__(self, db: DatabaseManager):
|
||||
self.db = db
|
||||
|
||||
async def __call__(
|
||||
|
||||
9
run1.cmd
Normal file
9
run1.cmd
Normal file
@ -0,0 +1,9 @@
|
||||
set DEBUG=1
|
||||
set BOT_TOKEN=6302522437:AAEDAYNrMuJCX5kt-IJ7CjT8AzfG3g-0mo0
|
||||
set BOT_NAME=salvagedb_bot
|
||||
set db_user=salvagebot
|
||||
set db_password=hlz1zm4n39nq6et0
|
||||
set db_dsn=89.110.92.87:17921/db1
|
||||
set ADMIN_USER_ID=604563487
|
||||
:1
|
||||
uv run main.py
|
||||
@ -2,6 +2,8 @@
|
||||
Системные утилиты и функции проверки ОС
|
||||
"""
|
||||
import platform
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
def get_operating_system() -> str:
|
||||
@ -35,3 +37,40 @@ def is_linux() -> bool:
|
||||
def is_macos() -> bool:
|
||||
"""Проверяет, запущен ли код на macOS"""
|
||||
return get_operating_system() == 'macOS'
|
||||
|
||||
|
||||
def log_system_info():
|
||||
"""
|
||||
Логирует информацию о системе при запуске
|
||||
"""
|
||||
os_name = get_operating_system()
|
||||
python_version = platform.python_version()
|
||||
platform_info = platform.platform()
|
||||
|
||||
logging.info("=== SYSTEM INFORMATION ===")
|
||||
logging.info(f"Operating System: {os_name}")
|
||||
logging.info(f"Platform: {platform_info}")
|
||||
logging.info(f"Python Version: {python_version}")
|
||||
logging.info(f"Architecture: {platform.architecture()[0]}")
|
||||
logging.info(f"Processor: {platform.processor()}")
|
||||
|
||||
# Проверяем переменные окружения
|
||||
if os.getenv('BOT_TOKEN'):
|
||||
logging.info("BOT_TOKEN: ✅ Set")
|
||||
else:
|
||||
logging.warning("BOT_TOKEN: ❌ Not set")
|
||||
|
||||
if os.getenv('DB_USER'):
|
||||
logging.info("Database credentials: ✅ Set")
|
||||
else:
|
||||
logging.warning("Database credentials: ❌ Not set")
|
||||
|
||||
# Проверяем запуск в Docker
|
||||
if os.path.exists('/.dockerenv'):
|
||||
logging.info("Environment: 🐳 Docker container")
|
||||
logging.info(f"Container timezone: {os.getenv('TZ', 'UTC')}")
|
||||
logging.info(f"Container user: {os.getenv('USER', 'unknown')}")
|
||||
else:
|
||||
logging.info("Environment: 🖥️ Host system")
|
||||
|
||||
logging.info("=== SYSTEM CHECK COMPLETE ===")
|
||||
Loading…
x
Reference in New Issue
Block a user