Обновлен основной файл main.py для улучшения структуры кода и логирования. Реализована новая система инициализации базы данных с использованием композиции вместо наследования. Обновлены настройки цен на услуги в settings.py для более удобного управления. Внесены изменения в middleware для работы с новым классом DatabaseManager. Эти изменения улучшают читаемость кода, упрощают управление базой данных и повышают функциональность бота.

This commit is contained in:
Vlad 2025-06-07 20:19:41 +03:00
parent bf23b80ad9
commit ac94dc27e5
15 changed files with 4829 additions and 3599 deletions

View File

@ -6,10 +6,17 @@ from os import getenv
# Telegram Bot настройки # Telegram Bot настройки
TOKEN = getenv("BOT_TOKEN") BOT_TOKEN = getenv("BOT_TOKEN")
BOTNAME = getenv("BOT_NAME") 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")) DECODE_PRICE = int(getenv("DECODE_PRICE", "1"))
CHECK_PRICE = int(getenv("CHECK_PRICE", "10")) CHECK_PRICE = int(getenv("CHECK_PRICE", "10"))
IMG_PRICE = int(getenv("IMG_PRICE", "100")) IMG_PRICE = int(getenv("IMG_PRICE", "100"))

View File

@ -13,20 +13,134 @@ from .analytics.finance_stats import FinanceAnalytics
from .analytics.business_stats import BusinessAnalytics from .analytics.business_stats import BusinessAnalytics
# Главный класс базы данных - агрегатор всех модулей # Главный класс базы данных - агрегатор всех модулей
class DatabaseManager( class DatabaseManager(OracleDatabase):
OracleDatabase,
VinQueries,
UserManager,
PaymentTracker,
UserAnalytics,
FinanceAnalytics,
BusinessAnalytics
):
""" """
Главный класс для работы с базой данных Главный класс для работы с базой данных
Наследует все функциональности от специализированных классов Использует композицию для объединения всех модулей
""" """
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__ = [ __all__ = [

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

3735
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 # middlewares/db.py
from aiogram import BaseMiddleware from aiogram import BaseMiddleware
from typing import Callable, Dict, Any, Awaitable from typing import Callable, Dict, Any, Awaitable
from db import OracleDatabase from database import DatabaseManager
class DbSessionMiddleware(BaseMiddleware): class DbSessionMiddleware(BaseMiddleware):
def __init__(self, db: OracleDatabase): def __init__(self, db: DatabaseManager):
self.db = db self.db = db
async def __call__( 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 platform
import logging
import os
def get_operating_system() -> str: def get_operating_system() -> str:
@ -35,3 +37,40 @@ def is_linux() -> bool:
def is_macos() -> bool: def is_macos() -> bool:
"""Проверяет, запущен ли код на macOS""" """Проверяет, запущен ли код на 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 ===")