Spaces:
Paused
Paused
| import asyncio | |
| import json | |
| import os | |
| import urllib.parse | |
| from datetime import datetime | |
| from aiogram import Bot, Dispatcher, types, F | |
| from aiogram.filters import Command | |
| from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder | |
| from flask import Flask, request, jsonify, render_template_string, redirect | |
| import logging | |
| import threading | |
| from huggingface_hub import HfApi, hf_hub_download | |
| from huggingface_hub.utils import RepositoryNotFoundError | |
| from werkzeug.utils import secure_filename | |
| # Настройка логирования | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # Инициализация бота и Flask | |
| BOT_TOKEN = '7595736142:AAHSU3WGItBkebIgjO293J2WjX5qWAne8Y8' | |
| bot = Bot(token=BOT_TOKEN) | |
| dp = Dispatcher() | |
| app = Flask(__name__) | |
| # Путь для хранения данных | |
| DATA_FILE = 'data2.json' | |
| # Настройки Hugging Face | |
| REPO_ID = "flpolprojects/Clients" | |
| HF_TOKEN_WRITE = os.getenv("HF_TOKEN") | |
| HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") | |
| # Функции для работы с данными | |
| def load_data(): | |
| try: | |
| download_db_from_hf() | |
| with open(DATA_FILE, 'r', encoding='utf-8') as f: | |
| loaded_data = json.load(f) | |
| if not (isinstance(loaded_data, dict) and 'products' in loaded_data and 'orders' in loaded_data): | |
| logger.error("Неверная структура JSON файла") | |
| loaded_data = {'products': [], 'orders': []} | |
| if "categories" not in loaded_data: | |
| loaded_data["categories"] = [] | |
| return loaded_data | |
| except Exception as e: | |
| logger.error(f"Ошибка при загрузке данных: {e}") | |
| return {'products': [], 'orders': [], 'categories': []} | |
| def save_data(data): | |
| try: | |
| with open(DATA_FILE, 'w', encoding='utf-8') as f: | |
| json.dump(data, f, ensure_ascii=False, indent=4) | |
| # upload_db_to_hf() убрано отсюда | |
| except Exception as e: | |
| logger.error(f"Ошибка при сохранении данных: {e}") | |
| def upload_db_to_hf(): | |
| try: | |
| api = HfApi() | |
| api.upload_file( | |
| path_or_fileobj=DATA_FILE, | |
| path_in_repo=DATA_FILE, | |
| repo_id=REPO_ID, | |
| repo_type="dataset", | |
| token=HF_TOKEN_WRITE, | |
| commit_message=f"Автоматическое резервное копирование {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" | |
| ) | |
| logger.info("База загружена на Hugging Face") | |
| except Exception as e: | |
| logger.error(f"Ошибка при загрузке резервной копии: {e}") | |
| def download_db_from_hf(): | |
| try: | |
| hf_hub_download( | |
| repo_id=REPO_ID, | |
| filename=DATA_FILE, | |
| repo_type="dataset", | |
| token=HF_TOKEN_READ, | |
| local_dir=".", | |
| local_dir_use_symlinks=False | |
| ) | |
| logger.info("База скачана из Hugging Face") | |
| except Exception as e: | |
| logger.error(f"Ошибка при скачивании: {e}") | |
| raise | |
| # Периодическое копирование каждые 30 секунд | |
| def start_periodic_backup(): | |
| def backup_loop(): | |
| upload_db_to_hf() | |
| # Запускаем следующий вызов через 30 секунд | |
| threading.Timer(30, backup_loop).start() | |
| # Запускаем первый вызов | |
| threading.Timer(30, backup_loop).start() | |
| logger.info("Периодическое копирование каждые 30 секунд запущено") | |
| # Загрузка данных | |
| data = load_data() | |
| # Формирование клавиатур | |
| def get_main_keyboard(): | |
| builder = ReplyKeyboardBuilder() | |
| builder.button(text="📋 Каталог") | |
| builder.button(text="🛒 Корзина") | |
| builder.button(text="📦 Заказы") | |
| builder.adjust(2) | |
| return builder.as_markup(resize_keyboard=True) | |
| def get_category_keyboard(): | |
| builder = InlineKeyboardBuilder() | |
| for category in data['categories']: | |
| builder.button(text=category['name'], callback_data=f"cat_{category['id']}") | |
| builder.adjust(2) | |
| return builder.as_markup() | |
| def get_product_keyboard(product_id): | |
| builder = InlineKeyboardBuilder() | |
| builder.button(text="Добавить в корзину", callback_data=f"add_{product_id}") | |
| return builder.as_markup() | |
| # Обработчики бота | |
| async def cmd_start(message: types.Message): | |
| await message.answer("Здравствуйте ! это магазин Routine!. Выберите действие:", reply_markup=get_main_keyboard()) | |
| # Изменено условие на "📋 Каталог", чтобы соответствовать кнопке | |
| async def show_categories(message: types.Message): | |
| if not data['categories']: | |
| await message.answer("Нет доступных категорий.") | |
| return | |
| await message.answer("Выберите категорию:", reply_markup=get_category_keyboard()) | |
| async def show_products_in_category(callback_query: types.CallbackQuery): | |
| try: | |
| cat_id = int(callback_query.data.split('_')[1]) | |
| products_in_cat = [p for p in data['products'] if p.get('category_id') == cat_id] | |
| if not products_in_cat: | |
| await bot.send_message(callback_query.from_user.id, "В этой категории нет товаров.") | |
| await bot.answer_callback_query(callback_query.id) | |
| return | |
| async def send_product_batch(products_batch): | |
| for product in products_batch: | |
| photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{product['photo']}" if product.get('photo') else None | |
| caption = f"🏷 {product['name']} - {product['price']} сом\nОписание: {product['description']}\n/id: {product['id']}" | |
| try: | |
| if photo_url: | |
| await bot.send_photo(chat_id=callback_query.from_user.id, photo=photo_url, caption=caption, reply_markup=get_product_keyboard(product['id'])) | |
| else: | |
| await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id'])) | |
| except Exception as e: | |
| logger.error(f"Ошибка при отправке: {e}") | |
| await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id'])) | |
| batch_size = 5 | |
| for i in range(0, len(products_in_cat), batch_size): | |
| batch = products_in_cat[i:i + batch_size] | |
| await send_product_batch(batch) | |
| await asyncio.sleep(0.1) | |
| await bot.answer_callback_query(callback_query.id) | |
| except Exception as e: | |
| logger.error(f"Ошибка в show_products_in_category: {e}") | |
| await bot.answer_callback_query(callback_query.id, "Ошибка при загрузке товаров") | |
| async def show_cart(message: types.Message): | |
| user_id = message.from_user.id | |
| cart = next((o for o in data['orders'] if o['user_id'] == user_id and not o.get('completed')), None) | |
| if not cart or not cart['items']: | |
| await message.answer("Ваша корзина пуста.") | |
| return | |
| total = 0 | |
| response = "Ваша корзина:\n" | |
| for item in cart['items']: | |
| product = next(p for p in data['products'] if p['id'] == item['product_id']) | |
| response += f"🏷 {product['name']} - {product['price']} сом x {item['quantity']}\n" | |
| total += product['price'] * item['quantity'] | |
| response += f"\nИтого: {total} сом" | |
| builder = InlineKeyboardBuilder() | |
| builder.button(text="Оформить заказ", callback_data=f"complete_{user_id}") | |
| await message.answer(response, reply_markup=builder.as_markup()) | |
| async def add_to_cart(callback_query: types.CallbackQuery): | |
| try: | |
| product_id = int(callback_query.data.split('_')[1]) | |
| product = next((p for p in data['products'] if p['id'] == product_id), None) | |
| if product: | |
| user_id = callback_query.from_user.id | |
| cart = next((o for o in data['orders'] if o['user_id'] == user_id and not o.get('completed')), None) | |
| if not cart: | |
| cart = {'user_id': user_id, 'items': [], 'date': datetime.now().isoformat()} | |
| data['orders'].append(cart) | |
| cart['items'].append({'product_id': product_id, 'quantity': 1}) | |
| save_data(data) | |
| await bot.answer_callback_query(callback_query.id, "Товар добавлен в корзину!") | |
| except Exception as e: | |
| logger.error(f"Ошибка при добавлении в корзину: {e}") | |
| await bot.answer_callback_query(callback_query.id, "Ошибка") | |
| async def complete_order(callback_query: types.CallbackQuery): | |
| try: | |
| user_id = int(callback_query.data.split('_')[1]) | |
| cart = next((o for o in data['orders'] if o['user_id'] == user_id and not o.get('completed')), None) | |
| if cart and cart['items']: | |
| total = 0 | |
| cart_text = "Привет, я хочу сделать заказ:\n" | |
| for item in cart['items']: | |
| product = next((p for p in data['products'] if p['id'] == item['product_id']), None) | |
| if product: | |
| cart_text += f"{product['name']} - {product['price']} сом x {item['quantity']}\n" | |
| total += product['price'] * item['quantity'] | |
| cart_text += f"\nИтого: {total} сом" | |
| encoded_text = urllib.parse.quote(cart_text) | |
| whatsapp_link = f"https://wa.me/996709513331?text={encoded_text}" | |
| data['orders'].remove(cart) | |
| save_data(data) | |
| await bot.send_message(user_id, f"Оформите заказ через WhatsApp:\n{whatsapp_link}") | |
| await bot.answer_callback_query(callback_query.id) | |
| except Exception as e: | |
| logger.error(f"Ошибка при оформлении заказа: {e}") | |
| await bot.answer_callback_query(callback_query.id, "Ошибка") | |
| async def show_orders(message: types.Message): | |
| user_id = message.from_user.id | |
| user_orders = [o for o in data['orders'] if o.get('completed')] | |
| if not user_orders: | |
| await message.answer("У вас нет оформленных заказов.") | |
| return | |
| for order in user_orders: | |
| response = "Ваш заказ:\n" | |
| total = 0 | |
| for item in order['items']: | |
| product = next(p for p in data['products'] if p['id'] == item['product_id']) | |
| response += f"🏷 {product['name']} - {product['price']} сом x {item['quantity']}\n" | |
| total += product['price'] * item['quantity'] | |
| response += f"\nИтого: {total} сом\nДата: {order['date']}" | |
| await message.answer(response) | |
| # Админ-панель | |
| admin_html = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Админ-панель</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 10px; | |
| background-color: #f0f0f0; | |
| } | |
| .container { | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| } | |
| .section { | |
| background-color: #fff; | |
| padding: 10px; | |
| margin-bottom: 15px; | |
| border-radius: 5px; | |
| } | |
| h1, h2, h3 { | |
| margin: 10px 0; | |
| } | |
| input, textarea, select { | |
| width: 100%; | |
| margin: 5px 0; | |
| padding: 8px; | |
| box-sizing: border-box; | |
| } | |
| button { | |
| background-color: #4CAF50; | |
| color: white; | |
| padding: 8px 12px; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| width: 100%; | |
| margin: 5px 0; | |
| } | |
| button:hover { | |
| background-color: #45a049; | |
| } | |
| img { | |
| max-width: 100%; | |
| height: auto; | |
| max-height: 150px; | |
| } | |
| .item { | |
| border: 1px solid #ccc; | |
| padding: 10px; | |
| margin: 5px 0; | |
| word-wrap: break-word; | |
| } | |
| @media (max-width: 600px) { | |
| .section { | |
| padding: 8px; | |
| } | |
| button { | |
| padding: 6px 10px; | |
| } | |
| h1 { | |
| font-size: 1.5em; | |
| } | |
| h2 { | |
| font-size: 1.2em; | |
| } | |
| h3 { | |
| font-size: 1em; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Админ-панель</h1> | |
| <div class="section"> | |
| <h2>Управление категориями</h2> | |
| <form id="addCategoryForm" method="POST" action="/add_category"> | |
| <input type="text" name="name" placeholder="Название категории" required> | |
| <button type="submit">Добавить категорию</button> | |
| </form> | |
| <h3>Существующие категории</h3> | |
| {% if categories %} | |
| {% for category in categories %} | |
| <div class="item"> | |
| {{ category.name }} (ID: {{ category.id }}) | |
| <button onclick="deleteCategory({{ category.id }})">Удалить</button> | |
| </div> | |
| {% endfor %} | |
| {% else %} | |
| <p>Нет категорий.</p> | |
| {% endif %} | |
| </div> | |
| <div class="section"> | |
| <h2>Управление товарами</h2> | |
| <form id="addProductForm" method="POST" enctype="multipart/form-data" action="/add_product"> | |
| <input type="text" name="name" placeholder="Название" required> | |
| <input type="number" name="price" placeholder="Цена" step="0.01" required> | |
| <textarea name="description" placeholder="Описание" required></textarea> | |
| <label>Категория:</label> | |
| <select name="category_id" required> | |
| <option value="">Выберите категорию</option> | |
| {% for category in categories %} | |
| <option value="{{ category.id }}">{{ category.name }}</option> | |
| {% endfor %} | |
| </select> | |
| <input type="file" name="photo" accept="image/*"> | |
| <button type="submit">Добавить товар</button> | |
| </form> | |
| <h3>Существующие товары</h3> | |
| {% if products %} | |
| {% for product in products %} | |
| <div class="item"> | |
| {{ product.name }} - {{ product.price }} сом<br> | |
| {{ product.description }}<br> | |
| {% if product.photo %} | |
| <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photo }}" alt="{{ product.name }}"> | |
| {% endif %} | |
| <button onclick="deleteProduct({{ product.id }})">Удалить</button> | |
| </div> | |
| {% endfor %} | |
| {% else %} | |
| <p>Нет товаров.</p> | |
| {% endif %} | |
| </div> | |
| <div class="section"> | |
| <h2>Заказы</h2> | |
| {% if orders %} | |
| {% for order in orders %} | |
| <div class="item"> | |
| Пользователь: {{ order.user_id }}<br> | |
| Дата: {{ order.date }}<br> | |
| Товары: | |
| {% for item in order['items'] %} | |
| {% for product in products %} | |
| {% if product.id == item.product_id %} | |
| {{ item.quantity }} x {{ product.name }}<br> | |
| {% endif %} | |
| {% endfor %} | |
| {% endfor %} | |
| </div> | |
| {% endfor %} | |
| {% else %} | |
| <p>Нет заказов.</p> | |
| {% endif %} | |
| </div> | |
| </div> | |
| <script> | |
| const eventSource = new EventSource('/updates'); | |
| eventSource.onmessage = function(event) { | |
| if (event.data === 'update') { | |
| window.location.reload(); | |
| } | |
| }; | |
| eventSource.onerror = function() { | |
| console.log("Ошибка SSE, reconnecting..."); | |
| }; | |
| async function deleteProduct(productId) { | |
| const response = await fetch(`/delete_product/${productId}`, { method: 'POST' }); | |
| if (response.ok) broadcastUpdate(); | |
| } | |
| async function deleteCategory(categoryId) { | |
| const response = await fetch(`/delete_category/${categoryId}`, { method: 'POST' }); | |
| if (response.ok) broadcastUpdate(); | |
| } | |
| function broadcastUpdate() { | |
| fetch('/broadcast_update', { method: 'POST' }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| update_event = threading.Event() | |
| def admin_panel(): | |
| try: | |
| return render_template_string(admin_html, products=data['products'], orders=data['orders'], categories=data['categories'], repo_id=REPO_ID) | |
| except Exception as e: | |
| logger.error(f"Ошибка в шаблоне: {e}") | |
| return "Ошибка сервера", 500 | |
| def add_product(): | |
| try: | |
| name = request.form['name'] | |
| price = float(request.form['price']) | |
| description = request.form['description'] | |
| category_id = int(request.form['category_id']) | |
| photo = request.files.get('photo') | |
| product_id = max((p['id'] for p in data['products']), default=0) + 1 | |
| photo_filename = None | |
| if photo and photo.filename: | |
| photo_filename = secure_filename(photo.filename) | |
| temp_path = os.path.join(".", photo_filename) | |
| photo.save(temp_path) | |
| api = HfApi() | |
| api.upload_file( | |
| path_or_fileobj=temp_path, | |
| path_in_repo=f"photos/{photo_filename}", | |
| repo_id=REPO_ID, | |
| repo_type="dataset", | |
| token=HF_TOKEN_WRITE, | |
| commit_message=f"Добавлено фото для товара {name}" | |
| ) | |
| os.remove(temp_path) | |
| data['products'].append({ | |
| 'id': product_id, | |
| 'name': name, | |
| 'price': price, | |
| 'description': description, | |
| 'category_id': category_id, | |
| 'photo': photo_filename | |
| }) | |
| save_data(data) | |
| update_event.set() | |
| return redirect("/") | |
| except Exception as e: | |
| logger.error(f"Ошибка при добавлении товара: {e}") | |
| return jsonify({'status': 'error', 'message': str(e)}), 500 | |
| def delete_product(product_id): | |
| try: | |
| data['products'] = [p for p in data['products'] if p['id'] != product_id] | |
| save_data(data) | |
| update_event.set() | |
| return jsonify({'status': 'success'}) | |
| except Exception as e: | |
| logger.error(f"Ошибка при удалении товара: {e}") | |
| return jsonify({'status': 'error', 'message': str(e)}), 500 | |
| def add_category(): | |
| try: | |
| name = request.form['name'] | |
| category_id = max((c['id'] for c in data['categories']), default=0) + 1 | |
| data['categories'].append({'id': category_id, 'name': name}) | |
| save_data(data) | |
| update_event.set() | |
| return redirect("/") | |
| except Exception as e: | |
| logger.error(f"Ошибка при добавлении категории: {e}") | |
| return jsonify({'status': 'error', 'message': str(e)}), 500 | |
| def delete_category(category_id): | |
| try: | |
| data['categories'] = [c for c in data['categories'] if c['id'] != category_id] | |
| save_data(data) | |
| update_event.set() | |
| return jsonify({'status': 'success'}) | |
| except Exception as e: | |
| logger.error(f"Ошибка при удалении категории: {e}") | |
| return jsonify({'status': 'error', 'message': str(e)}), 500 | |
| def sse_updates(): | |
| def stream(): | |
| while True: | |
| update_event.wait() | |
| yield "data: update\n\n" | |
| update_event.clear() | |
| return app.response_class(stream(), mimetype="text/event-stream") | |
| def broadcast_update(): | |
| update_event.set() | |
| return jsonify({'status': 'success'}) | |
| # Запуск | |
| async def on_startup(_): | |
| logger.info("Бот запущен!") | |
| def run_flask(): | |
| app.run(host='0.0.0.0', port=7860, debug=True, use_reloader=False) | |
| if __name__ == '__main__': | |
| flask_thread = threading.Thread(target=run_flask, daemon=True) | |
| flask_thread.start() | |
| logger.info("Flask запущен") | |
| # Запуск периодического копирования | |
| start_periodic_backup() | |
| try: | |
| asyncio.run(dp.start_polling(bot, on_startup=on_startup)) | |
| except KeyboardInterrupt: | |
| logger.info("Остановка") | |
| finally: | |
| flask_thread.join() |