import flask from flask import Flask, render_template_string, request, redirect, url_for, jsonify, send_from_directory import json import os import logging import threading import time import uuid from datetime import datetime from huggingface_hub import HfApi, hf_hub_download, list_repo_files from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError from werkzeug.utils import secure_filename import io import urllib.parse app = Flask(__name__) DATA_FILE = 'data_360_panorama.json' PANORAMA_UPLOADS = 'uploads/panoramas' PRODUCT_PHOTO_UPLOADS = 'uploads/product_photos' os.makedirs(PANORAMA_UPLOADS, exist_ok=True) os.makedirs(PRODUCT_PHOTO_UPLOADS, exist_ok=True) REPO_ID = "Kgshop/360testb" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') app_data = {'points': [], 'categories': [], 'standalone_products': [], 'whatsapp_number': ''} def generate_id(): return str(uuid.uuid4()) def get_hf_image_url(repo_id, file_path_in_repo): if not file_path_in_repo: return None if file_path_in_repo.startswith('/'): file_path_in_repo = file_path_in_repo[1:] return f"https://huggingface.co/datasets/{repo_id}/resolve/main/{file_path_in_repo}" def upload_file_to_hf(local_path, path_in_repo, repo_id, token, commit_message): if not token: logging.error("Hugging Face write token (HF_TOKEN) не установлен. Загрузка пропущена.") return False try: api = HfApi() api.upload_file( path_or_fileobj=local_path, path_in_repo=path_in_repo, repo_id=repo_id, repo_type="dataset", token=token, commit_message=commit_message ) logging.info(f"Успешно загружен {local_path} в {repo_id}/{path_in_repo}") return True except Exception as e: logging.error(f"Ошибка загрузки {local_path} в Hugging Face: {e}") return False def delete_file_from_hf(path_in_repo, repo_id, token, commit_message): if not token: logging.error("Hugging Face write token (HF_TOKEN) не установлен. Удаление пропущено.") return False if not path_in_repo: logging.warning("Попытка удалить файл из HF с пустым путем.") return True try: api = HfApi() api.delete_file( path_in_repo=path_in_repo, repo_id=repo_id, repo_type="dataset", token=token, commit_message=commit_message ) logging.info(f"Успешно удален {path_in_repo} из {repo_id}") return True except EntryNotFoundError: logging.warning(f"Файл {path_in_repo} не найден в репозитории {repo_id} для удаления.") return True except Exception as e: logging.error(f"Ошибка удаления {path_in_repo} из Hugging Face: {e}") return False def upload_db_to_hf(): if not HF_TOKEN_WRITE: logging.error("Hugging Face write token (HF_TOKEN) не установлен. Загрузка БД пропущена.") return try: api = HfApi() db_content = json.dumps(app_data, ensure_ascii=False, indent=4).encode('utf-8') db_stream = io.BytesIO(db_content) api.upload_file( path_or_fileobj=db_stream, 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')}" ) logging.info(f"База данных {DATA_FILE} успешно загружена в Hugging Face.") except Exception as e: logging.error(f"Ошибка загрузки базы данных в Hugging Face: {e}") def download_db_from_hf(): global app_data default_data = {'points': [], 'categories': [], 'standalone_products': [], 'whatsapp_number': ''} if not HF_TOKEN_READ: logging.error("Hugging Face read token не установлен. Невозможно скачать БД.") try: with open(DATA_FILE, 'r', encoding='utf-8') as f: app_data = json.load(f) logging.info(f"Загружены данные из локального файла {DATA_FILE} из-за отсутствия read token.") app_data.setdefault('standalone_products', []) app_data.setdefault('whatsapp_number', '') except FileNotFoundError: logging.warning(f"Локальный файл {DATA_FILE} не найден и read token отсутствует. Запуск с пустыми данными.") app_data = default_data except json.JSONDecodeError: logging.error(f"Ошибка декодирования локального JSON файла {DATA_FILE}. Запуск с пустыми данными.") app_data = default_data return try: local_path = hf_hub_download( repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False ) with open(local_path, 'r', encoding='utf-8') as file: app_data = json.load(file) if not isinstance(app_data, dict): raise ValueError("Данные не являются словарем") if 'points' not in app_data or not isinstance(app_data['points'], list): app_data['points'] = [] logging.warning("Отсутствует или неверный ключ 'points' в загруженных данных, инициализирован.") if 'categories' not in app_data or not isinstance(app_data['categories'], list): app_data['categories'] = [] logging.warning("Отсутствует или неверный ключ 'categories' в загруженных данных, инициализирован.") if 'standalone_products' not in app_data or not isinstance(app_data['standalone_products'], list): app_data['standalone_products'] = [] logging.warning("Отсутствует или неверный ключ 'standalone_products' в загруженных данных, инициализирован.") if 'whatsapp_number' not in app_data: app_data['whatsapp_number'] = '' logging.warning("Отсутствует ключ 'whatsapp_number' в загруженных данных, инициализирован.") logging.info(f"База данных {DATA_FILE} успешно скачана и загружена из Hugging Face.") except RepositoryNotFoundError: logging.error(f"Репозиторий Hugging Face {REPO_ID} не найден. Запуск с пустыми данными.") app_data = default_data except EntryNotFoundError: logging.warning(f"Файл базы данных {DATA_FILE} не найден в репозитории {REPO_ID}. Запуск с пустыми данными.") app_data = default_data except json.JSONDecodeError: logging.error(f"Ошибка декодирования JSON из скачанного файла {DATA_FILE}. Запуск с пустыми данными.") app_data = default_data except Exception as e: logging.error(f"Произошла непредвиденная ошибка при скачивании базы данных: {e}") app_data = default_data def save_data(): global app_data try: app_data.setdefault('points', []) app_data.setdefault('categories', []) app_data.setdefault('standalone_products', []) app_data.setdefault('whatsapp_number', '') with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(app_data, file, ensure_ascii=False, indent=4) logging.info(f"Данные успешно сохранены локально в {DATA_FILE}") upload_db_to_hf() except Exception as e: logging.error(f"Ошибка сохранения данных: {e}") def periodic_backup(): while True: time.sleep(800) logging.info("Выполнение периодического резервного копирования базы данных...") save_data() def find_point(point_id): return next((p for p in app_data.get('points', []) if p.get('id') == point_id), None) def find_category(category_id): return next((c for c in app_data.get('categories', []) if c.get('id') == category_id), None) def find_product_in_category(category_id, product_id): category = find_category(category_id) if category: return next((p for p in category.get('products', []) if p.get('id') == product_id), None) return None def find_standalone_product(product_id): return next((p for p in app_data.get('standalone_products', []) if p.get('id') == product_id), None) def delete_product_files_from_hf(product): hf_delete_failed = False product_name_for_log = product.get('name', product.get('id', 'N/A')) photo_paths = product.get('photo_paths', []) for hf_path in photo_paths: commit_msg = f"Удалить фото {os.path.basename(hf_path)} для удаленного товара {product_name_for_log}" if not delete_file_from_hf(hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg): logging.warning(f"Не удалось удалить фото товара {hf_path} из Hugging Face для товара {product_name_for_log}.") hf_delete_failed = True return hf_delete_failed def cleanup_local_product_files(product): photo_paths = product.get('photo_paths', []) for hf_path in photo_paths: local_filename = os.path.basename(hf_path) local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, local_filename) if os.path.exists(local_path): try: os.remove(local_path) except OSError as e: logging.warning(f"Не удалось удалить остаточный локальный файл фото товара {local_path}: {e}") @app.route('/') def index(): points_for_frontend = [] for point in app_data.get('points', []): hf_panorama_url = get_hf_image_url(REPO_ID, point.get('panorama_image_path', '')) hotspots_for_frontend = [] for hs in point.get('hotspots', []): hotspots_for_frontend.append({ 'id': hs.get('id'), 'pitch': hs.get('pitch'), 'yaw': hs.get('yaw'), 'type': hs.get('type'), 'target': hs.get('target'), 'text': hs.get('text', 'Инфо'), 'cssClass': 'custom-hotspot-button', }) points_for_frontend.append({ 'id': point.get('id'), 'name': point.get('name'), 'panoramaUrl': hf_panorama_url, 'hotspots': hotspots_for_frontend }) index_html = """ 360 Панорамный Каталог
Корзина (0)
""" return render_template_string(index_html, points_data=points_for_frontend, whatsapp_number=app_data.get('whatsapp_number', '')) @app.route('/category_products/') def get_category_products(category_id): category = find_category(category_id) if not category: return jsonify({"error": "Категория не найдена"}), 404 products_with_urls = [] for product in category.get('products', []): photo_url = None if product.get('photo_paths') and len(product['photo_paths']) > 0: photo_url = get_hf_image_url(REPO_ID, product['photo_paths'][0]) products_with_urls.append({ "id": product.get('id'), "name": product.get('name', 'N/A'), "price": product.get('price', 0), "description": product.get('description', ''), "photoUrl": photo_url }) return jsonify({ "category_id": category_id, "category_name": category.get('name', 'Неизвестная Категория'), "products": products_with_urls }) @app.route('/standalone_product_details/') def get_standalone_product_details(product_id): product = find_standalone_product(product_id) if not product: return jsonify({"error": "Товар не найден"}), 404 photo_url = None if product.get('photo_paths') and len(product['photo_paths']) > 0: photo_url = get_hf_image_url(REPO_ID, product['photo_paths'][0]) product_details = { "id": product.get('id'), "name": product.get('name', 'N/A'), "price": product.get('price', 0), "description": product.get('description', ''), "photoUrl": photo_url, "allPhotoUrls": [get_hf_image_url(REPO_ID, p) for p in product.get('photo_paths', []) if p] } return jsonify({"product": product_details}) @app.route('/admin', methods=['GET']) def admin_panel(): admin_html = """ Админ-панель

Админ-панель 360° Каталога

Управление Поинтами (Панорамами)

Добавить новый Поинт

Существующие Поинты

Управление Категориями

Добавить новую Категорию

Существующие Категории

    {% for category in categories %}
  • {{ category.name }} (ID: {{ category.id }})
  • {% else %}

    Пока нет ни одной категории.

    {% endfor %}

Управление Отдельными Товарами

Эти товары можно будет выбирать напрямую при создании хотспота типа "Товар".

Перейти к управлению отдельными товарами

Настройки WhatsApp

Резервное копирование

База данных автоматически сохраняется на Hugging Face при каждом изменении и периодически.

Примечание: Изображения (панорамы, фото товаров) загружаются на Hugging Face сразу при их добавлении/изменении.

""" return render_template_string(admin_html, points=app_data.get('points', []), categories=app_data.get('categories', []), standalone_products=app_data.get('standalone_products', []), whatsapp_number=app_data.get('whatsapp_number', '')) @app.route('/admin/settings/whatsapp', methods=['POST']) def admin_save_whatsapp_number(): whatsapp_number = request.form.get('whatsapp_number') app_data['whatsapp_number'] = whatsapp_number save_data() return redirect(url_for('admin_panel')) @app.route('/admin/points/add', methods=['POST']) def admin_add_point(): point_name = request.form.get('point_name') panorama_file = request.files.get('panorama_image') if not point_name or not panorama_file or not panorama_file.filename: return redirect(url_for('admin_panel')) if panorama_file: filename = secure_filename(panorama_file.filename) unique_filename = f"{generate_id()}_{filename}" local_path = os.path.join(PANORAMA_UPLOADS, unique_filename) hf_path = f"panoramas/{unique_filename}" try: panorama_file.save(local_path) logging.info(f"Панорама сохранена локально в {local_path}") commit_msg = f"Добавить панораму для поинта: {point_name}" if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg): new_point = { "id": generate_id(), "name": point_name, "panorama_image_path": hf_path, "hotspots": [] } app_data.setdefault('points', []).append(new_point) save_data() logging.info(f"Поинт '{point_name}' успешно добавлен.") try: os.remove(local_path) logging.info(f"Удален временный локальный файл {local_path}") except OSError as e: logging.warning(f"Не удалось удалить временный локальный файл {local_path}: {e}") else: logging.error(f"Не удалось загрузить панораму для поинта {point_name} в Hugging Face. Поинт не добавлен.") try: os.remove(local_path) except OSError: pass except Exception as e: logging.error(f"Ошибка обработки загрузки панорамы для {point_name}: {e}") return redirect(url_for('admin_panel')) @app.route('/admin/points/delete/', methods=['POST']) def admin_delete_point(point_id): point_index = next((index for (index, p) in enumerate(app_data.get('points', [])) if p.get('id') == point_id), None) if point_index is not None: point = app_data['points'][point_index] panorama_hf_path = point.get('panorama_image_path') deleted_from_hf = False if panorama_hf_path: commit_msg = f"Удалить панораму для поинта: {point.get('name', point_id)}" if delete_file_from_hf(panorama_hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg): deleted_from_hf = True else: logging.warning(f"Не удалось удалить панораму {panorama_hf_path} из Hugging Face. Данные поинта все равно будут удалены.") deleted_point = app_data['points'].pop(point_index) save_data() logging.info(f"Удален поинт: {deleted_point.get('name', point_id)}") if panorama_hf_path: local_filename = os.path.basename(panorama_hf_path) local_path = os.path.join(PANORAMA_UPLOADS, local_filename) if os.path.exists(local_path): try: os.remove(local_path) except OSError as e: logging.warning(f"Не удалось удалить остаточный локальный файл {local_path}: {e}") else: logging.warning(f"Попытка удалить несуществующий поинт с ID: {point_id}") return redirect(url_for('admin_panel')) @app.route('/admin/points/edit/', methods=['GET']) def admin_edit_point(point_id): point = find_point(point_id) if not point: return redirect(url_for('admin_panel')) panorama_url = get_hf_image_url(REPO_ID, point.get('panorama_image_path', '')) all_points = [{"id": p['id'], "name": p['name']} for p in app_data.get('points', []) if p['id'] != point_id] all_categories = [{"id": c['id'], "name": c['name']} for c in app_data.get('categories', [])] all_standalone_products = [{"id": p['id'], "name": p['name']} for p in app_data.get('standalone_products', [])] edit_point_html = """ Редактировать Поинт: {{ point.name }}

Редактировать Поинт: {{ point.name }}

« Назад к админ-панели

Предпросмотр Панорамы и Хотспоты

Кликните на панораму, чтобы получить координаты (Pitch/Yaw) для нового хотспота.

Добавить Хотспот

Существующие Хотспоты

    {% for hotspot in point.get('hotspots', []) %}
  • Тип: {{ hotspot.type }} | Цель: {{ hotspot.target }} | Текст: {{ hotspot.text }} | Коорд: (P: {{ "%.2f"|format(hotspot.pitch) }}, Y: {{ "%.2f"|format(hotspot.yaw) }})
  • {% else %}

    Нет хотспотов для этого поинта.

    {% endfor %}
""" return render_template_string(edit_point_html, point=point, panorama_url=panorama_url, all_points=all_points, all_categories=all_categories, all_standalone_products=all_standalone_products) @app.route('/admin/points//hotspots/save', methods=['POST']) def admin_save_hotspot(point_id): point = find_point(point_id) if not point: return redirect(url_for('admin_panel')) try: pitch_str = request.form.get('pitch') yaw_str = request.form.get('yaw') text = request.form.get('text') type = request.form.get('type') hotspot_id = request.form.get('hotspot_id') if not pitch_str or not yaw_str: logging.warning("Координаты Pitch/Yaw отсутствуют. Кликните на панораму.") return redirect(url_for('admin_edit_point', point_id=point_id)) pitch = float(pitch_str) yaw = float(yaw_str) target = None if type == 'point': target = request.form.get('target_point_id') elif type == 'link': target = request.form.get('target_link_url') elif type == 'category': target = request.form.get('target_category_id') elif type == 'product': target = request.form.get('target_product_id') if not all([pitch is not None, yaw is not None, text, type, target]): logging.warning("Отсутствуют данные для сохранения хотспота.") return redirect(url_for('admin_edit_point', point_id=point_id)) new_hotspot = { "id": generate_id(), "pitch": pitch, "yaw": yaw, "type": type, "target": target, "text": text } point.setdefault('hotspots', []).append(new_hotspot) save_data() logging.info(f"Добавлен хотспот к поинту {point_id}: Тип={type}, Цель={target}") except ValueError: logging.error("Неверный формат числа для pitch или yaw.") except Exception as e: logging.error(f"Ошибка сохранения хотспота для поинта {point_id}: {e}") return redirect(url_for('admin_edit_point', point_id=point_id)) @app.route('/admin/points//hotspots/delete/', methods=['POST']) def admin_delete_hotspot(point_id, hotspot_id): point = find_point(point_id) if point and 'hotspots' in point: initial_length = len(point['hotspots']) point['hotspots'] = [hs for hs in point['hotspots'] if hs.get('id') != hotspot_id] if len(point['hotspots']) < initial_length: save_data() logging.info(f"Удален хотспот {hotspot_id} из поинта {point_id}") else: logging.warning(f"Хотспот {hotspot_id} не найден в поинте {point_id} для удаления.") else: logging.warning(f"Поинт {point_id} не найден или не имеет хотспотов для удаления.") return redirect(url_for('admin_edit_point', point_id=point_id)) @app.route('/admin/categories/add', methods=['POST']) def admin_add_category(): category_name = request.form.get('category_name') if category_name: if any(c.get('name', '').lower() == category_name.lower() for c in app_data.get('categories', [])): logging.warning(f"Попытка добавить дубликат имени категории: {category_name}") else: new_category = { "id": generate_id(), "name": category_name, "products": [] } app_data.setdefault('categories', []).append(new_category) save_data() logging.info(f"Добавлена категория: {category_name}") else: logging.warning("Попытка добавить категорию с пустым именем.") return redirect(url_for('admin_panel')) @app.route('/admin/categories/delete/', methods=['POST']) def admin_delete_category(category_id): category_index = next((index for (index, c) in enumerate(app_data.get('categories', [])) if c.get('id') == category_id), None) if category_index is not None: category = app_data['categories'][category_index] category_name = category.get('name', category_id) products_to_delete = category.get('products', []) hf_delete_failed_overall = False for product in products_to_delete: if delete_product_files_from_hf(product): hf_delete_failed_overall = True deleted_category = app_data['categories'].pop(category_index) save_data() logging.info(f"Удалена категория: {category_name} и ее {len(products_to_delete)} товаров.") for product in products_to_delete: cleanup_local_product_files(product) if hf_delete_failed_overall: pass else: pass else: logging.warning(f"Попытка удалить несуществующую категорию с ID: {category_id}") return redirect(url_for('admin_panel')) @app.route('/admin/category//products', methods=['GET']) def admin_manage_category_products(category_id): category = find_category(category_id) if not category: return redirect(url_for('admin_panel')) products_with_urls = [] for product in category.get('products', []): photo_urls = [get_hf_image_url(REPO_ID, p) for p in product.get('photo_paths', []) if p] products_with_urls.append({**product, 'photo_urls': photo_urls}) manage_products_html = """ Управление Товарами: {{ category.name }}

Управление Товарами в Категории: {{ category.name }}

« Назад к админ-панели

Добавить Новый Товар

Существующие Товары в "{{ category.name }}"

    {% for product in products %}
  • {{ product.name }}
    Цена: {{ product.price }} SOM
    Описание: {{ product.description[:100] }}{% if product.description|length > 100 %}...{% endif %}
    ID: {{ product.id }}
    {% if product.photo_urls %} {% for url in product.photo_urls %} {{ product.name }} фото {% endfor %} {% else %} Нет фото {% endif %}

    Редактировать: {{ product.name }}

    Если выбраны новые фото, они ЗАМЕНЯТ все старые.

  • {% else %}

    В этой категории пока нет товаров.

    {% endfor %}
""" return render_template_string(manage_products_html, category=category, products=products_with_urls) @app.route('/admin/category//products/add', methods=['POST']) def admin_add_product_to_category(category_id): category = find_category(category_id) if not category: return redirect(url_for('admin_panel')) name = request.form.get('name') price = request.form.get('price') description = request.form.get('description') photos = request.files.getlist('photos') if not name or price is None or description is None: return redirect(url_for('admin_manage_category_products', category_id=category_id)) try: price_float = float(price) if price_float < 0: raise ValueError("Цена не может быть отрицательной") except ValueError: return redirect(url_for('admin_manage_category_products', category_id=category_id)) photo_hf_paths = [] upload_failed = False uploaded_local_paths = [] for photo in photos[:10]: if photo and photo.filename: filename = secure_filename(photo.filename) unique_filename = f"{generate_id()}_{filename}" local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, unique_filename) hf_path = f"product_photos/{unique_filename}" uploaded_local_paths.append(local_path) try: photo.save(local_path) commit_msg = f"Добавить фото для товара: {name} в категорию {category.get('name')}" if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg): photo_hf_paths.append(hf_path) else: upload_failed = True logging.error(f"Не удалось загрузить фото товара {filename} в HF.") except Exception as e: upload_failed = True logging.error(f"Ошибка обработки фото товара {filename}: {e}") new_product = { "id": generate_id(), "name": name, "price": price_float, "description": description, "photo_paths": photo_hf_paths } category.setdefault('products', []).append(new_product) save_data() logging.info(f"Добавлен товар '{name}' в категорию '{category.get('name')}'. Фото: {len(photo_hf_paths)}.") if upload_failed: pass else: pass for local_path in uploaded_local_paths: if os.path.exists(local_path): try: os.remove(local_path) except OSError as e: logging.warning(f"Не удалось удалить временный локальный файл фото товара {local_path}: {e}") return redirect(url_for('admin_manage_category_products', category_id=category_id)) @app.route('/admin/category//products/edit/', methods=['POST']) def admin_edit_product_in_category(category_id, product_id): category = find_category(category_id) product = find_product_in_category(category_id, product_id) if not category or not product: return redirect(url_for('admin_panel')) name = request.form.get('name') price = request.form.get('price') description = request.form.get('description') new_photos = request.files.getlist('photos') if not name or price is None or description is None: return redirect(url_for('admin_manage_category_products', category_id=category_id)) try: price_float = float(price) if price_float < 0: raise ValueError("Цена не может быть отрицательной") except ValueError: return redirect(url_for('admin_manage_category_products', category_id=category_id)) product['name'] = name product['price'] = price_float product['description'] = description if new_photos and any(p.filename for p in new_photos): logging.info(f"Замена фото для товара {product_id} в категории {category_id}...") old_photo_paths = product.get('photo_paths', []) new_photo_hf_paths = [] upload_failed = False uploaded_local_paths = [] for photo in new_photos[:10]: if photo and photo.filename: filename = secure_filename(photo.filename) unique_filename = f"{generate_id()}_{filename}" local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, unique_filename) hf_path = f"product_photos/{unique_filename}" uploaded_local_paths.append(local_path) try: photo.save(local_path) commit_msg = f"Обновить фото для товара: {name}" if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg): new_photo_hf_paths.append(hf_path) else: upload_failed = True logging.error(f"Не удалось загрузить новое фото {filename} в HF.") except Exception as e: upload_failed = True logging.error(f"Ошибка обработки нового фото {filename}: {e}") if len(new_photo_hf_paths) > 0: logging.info(f"Удаление {len(old_photo_paths)} старых фото для товара {product_id} из HF...") hf_delete_failed = False for old_hf_path in old_photo_paths: commit_msg = f"Удалить старое фото {os.path.basename(old_hf_path)} для товара {name} при обновлении" if not delete_file_from_hf(old_hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg): logging.warning(f"Не удалось удалить старое фото {old_hf_path} из HF при обновлении.") hf_delete_failed = True product['photo_paths'] = new_photo_hf_paths if hf_delete_failed: pass elif upload_failed: logging.error(f"Замена фото не удалась для товара {product_id}. Старые фото сохранены.") for local_path in uploaded_local_paths: if os.path.exists(local_path): try: os.remove(local_path) except OSError as e: logging.warning(f"Не удалось удалить временный локальный файл нового фото {local_path}: {e}") save_data() logging.info(f"Обновлен товар '{name}' (ID: {product_id}) в категории '{category.get('name')}'.") return redirect(url_for('admin_manage_category_products', category_id=category_id)) @app.route('/admin/category//products/delete/', methods=['POST']) def admin_delete_product_from_category(category_id, product_id): category = find_category(category_id) if not category or 'products' not in category: return redirect(url_for('admin_panel')) product_index = next((index for (index, p) in enumerate(category['products']) if p.get('id') == product_id), None) if product_index is not None: product = category['products'][product_index] product_name = product.get('name', product_id) hf_delete_failed = delete_product_files_from_hf(product) deleted_product = category['products'].pop(product_index) save_data() logging.info(f"Удален товар: {product_name} из категории {category.get('name')}") cleanup_local_product_files(product) if hf_delete_failed: pass else: pass else: logging.warning(f"Товар {product_id} не найден в категории {category_id} для удаления.") return redirect(url_for('admin_manage_category_products', category_id=category_id)) @app.route('/admin/products/standalone', methods=['GET']) def admin_manage_standalone_products(): products_with_urls = [] for product in app_data.get('standalone_products', []): photo_urls = [get_hf_image_url(REPO_ID, p) for p in product.get('photo_paths', []) if p] products_with_urls.append({**product, 'photo_urls': photo_urls}) manage_standalone_html = """ Управление Отдельными Товарами

Управление Отдельными Товарами

Эти товары можно выбирать напрямую при создании хотспота типа "Товар". Они не принадлежат к какой-либо категории.

« Назад к главной админ-панели

Добавить Новый Отдельный Товар

Существующие Отдельные Товары

    {% for product in products %}
  • {{ product.name }}
    Цена: {{ product.price }} SOM
    Описание: {{ product.description[:100] }}{% if product.description|length > 100 %}...{% endif %}
    ID: {{ product.id }}
    {% if product.photo_urls %} {% for url in product.photo_urls %} {{ product.name }} фото {% endfor %} {% else %} Нет фото {% endif %}

    Редактировать: {{ product.name }}

    Если выбраны новые фото, они ЗАМЕНЯТ все старые.

  • {% else %}

    Пока нет отдельных товаров.

    {% endfor %}
""" return render_template_string(manage_standalone_html, products=products_with_urls) @app.route('/admin/products/standalone/add', methods=['POST']) def admin_add_standalone_product(): name = request.form.get('name') price = request.form.get('price') description = request.form.get('description') photos = request.files.getlist('photos') if not name or price is None or description is None: return redirect(url_for('admin_manage_standalone_products')) try: price_float = float(price) if price_float < 0: raise ValueError("Цена не может быть отрицательной") except ValueError: return redirect(url_for('admin_manage_standalone_products')) photo_hf_paths = [] upload_failed = False uploaded_local_paths = [] for photo in photos[:10]: if photo and photo.filename: filename = secure_filename(photo.filename) unique_filename = f"{generate_id()}_{filename}" local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, unique_filename) hf_path = f"product_photos/standalone/{unique_filename}" uploaded_local_paths.append(local_path) try: photo.save(local_path) commit_msg = f"Добавить фото для отдельного товара: {name}" if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg): photo_hf_paths.append(hf_path) else: upload_failed = True logging.error(f"Не удалось загрузить фото отдельного товара {filename} в HF.") except Exception as e: upload_failed = True logging.error(f"Ошибка обработки фото отдельного товара {filename}: {e}") new_product = { "id": generate_id(), "name": name, "price": price_float, "description": description, "photo_paths": photo_hf_paths } app_data.setdefault('standalone_products', []).append(new_product) save_data() logging.info(f"Добавлен отдельный товар '{name}'. Фото: {len(photo_hf_paths)}.") if upload_failed: pass else: pass for local_path in uploaded_local_paths: if os.path.exists(local_path): try: os.remove(local_path) except OSError as e: logging.warning(f"Не удалось удалить временный локальный файл фото товара {local_path}: {e}") return redirect(url_for('admin_manage_standalone_products')) @app.route('/admin/products/standalone/edit/', methods=['POST']) def admin_edit_standalone_product(product_id): product = find_standalone_product(product_id) if not product: return redirect(url_for('admin_manage_standalone_products')) name = request.form.get('name') price = request.form.get('price') description = request.form.get('description') new_photos = request.files.getlist('photos') if not name or price is None or description is None: return redirect(url_for('admin_manage_standalone_products')) try: price_float = float(price) if price_float < 0: raise ValueError("Цена не может быть отрицательной") except ValueError: return redirect(url_for('admin_manage_standalone_products')) product['name'] = name product['price'] = price_float product['description'] = description if new_photos and any(p.filename for p in new_photos): logging.info(f"Замена фото для отдельного товара {product_id}...") old_photo_paths = product.get('photo_paths', []) new_photo_hf_paths = [] upload_failed = False uploaded_local_paths = [] for photo in new_photos[:10]: if photo and photo.filename: filename = secure_filename(photo.filename) unique_filename = f"{generate_id()}_{filename}" local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, unique_filename) hf_path = f"product_photos/standalone/{unique_filename}" uploaded_local_paths.append(local_path) try: photo.save(local_path) commit_msg = f"Обновить фото для отдельного товара: {name}" if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg): new_photo_hf_paths.append(hf_path) else: upload_failed = True logging.error(f"Не удалось загрузить новое фото {filename} в HF.") except Exception as e: upload_failed = True logging.error(f"Ошибка обработки нового фото {filename}: {e}") if len(new_photo_hf_paths) > 0: logging.info(f"Удаление {len(old_photo_paths)} старых фото для товара {product_id} из HF...") hf_delete_failed = False for old_hf_path in old_photo_paths: commit_msg = f"Удалить старое фото {os.path.basename(old_hf_path)} для товара {name} при обновлении" if not delete_file_from_hf(old_hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg): logging.warning(f"Не удалось удалить старое фото {old_hf_path} из HF при обновлении.") hf_delete_failed = True product['photo_paths'] = new_photo_hf_paths if hf_delete_failed: pass elif upload_failed: logging.error(f"Замена фото не удалась для товара {product_id}. Старые фото сохранены.") for local_path in uploaded_local_paths: if os.path.exists(local_path): try: os.remove(local_path) except OSError as e: logging.warning(f"Не удалось удалить временный локальный файл нового фото {local_path}: {e}") save_data() logging.info(f"Обновлен отдельный товар '{name}' (ID: {product_id}).") return redirect(url_for('admin_manage_standalone_products', product_id=product_id)) @app.route('/admin/products/standalone/delete/', methods=['POST']) def admin_delete_standalone_product(product_id): product_index = next((index for (index, p) in enumerate(app_data.get('standalone_products', [])) if p.get('id') == product_id), None) if product_index is not None: product = app_data['standalone_products'][product_index] product_name = product.get('name', product_id) hf_delete_failed = delete_product_files_from_hf(product) deleted_product = app_data['standalone_products'].pop(product_index) save_data() logging.info(f"Удален отдельный товар: {product_name}") cleanup_local_product_files(product) if hf_delete_failed: pass else: pass else: logging.warning(f"Отдельный товар {product_id} не найден для удаления.") return redirect(url_for('admin_manage_standalone_products')) @app.route('/backup', methods=['POST']) def manual_backup(): logging.info("Запущено ручное резервное копирование базы данных.") save_data() return redirect(url_for('admin_panel')) if __name__ == '__main__': logging.info("Попытка начальной загрузки данных...") download_db_from_hf() backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() logging.info("Поток периодического резервного копирования запущен.") logging.info("Запуск приложения Flask...") app.run(debug=False, host='0.0.0.0', port=7860)