Compare commits

...

3 Commits

Author SHA1 Message Date
e479537d16 Добавлен новый файл run1.cmd в .cursorignore для исключения из индексации. Удалены устаревшие файлы HARBOR_SETUP_INSTRUCTIONS.md, HARBOR_TAG_FORMAT.md, read_dev.md, README_DOCKER.md, README_GITEA_ACTIONS.md, README_USER_TRACKING.md, TEST_DOCKER_FIX.md и TEST_GITEA_ACTIONS.md для упрощения структуры проекта и улучшения читаемости. Эти изменения помогают поддерживать актуальность документации и упрощают навигацию по проекту. 2025-06-08 09:27:05 +03:00
ac94dc27e5 Обновлен основной файл main.py для улучшения структуры кода и логирования. Реализована новая система инициализации базы данных с использованием композиции вместо наследования. Обновлены настройки цен на услуги в settings.py для более удобного управления. Внесены изменения в middleware для работы с новым классом DatabaseManager. Эти изменения улучшают читаемость кода, упрощают управление базой данных и повышают функциональность бота. 2025-06-07 20:19:41 +03:00
bf23b80ad9 Добавлены новые классы аналитики: FinanceAnalytics и BusinessAnalytics в модуль базы данных. Эти изменения расширяют функциональность аналитики, предоставляя более полное представление о финансовых и бизнес-показателях. 2025-06-07 15:39:59 +03:00
26 changed files with 5838 additions and 3597 deletions

4
.cursorignore Normal file
View File

@ -0,0 +1,4 @@
run.cmd
run1.cmd
env.example
salvagebot*.dockerimg

View File

@ -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"))

View File

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

View 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)

View 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": []
}

View File

@ -1 +1,3 @@
# Обработчики админ-панели
# Обработчики админ-панели
# Админ хэндлеры

View 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
View 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()

View 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
View 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"
)

View File

@ -1 +1 @@
# Клавиатуры Telegram бота
# Клавиатуры бота

View 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()

3739
main.py

File diff suppressed because it is too large Load Diff

187
main_new.py Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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

View File

@ -2,6 +2,8 @@
Системные утилиты и функции проверки ОС
"""
import platform
import logging
import os
def get_operating_system() -> str:
@ -34,4 +36,41 @@ def is_linux() -> bool:
def is_macos() -> bool:
"""Проверяет, запущен ли код на macOS"""
return get_operating_system() == '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 ===")