Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -18,46 +18,35 @@ logging.basicConfig(level=logging.INFO)
|
|
| 18 |
logger = logging.getLogger(__name__)
|
| 19 |
|
| 20 |
# Инициализация бота и Flask
|
| 21 |
-
BOT_TOKEN = '7734802681:AAGKHGG8O9uNk64JWTHH5yqXzvSxCcoLUdA'
|
| 22 |
bot = Bot(token=BOT_TOKEN)
|
| 23 |
dp = Dispatcher()
|
| 24 |
app = Flask(__name__)
|
| 25 |
|
| 26 |
-
# Путь для хранения данных
|
| 27 |
DATA_FILE = 'data.json'
|
| 28 |
|
| 29 |
# Настройки Hugging Face
|
| 30 |
-
REPO_ID = "flpolprojects/Clients"
|
| 31 |
-
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 32 |
-
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 33 |
|
| 34 |
-
#
|
| 35 |
def load_data():
|
| 36 |
try:
|
| 37 |
download_db_from_hf()
|
| 38 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 39 |
loaded_data = json.load(f)
|
| 40 |
-
# Проверка структуры JSON: ожидается словарь с ключами 'products', 'orders' и 'categories'
|
| 41 |
if not (isinstance(loaded_data, dict) and 'products' in loaded_data and 'orders' in loaded_data):
|
| 42 |
-
logger.error("Неверная структура JSON
|
| 43 |
loaded_data = {'products': [], 'orders': []}
|
| 44 |
if "categories" not in loaded_data:
|
| 45 |
loaded_data["categories"] = []
|
| 46 |
return loaded_data
|
| 47 |
-
except FileNotFoundError:
|
| 48 |
-
logger.warning("Локальный файл базы данных не найден после скачивания.")
|
| 49 |
-
return {'products': [], 'orders': [], 'categories': []}
|
| 50 |
-
except json.JSONDecodeError:
|
| 51 |
-
logger.error("Ошибка при загрузке данных: Невозможно декодировать JSON файл.")
|
| 52 |
-
return {'products': [], 'orders': [], 'categories': []}
|
| 53 |
-
except RepositoryNotFoundError:
|
| 54 |
-
logger.error("Репозиторий не найден. Создание локальной базы данных.")
|
| 55 |
-
return {'products': [], 'orders': [], 'categories': []}
|
| 56 |
except Exception as e:
|
| 57 |
logger.error(f"Ошибка при загрузке данных: {e}")
|
| 58 |
return {'products': [], 'orders': [], 'categories': []}
|
| 59 |
|
| 60 |
-
# Функция для сохранения данных в Hugging Face Hub
|
| 61 |
def save_data(data):
|
| 62 |
try:
|
| 63 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
|
@@ -66,7 +55,6 @@ def save_data(data):
|
|
| 66 |
except Exception as e:
|
| 67 |
logger.error(f"Ошибка при сохранении данных: {e}")
|
| 68 |
|
| 69 |
-
# Функция для загрузки базы данных из Hugging Face Hub
|
| 70 |
def upload_db_to_hf():
|
| 71 |
try:
|
| 72 |
api = HfApi()
|
|
@@ -76,13 +64,12 @@ def upload_db_to_hf():
|
|
| 76 |
repo_id=REPO_ID,
|
| 77 |
repo_type="dataset",
|
| 78 |
token=HF_TOKEN_WRITE,
|
| 79 |
-
commit_message=f"Автоматическое резервное копирование
|
| 80 |
)
|
| 81 |
-
logger.info("
|
| 82 |
except Exception as e:
|
| 83 |
logger.error(f"Ошибка при загрузке резервной копии: {e}")
|
| 84 |
|
| 85 |
-
# Функция для скачивания базы данных из Hugging Face Hub
|
| 86 |
def download_db_from_hf():
|
| 87 |
try:
|
| 88 |
hf_hub_download(
|
|
@@ -93,20 +80,16 @@ def download_db_from_hf():
|
|
| 93 |
local_dir=".",
|
| 94 |
local_dir_use_symlinks=False
|
| 95 |
)
|
| 96 |
-
logger.info("
|
| 97 |
-
except RepositoryNotFoundError as e:
|
| 98 |
-
logger.error(f"Репозиторий не найден: {e}")
|
| 99 |
-
raise
|
| 100 |
except Exception as e:
|
| 101 |
-
logger.error(f"Ошибка при
|
| 102 |
raise
|
| 103 |
|
| 104 |
# Загрузка данных
|
| 105 |
data = load_data()
|
| 106 |
|
| 107 |
-
#
|
| 108 |
def get_main_keyboard():
|
| 109 |
-
# Теперь кнопка "Меню" покажет категории товаров
|
| 110 |
builder = ReplyKeyboardBuilder()
|
| 111 |
builder.button(text="📋 Меню")
|
| 112 |
builder.button(text="🛒 Корзина")
|
|
@@ -118,7 +101,6 @@ def get_category_keyboard():
|
|
| 118 |
builder = InlineKeyboardBuilder()
|
| 119 |
for category in data['categories']:
|
| 120 |
builder.button(text=category['name'], callback_data=f"cat_{category['id']}")
|
| 121 |
-
# Добавляем кнопку "Назад" для возврата в главное меню, если понадобится
|
| 122 |
builder.adjust(2)
|
| 123 |
return builder.as_markup()
|
| 124 |
|
|
@@ -127,12 +109,11 @@ def get_product_keyboard(product_id):
|
|
| 127 |
builder.button(text="Добавить в корзину", callback_data=f"add_{product_id}")
|
| 128 |
return builder.as_markup()
|
| 129 |
|
| 130 |
-
# Обработчики
|
| 131 |
@dp.message(Command("start"))
|
| 132 |
async def cmd_start(message: types.Message):
|
| 133 |
await message.answer("Привет! Я твой бот-магазин. Выбери действие:", reply_markup=get_main_keyboard())
|
| 134 |
|
| 135 |
-
# При нажатии на "Меню" выводятся категории
|
| 136 |
@dp.message(F.text == "📋 Меню")
|
| 137 |
async def show_categories(message: types.Message):
|
| 138 |
if not data['categories']:
|
|
@@ -140,34 +121,43 @@ async def show_categories(message: types.Message):
|
|
| 140 |
return
|
| 141 |
await message.answer("Выберите категорию:", reply_markup=get_category_keyboard())
|
| 142 |
|
| 143 |
-
#
|
| 144 |
@dp.callback_query(F.data.startswith("cat_"))
|
| 145 |
async def show_products_in_category(callback_query: types.CallbackQuery):
|
| 146 |
try:
|
| 147 |
cat_id = int(callback_query.data.split('_')[1])
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
await bot.answer_callback_query(callback_query.id)
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{product['photo']}" if product.get('photo') else None
|
| 161 |
-
caption = f"🏷 {product['name']} - {product['price']} руб.\nОписание: {product['description']}\n/id: {product['id']}"
|
| 162 |
-
if photo_url:
|
| 163 |
-
try:
|
| 164 |
-
await bot.send_photo(chat_id=callback_query.from_user.id, photo=photo_url, caption=caption, reply_markup=get_product_keyboard(product['id']))
|
| 165 |
-
except Exception as e:
|
| 166 |
-
logger.error(f"Ошибка при отправке фото: {e}")
|
| 167 |
-
await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id']))
|
| 168 |
-
else:
|
| 169 |
-
await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id']))
|
| 170 |
-
await bot.answer_callback_query(callback_query.id)
|
| 171 |
|
| 172 |
@dp.message(F.text == "🛒 Корзина")
|
| 173 |
async def show_cart(message: types.Message):
|
|
@@ -180,9 +170,9 @@ async def show_cart(message: types.Message):
|
|
| 180 |
response = "Ваша корзина:\n"
|
| 181 |
for item in cart['items']:
|
| 182 |
product = next(p for p in data['products'] if p['id'] == item['product_id'])
|
| 183 |
-
response += f"🏷 {product['name']} - {product['price']}
|
| 184 |
total += product['price'] * item['quantity']
|
| 185 |
-
response += f"\nИтого: {total}
|
| 186 |
builder = InlineKeyboardBuilder()
|
| 187 |
builder.button(text="Оформить заказ", callback_data=f"complete_{user_id}")
|
| 188 |
await message.answer(response, reply_markup=builder.as_markup())
|
|
@@ -191,55 +181,47 @@ async def show_cart(message: types.Message):
|
|
| 191 |
async def add_to_cart(callback_query: types.CallbackQuery):
|
| 192 |
try:
|
| 193 |
product_id = int(callback_query.data.split('_')[1])
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
await bot.answer_callback_query(callback_query.id, "
|
| 207 |
-
else:
|
| 208 |
-
await bot.answer_callback_query(callback_query.id, "Товар не найден.")
|
| 209 |
|
| 210 |
-
# Изменённый обработчик "Оформить заказ" – заказ сразу отправляется в WhatsApp и удаляется из корзины
|
| 211 |
@dp.callback_query(F.data.startswith("complete_"))
|
| 212 |
async def complete_order(callback_query: types.CallbackQuery):
|
| 213 |
try:
|
| 214 |
user_id = int(callback_query.data.split('_')[1])
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
await bot.send_message(user_id, f"Пожалуйста, оформите заказ через WhatsApp, перейдя по ссылке:\n{whatsapp_link}")
|
| 235 |
-
await bot.answer_callback_query(callback_query.id)
|
| 236 |
-
else:
|
| 237 |
-
await bot.answer_callback_query(callback_query.id, "Корзина пуста или заказ уже оформлен.")
|
| 238 |
|
| 239 |
@dp.message(F.text == "📦 Заказы")
|
| 240 |
async def show_orders(message: types.Message):
|
| 241 |
user_id = message.from_user.id
|
| 242 |
-
# Так как оформленные заказы больше не сохраняются, здесь показываются только те, которые остались (если вдруг)
|
| 243 |
user_orders = [o for o in data['orders'] if o.get('completed')]
|
| 244 |
if not user_orders:
|
| 245 |
await message.answer("У вас нет оформленных заказов.")
|
|
@@ -249,26 +231,84 @@ async def show_orders(message: types.Message):
|
|
| 249 |
total = 0
|
| 250 |
for item in order['items']:
|
| 251 |
product = next(p for p in data['products'] if p['id'] == item['product_id'])
|
| 252 |
-
response += f"🏷 {product['name']} - {product['price']}
|
| 253 |
total += product['price'] * item['quantity']
|
| 254 |
-
response += f"\nИтого: {total}
|
| 255 |
await message.answer(response)
|
| 256 |
|
| 257 |
-
# Админ-панель
|
| 258 |
admin_html = """
|
| 259 |
<!DOCTYPE html>
|
| 260 |
<html>
|
| 261 |
<head>
|
| 262 |
<title>Админ-панель</title>
|
|
|
|
| 263 |
<style>
|
| 264 |
-
body {
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
</style>
|
| 273 |
</head>
|
| 274 |
<body>
|
|
@@ -277,7 +317,7 @@ admin_html = """
|
|
| 277 |
<div class="section">
|
| 278 |
<h2>Управление категориями</h2>
|
| 279 |
<form id="addCategoryForm" method="POST" action="/add_category">
|
| 280 |
-
<input type="text" name="name" placeholder="Название категории" required
|
| 281 |
<button type="submit">Добавить категорию</button>
|
| 282 |
</form>
|
| 283 |
<h3>Существующие категории</h3>
|
|
@@ -295,24 +335,24 @@ admin_html = """
|
|
| 295 |
<div class="section">
|
| 296 |
<h2>Управление товарами</h2>
|
| 297 |
<form id="addProductForm" method="POST" enctype="multipart/form-data" action="/add_product">
|
| 298 |
-
<input type="text" name="name" placeholder="Название" required
|
| 299 |
-
<input type="number" name="price" placeholder="Цена" step="0.01" required
|
| 300 |
-
<textarea name="description" placeholder="Описание" required></textarea
|
| 301 |
<label>Категория:</label>
|
| 302 |
<select name="category_id" required>
|
| 303 |
<option value="">Выберите категорию</option>
|
| 304 |
{% for category in categories %}
|
| 305 |
<option value="{{ category.id }}">{{ category.name }}</option>
|
| 306 |
{% endfor %}
|
| 307 |
-
</select
|
| 308 |
-
<input type="file" name="photo" accept="image/*"
|
| 309 |
<button type="submit">Добавить товар</button>
|
| 310 |
</form>
|
| 311 |
<h3>Существующие товары</h3>
|
| 312 |
{% if products %}
|
| 313 |
{% for product in products %}
|
| 314 |
<div class="item">
|
| 315 |
-
{{ product.name }} - {{ product.price }}
|
| 316 |
{{ product.description }}<br>
|
| 317 |
{% if product.photo %}
|
| 318 |
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photo }}" alt="{{ product.name }}">
|
|
@@ -331,7 +371,7 @@ admin_html = """
|
|
| 331 |
<div class="item">
|
| 332 |
Пользователь: {{ order.user_id }}<br>
|
| 333 |
Дата: {{ order.date }}<br>
|
| 334 |
-
Товары:
|
| 335 |
{% for item in order['items'] %}
|
| 336 |
{% for product in products %}
|
| 337 |
{% if product.id == item.product_id %}
|
|
@@ -347,73 +387,81 @@ admin_html = """
|
|
| 347 |
</div>
|
| 348 |
</div>
|
| 349 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
async function deleteProduct(productId) {
|
| 351 |
const response = await fetch(`/delete_product/${productId}`, { method: 'POST' });
|
| 352 |
-
if (response.ok)
|
| 353 |
}
|
| 354 |
async function deleteCategory(categoryId) {
|
| 355 |
const response = await fetch(`/delete_category/${categoryId}`, { method: 'POST' });
|
| 356 |
-
if (response.ok)
|
|
|
|
|
|
|
|
|
|
| 357 |
}
|
| 358 |
</script>
|
| 359 |
</body>
|
| 360 |
</html>
|
| 361 |
"""
|
| 362 |
|
|
|
|
|
|
|
|
|
|
| 363 |
@app.route('/')
|
| 364 |
def admin_panel():
|
| 365 |
try:
|
| 366 |
-
logger.info("Rendering admin panel with products, orders and categories")
|
| 367 |
return render_template_string(admin_html, products=data['products'], orders=data['orders'], categories=data['categories'], repo_id=REPO_ID)
|
| 368 |
except Exception as e:
|
| 369 |
logger.error(f"Ошибка в шаблоне: {e}")
|
| 370 |
-
return "Ошибка
|
| 371 |
|
| 372 |
@app.route('/add_product', methods=['POST'])
|
| 373 |
def add_product():
|
| 374 |
try:
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
photo_filename =
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
'name': name,
|
| 410 |
-
'price': price,
|
| 411 |
-
'description': description,
|
| 412 |
-
'category_id': category_id,
|
| 413 |
-
'photo': photo_filename
|
| 414 |
-
})
|
| 415 |
-
save_data(data)
|
| 416 |
-
return redirect("/")
|
| 417 |
except Exception as e:
|
| 418 |
logger.error(f"Ошибка при добавлении товара: {e}")
|
| 419 |
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
@@ -421,9 +469,9 @@ def add_product():
|
|
| 421 |
@app.route('/delete_product/<int:product_id>', methods=['POST'])
|
| 422 |
def delete_product(product_id):
|
| 423 |
try:
|
| 424 |
-
logger.info(f"Deleting product with id={product_id}")
|
| 425 |
data['products'] = [p for p in data['products'] if p['id'] != product_id]
|
| 426 |
save_data(data)
|
|
|
|
| 427 |
return jsonify({'status': 'success'})
|
| 428 |
except Exception as e:
|
| 429 |
logger.error(f"Ошибка при удалении товара: {e}")
|
|
@@ -436,6 +484,7 @@ def add_category():
|
|
| 436 |
category_id = max((c['id'] for c in data['categories']), default=0) + 1
|
| 437 |
data['categories'].append({'id': category_id, 'name': name})
|
| 438 |
save_data(data)
|
|
|
|
| 439 |
return redirect("/")
|
| 440 |
except Exception as e:
|
| 441 |
logger.error(f"Ошибка при добавлении категории: {e}")
|
|
@@ -444,35 +493,42 @@ def add_category():
|
|
| 444 |
@app.route('/delete_category/<int:category_id>', methods=['POST'])
|
| 445 |
def delete_category(category_id):
|
| 446 |
try:
|
| 447 |
-
logger.info(f"Deleting category with id={category_id}")
|
| 448 |
data['categories'] = [c for c in data['categories'] if c['id'] != category_id]
|
| 449 |
save_data(data)
|
|
|
|
| 450 |
return jsonify({'status': 'success'})
|
| 451 |
except Exception as e:
|
| 452 |
logger.error(f"Ошибка при удалении категории: {e}")
|
| 453 |
return jsonify({'status': 'error', 'message': str(e)}), 500
|
| 454 |
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
async def on_startup(_):
|
| 457 |
logger.info("Бот запущен!")
|
| 458 |
|
| 459 |
def run_flask():
|
| 460 |
-
|
| 461 |
-
logger.info("Starting Flask server on port 7860")
|
| 462 |
-
app.run(host='0.0.0.0', port=7860, debug=True, use_reloader=False)
|
| 463 |
-
except Exception as e:
|
| 464 |
-
logger.error(f"Ошибка в Flask: {e}")
|
| 465 |
|
| 466 |
if __name__ == '__main__':
|
| 467 |
-
# Создаём и запускаем поток для Flask
|
| 468 |
flask_thread = threading.Thread(target=run_flask, daemon=True)
|
| 469 |
flask_thread.start()
|
| 470 |
-
logger.info("Flask
|
| 471 |
-
|
| 472 |
-
# Запускаем бота в главном потоке
|
| 473 |
try:
|
| 474 |
asyncio.run(dp.start_polling(bot, on_startup=on_startup))
|
| 475 |
except KeyboardInterrupt:
|
| 476 |
-
logger.info("
|
| 477 |
finally:
|
| 478 |
flask_thread.join()
|
|
|
|
| 18 |
logger = logging.getLogger(__name__)
|
| 19 |
|
| 20 |
# Инициализация бота и Flask
|
| 21 |
+
BOT_TOKEN = '7734802681:AAGKHGG8O9uNk64JWTHH5yqXzvSxCcoLUdA'
|
| 22 |
bot = Bot(token=BOT_TOKEN)
|
| 23 |
dp = Dispatcher()
|
| 24 |
app = Flask(__name__)
|
| 25 |
|
| 26 |
+
# Путь для хранения данных
|
| 27 |
DATA_FILE = 'data.json'
|
| 28 |
|
| 29 |
# Настройки Hugging Face
|
| 30 |
+
REPO_ID = "flpolprojects/Clients"
|
| 31 |
+
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 32 |
+
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 33 |
|
| 34 |
+
# Функции для работы с данными
|
| 35 |
def load_data():
|
| 36 |
try:
|
| 37 |
download_db_from_hf()
|
| 38 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 39 |
loaded_data = json.load(f)
|
|
|
|
| 40 |
if not (isinstance(loaded_data, dict) and 'products' in loaded_data and 'orders' in loaded_data):
|
| 41 |
+
logger.error("Неверная структура JSON файла")
|
| 42 |
loaded_data = {'products': [], 'orders': []}
|
| 43 |
if "categories" not in loaded_data:
|
| 44 |
loaded_data["categories"] = []
|
| 45 |
return loaded_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
except Exception as e:
|
| 47 |
logger.error(f"Ошибка при загрузке данных: {e}")
|
| 48 |
return {'products': [], 'orders': [], 'categories': []}
|
| 49 |
|
|
|
|
| 50 |
def save_data(data):
|
| 51 |
try:
|
| 52 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
|
|
|
| 55 |
except Exception as e:
|
| 56 |
logger.error(f"Ошибка при сохранении данных: {e}")
|
| 57 |
|
|
|
|
| 58 |
def upload_db_to_hf():
|
| 59 |
try:
|
| 60 |
api = HfApi()
|
|
|
|
| 64 |
repo_id=REPO_ID,
|
| 65 |
repo_type="dataset",
|
| 66 |
token=HF_TOKEN_WRITE,
|
| 67 |
+
commit_message=f"Автоматическое резервное копирование {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 68 |
)
|
| 69 |
+
logger.info("База загружена на Hugging Face")
|
| 70 |
except Exception as e:
|
| 71 |
logger.error(f"Ошибка при загрузке резервной копии: {e}")
|
| 72 |
|
|
|
|
| 73 |
def download_db_from_hf():
|
| 74 |
try:
|
| 75 |
hf_hub_download(
|
|
|
|
| 80 |
local_dir=".",
|
| 81 |
local_dir_use_symlinks=False
|
| 82 |
)
|
| 83 |
+
logger.info("База скачана из Hugging Face")
|
|
|
|
|
|
|
|
|
|
| 84 |
except Exception as e:
|
| 85 |
+
logger.error(f"Ошибка при скачивании: {e}")
|
| 86 |
raise
|
| 87 |
|
| 88 |
# Загрузка данных
|
| 89 |
data = load_data()
|
| 90 |
|
| 91 |
+
# Формирование кла��иатур
|
| 92 |
def get_main_keyboard():
|
|
|
|
| 93 |
builder = ReplyKeyboardBuilder()
|
| 94 |
builder.button(text="📋 Меню")
|
| 95 |
builder.button(text="🛒 Корзина")
|
|
|
|
| 101 |
builder = InlineKeyboardBuilder()
|
| 102 |
for category in data['categories']:
|
| 103 |
builder.button(text=category['name'], callback_data=f"cat_{category['id']}")
|
|
|
|
| 104 |
builder.adjust(2)
|
| 105 |
return builder.as_markup()
|
| 106 |
|
|
|
|
| 109 |
builder.button(text="Добавить в корзину", callback_data=f"add_{product_id}")
|
| 110 |
return builder.as_markup()
|
| 111 |
|
| 112 |
+
# Обработчики бота
|
| 113 |
@dp.message(Command("start"))
|
| 114 |
async def cmd_start(message: types.Message):
|
| 115 |
await message.answer("Привет! Я твой бот-магазин. Выбери действие:", reply_markup=get_main_keyboard())
|
| 116 |
|
|
|
|
| 117 |
@dp.message(F.text == "📋 Меню")
|
| 118 |
async def show_categories(message: types.Message):
|
| 119 |
if not data['categories']:
|
|
|
|
| 121 |
return
|
| 122 |
await message.answer("Выберите категорию:", reply_markup=get_category_keyboard())
|
| 123 |
|
| 124 |
+
# Оптимизированный вывод товаров
|
| 125 |
@dp.callback_query(F.data.startswith("cat_"))
|
| 126 |
async def show_products_in_category(callback_query: types.CallbackQuery):
|
| 127 |
try:
|
| 128 |
cat_id = int(callback_query.data.split('_')[1])
|
| 129 |
+
products_in_cat = [p for p in data['products'] if p.get('category_id') == cat_id]
|
| 130 |
+
|
| 131 |
+
if not products_in_cat:
|
| 132 |
+
await bot.send_message(callback_query.from_user.id, "В этой категории нет товаров.")
|
| 133 |
+
await bot.answer_callback_query(callback_query.id)
|
| 134 |
+
return
|
| 135 |
+
|
| 136 |
+
# Отправка товаров пачками асинхронно
|
| 137 |
+
async def send_product_batch(products_batch):
|
| 138 |
+
for product in products_batch:
|
| 139 |
+
photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{product['photo']}" if product.get('photo') else None
|
| 140 |
+
caption = f"🏷 {product['name']} - {product['price']} сом\nОписание: {product['description']}\n/id: {product['id']}"
|
| 141 |
+
try:
|
| 142 |
+
if photo_url:
|
| 143 |
+
await bot.send_photo(chat_id=callback_query.from_user.id, photo=photo_url, caption=caption, reply_markup=get_product_keyboard(product['id']))
|
| 144 |
+
else:
|
| 145 |
+
await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id']))
|
| 146 |
+
except Exception as e:
|
| 147 |
+
logger.error(f"Ошибка при отправке: {e}")
|
| 148 |
+
await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id']))
|
| 149 |
+
|
| 150 |
+
# Разбиваем на пачки по 5 товаров
|
| 151 |
+
batch_size = 5
|
| 152 |
+
for i in range(0, len(products_in_cat), batch_size):
|
| 153 |
+
batch = products_in_cat[i:i + batch_size]
|
| 154 |
+
await send_product_batch(batch)
|
| 155 |
+
await asyncio.sleep(0.1) # Небольшая задержка между пачками
|
| 156 |
+
|
| 157 |
await bot.answer_callback_query(callback_query.id)
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"Ошибка в show_products_in_category: {e}")
|
| 160 |
+
await bot.answer_callback_query(callback_query.id, "Ошибка при загрузке товаров")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
@dp.message(F.text == "🛒 Корзина")
|
| 163 |
async def show_cart(message: types.Message):
|
|
|
|
| 170 |
response = "Ваша корзина:\n"
|
| 171 |
for item in cart['items']:
|
| 172 |
product = next(p for p in data['products'] if p['id'] == item['product_id'])
|
| 173 |
+
response += f"🏷 {product['name']} - {product['price']} сом x {item['quantity']}\n"
|
| 174 |
total += product['price'] * item['quantity']
|
| 175 |
+
response += f"\nИтого: {total} сом"
|
| 176 |
builder = InlineKeyboardBuilder()
|
| 177 |
builder.button(text="Оформить заказ", callback_data=f"complete_{user_id}")
|
| 178 |
await message.answer(response, reply_markup=builder.as_markup())
|
|
|
|
| 181 |
async def add_to_cart(callback_query: types.CallbackQuery):
|
| 182 |
try:
|
| 183 |
product_id = int(callback_query.data.split('_')[1])
|
| 184 |
+
product = next((p for p in data['products'] if p['id'] == product_id), None)
|
| 185 |
+
if product:
|
| 186 |
+
user_id = callback_query.from_user.id
|
| 187 |
+
cart = next((o for o in data['orders'] if o['user_id'] == user_id and not o.get('completed')), None)
|
| 188 |
+
if not cart:
|
| 189 |
+
cart = {'user_id': user_id, 'items': [], 'date': datetime.now().isoformat()}
|
| 190 |
+
data['orders'].append(cart)
|
| 191 |
+
cart['items'].append({'product_id': product_id, 'quantity': 1})
|
| 192 |
+
save_data(data)
|
| 193 |
+
await bot.answer_callback_query(callback_query.id, "Товар добавлен в корзину!")
|
| 194 |
+
except Exception as e:
|
| 195 |
+
logger.error(f"Ошибка при добавлении в корзину: {e}")
|
| 196 |
+
await bot.answer_callback_query(callback_query.id, "Ошибка")
|
|
|
|
|
|
|
| 197 |
|
|
|
|
| 198 |
@dp.callback_query(F.data.startswith("complete_"))
|
| 199 |
async def complete_order(callback_query: types.CallbackQuery):
|
| 200 |
try:
|
| 201 |
user_id = int(callback_query.data.split('_')[1])
|
| 202 |
+
cart = next((o for o in data['orders'] if o['user_id'] == user_id and not o.get('completed')), None)
|
| 203 |
+
if cart and cart['items']:
|
| 204 |
+
total = 0
|
| 205 |
+
cart_text = "Привет, я хочу сделать заказ:\n"
|
| 206 |
+
for item in cart['items']:
|
| 207 |
+
product = next((p for p in data['products'] if p['id'] == item['product_id']), None)
|
| 208 |
+
if product:
|
| 209 |
+
cart_text += f"{product['name']} - {product['price']} сом x {item['quantity']}\n"
|
| 210 |
+
total += product['price'] * item['quantity']
|
| 211 |
+
cart_text += f"\nИтого: {total} сом"
|
| 212 |
+
encoded_text = urllib.parse.quote(cart_text)
|
| 213 |
+
whatsapp_link = f"https://wa.me/996709513331?text={encoded_text}"
|
| 214 |
+
data['orders'].remove(cart)
|
| 215 |
+
save_data(data)
|
| 216 |
+
await bot.send_message(user_id, f"Оформите заказ через WhatsApp:\n{whatsapp_link}")
|
| 217 |
+
await bot.answer_callback_query(callback_query.id)
|
| 218 |
+
except Exception as e:
|
| 219 |
+
logger.error(f"Ошибка при оформлении заказа: {e}")
|
| 220 |
+
await bot.answer_callback_query(callback_query.id, "Ошибка")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
@dp.message(F.text == "📦 Заказы")
|
| 223 |
async def show_orders(message: types.Message):
|
| 224 |
user_id = message.from_user.id
|
|
|
|
| 225 |
user_orders = [o for o in data['orders'] if o.get('completed')]
|
| 226 |
if not user_orders:
|
| 227 |
await message.answer("У вас нет оформленных заказов.")
|
|
|
|
| 231 |
total = 0
|
| 232 |
for item in order['items']:
|
| 233 |
product = next(p for p in data['products'] if p['id'] == item['product_id'])
|
| 234 |
+
response += f"🏷 {product['name']} - {product['price']} сом x {item['quantity']}\n"
|
| 235 |
total += product['price'] * item['quantity']
|
| 236 |
+
response += f"\nИтого: {total} сом\nДата: {order['date']}"
|
| 237 |
await message.answer(response)
|
| 238 |
|
| 239 |
+
# Админ-панель с адаптивным дизайном
|
| 240 |
admin_html = """
|
| 241 |
<!DOCTYPE html>
|
| 242 |
<html>
|
| 243 |
<head>
|
| 244 |
<title>Админ-панель</title>
|
| 245 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 246 |
<style>
|
| 247 |
+
body {
|
| 248 |
+
font-family: Arial, sans-serif;
|
| 249 |
+
margin: 10px;
|
| 250 |
+
background-color: #f0f0f0;
|
| 251 |
+
}
|
| 252 |
+
.container {
|
| 253 |
+
max-width: 1000px;
|
| 254 |
+
margin: 0 auto;
|
| 255 |
+
}
|
| 256 |
+
.section {
|
| 257 |
+
background-color: #fff;
|
| 258 |
+
padding: 10px;
|
| 259 |
+
margin-bottom: 15px;
|
| 260 |
+
border-radius: 5px;
|
| 261 |
+
}
|
| 262 |
+
h1, h2, h3 {
|
| 263 |
+
margin: 10px 0;
|
| 264 |
+
}
|
| 265 |
+
input, textarea, select {
|
| 266 |
+
width: 100%;
|
| 267 |
+
margin: 5px 0;
|
| 268 |
+
padding: 8px;
|
| 269 |
+
box-sizing: border-box;
|
| 270 |
+
}
|
| 271 |
+
button {
|
| 272 |
+
background-color: #4CAF50;
|
| 273 |
+
color: white;
|
| 274 |
+
padding: 8px 12px;
|
| 275 |
+
border: none;
|
| 276 |
+
border-radius: 5px;
|
| 277 |
+
cursor: pointer;
|
| 278 |
+
width: 100%;
|
| 279 |
+
margin: 5px 0;
|
| 280 |
+
}
|
| 281 |
+
button:hover {
|
| 282 |
+
background-color: #45a049;
|
| 283 |
+
}
|
| 284 |
+
img {
|
| 285 |
+
max-width: 100%;
|
| 286 |
+
height: auto;
|
| 287 |
+
max-height: 150px;
|
| 288 |
+
}
|
| 289 |
+
.item {
|
| 290 |
+
border: 1px solid #ccc;
|
| 291 |
+
padding: 10px;
|
| 292 |
+
margin: 5px 0;
|
| 293 |
+
word-wrap: break-word;
|
| 294 |
+
}
|
| 295 |
+
@media (max-width: 600px) {
|
| 296 |
+
.section {
|
| 297 |
+
padding: 8px;
|
| 298 |
+
}
|
| 299 |
+
button {
|
| 300 |
+
padding: 6px 10px;
|
| 301 |
+
}
|
| 302 |
+
h1 {
|
| 303 |
+
font-size: 1.5em;
|
| 304 |
+
}
|
| 305 |
+
h2 {
|
| 306 |
+
font-size: 1.2em;
|
| 307 |
+
}
|
| 308 |
+
h3 {
|
| 309 |
+
font-size: 1em;
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
</style>
|
| 313 |
</head>
|
| 314 |
<body>
|
|
|
|
| 317 |
<div class="section">
|
| 318 |
<h2>Управление категориями</h2>
|
| 319 |
<form id="addCategoryForm" method="POST" action="/add_category">
|
| 320 |
+
<input type="text" name="name" placeholder="Название категории" required>
|
| 321 |
<button type="submit">Добавить категорию</button>
|
| 322 |
</form>
|
| 323 |
<h3>Существующие категории</h3>
|
|
|
|
| 335 |
<div class="section">
|
| 336 |
<h2>Управление товарами</h2>
|
| 337 |
<form id="addProductForm" method="POST" enctype="multipart/form-data" action="/add_product">
|
| 338 |
+
<input type="text" name="name" placeholder="Название" required>
|
| 339 |
+
<input type="number" name="price" placeholder="Цена" step="0.01" required>
|
| 340 |
+
<textarea name="description" placeholder="Описание" required></textarea>
|
| 341 |
<label>Категория:</label>
|
| 342 |
<select name="category_id" required>
|
| 343 |
<option value="">Выберите категорию</option>
|
| 344 |
{% for category in categories %}
|
| 345 |
<option value="{{ category.id }}">{{ category.name }}</option>
|
| 346 |
{% endfor %}
|
| 347 |
+
</select>
|
| 348 |
+
<input type="file" name="photo" accept="image/*">
|
| 349 |
<button type="submit">Добавить товар</button>
|
| 350 |
</form>
|
| 351 |
<h3>Существующие товары</h3>
|
| 352 |
{% if products %}
|
| 353 |
{% for product in products %}
|
| 354 |
<div class="item">
|
| 355 |
+
{{ product.name }} - {{ product.price }} сом<br>
|
| 356 |
{{ product.description }}<br>
|
| 357 |
{% if product.photo %}
|
| 358 |
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photo }}" alt="{{ product.name }}">
|
|
|
|
| 371 |
<div class="item">
|
| 372 |
Пользователь: {{ order.user_id }}<br>
|
| 373 |
Дата: {{ order.date }}<br>
|
| 374 |
+
Товары:
|
| 375 |
{% for item in order['items'] %}
|
| 376 |
{% for product in products %}
|
| 377 |
{% if product.id == item.product_id %}
|
|
|
|
| 387 |
</div>
|
| 388 |
</div>
|
| 389 |
<script>
|
| 390 |
+
// Автоматическое обновление через Server-Sent Events
|
| 391 |
+
const eventSource = new EventSource('/updates');
|
| 392 |
+
eventSource.onmessage = function(event) {
|
| 393 |
+
if (event.data === 'update') {
|
| 394 |
+
window.location.reload();
|
| 395 |
+
}
|
| 396 |
+
};
|
| 397 |
+
eventSource.onerror = function() {
|
| 398 |
+
console.log("Ошибка SSE, reconnecting...");
|
| 399 |
+
};
|
| 400 |
+
|
| 401 |
async function deleteProduct(productId) {
|
| 402 |
const response = await fetch(`/delete_product/${productId}`, { method: 'POST' });
|
| 403 |
+
if (response.ok) broadcastUpdate();
|
| 404 |
}
|
| 405 |
async function deleteCategory(categoryId) {
|
| 406 |
const response = await fetch(`/delete_category/${categoryId}`, { method: 'POST' });
|
| 407 |
+
if (response.ok) broadcastUpdate();
|
| 408 |
+
}
|
| 409 |
+
function broadcastUpdate() {
|
| 410 |
+
fetch('/broadcast_update', { method: 'POST' });
|
| 411 |
}
|
| 412 |
</script>
|
| 413 |
</body>
|
| 414 |
</html>
|
| 415 |
"""
|
| 416 |
|
| 417 |
+
# Глобальная переменная для отслеживания обновлений
|
| 418 |
+
update_event = threading.Event()
|
| 419 |
+
|
| 420 |
@app.route('/')
|
| 421 |
def admin_panel():
|
| 422 |
try:
|
|
|
|
| 423 |
return render_template_string(admin_html, products=data['products'], orders=data['orders'], categories=data['categories'], repo_id=REPO_ID)
|
| 424 |
except Exception as e:
|
| 425 |
logger.error(f"Ошибка в шаблоне: {e}")
|
| 426 |
+
return "Ошибка сервера", 500
|
| 427 |
|
| 428 |
@app.route('/add_product', methods=['POST'])
|
| 429 |
def add_product():
|
| 430 |
try:
|
| 431 |
+
name = request.form['name']
|
| 432 |
+
price = float(request.form['price'])
|
| 433 |
+
description = request.form['description']
|
| 434 |
+
category_id = int(request.form['category_id'])
|
| 435 |
+
photo = request.files.get('photo')
|
| 436 |
+
product_id = max((p['id'] for p in data['products']), default=0) + 1
|
| 437 |
+
|
| 438 |
+
photo_filename = None
|
| 439 |
+
if photo and photo.filename:
|
| 440 |
+
photo_filename = secure_filename(photo.filename)
|
| 441 |
+
temp_path = os.path.join(".", photo_filename)
|
| 442 |
+
photo.save(temp_path)
|
| 443 |
+
api = HfApi()
|
| 444 |
+
api.upload_file(
|
| 445 |
+
path_or_fileobj=temp_path,
|
| 446 |
+
path_in_repo=f"photos/{photo_filename}",
|
| 447 |
+
repo_id=REPO_ID,
|
| 448 |
+
repo_type="dataset",
|
| 449 |
+
token=HF_TOKEN_WRITE,
|
| 450 |
+
commit_message=f"Добавлено фото для товара {name}"
|
| 451 |
+
)
|
| 452 |
+
os.remove(temp_path)
|
| 453 |
+
|
| 454 |
+
data['products'].append({
|
| 455 |
+
'id': product_id,
|
| 456 |
+
'name': name,
|
| 457 |
+
'price': price,
|
| 458 |
+
'description': description,
|
| 459 |
+
'category_id': category_id,
|
| 460 |
+
'photo': photo_filename
|
| 461 |
+
})
|
| 462 |
+
save_data(data)
|
| 463 |
+
update_event.set() # Сигнал об обновлении
|
| 464 |
+
return redirect("/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
except Exception as e:
|
| 466 |
logger.error(f"Ошибка при добавлении товара: {e}")
|
| 467 |
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
| 469 |
@app.route('/delete_product/<int:product_id>', methods=['POST'])
|
| 470 |
def delete_product(product_id):
|
| 471 |
try:
|
|
|
|
| 472 |
data['products'] = [p for p in data['products'] if p['id'] != product_id]
|
| 473 |
save_data(data)
|
| 474 |
+
update_event.set()
|
| 475 |
return jsonify({'status': 'success'})
|
| 476 |
except Exception as e:
|
| 477 |
logger.error(f"Ошибка при удалении товара: {e}")
|
|
|
|
| 484 |
category_id = max((c['id'] for c in data['categories']), default=0) + 1
|
| 485 |
data['categories'].append({'id': category_id, 'name': name})
|
| 486 |
save_data(data)
|
| 487 |
+
update_event.set()
|
| 488 |
return redirect("/")
|
| 489 |
except Exception as e:
|
| 490 |
logger.error(f"Ошибка при добавлении категории: {e}")
|
|
|
|
| 493 |
@app.route('/delete_category/<int:category_id>', methods=['POST'])
|
| 494 |
def delete_category(category_id):
|
| 495 |
try:
|
|
|
|
| 496 |
data['categories'] = [c for c in data['categories'] if c['id'] != category_id]
|
| 497 |
save_data(data)
|
| 498 |
+
update_event.set()
|
| 499 |
return jsonify({'status': 'success'})
|
| 500 |
except Exception as e:
|
| 501 |
logger.error(f"Ошибка при удалении категории: {e}")
|
| 502 |
return jsonify({'status': 'error', 'message': str(e)}), 500
|
| 503 |
|
| 504 |
+
@app.route('/updates')
|
| 505 |
+
def sse_updates():
|
| 506 |
+
def stream():
|
| 507 |
+
while True:
|
| 508 |
+
update_event.wait()
|
| 509 |
+
yield "data: update\n\n"
|
| 510 |
+
update_event.clear()
|
| 511 |
+
return app.response_class(stream(), mimetype="text/event-stream")
|
| 512 |
+
|
| 513 |
+
@app.route('/broadcast_update', methods=['POST'])
|
| 514 |
+
def broadcast_update():
|
| 515 |
+
update_event.set()
|
| 516 |
+
return jsonify({'status': 'success'})
|
| 517 |
+
|
| 518 |
+
# Запуск
|
| 519 |
async def on_startup(_):
|
| 520 |
logger.info("Бот запущен!")
|
| 521 |
|
| 522 |
def run_flask():
|
| 523 |
+
app.run(host='0.0.0.0', port=7860, debug=True, use_reloader=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
|
| 525 |
if __name__ == '__main__':
|
|
|
|
| 526 |
flask_thread = threading.Thread(target=run_flask, daemon=True)
|
| 527 |
flask_thread.start()
|
| 528 |
+
logger.info("Flask запущен")
|
|
|
|
|
|
|
| 529 |
try:
|
| 530 |
asyncio.run(dp.start_polling(bot, on_startup=on_startup))
|
| 531 |
except KeyboardInterrupt:
|
| 532 |
+
logger.info("Остановка")
|
| 533 |
finally:
|
| 534 |
flask_thread.join()
|