Compare commits
No commits in common. "master" and "new" have entirely different histories.
@ -1,107 +0,0 @@
|
||||
Вы — эксперт в разработке веб-приложений с использованием **Python, FastAPI, SQLAlchemy, Next.js, React, TypeScript, Tailwind CSS** и **Shadcn UI**.
|
||||
|
||||
### Ключевые принципы
|
||||
|
||||
- Пишите лаконичные технические ответы с точными примерами как на Python, так и на TypeScript.
|
||||
- Используйте **функциональные и декларативные паттерны программирования**; избегайте классов, если они не необходимы.
|
||||
- Предпочитайте **итерацию и модуляризацию** вместо дублирования кода.
|
||||
- Используйте описательные имена переменных с вспомогательными глаголами (например, `is_active`, `has_permission`, `isLoading`, `hasError`).
|
||||
- Следуйте правильным **соглашениям об именовании**:
|
||||
- Для Python: используйте нижний регистр с подчеркиваниями (например, `routers/user_routes.py`).
|
||||
- Для TypeScript: используйте нижний регистр с дефисами для директорий (например, `components/auth-wizard`).
|
||||
|
||||
### Структура проекта
|
||||
|
||||
- **Фронтенд**:
|
||||
- **Язык**: TypeScript
|
||||
- **Фреймворк**: Next.js 15 с App Router
|
||||
- **UI Библиотеки**: Tailwind CSS, Shadcn UI, Radix UI
|
||||
- **Директории**:
|
||||
- `frontend/app/`: Основной код с маршрутизацией (App Router)
|
||||
- `frontend/components/`: Компоненты React
|
||||
- `frontend/hooks/`: React хуки
|
||||
- `frontend/lib/`: Служебные функции и утилиты
|
||||
- `frontend/public/`: Статические файлы
|
||||
- `frontend/styles/`: CSS стили
|
||||
- **Конфигурационные файлы**:
|
||||
- `next.config.mjs`
|
||||
- `tsconfig.json`
|
||||
- `tailwind.config.ts`
|
||||
- `postcss.config.mjs`
|
||||
|
||||
- **Бэкенд**:
|
||||
- **Язык**: Python
|
||||
- **Фреймворк**: FastAPI
|
||||
- **База данных**: PostgreSQL
|
||||
- **ORM**: SQLAlchemy 2.0
|
||||
- **Директории**:
|
||||
- `backend/app/`: Основной код
|
||||
- `models/`: Модели базы данных
|
||||
- `repositories/`: Репозитории для работы с данными
|
||||
- `schemas/`: Pydantic схемы
|
||||
- `services/`: Бизнес-логика
|
||||
- `routers/`: Endpoints API
|
||||
- `backend/uploads/`: Загруженные файлы
|
||||
- **Конфигурационные файлы**:
|
||||
- `alembic.ini`: Конфигурация миграций
|
||||
- `.env`: Переменные окружения
|
||||
|
||||
### Стиль кода и структура
|
||||
|
||||
**Бэкенд (Python/FastAPI)**:
|
||||
|
||||
- Используйте `def` для чистых функций и `async def` для асинхронных операций.
|
||||
- **Типизация**: Используйте аннотации типов для всех функций. Предпочитайте Pydantic-модели для валидации данных.
|
||||
- **Структура файлов**: Следуйте чёткому разделению с директориями для маршрутов, утилит, статического контента и моделей/схем.
|
||||
- **RORO паттерн**: Используйте паттерн «Получить объект, вернуть объект» для структурирования функций.
|
||||
- **Обработка ошибок**:
|
||||
- Обрабатывайте ошибки в начале функций с ранним возвратом.
|
||||
- Используйте защитные условия и избегайте глубоко вложенных условий.
|
||||
- Реализуйте правильное логирование и пользовательские типы ошибок.
|
||||
|
||||
**Фронтенд (TypeScript/React/Next.js)**:
|
||||
|
||||
- **TypeScript**: Используйте TypeScript для всего кода. Предпочитайте интерфейсы типам. Избегайте перечислений; используйте объекты вместо них.
|
||||
- **Компоненты**: Пишите все компоненты как функциональные с правильной типизацией TypeScript.
|
||||
- **UI и стилизация**: Реализуйте отзывчивый дизайн с использованием Tailwind CSS и Shadcn UI, начиная с мобильной версии.
|
||||
- **Рендеринг**: Используйте серверные и клиентские компоненты Next.js правильно:
|
||||
- Предпочитайте серверные компоненты, где это возможно
|
||||
- Используйте директиву `"use client"` только для компонентов, требующих клиентских возможностей
|
||||
- Оборачивайте клиентские компоненты в `Suspense` для улучшения производительности
|
||||
|
||||
### Оптимизация производительности
|
||||
|
||||
**Бэкенд**:
|
||||
|
||||
- **Асинхронные операции**: Минимизируйте блокирующие операции ввода-вывода, используя асинхронные функции.
|
||||
- **Кэширование**: Внедряйте стратегии кэширования для часто используемых данных.
|
||||
- **Ленивая загрузка**: Используйте технику ленивой загрузки для больших наборов данных и ответов API.
|
||||
|
||||
**Фронтенд**:
|
||||
|
||||
- **Компоненты React**: Предпочитайте серверный рендеринг и оптимизируйте клиентский рендеринг.
|
||||
- **Изображения**: Оптимизируйте загрузку изображений с помощью компонента Next Image.
|
||||
- **Метрики**: Оптимизируйте Core Web Vitals (LCP, CLS, FID).
|
||||
|
||||
### Проектные соглашения
|
||||
|
||||
**Бэкенд**:
|
||||
|
||||
1. Следуйте **принципам проектирования RESTful API**.
|
||||
2. Используйте **систему внедрения зависимостей FastAPI** для управления состоянием и общими ресурсами.
|
||||
3. Используйте **SQLAlchemy 2.0** для функций ORM.
|
||||
4. Обеспечьте правильную настройку **CORS** для локальной разработки.
|
||||
5. Реализуйте надлежащую аутентификацию и авторизацию для защиты API.
|
||||
|
||||
**Фронтенд**:
|
||||
|
||||
1. Следуйте рекомендациям Next.js по использованию серверных и клиентских компонентов.
|
||||
2. Ограничивайте директиву `"use client"` небольшими, специфичными компонентами.
|
||||
3. Используйте хуки React эффективно, избегая ненужных рендеров.
|
||||
4. Реализуйте интернационализацию для поддержки русского языка.
|
||||
|
||||
### Тестирование и развертывание
|
||||
|
||||
- Реализуйте **юнит-тесты** как для фронтенда, так и для бэкенда.
|
||||
- Используйте **Docker** и **docker compose** для оркестрации в средах разработки и производства.
|
||||
- Обеспечьте надлежащую валидацию входных данных, санитизацию и обработку ошибок во всем приложении.
|
||||
@ -1,166 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
use context7 always for docs frameworks
|
||||
|
||||
Ты — мой ИИ-ассистент для разработки веб-приложения. Ты эксперт в стеке: Python, FastAPI, SQLAlchemy 2.0, PostgreSQL (бэкенд) и Next.js 15 (App Router), React, TypeScript, Tailwind CSS, Shadcn UI, Radix UI (фронтенд).
|
||||
|
||||
Твоя главная цель: Помогать мне в улучшении существующего кода и написании нового, строго следуя приведенным ниже гайдлайнам, ориентируясь на существующие паттерны в проекте, и используя предоставленный контекст.
|
||||
|
||||
Основные Директивы
|
||||
Анализ Контекста и Существующего Кода: Прежде чем генерировать новый код (компоненты, функции, эндпоинты), проанализируй существующие аналогичные части проекта (используй @symbol для доступа к файлам/символам). Следуй установленным паттернам, структуре и стилю. Консистентность с текущей кодовой базой — ключ.
|
||||
|
||||
Лаконичность, Оптимизация, Точность: Пиши лаконичный и оптимизированный код. Отвечай технически точно, кратко и по делу. Приводи конкретные, рабочие примеры кода на Python и TypeScript.
|
||||
|
||||
Стиль Программирования:
|
||||
|
||||
Функциональный/Декларативный: Предпочитай эти паттерны. Избегай классов, если нет явной необходимости (модели SQLAlchemy, React-компоненты).
|
||||
|
||||
DRY (Don't Repeat Yourself): Пиши модульный код, используй функции/компоненты для переиспользования логики. На фронтенде разделяй сложную логику и UI на небольшие, переиспользуемые компоненты.
|
||||
|
||||
Именование:
|
||||
|
||||
Переменные: Описательные имена с вспомогательными глаголами (is_active, has_permission, isLoading, itemCount, fetchUsers).
|
||||
|
||||
Файлы/Директории: Python: snake_case (routers/user_routes.py). TypeScript: kebab-case (components/auth-wizard/, lib/api-client.ts).
|
||||
|
||||
Комментирование: Кратко комментируй генерируемый код, объясняя что и почему делается, особенно для нетривиальной логики.
|
||||
|
||||
Важные Ограничения:
|
||||
|
||||
НЕ ЗАПУСКАЙ СЕРВЕРЫ: Никогда не предлагай и не пытайся запускать frontend или backend серверы. Я делаю это сам.
|
||||
|
||||
Контекст Проекта
|
||||
Используй эту информацию как основу для генерации и модификации кода.
|
||||
|
||||
1. Структура Проекта:
|
||||
|
||||
.
|
||||
├── backend/
|
||||
│ ├── alembic/
|
||||
│ ├── app/
|
||||
│ │ ├── models/ # SQLAlchemy Модели
|
||||
│ │ ├── repositories/ # Логика доступа к данным
|
||||
│ │ ├── routers/ # FastAPI Роутеры (Эндпоинты)
|
||||
│ │ ├── schemas/ # Pydantic Схемы
|
||||
│ │ ├── services/ # Бизнес-логика
|
||||
│ │ ├── config.py
|
||||
│ │ ├── core.py
|
||||
│ │ └── main.py
|
||||
│ ├── docs/ # Документация (.md)
|
||||
│ ├── uploads/ # Загруженные файлы (статичные)
|
||||
│ ├── .env
|
||||
│ ├── alembic.ini
|
||||
│ └── requirements.txt
|
||||
├── frontend/
|
||||
│ ├── app/ # Next.js App Router (основной код, маршруты)
|
||||
│ │ ├── (main)/ # Группа маршрутов
|
||||
│ │ ├── account/
|
||||
│ │ ├── admin/
|
||||
│ │ ├── layout.tsx
|
||||
│ │ └── globals.css
|
||||
│ ├── components/ # React Компоненты (UI, layout, etc.)
|
||||
│ │ ├── admin/
|
||||
│ │ ├── cart/
|
||||
│ │ ├── ui/ # Shadcn UI компоненты
|
||||
│ │ └── ...
|
||||
│ ├── hooks/ # React Хуки (use...)
|
||||
│ ├── lib/ # Утилиты, API клиенты, константы
|
||||
│ │ ├── api.ts # Базовый API клиент (fetchApi)
|
||||
│ │ ├── auth.ts # Функции API для аутентификации
|
||||
│ │ ├── ... (и другие модули API: users, cart, catalog, etc.)
|
||||
│ │ ├── utils.ts # Общие утилиты
|
||||
│ │ └── auth.tsx # React контекст/провайдер для Auth
|
||||
│ ├── public/ # Статические ассеты
|
||||
│ ├── styles/ # Глобальные стили
|
||||
│ ├── types/ # Глобальные TypeScript типы/интерфейсы
|
||||
│ ├── next.config.mjs
|
||||
│ ├── tsconfig.json
|
||||
│ ├── tailwind.config.ts
|
||||
│ ├── postcss.config.mjs
|
||||
│ └── package.json
|
||||
├── docker-compose.yml
|
||||
└── package.json
|
||||
Use code with caution.
|
||||
2. Спецификация Backend API (OpenAPI 3.1.0):
|
||||
|
||||
Полная спецификация OpenAPI JSON должна быть доступна Cursor как контекст.
|
||||
|
||||
Ключевые эндпоинты: Auth, Users, Addresses, Catalog (Categories, Collections, Products, Variants, Images, Sizes), Cart, Orders, Reviews, Content, Analytics.
|
||||
|
||||
Аутентификация: OAuth2 Password Bearer.
|
||||
|
||||
3. Документация Frontend API Client (frontend/lib/):
|
||||
|
||||
Документация по модулям auth.ts, users.ts и т.д. должна быть доступна Cursor как контекст.
|
||||
|
||||
Содержит функции-обертки для вызова API бэкенда.
|
||||
|
||||
Стиль Кода и Паттерны
|
||||
Бэкенд (Python / FastAPI / SQLAlchemy):
|
||||
|
||||
Функции: def / async def.
|
||||
|
||||
Типизация: Обязательные аннотации типов. Pydantic (schemas/) для API.
|
||||
|
||||
Структура: routers, services, repositories, models, schemas.
|
||||
|
||||
RORO Паттерн: Принимай/возвращай объекты (схемы/модели).
|
||||
|
||||
Обработка Ошибок:
|
||||
|
||||
Ранний возврат / guard clauses. Избегай вложенности if.
|
||||
|
||||
Используй стандартные HTTPException FastAPI для ошибок API.
|
||||
|
||||
В сервисах/репозиториях используй try...except для отлова ожидаемых runtime-ошибок (e.g., NoResultFound от SQLAlchemy). Логируй ошибки.
|
||||
|
||||
Зависимости: FastAPI Dependency Injection (Depends()).
|
||||
|
||||
ORM: SQLAlchemy 2.0 (async Session).
|
||||
|
||||
Фронтенд (TypeScript / React / Next.js):
|
||||
|
||||
TypeScript: Строгая типизация. interface для объектов, type для остального. Избегай enum, используй as const или union types. unknown лучше any.
|
||||
|
||||
Компоненты React:
|
||||
|
||||
Функциональные компоненты с хуками. Строгая типизация props.
|
||||
|
||||
Компонентный подход: Активно разбивай UI и логику на меньшие, переиспользуемые компоненты, особенно при работе с Shadcn UI. Создавай компоненты рядом с местом использования или в components/, если они общие.
|
||||
|
||||
Next.js App Router:
|
||||
|
||||
Приоритет Серверным Компонентам (RSC).
|
||||
|
||||
"use client": Только для интерактива/хуков/browser API. Минимизируй их размер.
|
||||
|
||||
Suspense: Используй для асинхронной загрузки и улучшения UX.
|
||||
|
||||
Стилизация: Tailwind CSS first. Shadcn UI / Radix UI для примитивов. Mobile-first.
|
||||
|
||||
API Взаимодействие: Используй функции из frontend/lib/*.ts. Обрабатывай состояния isLoading / error.
|
||||
|
||||
Обработка Ошибок: В местах вызова API и в UI используй try...catch. Предоставляй обратную связь пользователю (например, через react-hot-toast или аналоги) и логируй ошибки в консоль или систему мониторинга.
|
||||
|
||||
Оптимизация Производительности
|
||||
Бэкенд: Асинхронность, оптимизация запросов SQLAlchemy, пагинация. Рассмотри кэширование, если потребуется.
|
||||
|
||||
Фронтенд: Динамические импорты (next/dynamic), next/image, минимизация клиентского JS, Core Web Vitals.
|
||||
|
||||
Проектные Соглашения
|
||||
API: RESTful.
|
||||
|
||||
CORS: Настроен для localhost.
|
||||
|
||||
Аутентификация/Авторизация: Реализована (OAuth2).
|
||||
|
||||
Тестирование и Развертывание
|
||||
Пиши код, который легко тестировать (чистые функции, DI), но не генерируй тесты по умолчанию.
|
||||
|
||||
Используй Docker и docker-compose.yml.
|
||||
|
||||
Обеспечивай валидацию и санитизацию данных.
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
# Общие файлы
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
.DS_Store
|
||||
README.md
|
||||
**/test
|
||||
**/.env
|
||||
|
||||
# Файлы и директории для Node.js
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/.next
|
||||
**/dist
|
||||
**/build
|
||||
**/.coverage
|
||||
**/coverage
|
||||
|
||||
# Файлы и директории для Python
|
||||
**/__pycache__
|
||||
**/*.py[cod]
|
||||
**/*.so
|
||||
**/*.egg
|
||||
**/*.egg-info
|
||||
**/dist
|
||||
**/build
|
||||
**/.pytest_cache
|
||||
**/.coverage
|
||||
**/htmlcov
|
||||
**/.tox
|
||||
**/.mypy_cache
|
||||
|
||||
# Виртуальное окружение Python
|
||||
**/venv
|
||||
**/.venv
|
||||
**/env
|
||||
**/.env
|
||||
|
||||
# Файлы базы данных
|
||||
**/*.sqlite
|
||||
**/*.db
|
||||
|
||||
# Логи и временные файлы
|
||||
**/logs
|
||||
**/*.log
|
||||
**/tmp
|
||||
**/*.tmp
|
||||
|
||||
# Файлы IDE
|
||||
**/.idea
|
||||
**/.vscode
|
||||
**/*.swp
|
||||
**/*.swo
|
||||
|
||||
# Загружаемые файлы, кроме необходимых для работы
|
||||
# backend/uploads/*
|
||||
# !backend/uploads/products
|
||||
# backend/uploads/products/*
|
||||
# !backend/uploads/products/.gitkeep
|
||||
@ -1,17 +0,0 @@
|
||||
# Доменное имя сайта без протокола (http/https)
|
||||
DOMAIN_NAME=dressedforsuccess.shop
|
||||
|
||||
# Протокол (http или https)
|
||||
PROTOCOL=https
|
||||
|
||||
# API URL для браузера (клиентская сторона)
|
||||
NEXT_PUBLIC_API_URL=${PROTOCOL}://${DOMAIN_NAME}/api
|
||||
|
||||
# Base URL для статических файлов
|
||||
NEXT_PUBLIC_BASE_URL=${PROTOCOL}://${DOMAIN_NAME}
|
||||
|
||||
# Режим отладки
|
||||
NEXT_PUBLIC_DEBUG=false
|
||||
|
||||
# Мокирование API
|
||||
NEXT_PUBLIC_MOCK_API=false
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
||||
node_modules
|
||||
@ -1,28 +0,0 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Установка зависимостей системы
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Установка зависимостей Python
|
||||
COPY backend/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копирование кода приложения
|
||||
COPY backend/ .
|
||||
|
||||
# Копирование .env.docker в .env для использования в контейнере
|
||||
COPY backend/.env.docker ./.env
|
||||
|
||||
# Создание директории для загрузок если её нет
|
||||
RUN mkdir -p /app/uploads/products
|
||||
|
||||
# Настройка разрешений для директории загрузок
|
||||
RUN chmod -R 777 /app/uploads
|
||||
|
||||
# Открытие порта
|
||||
EXPOSE 8000
|
||||
|
||||
# Запуск приложения с Uvicorn
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@ -1,24 +0,0 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копирование файлов package.json и package-lock.json
|
||||
COPY frontend/package*.json ./
|
||||
|
||||
# Установка зависимостей с флагом --legacy-peer-deps
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
# Копирование .env.docker в .env.local для использования в контейнере
|
||||
COPY frontend/.env.docker ./.env.local
|
||||
|
||||
# Копирование исходного кода
|
||||
COPY frontend/ ./
|
||||
|
||||
# Сборка приложения
|
||||
RUN npm run build
|
||||
|
||||
# Открытие порта
|
||||
EXPOSE 3000
|
||||
|
||||
# Запуск приложения
|
||||
CMD ["sh", "-c", "npm run build && npm start"]
|
||||
BIN
Logo DRESSED FOR SUCCESS/.DS_Store
vendored
BIN
Logo DRESSED FOR SUCCESS/.DS_Store
vendored
Binary file not shown.
@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2021.5 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="101.681mm" height="52.7148mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 9795.77 5078.45"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:#2B2B2A;fill-rule:nonzero}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Слой_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_105553243841888">
|
||||
<path class="fil0" d="M4908.27 805.19c54.07,19.97 154.75,32.69 273.42,7.79 134.29,-28.17 227.42,-126.87 210.47,-234.42 -15.58,-98.88 -78.5,-144.79 -183.21,-184.87 -5,-103.57 -48.42,-197.68 -117.48,-266.17 -25.62,-25.61 -54.57,-47.33 -86.3,-65.14 -19.36,-12.43 -50.13,-24.51 -52.91,-25.62 30.07,-7.24 62.93,-11.37 101.34,-12.25 160.91,-3.71 233.25,103.02 241.68,154.81l67.38 1.11c-5.58,-28.4 -59.6,-180.41 -318.52,-180.41 -47.89,0 -94.65,8.35 -136.41,22.27 -33.41,-8.91 -66.83,-13.92 -102.48,-13.92 -101.33,0 -403.71,0.56 -403.71,0.56l0 17.25c35.1,0 57.37,8.36 71.83,20.61 26.18,21.17 26.18,55.13 26.18,74.61 0,145.76 0,437.26 0,583.01 0,22.83 0,67.94 -45.67,86.87 -12.8,5.57 -30.08,9.47 -52.34,9.47l0 17.81c0,0 245.13,0 396.09,0 61.7,0 110.63,-13.36 110.63,-13.36zm-333.56 -11.14l0 0 0 -760.64 180.43 0c66.83,0 127.42,11.35 204.92,62.92 94.34,62.78 161.59,156.48 172.05,270.63 16.65,181.55 -83.66,312.64 -175.39,368.06 23.38,10.03 47.31,17.26 62.92,21.17 96.9,-61.25 165.06,-170.19 183.75,-279.54 84.09,39.54 123.05,81.3 129.2,142.55 5,52.9 -21.16,95.78 -63.49,126.95 -57.91,43.44 -148.19,64.63 -225.51,56.8 -162.62,-16.47 -250.22,-113.71 -270.64,-191.55l-70.14 0c12.85,56.55 58.58,124.7 147.55,169.83 0,0 -38.54,12.8 -97.45,12.8 -44.55,0 -178.2,0 -178.2,0z"/>
|
||||
<path class="fil0" d="M5019.09 410.39c23.38,7.24 44.54,13.92 64.58,20.6 1.12,-21.71 1.69,-51.78 -3.34,-80.74 -187.66,-59.58 -275.07,-87.98 -273.39,-189.32 0.55,-23.94 12.23,-47.33 34.5,-68.49 -13.34,-5.01 -27.84,-9.47 -41.18,-12.25 -42.5,35.61 -54.6,75.9 -54.57,107.47 0.11,118.63 90.75,168.16 273.41,222.73z"/>
|
||||
</g>
|
||||
<g id="_105553243834656">
|
||||
<g>
|
||||
<path class="fil0" d="M2921.05 1914.15c14.17,-25.91 21.24,-57.16 21.24,-93.77 0,-54.25 -15.78,-95.9 -47.37,-124.97 -31.56,-29.06 -76.18,-43.59 -133.82,-43.59l-123.88 0 0 342.24 138.46 0c32.71,0 61.58,-6.84 86.58,-20.54 25.03,-13.67 44.62,-33.47 58.8,-59.37zm-98.13 10.09l0 0c-14.98,9.62 -32.76,14.44 -53.33,14.44l-60.71 0 0 -231.48 50.76 0c35.47,0 62.75,9.68 81.85,29.03 19.12,19.35 28.68,47.4 28.68,84.16 0,24.29 -4.13,45.26 -12.4,62.91 -8.25,17.66 -19.87,31.3 -34.85,40.94z"/>
|
||||
<path class="fil0" d="M3167.62 1864.11l84.03 0 79.44 129.96 80.64 0 -92.53 -143.8c23.15,-5.49 41.54,-16.96 55.14,-34.36 13.59,-17.4 20.39,-38.1 20.39,-62.06 0,-32.87 -11.09,-58.09 -33.28,-75.67 -22.19,-17.56 -53.68,-26.34 -94.49,-26.34l-171 0 0 342.24 71.67 0 0 -129.96zm0 -156.67l0 0 91.8 0c21.07,0 36.85,4.12 47.37,12.39 10.53,8.26 15.79,20.57 15.79,36.93 0,16.35 -5.17,29.06 -15.55,38.13 -10.36,9.08 -25.57,13.6 -45.66,13.6l-93.75 0 0 -101.05z"/>
|
||||
<polygon class="fil0" points="3833.78,1938.68 3626.36,1938.68 3626.36,1848.56 3809.02,1848.56 3809.02,1793.18 3626.36,1793.18 3626.36,1707.2 3823.83,1707.2 3823.83,1651.83 3554.7,1651.83 3554.7,1994.07 3833.78,1994.07 "/>
|
||||
<path class="fil0" d="M4239.57 1829.39c-4.06,-3.88 -8.85,-7.53 -14.45,-10.94 -5.59,-3.39 -11.96,-6.64 -19.07,-9.72 -7.13,-2.91 -16.61,-5.93 -28.43,-9.1 -11.81,-3.16 -25.91,-6.51 -42.25,-10.07 -28.18,-5.99 -46.57,-10.94 -55.14,-14.83 -9.08,-4.04 -15.92,-8.89 -20.52,-14.58 -4.61,-5.66 -6.93,-12.95 -6.93,-21.85 0,-13.6 5.42,-23.81 16.28,-30.62 10.84,-6.8 27.11,-10.19 48.81,-10.19 20.4,0 36.08,3.72 47,11.17 10.92,7.46 18.17,18.55 21.74,33.28l69.46 -9.47c-6.16,-30.62 -19.87,-52.56 -41.17,-65.84 -21.28,-13.27 -53.14,-19.9 -95.58,-19.9 -44.36,0 -78.17,8.21 -101.4,24.63 -23.25,16.44 -34.86,40.13 -34.86,71.06 0,18.13 3.4,33.27 10.2,45.42 6.8,12.14 16.26,21.85 28.42,29.15 5.5,3.24 11.17,6.19 16.99,8.85 5.83,2.68 13.6,5.42 23.31,8.27 9.72,2.84 23.18,6.27 40.34,10.32 14.9,3.08 27.37,5.87 37.4,8.38 10.04,2.51 17.65,4.82 22.84,6.93 10.52,4.22 18.62,9.48 24.29,15.79 5.66,6.32 8.51,14.34 8.51,24.05 0,15.22 -6.23,26.64 -18.72,34.25 -12.47,7.6 -31.24,11.42 -56.36,11.42 -24.12,0 -42.99,-4.08 -56.59,-12.26 -13.6,-8.17 -22.75,-21.34 -27.44,-39.47l-69.23 11.41c14.25,62.68 64.7,94.01 151.32,94.01 48.1,0 84.59,-8.87 109.44,-26.6 24.84,-17.73 37.26,-43.36 37.26,-76.87 0,-14.26 -2.18,-26.86 -6.54,-37.78 -4.38,-10.92 -10.68,-20.37 -18.95,-28.28z"/>
|
||||
<path class="fil0" d="M4549.65 1998.92c48.1,0 84.59,-8.87 109.44,-26.6 24.84,-17.73 37.27,-43.36 37.27,-76.87 0,-14.26 -2.19,-26.86 -6.55,-37.78 -4.38,-10.92 -10.68,-20.37 -18.95,-28.28 -4.06,-3.88 -8.85,-7.53 -14.45,-10.94 -5.59,-3.39 -11.96,-6.64 -19.07,-9.72 -7.13,-2.91 -16.61,-5.93 -28.43,-9.1 -11.81,-3.16 -25.91,-6.51 -42.25,-10.07 -28.18,-5.99 -46.57,-10.94 -55.14,-14.83 -9.08,-4.04 -15.92,-8.89 -20.52,-14.58 -4.61,-5.66 -6.93,-12.95 -6.93,-21.85 0,-13.6 5.42,-23.81 16.28,-30.62 10.84,-6.8 27.11,-10.19 48.81,-10.19 20.4,0 36.08,3.72 47,11.17 10.92,7.46 18.17,18.55 21.74,33.28l69.46 -9.47c-6.16,-30.62 -19.87,-52.56 -41.17,-65.84 -21.28,-13.27 -53.14,-19.9 -95.58,-19.9 -44.36,0 -78.17,8.21 -101.4,24.63 -23.25,16.44 -34.86,40.13 -34.86,71.06 0,18.13 3.4,33.27 10.2,45.42 6.8,12.14 16.26,21.85 28.42,29.15 5.5,3.24 11.17,6.19 16.99,8.85 5.83,2.68 13.6,5.42 23.31,8.27 9.72,2.84 23.18,6.27 40.34,10.32 14.9,3.08 27.37,5.87 37.4,8.38 10.04,2.51 17.65,4.82 22.84,6.93 10.52,4.22 18.62,9.48 24.29,15.79 5.66,6.32 8.51,14.34 8.51,24.05 0,15.22 -6.23,26.64 -18.72,34.25 -12.47,7.6 -31.24,11.42 -56.36,11.42 -24.12,0 -42.99,-4.08 -56.59,-12.26 -13.6,-8.17 -22.75,-21.34 -27.44,-39.47l-69.23 11.41c14.25,62.68 64.7,94.01 151.32,94.01z"/>
|
||||
<polygon class="fil0" points="5117.7,1707.2 5117.7,1651.83 4848.57,1651.83 4848.57,1994.07 5127.66,1994.07 5127.66,1938.68 4920.23,1938.68 4920.23,1848.56 5102.89,1848.56 5102.89,1793.18 4920.23,1793.18 4920.23,1707.2 "/>
|
||||
<path class="fil0" d="M5279.85 1994.07l138.47 0c32.7,0 61.57,-6.84 86.58,-20.54 25.02,-13.67 44.62,-33.47 58.8,-59.37 14.16,-25.91 21.24,-57.16 21.24,-93.77 0,-54.25 -15.78,-95.9 -47.37,-124.97 -31.57,-29.06 -76.18,-43.59 -133.82,-43.59l-123.89 0 0 342.24zm71.67 -286.87l0 0 50.76 0c35.47,0 62.75,9.68 81.85,29.03 19.12,19.35 28.67,47.4 28.67,84.16 0,24.29 -4.12,45.26 -12.39,62.91 -8.26,17.66 -19.87,31.3 -34.86,40.94 -14.97,9.62 -32.76,14.44 -53.32,14.44l-60.71 0 0 -231.48z"/>
|
||||
<polygon class="fil0" points="6047.96,1868.49 6223.09,1868.49 6223.09,1813.11 6047.96,1813.11 6047.96,1707.2 6228.67,1707.2 6228.67,1651.83 5976.31,1651.83 5976.31,1994.07 6047.96,1994.07 "/>
|
||||
<path class="fil0" d="M6446.68 1977.42c25.76,14.34 56.52,21.5 92.3,21.5 34.98,0 65.47,-7.37 91.46,-22.11 25.99,-14.72 46.14,-35.41 60.48,-62.05 14.34,-26.64 21.5,-57.76 21.5,-93.4 0,-36.26 -6.9,-67.45 -20.66,-93.52 -13.75,-26.07 -33.51,-46.11 -59.27,-60.12 -25.74,-14 -56.75,-21 -93.01,-21 -36.12,0 -67.1,6.91 -92.91,20.75 -25.83,13.85 -45.59,33.81 -59.27,59.89 -13.69,26.06 -20.53,57.39 -20.53,93.99 0,36.61 6.87,68.17 20.64,94.74 13.78,26.56 33.54,47 59.27,61.32zm18.47 -243.14l0 0c17.49,-20.81 42.27,-31.2 74.33,-31.2 31.57,0 56.1,10.49 73.58,31.44 17.49,20.97 26.23,49.91 26.23,86.84 0,38.71 -8.69,68.57 -26.11,89.64 -17.41,21.04 -42.14,31.56 -74.21,31.56 -20.57,0 -38.32,-4.89 -53.3,-14.7 -14.98,-9.79 -26.53,-23.76 -34.61,-41.89 -8.1,-18.13 -12.15,-39.67 -12.15,-64.61 0,-37.23 8.74,-66.27 26.23,-87.08z"/>
|
||||
<path class="fil0" d="M6937.74 1864.11l84.04 0 79.43 129.96 80.64 0 -92.54 -143.8c23.15,-5.49 41.54,-16.96 55.14,-34.36 13.6,-17.4 20.4,-38.1 20.4,-62.06 0,-32.87 -11.1,-58.09 -33.29,-75.67 -22.18,-17.56 -53.68,-26.34 -94.48,-26.34l-171 0 0 342.24 71.66 0 0 -129.96zm0 -156.67l0 0 91.81 0c21.06,0 36.84,4.12 47.37,12.39 10.52,8.26 15.78,20.57 15.78,36.93 0,16.35 -5.17,29.06 -15.55,38.13 -10.36,9.08 -25.57,13.6 -45.66,13.6l-93.75 0 0 -101.05z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="fil0" d="M627.35 3128.5c-312.41,-99.91 -458.08,-146.06 -455.14,-313.6 1.93,-109.46 154.21,-225.53 405.68,-225.47 287.79,0.07 385.03,181.46 397.9,255.57l111.56 1.45c-9.62,-46.47 -97.83,-297.67 -524.58,-297.67 -259.34,0 -495.04,135.69 -491.46,309.29 4.01,196.05 149.13,276.7 449.94,366.97 355.51,106.69 500.8,187.23 515.93,345.01 19.96,208.26 -256.33,323.28 -475.22,301.64 -358.92,-35.49 -444.44,-293.58 -445.96,-314.95l-115.99 0c39.03,195.14 288.44,356.1 562.16,356.1 379.65,0 604.74,-178.57 573.21,-410.47 -30.07,-221.31 -214.33,-279.92 -508.03,-373.87z"/>
|
||||
<path class="fil0" d="M9792.83 3502.37c-30.09,-221.31 -214.33,-279.92 -508.03,-373.87 -312.41,-99.91 -458.08,-146.06 -455.14,-313.6 1.9,-109.46 154.21,-225.53 405.68,-225.47 287.79,0.07 385,181.46 397.9,255.57l111.56 1.45c-9.62,-46.47 -97.83,-297.67 -524.58,-297.67 -259.34,0 -495.04,135.69 -491.46,309.29 4.01,196.05 149.13,276.7 449.95,366.97 355.48,106.69 500.8,187.23 515.92,345.01 19.96,208.26 -256.33,323.28 -475.22,301.64 -358.92,-35.49 -444.44,-293.58 -445.96,-314.95l-116.01 0c39.05,195.14 288.46,356.1 562.18,356.1 379.65,0 604.74,-178.57 573.21,-410.47z"/>
|
||||
<path class="fil0" d="M5602.13 3400.62c-63.13,271.92 -276.29,471.98 -529.27,471.98 -303.48,0 -549.2,-287.46 -549.2,-641.93 0,-354.48 245.71,-641.94 549.2,-641.94 253.46,0 466.14,200.06 529.27,472.47l132.07 0c-75.26,-294.75 -343.3,-512.77 -661.34,-512.77 -376.82,0 -682.73,305.43 -682.73,682.24 0,377.29 305.91,682.72 682.73,682.72 318.04,0 585.59,-217.54 661.34,-512.77l-132.07 0z"/>
|
||||
<path class="fil0" d="M7158.78 3127.72l54.39 0 78.2 0c-49.56,-327.76 -332.66,-579.29 -674.5,-579.29 -377.27,0 -682.7,305.43 -682.7,682.24 0,377.29 305.43,682.72 682.7,682.72 258.81,0 483.69,-143.73 599.24,-356.41l-126.28 0c-95.64,188.89 -271.91,315.62 -472.96,315.62 -303.48,0 -549.64,-287.46 -549.64,-641.93 0,-34.96 2.42,-69.44 7.25,-102.95l1084.3 0zm-541.91 -538.99l0 0c261.28,0 480.27,213.66 535.61,499.67l-1071.65 0c55.35,-286.01 274.32,-499.67 536.04,-499.67z"/>
|
||||
<path class="fil0" d="M8004.3 3128.5c-312.38,-99.91 -458.08,-146.06 -455.14,-313.6 1.93,-109.46 154.21,-225.53 405.7,-225.47 287.79,0.07 385.01,181.46 397.88,255.57l111.56 1.45c-9.62,-46.47 -97.8,-297.67 -524.56,-297.67 -259.33,0 -495.03,135.69 -491.48,309.29 4.03,196.05 149.13,276.7 449.94,366.97 355.51,106.69 500.8,187.23 515.93,345.01 19.96,208.26 -256.3,323.28 -475.22,301.64 -358.9,-35.49 -444.45,-293.58 -445.96,-314.95l-115.99 0c39.03,195.14 288.46,356.1 562.16,356.1 379.64,0 604.74,-178.57 573.23,-410.47 -30.09,-221.31 -214.35,-279.92 -508.05,-373.87z"/>
|
||||
<path class="fil0" d="M3529.31 2588.72c252.98,0 466.16,200.06 529.28,472.47l131.59 0c-75.26,-294.75 -342.82,-512.77 -660.87,-512.77 -377.29,0 -682.72,305.43 -682.72,682.24 0,377.29 305.43,682.72 682.72,682.72 318.05,0 585.12,-217.54 660.87,-512.77l-131.59 0c-63.12,271.92 -276.77,471.98 -529.28,471.98 -303.48,0 -549.67,-287.46 -549.67,-641.93 0,-354.48 246.18,-641.94 549.67,-641.94z"/>
|
||||
<path class="fil0" d="M2453.98 2562.09l-0.19 0 0 849.19c0,254.83 -206.58,461.42 -461.41,461.42 -254.86,0 -461.44,-206.59 -461.44,-461.42l0 -849.19 -123.81 0 0 852.58c0,336.18 313.22,498.69 588.65,498.69 205.52,0 381.87,-124.37 458.21,-301.9l0 261.24 123.81 0 0 -1310.94 -123.81 0 0 0.33z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="fil0" d="M2863.3 4731.51c41.79,0 74.39,11.02 97.36,33.06 22.96,22.04 35.82,46.84 38.11,74.85l-43.17 0c-5.05,-21.13 -15.15,-38.11 -29.85,-50.51 -14.7,-12.4 -35.37,-18.37 -61.99,-18.37 -32.15,0 -58.32,11.02 -78.53,33.98 -19.75,22.97 -29.86,57.87 -29.86,104.71 0,38.57 9.19,69.8 27.1,94.13 17.91,23.88 45,35.82 80.36,35.82 33.06,0 58.32,-12.4 75.32,-37.65 9.18,-13.32 16.07,-30.77 20.2,-52.81l43.63 0c-3.68,34.9 -16.53,63.83 -38.57,87.25 -26.18,28.47 -61.54,42.71 -105.63,42.71 -38.57,0 -70.72,-11.95 -96.43,-34.9 -34.44,-30.77 -51.44,-78.06 -51.44,-141.9 0,-48.68 12.86,-88.63 38.57,-119.4 27.55,-33.98 66.13,-50.97 114.81,-50.97z"/>
|
||||
<polygon id="_1" class="fil0" points="3146.94,4740.7 3191.94,4740.7 3191.94,4876.62 3363.23,4876.62 3363.23,4740.7 3408.23,4740.7 3408.23,5069.96 3363.23,5069.96 3363.23,4916.12 3191.94,4916.12 3191.94,5069.96 3146.94,5069.96 "/>
|
||||
<polygon id="_2" class="fil0" points="3576.6,4740.7 3621.61,4740.7 3621.61,5069.96 3576.6,5069.96 "/>
|
||||
<polygon id="_3" class="fil0" points="3783.39,4740.7 3827.01,4740.7 3827.01,4900.97 3987.74,4740.7 4049.27,4740.7 3911.96,4873.42 4052.94,5069.96 3995.08,5069.96 3879.82,4904.64 3827.01,4955.15 3827.01,5069.96 3783.39,5069.96 "/>
|
||||
<path id="_4" class="fil0" d="M4492.31 4868.82c14.23,-10.11 23.87,-18.37 29.38,-24.8 8.73,-10.11 12.86,-21.13 12.86,-33.53 0,-10.11 -3.22,-18.37 -9.64,-25.26 -6.43,-6.89 -14.69,-10.56 -25.71,-10.56 -16.53,0 -28.02,5.51 -34.44,16.53 -3.68,5.51 -5.06,11.94 -5.06,18.82 0,8.73 2.3,17.46 7.35,26.18 5.05,8.28 13.31,19.3 25.26,32.61zm-10.57 173.12c16.53,0 30.77,-3.67 42.71,-11.02 11.95,-7.8 21.59,-16.53 28.02,-25.71l-74.85 -90.92c-21.12,14.24 -34.44,24.8 -40.87,32.15 -10.11,11.47 -15.15,25.25 -15.15,41.33 0,17.45 6.43,30.76 19.29,40.4 12.86,9.19 26.64,13.78 40.87,13.78zm-26.18 -155.21c-14.23,-16.07 -23.42,-29.39 -28.01,-40.41 -4.6,-11.02 -7.35,-21.58 -7.35,-31.69 0,-21.13 7.35,-38.57 21.58,-52.81 14.7,-13.78 33.53,-20.66 57.86,-20.66 22.97,0 40.41,6.44 53.28,19.29 12.85,12.86 19.29,28.48 19.29,46.84 0,21.13 -6.44,39.5 -19.75,55.12 -7.8,9.64 -20.66,20.2 -39.04,32.14l60.16 71.64c4.13,-11.94 6.89,-20.66 8.27,-26.63 1.83,-5.97 3.21,-14.24 5.05,-24.8l38.12 0c-2.3,21.12 -7.35,41.33 -15.15,60.62 -7.81,19.29 -11.48,27.09 -11.48,23.42l58.78 71.17 -52.35 0 -30.77 -37.66c-12.4,13.32 -23.42,22.97 -33.52,28.93 -17.91,11.02 -38.57,16.53 -61.54,16.53 -34.44,0 -59.24,-9.18 -74.85,-28.01 -15.62,-18.37 -23.42,-39.04 -23.42,-62.46 0,-24.8 7.8,-45.46 22.96,-62.46 9.18,-10.1 26.64,-22.96 51.89,-38.11z"/>
|
||||
<path id="_5" class="fil0" d="M5152.26 4934.95l-50.06 -145.58 -52.8 145.58 102.86 0zm-73.01 -194.25l50.51 0 119.4 329.26 -49.13 0 -33.07 -98.73 -130.41 0 -35.82 98.73 -45.46 0 123.99 -329.26z"/>
|
||||
<path id="_6" class="fil0" d="M5517.55 4731.51c41.79,0 74.39,11.02 97.36,33.06 22.96,22.04 35.82,46.84 38.11,74.85l-43.17 0c-5.05,-21.13 -15.15,-38.11 -29.85,-50.51 -14.7,-12.4 -35.37,-18.37 -61.99,-18.37 -32.15,0 -58.32,11.02 -78.53,33.98 -19.75,22.97 -29.86,57.87 -29.86,104.71 0,38.57 9.19,69.8 27.1,94.13 17.91,23.88 45,35.82 80.36,35.82 33.06,0 58.32,-12.4 75.32,-37.65 9.18,-13.32 16.07,-30.77 20.2,-52.81l43.63 0c-3.68,34.9 -16.53,63.83 -38.57,87.25 -26.18,28.47 -61.54,42.71 -105.63,42.71 -38.57,0 -70.72,-11.95 -96.43,-34.9 -34.44,-30.77 -51.44,-78.06 -51.44,-141.9 0,-48.68 12.86,-88.63 38.57,-119.4 27.55,-33.98 66.13,-50.97 114.81,-50.97z"/>
|
||||
<polygon id="_7" class="fil0" points="6039.52,4740.7 6039.52,4779.73 5928.39,4779.73 5928.39,5069.96 5883.39,5069.96 5883.39,4779.73 5772.26,4779.73 5772.26,4740.7 "/>
|
||||
<polygon id="_8" class="fil0" points="6179.72,4740.7 6224.73,4740.7 6224.73,5069.96 6179.72,5069.96 "/>
|
||||
<polygon id="_9" class="fil0" points="6412.69,4740.7 6507.28,5021.28 6600.51,4740.7 6650.56,4740.7 6530.7,5069.96 6483.4,5069.96 6363.09,4740.7 "/>
|
||||
<polygon id="_10" class="fil0" points="6786.25,4740.7 7026.42,4740.7 7026.42,4781.1 6829.87,4781.1 6829.87,4880.76 7011.72,4880.76 7011.72,4918.88 6829.87,4918.88 6829.87,5030.92 7030.09,5030.92 7030.09,5069.96 6786.25,5069.96 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 15 KiB |
@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2021.5 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="101.681mm" height="52.664mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 9785.87 5068.43"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:#2B2B2A;fill-rule:nonzero}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Слой_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_105553243821280">
|
||||
<path class="fil0" d="M4903.31 804.37c54.01,19.95 154.59,32.65 273.14,7.79 134.15,-28.14 227.19,-126.74 210.26,-234.18 -15.56,-98.78 -78.42,-144.64 -183.02,-184.69 -4.99,-103.47 -48.37,-197.48 -117.37,-265.9 -25.59,-25.58 -54.51,-47.28 -86.21,-65.08 -19.34,-12.42 -50.08,-24.48 -52.86,-25.59 30.04,-7.23 62.86,-11.36 101.24,-12.24 160.75,-3.71 233.02,102.92 241.44,154.65l67.31 1.11c-5.57,-28.37 -59.54,-180.23 -318.2,-180.23 -47.84,0 -94.56,8.34 -136.28,22.25 -33.38,-8.9 -66.76,-13.91 -102.37,-13.91 -101.23,0 -403.3,0.56 -403.3,0.56l0 17.24c35.06,0 57.31,8.35 71.76,20.59 26.16,21.14 26.16,55.08 26.16,74.54 0,145.61 0,436.82 0,582.42 0,22.81 0,67.87 -45.63,86.78 -12.79,5.56 -30.05,9.46 -52.29,9.46l0 17.79c0,0 244.88,0 395.69,0 61.64,0 110.52,-13.35 110.52,-13.35zm-333.22 -11.13l0 0 0 -759.87 180.25 0c66.76,0 127.29,11.34 204.71,62.85 94.25,62.72 161.42,156.32 171.88,270.36 16.63,181.37 -83.58,312.32 -175.22,367.69 23.36,10.02 47.26,17.25 62.85,21.14 96.8,-61.19 164.89,-170.02 183.56,-279.25 84.01,39.5 122.93,81.22 129.07,142.41 4.99,52.85 -21.13,95.68 -63.42,126.83 -57.85,43.4 -148.04,64.57 -225.28,56.74 -162.45,-16.46 -249.97,-113.59 -270.37,-191.36l-70.07 0c12.84,56.49 58.52,124.57 147.4,169.66 0,0 -38.51,12.79 -97.35,12.79 -44.5,0 -178.02,0 -178.02,0z"/>
|
||||
<path class="fil0" d="M5014.02 409.98c23.36,7.23 44.49,13.91 64.51,20.58 1.12,-21.69 1.68,-51.73 -3.34,-80.66 -187.47,-59.52 -274.8,-87.89 -273.11,-189.13 0.55,-23.92 12.22,-47.28 34.46,-68.42 -13.33,-5 -27.81,-9.46 -41.14,-12.24 -42.46,35.57 -54.55,75.83 -54.51,107.37 0.11,118.51 90.66,167.99 273.13,222.51z"/>
|
||||
</g>
|
||||
<g id="_105553243768096">
|
||||
<g>
|
||||
<path class="fil0" d="M2918.1 1912.22c14.16,-25.88 21.22,-57.1 21.22,-93.67 0,-54.19 -15.76,-95.8 -47.32,-124.84 -31.53,-29.03 -76.11,-43.55 -133.69,-43.55l-123.76 0 0 341.9 138.32 0c32.67,0 61.52,-6.83 86.49,-20.52 25,-13.66 44.58,-33.43 58.74,-59.31zm-98.03 10.08l0 0c-14.97,9.61 -32.73,14.43 -53.28,14.43l-60.65 0 0 -231.25 50.71 0c35.44,0 62.69,9.67 81.77,29 19.1,19.33 28.65,47.35 28.65,84.08 0,24.26 -4.13,45.21 -12.39,62.85 -8.24,17.64 -19.85,31.27 -34.81,40.9z"/>
|
||||
<path class="fil0" d="M3164.42 1862.22l83.94 0 79.36 129.83 80.55 0 -92.44 -143.66c23.13,-5.49 41.5,-16.94 55.09,-34.33 13.58,-17.38 20.37,-38.06 20.37,-62 0,-32.84 -11.08,-58.03 -33.25,-75.6 -22.16,-17.54 -53.63,-26.31 -94.39,-26.31l-170.83 0 0 341.9 71.59 0 0 -129.83zm0 -156.51l0 0 91.71 0c21.05,0 36.81,4.12 47.32,12.38 10.52,8.25 15.77,20.55 15.77,36.89 0,16.33 -5.17,29.03 -15.53,38.09 -10.35,9.07 -25.54,13.59 -45.62,13.59l-93.65 0 0 -100.95z"/>
|
||||
<polygon class="fil0" points="3829.91,1936.72 3622.69,1936.72 3622.69,1846.69 3805.17,1846.69 3805.17,1791.37 3622.69,1791.37 3622.69,1705.48 3819.96,1705.48 3819.96,1650.16 3551.11,1650.16 3551.11,1992.05 3829.91,1992.05 "/>
|
||||
<path class="fil0" d="M4235.28 1827.54c-4.05,-3.88 -8.84,-7.53 -14.44,-10.93 -5.58,-3.39 -11.94,-6.63 -19.05,-9.71 -7.12,-2.91 -16.59,-5.93 -28.4,-9.09 -11.8,-3.16 -25.88,-6.51 -42.21,-10.06 -28.15,-5.99 -46.52,-10.93 -55.09,-14.81 -9.07,-4.03 -15.9,-8.88 -20.5,-14.56 -4.61,-5.66 -6.92,-12.93 -6.92,-21.83 0,-13.59 5.42,-23.79 16.26,-30.59 10.83,-6.79 27.08,-10.18 48.77,-10.18 20.38,0 36.04,3.71 46.96,11.15 10.91,7.45 18.15,18.53 21.72,33.25l69.39 -9.46c-6.15,-30.59 -19.85,-52.51 -41.13,-65.77 -21.26,-13.25 -53.09,-19.88 -95.48,-19.88 -44.32,0 -78.09,8.2 -101.29,24.61 -23.22,16.43 -34.83,40.09 -34.83,70.99 0,18.11 3.4,33.23 10.19,45.38 6.79,12.13 16.25,21.83 28.39,29.12 5.5,3.23 11.15,6.19 16.98,8.84 5.82,2.68 13.59,5.42 23.29,8.26 9.71,2.84 23.16,6.27 40.3,10.31 14.89,3.08 27.34,5.86 37.36,8.37 10.03,2.51 17.63,4.81 22.82,6.92 10.51,4.22 18.6,9.47 24.26,15.77 5.66,6.31 8.5,14.32 8.5,24.02 0,15.21 -6.23,26.61 -18.7,34.21 -12.45,7.59 -31.21,11.4 -56.3,11.4 -24.1,0 -42.94,-4.08 -56.53,-12.25 -13.59,-8.16 -22.72,-21.32 -27.41,-39.43l-69.16 11.39c14.23,62.61 64.64,93.91 151.17,93.91 48.05,0 84.5,-8.86 109.33,-26.57 24.81,-17.71 37.23,-43.32 37.23,-76.79 0,-14.24 -2.18,-26.83 -6.53,-37.75 -4.38,-10.91 -10.67,-20.35 -18.93,-28.26z"/>
|
||||
<path class="fil0" d="M4545.05 1996.9c48.05,0 84.5,-8.86 109.33,-26.57 24.81,-17.71 37.24,-43.32 37.24,-76.79 0,-14.24 -2.18,-26.83 -6.54,-37.75 -4.38,-10.91 -10.67,-20.35 -18.93,-28.26 -4.05,-3.88 -8.84,-7.53 -14.44,-10.93 -5.58,-3.39 -11.94,-6.63 -19.05,-9.71 -7.12,-2.91 -16.59,-5.93 -28.4,-9.09 -11.8,-3.16 -25.88,-6.51 -42.21,-10.06 -28.15,-5.99 -46.52,-10.93 -55.09,-14.81 -9.07,-4.03 -15.9,-8.88 -20.5,-14.56 -4.61,-5.66 -6.92,-12.93 -6.92,-21.83 0,-13.59 5.42,-23.79 16.26,-30.59 10.83,-6.79 27.08,-10.18 48.77,-10.18 20.38,0 36.04,3.71 46.96,11.15 10.91,7.45 18.15,18.53 21.72,33.25l69.39 -9.46c-6.15,-30.59 -19.85,-52.51 -41.13,-65.77 -21.26,-13.25 -53.09,-19.88 -95.48,-19.88 -44.32,0 -78.09,8.2 -101.29,24.61 -23.22,16.43 -34.83,40.09 -34.83,70.99 0,18.11 3.4,33.23 10.19,45.38 6.79,12.13 16.25,21.83 28.39,29.12 5.5,3.23 11.15,6.19 16.98,8.84 5.82,2.68 13.59,5.42 23.29,8.26 9.71,2.84 23.16,6.27 40.3,10.31 14.89,3.08 27.34,5.86 37.36,8.37 10.03,2.51 17.63,4.81 22.82,6.92 10.51,4.22 18.6,9.47 24.26,15.77 5.66,6.31 8.5,14.32 8.5,24.02 0,15.21 -6.23,26.61 -18.7,34.21 -12.45,7.59 -31.21,11.4 -56.3,11.4 -24.1,0 -42.94,-4.08 -56.53,-12.25 -13.59,-8.16 -22.72,-21.32 -27.41,-39.43l-69.16 11.39c14.23,62.61 64.64,93.91 151.17,93.91z"/>
|
||||
<polygon class="fil0" points="5112.53,1705.48 5112.53,1650.16 4843.67,1650.16 4843.67,1992.05 5122.48,1992.05 5122.48,1936.72 4915.25,1936.72 4915.25,1846.69 5097.74,1846.69 5097.74,1791.37 4915.25,1791.37 4915.25,1705.48 "/>
|
||||
<path class="fil0" d="M5274.51 1992.05l138.33 0c32.66,0 61.51,-6.83 86.49,-20.52 24.99,-13.66 44.58,-33.43 58.74,-59.31 14.15,-25.88 21.22,-57.1 21.22,-93.67 0,-54.19 -15.76,-95.8 -47.32,-124.84 -31.54,-29.03 -76.11,-43.55 -133.69,-43.55l-123.77 0 0 341.9zm71.59 -286.58l0 0 50.71 0c35.44,0 62.68,9.67 81.77,29 19.1,19.33 28.64,47.35 28.64,84.08 0,24.26 -4.12,45.21 -12.38,62.85 -8.25,17.64 -19.85,31.27 -34.82,40.9 -14.96,9.61 -32.72,14.43 -53.27,14.43l-60.65 0 0 -231.25z"/>
|
||||
<polygon class="fil0" points="6041.85,1866.6 6216.8,1866.6 6216.8,1811.27 6041.85,1811.27 6041.85,1705.48 6222.38,1705.48 6222.38,1650.16 5970.26,1650.16 5970.26,1992.05 6041.85,1992.05 "/>
|
||||
<path class="fil0" d="M6440.16 1975.42c25.73,14.32 56.46,21.48 92.21,21.48 34.95,0 65.41,-7.36 91.37,-22.09 25.97,-14.71 46.09,-35.38 60.42,-61.99 14.32,-26.61 21.48,-57.71 21.48,-93.31 0,-36.23 -6.89,-67.38 -20.64,-93.42 -13.73,-26.04 -33.47,-46.06 -59.21,-60.05 -25.72,-13.98 -56.7,-20.98 -92.92,-20.98 -36.08,0 -67.03,6.9 -92.81,20.73 -25.8,13.84 -45.54,33.77 -59.21,59.83 -13.68,26.03 -20.51,57.33 -20.51,93.89 0,36.57 6.86,68.1 20.61,94.64 13.76,26.53 33.5,46.96 59.21,61.26zm18.45 -242.89l0 0c17.47,-20.79 42.23,-31.17 74.26,-31.17 31.54,0 56.04,10.48 73.51,31.41 17.47,20.95 26.21,49.86 26.21,86.75 0,38.67 -8.68,68.5 -26.08,89.55 -17.39,21.02 -42.1,31.53 -74.13,31.53 -20.55,0 -38.28,-4.89 -53.25,-14.69 -14.97,-9.78 -26.5,-23.73 -34.58,-41.85 -8.09,-18.11 -12.14,-39.63 -12.14,-64.55 0,-37.2 8.73,-66.2 26.21,-86.99z"/>
|
||||
<path class="fil0" d="M6930.73 1862.22l83.95 0 79.35 129.83 80.56 0 -92.45 -143.66c23.13,-5.49 41.5,-16.94 55.09,-34.33 13.59,-17.38 20.38,-38.06 20.38,-62 0,-32.84 -11.09,-58.03 -33.26,-75.6 -22.15,-17.54 -53.63,-26.31 -94.38,-26.31l-170.83 0 0 341.9 71.58 0 0 -129.83zm0 -156.51l0 0 91.72 0c21.04,0 36.8,4.12 47.32,12.38 10.51,8.25 15.76,20.55 15.76,36.89 0,16.33 -5.17,29.03 -15.53,38.09 -10.35,9.07 -25.54,13.59 -45.62,13.59l-93.65 0 0 -100.95z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="fil0" d="M626.71 3125.34c-312.09,-99.81 -457.62,-145.91 -454.68,-313.28 1.92,-109.35 154.05,-225.3 405.27,-225.24 287.5,0.07 384.64,181.28 397.49,255.31l111.45 1.45c-9.61,-46.43 -97.73,-297.37 -524.05,-297.37 -259.08,0 -494.54,135.56 -490.96,308.98 4,195.85 148.98,276.42 449.48,366.6 355.15,106.58 500.3,187.04 515.41,344.66 19.94,208.05 -256.07,322.96 -474.74,301.33 -358.56,-35.46 -443.99,-293.28 -445.51,-314.63l-115.87 0c38.99,194.95 288.15,355.74 561.59,355.74 379.27,0 604.13,-178.39 572.63,-410.05 -30.04,-221.08 -214.12,-279.64 -507.52,-373.49z"/>
|
||||
<path class="fil0" d="M9782.93 3498.83c-30.06,-221.08 -214.12,-279.64 -507.52,-373.49 -312.09,-99.81 -457.62,-145.91 -454.68,-313.28 1.9,-109.35 154.05,-225.3 405.27,-225.24 287.5,0.07 384.61,181.28 397.49,255.31l111.45 1.45c-9.61,-46.43 -97.73,-297.37 -524.05,-297.37 -259.08,0 -494.54,135.56 -490.96,308.98 4,195.85 148.98,276.42 449.49,366.6 355.12,106.58 500.29,187.04 515.4,344.66 19.94,208.05 -256.07,322.96 -474.74,301.33 -358.56,-35.46 -443.99,-293.28 -445.51,-314.63l-115.89 0c39.01,194.95 288.16,355.74 561.61,355.74 379.27,0 604.13,-178.39 572.63,-410.05z"/>
|
||||
<path class="fil0" d="M5596.47 3397.18c-63.07,271.65 -276.01,471.5 -528.74,471.5 -303.18,0 -548.64,-287.17 -548.64,-641.28 0,-354.12 245.46,-641.29 548.64,-641.29 253.2,0 465.67,199.85 528.74,471.99l131.94 0c-75.18,-294.45 -342.95,-512.25 -660.67,-512.25 -376.44,0 -682.04,305.12 -682.04,681.55 0,376.91 305.6,682.03 682.04,682.03 317.72,0 585,-217.32 660.67,-512.25l-131.94 0z"/>
|
||||
<path class="fil0" d="M7151.54 3124.56l54.34 0 78.12 0c-49.51,-327.43 -332.32,-578.71 -673.82,-578.71 -376.89,0 -682.01,305.12 -682.01,681.55 0,376.91 305.12,682.03 682.01,682.03 258.55,0 483.2,-143.58 598.64,-356.05l-126.15 0c-95.55,188.7 -271.63,315.3 -472.49,315.3 -303.18,0 -549.08,-287.17 -549.08,-641.28 0,-34.93 2.42,-69.37 7.25,-102.84l1083.2 0zm-541.36 -538.45l0 0c261.01,0 479.78,213.44 535.07,499.16l-1070.56 0c55.29,-285.72 274.05,-499.16 535.49,-499.16z"/>
|
||||
<path class="fil0" d="M7996.21 3125.34c-312.06,-99.81 -457.62,-145.91 -454.68,-313.28 1.92,-109.35 154.05,-225.3 405.29,-225.24 287.5,0.07 384.62,181.28 397.47,255.31l111.45 1.45c-9.61,-46.43 -97.7,-297.37 -524.03,-297.37 -259.07,0 -494.53,135.56 -490.98,308.98 4.02,195.85 148.98,276.42 449.48,366.6 355.15,106.58 500.3,187.04 515.41,344.66 19.94,208.05 -256.04,322.96 -474.74,301.33 -358.54,-35.46 -444,-293.28 -445.51,-314.63l-115.87 0c38.99,194.95 288.16,355.74 561.59,355.74 379.26,0 604.13,-178.39 572.65,-410.05 -30.06,-221.08 -214.14,-279.64 -507.54,-373.49z"/>
|
||||
<path class="fil0" d="M3525.75 2586.11c252.73,0 465.69,199.85 528.75,471.99l131.46 0c-75.18,-294.45 -342.47,-512.25 -660.2,-512.25 -376.91,0 -682.03,305.12 -682.03,681.55 0,376.91 305.12,682.03 682.03,682.03 317.73,0 584.53,-217.32 660.2,-512.25l-131.46 0c-63.06,271.65 -276.49,471.5 -528.75,471.5 -303.18,0 -549.11,-287.17 -549.11,-641.28 0,-354.12 245.93,-641.29 549.11,-641.29z"/>
|
||||
<path class="fil0" d="M2451.5 2559.5l-0.19 0 0 848.33c0,254.58 -206.37,460.96 -460.95,460.96 -254.61,0 -460.97,-206.38 -460.97,-460.96l0 -848.33 -123.69 0 0 851.72c0,335.84 312.91,498.18 588.05,498.18 205.31,0 381.48,-124.25 457.75,-301.59l0 260.98 123.69 0 0 -1309.62 -123.69 0 0 0.33z"/>
|
||||
</g>
|
||||
</g>
|
||||
<polygon class="fil0" points="2756.34,4729.87 2801.29,4729.87 2801.29,4865.66 2972.41,4865.66 2972.41,4729.87 3017.36,4729.87 3017.36,5058.8 2972.41,5058.8 2972.41,4905.12 2801.29,4905.12 2801.29,5058.8 2756.34,5058.8 "/>
|
||||
<path id="_1" class="fil0" d="M3317.68 4720.69c57.81,0 100.93,18.82 128.91,55.97 22.02,29.35 32.57,66.52 32.57,111.94 0,49.08 -12.39,89.91 -37.16,122.49 -29.35,38.07 -71.1,57.34 -125.24,57.34 -50.92,0 -90.37,-16.51 -119.27,-50.01 -26.15,-32.57 -39,-73.39 -39,-122.49 0,-44.49 11.01,-82.57 33.03,-114.23 28.44,-40.83 70.65,-61.02 126.15,-61.02zm4.59 307.83c39,0 67.44,-14.22 84.87,-42.2 17.89,-27.99 26.6,-60.55 26.6,-97.26 0,-38.53 -10.09,-69.73 -30.28,-93.12 -20.64,-23.86 -48.17,-35.33 -83.04,-35.33 -34.41,0 -61.93,11.47 -83.49,34.87 -21.56,23.4 -32.12,57.8 -32.12,103.22 0,36.71 9.18,67.44 27.52,92.21 18.35,25.23 48.17,37.62 89.92,37.62z"/>
|
||||
<path id="_2" class="fil0" d="M3620.6 4729.87l63.77 0 94.5 278 94.05 -278 62.85 0 0 328.92 -42.21 0 0 -194.05c0,-6.88 0,-17.89 0.46,-33.49 0.46,-15.6 0.46,-32.12 0.46,-50.01l-94.05 277.55 -44.04 0 -94.51 -277.55 0 10.1c0,8.26 0,20.18 0.46,36.71 0.46,16.51 0.91,28.9 0.91,36.7l0 194.05 -42.66 0 0 -328.92z"/>
|
||||
<polygon id="_3" class="fil0" points="4097.01,4729.87 4336.94,4729.87 4336.94,4770.24 4140.59,4770.24 4140.59,4869.79 4322.26,4869.79 4322.26,4907.87 4140.59,4907.87 4140.59,5019.8 4340.61,5019.8 4340.61,5058.8 4097.01,5058.8 "/>
|
||||
<path id="_4" class="fil0" d="M5345.98 4952.83c0.92,18.35 5.5,33.49 12.85,45.42 15.14,21.56 40.83,32.57 78.45,32.57 16.51,0 32.12,-2.29 45.42,-7.33 26.61,-9.18 39.91,-25.7 39.91,-49.54 0,-17.89 -5.5,-30.74 -16.51,-38.53 -11.47,-7.34 -29.36,-13.76 -53.21,-19.27l-44.96 -10.1c-28.9,-6.42 -49.54,-13.76 -61.47,-21.56 -21.11,-13.76 -31.65,-34.41 -31.65,-61.94 0,-29.35 10.55,-53.67 30.74,-72.48 20.64,-19.27 49.54,-28.44 87.16,-28.44 34.41,0 63.77,8.26 87.63,24.77 24.31,16.51 36.24,43.12 36.24,79.82l-41.75 0c-2.3,-17.44 -6.88,-31.2 -14.22,-40.37 -13.76,-17.43 -36.7,-25.69 -69.27,-25.69 -26.6,0 -45.42,5.5 -56.88,16.51 -11.47,11.01 -16.98,23.85 -16.98,38.53 0,16.05 6.42,27.99 19.73,35.32 9.17,4.59 28.9,10.56 60.09,17.89l45.88 10.56c22.47,5.04 39.45,11.92 51.84,20.64 20.64,15.59 31.2,38.07 31.2,66.97 0,36.7 -13.31,62.85 -39.91,78.45 -26.16,15.6 -56.89,23.4 -92.22,23.4 -40.83,0 -72.94,-10.55 -95.88,-31.19 -23.4,-21.11 -34.87,-49.09 -34.41,-84.41l42.2 0z"/>
|
||||
<polygon id="_5" class="fil0" points="5711.83,4729.87 5756.32,4729.87 5756.32,5019.8 5923.31,5019.8 5923.31,5058.8 5711.83,5058.8 "/>
|
||||
<polygon id="_6" class="fil0" points="6060.32,4729.87 6300.25,4729.87 6300.25,4770.24 6103.9,4770.24 6103.9,4869.79 6285.56,4869.79 6285.56,4907.87 6103.9,4907.87 6103.9,5019.8 6303.91,5019.8 6303.91,5058.8 6060.32,5058.8 "/>
|
||||
<polygon id="_7" class="fil0" points="6455.52,4729.87 6695.45,4729.87 6695.45,4770.24 6499.1,4770.24 6499.1,4869.79 6680.76,4869.79 6680.76,4907.87 6499.1,4907.87 6499.1,5019.8 6699.12,5019.8 6699.12,5058.8 6455.52,5058.8 "/>
|
||||
<path id="_8" class="fil0" d="M6850.72 4729.87l148.17 0c29.36,0 52.76,8.26 71.11,24.77 17.89,16.51 26.6,39.45 26.6,69.26 0,25.7 -7.8,48.18 -23.85,67.45 -16.06,18.81 -40.84,28.44 -73.86,28.44l-103.22 0 0 139 -44.95 0 0 -328.92zm200.93 94.5c0,-24.31 -9.18,-40.83 -27.07,-49.54 -9.63,-4.59 -23.4,-6.88 -40.36,-6.88l-88.54 0 0 114.23 88.54 0c20.18,0 36.23,-4.58 48.62,-12.84 12.4,-8.72 18.82,-23.4 18.82,-44.96z"/>
|
||||
<path id="_9" class="fil0" d="M4801.09 4857.86c14.21,-10.1 23.85,-18.35 29.35,-24.77 8.72,-10.1 12.85,-21.11 12.85,-33.49 0,-10.1 -3.21,-18.35 -9.63,-25.23 -6.42,-6.88 -14.68,-10.55 -25.69,-10.55 -16.51,0 -27.99,5.5 -34.41,16.51 -3.68,5.5 -5.05,11.92 -5.05,18.81 0,8.72 2.3,17.44 7.34,26.15 5.04,8.27 13.3,19.28 25.23,32.58zm-10.56 172.94c16.51,0 30.74,-3.67 42.66,-11.01 11.93,-7.8 21.57,-16.51 27.99,-25.69l-74.78 -90.83c-21.1,14.22 -34.41,24.77 -40.83,32.12 -10.1,11.46 -15.14,25.22 -15.14,41.29 0,17.43 6.42,30.73 19.27,40.36 12.85,9.18 26.61,13.76 40.83,13.76zm-26.15 -155.05c-14.21,-16.05 -23.4,-29.36 -27.98,-40.37 -4.59,-11.01 -7.34,-21.56 -7.34,-31.65 0,-21.11 7.34,-38.53 21.56,-52.76 14.69,-13.76 33.49,-20.64 57.8,-20.64 22.94,0 40.37,6.43 53.22,19.27 12.84,12.85 19.27,28.45 19.27,46.79 0,21.11 -6.43,39.46 -19.73,55.06 -7.8,9.63 -20.64,20.18 -39,32.11l60.1 71.56c4.13,-11.92 6.88,-20.64 8.26,-26.6 1.83,-5.97 3.2,-14.22 5.04,-24.77l38.08 0c-2.3,21.1 -7.34,41.29 -15.14,60.55 -7.81,19.27 -11.47,27.06 -11.47,23.4l58.72 71.1 -52.3 0 -30.74 -37.62c-12.39,13.31 -23.4,22.94 -33.48,28.9 -17.89,11.01 -38.53,16.51 -61.48,16.51 -34.41,0 -59.18,-9.17 -74.77,-27.98 -15.6,-18.35 -23.4,-39 -23.4,-62.39 0,-24.77 7.8,-45.42 22.93,-62.39 9.17,-10.09 26.61,-22.93 51.84,-38.07z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2021.5 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100.188mm" height="15.1954mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 36141.04 5481.48"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:#2B2B2A;fill-rule:nonzero}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Слой_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_105553227186208">
|
||||
<g>
|
||||
<path class="fil0" d="M4500.43 2322.22c-1244.24,-395.04 -1823.79,-583.31 -1812.61,-1255.24 3.61,-158.76 81.13,-313.84 228.74,-454.13 -88.49,-33.22 -184.59,-62.77 -273.07,-81.2 -281.8,236.03 -362,503.22 -361.82,712.52 0.72,786.61 601.74,1114.99 1812.79,1476.77 155.04,48.01 295.3,92.31 428.15,136.61 7.36,-144 11.15,-343.35 -22.19,-535.33zm2067.5 1513.71l0 0c-103.28,-655.56 -520.43,-959.91 -1214.66,-1225.73 -33.15,-686.69 -321.05,-1310.61 -778.93,-1764.74 -169.83,-169.83 -361.82,-313.8 -572.19,-431.94 -128.42,-82.39 -332.42,-162.44 -350.78,-169.83 199.38,-47.98 417.19,-75.36 671.83,-81.24 1066.9,-24.6 1546.57,683.08 1602.45,1026.36l446.7 7.4c-36.94,-188.3 -395.15,-1196.19 -2111.87,-1196.19 -317.44,0 -627.49,55.41 -904.39,147.68 -221.56,-59.09 -443.09,-92.28 -679.44,-92.28 -671.87,0 -2676.64,3.64 -2676.64,3.64l0 114.46c232.71,0 380.36,55.37 476.28,136.61 173.58,140.29 173.58,365.49 173.58,494.71 0,966.37 0,2899.1 0,3865.47 0,151.36 0,450.41 -302.84,575.95 -84.92,36.9 -199.38,62.73 -347.03,62.73l0 118.14c0,0 1625.25,0 2626.17,0 409.11,0 733.51,-88.6 733.51,-88.6 358.5,132.39 1026.03,216.73 1812.79,51.69 890.32,-186.82 1507.79,-841.16 1395.46,-1554.29zm-815.91 1111.27l0 0c-383.96,287.94 -982.49,428.48 -1495.17,376.53 -1078.19,-109.19 -1658.97,-753.86 -1794.39,-1270l-465.09 0c85.21,374.98 388.47,826.8 978.34,1126.03 0,0 -255.58,84.92 -646.11,84.92 -295.37,0 -1181.47,0 -1181.47,0l0 -5043.15 1196.26 0c443.12,0 844.84,75.21 1358.7,417.19 625.51,416.25 1071.38,1037.47 1140.75,1794.29 110.35,1203.73 -554.74,2072.88 -1162.93,2440.36 155.04,66.45 313.66,114.42 417.15,140.29 642.47,-406.15 1094.39,-1128.41 1218.27,-1853.37 557.55,262.14 815.91,539.04 856.63,945.16 33.15,350.74 -140.25,635 -420.94,841.77z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="fil0" d="M12099.09 1010.7c0,205.29 -39.93,381.94 -119.58,530.17 -79.61,148.26 -190.39,262.36 -332.6,342.26 -142.2,80.19 -307.24,120.12 -495.18,120.12l-727.45 0 0 -1945.43 643.4 0c329.31,0 583.63,82.68 762.77,248 179.1,165.07 268.64,400.12 268.64,704.87zm-265.07 0l0 0c0,-241.11 -66.12,-425.16 -198.11,-551.67 -132.28,-126.47 -323.51,-189.85 -573.75,-189.85l-374.19 0 0 1523.01 433.46 0c144.4,0 270.04,-31.96 376.14,-95.34 106.34,-63.67 189.02,-153.74 248,-270.59 58.98,-116.84 88.45,-255.47 88.45,-415.56z"/>
|
||||
<path class="fil0" d="M13295.57 1195.61l0 807.64 -263.7 0 0 -1945.43 915.4 0c219.07,0 388.26,49.06 507.3,147.14 119.33,98.12 178.85,234.51 178.85,409.5 0,144.37 -42.17,265.9 -126.22,364.27 -84.3,98.66 -200.6,160.67 -348.83,186.57l552.46 837.95 -303.92 0 -505.39 -807.64 -605.96 0zm1072.75 -578.4l0 0c0,-113.27 -38.56,-199.52 -115.18,-259.04 -76.87,-59.23 -187.65,-88.99 -332.05,-88.99l-625.51 0 0 717.82 636.55 0c138.88,0 246.34,-32.21 322.42,-97.25 75.75,-64.75 113.78,-155.69 113.78,-272.53z"/>
|
||||
<polygon class="fil0" points="17171.86,1788.04 17171.86,2003.26 15639.47,2003.26 15639.47,57.83 17115.37,57.83 17115.37,273.33 15903.2,273.33 15903.2,897.47 17032.44,897.47 17032.44,1109.9 15903.2,1109.9 15903.2,1788.04 "/>
|
||||
<path class="fil0" d="M19615.25 1466.2c0,179.68 -70.02,318.56 -210.52,416.94 -140.29,98.66 -338.11,147.68 -593.01,147.68 -473.97,0 -748.7,-164.49 -824.2,-494.06l255.43 -51.26c29.51,117.13 92.06,202.8 187.65,257.64 95.92,54.83 225.96,82.14 390.75,82.14 170.3,0 301.75,-29.22 394.32,-87.62 92.6,-58.44 138.63,-144.4 138.63,-257.67 0,-63.38 -14.36,-114.89 -43.29,-154.57 -29.18,-39.68 -69.69,-72.22 -122.32,-98.12 -52.38,-25.61 -114.93,-47.36 -187.65,-64.75 -72.76,-17.64 -153.24,-36.36 -241.69,-56.74 -102.23,-22.87 -187.65,-45.49 -256.26,-67.53 -68.32,-22.04 -124.81,-45.45 -168.9,-69.69 -44.37,-24.53 -82.14,-50.43 -113.27,-77.99 -46.03,-42.46 -81.27,-91.77 -105.8,-147.72 -24.24,-56.2 -36.4,-120.67 -36.4,-193.42 0,-166.44 63.67,-294.86 191.26,-385.23 127.3,-90.11 310.01,-135.31 547.27,-135.31 220.98,0 389.92,33.91 506.72,101.4 116.84,67.78 198.98,182.96 245.8,346.12l-259.58 45.45c-28.35,-103.06 -82.93,-178.02 -162.83,-224.3 -80.19,-46.57 -190.97,-69.73 -332.88,-69.73 -155.4,0 -274.16,25.65 -356.01,77.16 -82.14,51.55 -122.9,128.42 -122.9,230.65 0,59.81 15.69,109.41 47.4,148.51 31.96,39.14 77.7,71.93 137.51,98.66 56.2,26.73 175.24,60.35 357.67,100.86 74.38,17.35 148.23,35.53 220.7,54.54 72.76,18.76 141.37,42.46 205.87,71.1 37.73,15.44 73.84,34.74 108.29,57.32 34.7,22.33 64.75,48.48 90.65,77.99 34.7,38.56 62.26,84.05 82.1,136.68 19.59,52.34 29.51,114.06 29.51,184.88z"/>
|
||||
<path class="fil0" d="M22066.61 1466.2c0,179.68 -69.98,318.56 -210.52,416.94 -140.25,98.66 -338.11,147.68 -593.01,147.68 -473.97,0 -748.7,-164.49 -824.2,-494.06l255.43 -51.26c29.51,117.13 92.06,202.8 187.65,257.64 95.92,54.83 225.96,82.14 390.75,82.14 170.3,0 301.75,-29.22 394.35,-87.62 92.56,-58.44 138.59,-144.4 138.59,-257.67 0,-63.38 -14.32,-114.89 -43.25,-154.57 -29.22,-39.68 -69.73,-72.22 -122.36,-98.12 -52.34,-25.61 -114.89,-47.36 -187.65,-64.75 -72.76,-17.64 -153.2,-36.36 -241.65,-56.74 -102.23,-22.87 -187.65,-45.49 -256.3,-67.53 -68.32,-22.04 -124.81,-45.45 -168.9,-69.69 -44.37,-24.53 -82.14,-50.43 -113.27,-77.99 -45.99,-42.46 -81.27,-91.77 -105.8,-147.72 -24.24,-56.2 -36.36,-120.67 -36.36,-193.42 0,-166.44 63.63,-294.86 191.22,-385.23 127.3,-90.11 310.01,-135.31 547.27,-135.31 220.98,0 389.92,33.91 506.76,101.4 116.81,67.78 198.94,182.96 245.77,346.12l-259.55 45.45c-28.39,-103.06 -82.97,-178.02 -162.87,-224.3 -80.19,-46.57 -190.97,-69.73 -332.88,-69.73 -155.4,0 -274.16,25.65 -356.01,77.16 -82.1,51.55 -122.9,128.42 -122.9,230.65 0,59.81 15.69,109.41 47.4,148.51 31.96,39.14 77.7,71.93 137.51,98.66 56.2,26.73 175.24,60.35 357.67,100.86 74.38,17.35 148.26,35.53 220.73,54.54 72.72,18.76 141.34,42.46 205.83,71.1 37.73,15.44 73.84,34.74 108.29,57.32 34.7,22.33 64.75,48.48 90.65,77.99 34.74,38.56 62.3,84.05 82.14,136.68 19.55,52.34 29.47,114.06 29.47,184.88z"/>
|
||||
<polygon class="fil0" points="24526.52,1788.04 24526.52,2003.26 22994.13,2003.26 22994.13,57.83 24470.03,57.83 24470.03,273.33 23257.86,273.33 23257.86,897.47 24387.1,897.47 24387.1,1109.9 23257.86,1109.9 23257.86,1788.04 "/>
|
||||
<path class="fil0" d="M27120.62 1010.7c0,205.29 -39.97,381.94 -119.58,530.17 -79.65,148.26 -190.43,262.36 -332.6,342.26 -142.2,80.19 -307.27,120.12 -495.18,120.12l-727.49 0 0 -1945.43 643.44 0c329.28,0 583.63,82.68 762.73,248 179.1,165.07 268.67,400.12 268.67,704.87zm-265.1 0l0 0c0,-241.11 -66.12,-425.16 -198.11,-551.67 -132.28,-126.47 -323.51,-189.85 -573.71,-189.85l-374.22 0 0 1523.01 433.46 0c144.4,0 270.04,-31.96 376.14,-95.34 106.38,-63.67 189.06,-153.74 248,-270.59 58.98,-116.84 88.45,-255.47 88.45,-415.56z"/>
|
||||
<polygon class="fil0" points="29668.16,1214.91 29668.16,2003.26 29404.46,2003.26 29404.46,57.83 30786.39,57.83 30786.39,273.33 29668.16,273.33 29668.16,996.92 30753.31,996.92 30753.31,1214.91 "/>
|
||||
<path class="fil0" d="M33529.3 1021.74c0,208.03 -39.43,387.43 -118.21,538.43 -78.53,151 -190.14,267.01 -334.83,348.58 -144.37,81.56 -315.5,122.07 -513.61,122.07 -198.69,0 -370.08,-40.22 -514.19,-121.24 -144.15,-81.02 -254.89,-197.03 -332.88,-348.04 -77.7,-151 -116.55,-330.94 -116.55,-539.8 0,-208.03 38.27,-386.06 115.18,-533.78 76.87,-147.68 187.36,-261.21 331.48,-340.32 144.11,-79.07 317.16,-118.75 519.71,-118.75 198.94,0 370.36,39.14 514.48,117.38 144.11,78.28 254.89,191.26 332.6,338.94 77.99,147.68 116.84,326.54 116.84,536.52zm-269.21 0l0 0c0,-242.23 -60.64,-432.37 -181.59,-570.43 -120.99,-138.05 -292.12,-206.92 -513.11,-206.92 -222.64,0 -394.86,68.03 -516.39,204.17 -121.24,136.39 -182.13,327.37 -182.13,573.17 0,162.01 27.52,302.55 82.93,421.59 55.12,119.33 134.73,211.35 238.91,276.39 103.89,64.75 228.7,97.29 373.94,97.29 224.56,0 397.06,-69.19 517.22,-207.78 120.12,-138.59 180.22,-334.54 180.22,-587.49z"/>
|
||||
<path class="fil0" d="M34725.78 1195.61l0 807.64 -263.73 0 0 -1945.43 915.4 0c219.07,0 388.29,49.06 507.33,147.14 119.29,98.12 178.82,234.51 178.82,409.5 0,144.37 -42.17,265.9 -126.18,364.27 -84.34,98.66 -200.6,160.67 -348.86,186.57l552.5 837.95 -303.95 0 -505.39 -807.64 -605.92 0zm1072.75 -578.4l0 0c0,-113.27 -38.6,-199.52 -115.18,-259.04 -76.91,-59.23 -187.65,-88.99 -332.05,-88.99l-625.51 0 0 717.82 636.51 0c138.88,0 246.34,-32.21 322.42,-97.25 75.75,-64.75 113.81,-155.69 113.81,-272.53z"/>
|
||||
<path class="fil0" d="M11948.38 4859.43c0,179.68 -69.98,318.56 -210.52,416.94 -140.25,98.62 -338.11,147.68 -593.01,147.68 -473.97,0 -748.7,-164.49 -824.2,-494.06l255.47 -51.26c29.47,117.09 92.02,202.8 187.65,257.64 95.88,54.83 225.93,82.14 390.71,82.14 170.3,0 301.75,-29.22 394.35,-87.66 92.56,-58.4 138.59,-144.37 138.59,-257.64 0,-63.38 -14.32,-114.89 -43.25,-154.57 -29.22,-39.68 -69.73,-72.22 -122.36,-98.12 -52.34,-25.61 -114.89,-47.4 -187.65,-64.75 -72.76,-17.64 -153.2,-36.36 -241.65,-56.78 -102.23,-22.87 -187.65,-45.45 -256.26,-67.49 -68.36,-22.04 -124.85,-45.45 -168.93,-69.73 -44.37,-24.53 -82.1,-50.43 -113.27,-77.95 -45.99,-42.46 -81.27,-91.77 -105.8,-147.72 -24.24,-56.2 -36.36,-120.7 -36.36,-193.42 0,-166.44 63.63,-294.86 191.22,-385.23 127.3,-90.11 310.01,-135.31 547.27,-135.31 220.98,0 389.92,33.87 506.76,101.4 116.84,67.78 198.94,182.96 245.8,346.09l-259.58 45.49c-28.39,-103.06 -82.97,-178.02 -162.87,-224.3 -80.19,-46.57 -190.97,-69.73 -332.88,-69.73 -155.4,0 -274.16,25.65 -356.01,77.16 -82.1,51.51 -122.9,128.42 -122.9,230.65 0,59.77 15.73,109.37 47.4,148.51 31.96,39.14 77.7,71.93 137.51,98.66 56.2,26.73 175.24,60.35 357.67,100.86 74.42,17.35 148.26,35.53 220.73,54.54 72.72,18.76 141.37,42.42 205.83,71.1 37.77,15.44 73.84,34.7 108.29,57.32 34.74,22.33 64.75,48.48 90.65,77.99 34.74,38.56 62.3,84.01 82.14,136.65 19.55,52.38 29.47,114.1 29.47,184.91z"/>
|
||||
<path class="fil0" d="M13653.27 5424.05c-159.3,0 -298.43,-29.22 -416.94,-88.16 -118.75,-58.98 -210.81,-143.32 -276.39,-252.69 -65.29,-109.66 -97.83,-240.86 -97.83,-393.49l0 -1238.65 263.73 0 0 1216.57c0,177.48 44.91,312.5 135.27,404.53 90.11,92.06 220.48,138.05 390.75,138.05 174.7,0 310.84,-47.65 407.84,-142.99 97.25,-95.09 145.77,-234.51 145.77,-417.48l0 -1198.68 262.32 0 0 1213.83c0,157.35 -33.33,292.66 -100.03,405.9 -66.95,113.27 -161.21,200.35 -283.28,261.5 -121.78,61.18 -265.64,91.77 -431.22,91.77z"/>
|
||||
<path class="fil0" d="M16355.92 5207.47c272.53,0 477.29,-135.02 614.51,-404.53l215.5 107.75c-80.19,167.52 -192.63,294.83 -337.57,382.2 -144.94,87.62 -313.3,131.16 -504.81,131.16 -197.03,0 -366.22,-40.76 -508.13,-122.61 -141.66,-82.14 -250.49,-198.69 -326.54,-350.24 -75.79,-151.29 -113.81,-330.14 -113.81,-536.23 0,-207.24 37.73,-384.69 113.23,-532.95 75.5,-148.26 184.08,-262.04 325.71,-341.15 141.91,-79.07 311.13,-118.75 508.13,-118.75 207.24,0 380.28,40.51 519.17,121.53 139.17,80.98 241.11,201.14 306.7,360.41l-249.92 82.68c-45.2,-112.98 -117.67,-199.52 -217.41,-259.29 -100.03,-60.1 -218.53,-89.86 -355.75,-89.86 -215.21,0 -382.77,69.19 -502.36,207.78 -119.87,138.59 -179.64,328.48 -179.64,569.6 0,158.15 28.35,297.03 84.84,416.07 56.78,119.33 136.68,211.93 240.32,277.76 103.6,65.58 226.22,98.66 367.84,98.66z"/>
|
||||
<path class="fil0" d="M18963.55 5207.47c272.53,0 477.25,-135.02 614.47,-404.53l215.5 107.75c-80.19,167.52 -192.63,294.83 -337.57,382.2 -144.94,87.62 -313.3,131.16 -504.81,131.16 -197.03,0 -366.22,-40.76 -508.13,-122.61 -141.62,-82.14 -250.49,-198.69 -326.54,-350.24 -75.79,-151.29 -113.81,-330.14 -113.81,-536.23 0,-207.24 37.77,-384.69 113.27,-532.95 75.5,-148.26 184.05,-262.04 325.71,-341.15 141.91,-79.07 311.1,-118.75 508.13,-118.75 207.2,0 380.25,40.51 519.13,121.53 139.17,80.98 241.11,201.14 306.7,360.41l-249.92 82.68c-45.2,-112.98 -117.67,-199.52 -217.41,-259.29 -100.03,-60.1 -218.53,-89.86 -355.75,-89.86 -215.21,0 -382.74,69.19 -502.32,207.78 -119.87,138.59 -179.68,328.48 -179.68,569.6 0,158.15 28.39,297.03 84.88,416.07 56.74,119.33 136.68,211.93 240.28,277.76 103.6,65.58 226.22,98.66 367.88,98.66z"/>
|
||||
<polygon class="fil0" points="22230.85,5181.28 22230.85,5396.49 20698.46,5396.49 20698.46,3451.06 22174.36,3451.06 22174.36,3666.56 20962.15,3666.56 20962.15,4290.7 22091.39,4290.7 22091.39,4503.14 20962.15,4503.14 20962.15,5181.28 "/>
|
||||
<path class="fil0" d="M24674.2 4859.43c0,179.68 -69.98,318.56 -210.52,416.94 -140.25,98.62 -338.11,147.68 -593.01,147.68 -473.93,0 -748.67,-164.49 -824.17,-494.06l255.43 -51.26c29.47,117.09 92.02,202.8 187.65,257.64 95.88,54.83 225.96,82.14 390.75,82.14 170.3,0 301.72,-29.22 394.32,-87.66 92.6,-58.4 138.59,-144.37 138.59,-257.64 0,-63.38 -14.32,-114.89 -43.25,-154.57 -29.22,-39.68 -69.73,-72.22 -122.36,-98.12 -52.34,-25.61 -114.89,-47.4 -187.65,-64.75 -72.72,-17.64 -153.2,-36.36 -241.65,-56.78 -102.23,-22.87 -187.65,-45.45 -256.26,-67.49 -68.36,-22.04 -124.81,-45.45 -168.93,-69.73 -44.37,-24.53 -82.1,-50.43 -113.23,-77.95 -46.03,-42.46 -81.31,-91.77 -105.84,-147.72 -24.24,-56.2 -36.36,-120.7 -36.36,-193.42 0,-166.44 63.67,-294.86 191.22,-385.23 127.34,-90.11 310.01,-135.31 547.27,-135.31 221.02,0 389.92,33.87 506.76,101.4 116.84,67.78 198.94,182.96 245.8,346.09l-259.58 45.49c-28.39,-103.06 -82.93,-178.02 -162.83,-224.3 -80.19,-46.57 -190.97,-69.73 -332.88,-69.73 -155.4,0 -274.19,25.65 -356.04,77.16 -82.1,51.51 -122.9,128.42 -122.9,230.65 0,59.77 15.73,109.37 47.4,148.51 31.96,39.14 77.74,71.93 137.51,98.66 56.2,26.73 175.24,60.35 357.67,100.86 74.42,17.35 148.26,35.53 220.73,54.54 72.76,18.76 141.37,42.42 205.83,71.1 37.77,15.44 73.88,34.7 108.29,57.32 34.74,22.33 64.79,48.48 90.69,77.99 34.7,38.56 62.26,84.01 82.1,136.65 19.55,52.38 29.47,114.1 29.47,184.91z"/>
|
||||
<path class="fil0" d="M27125.85 4859.43c0,179.68 -69.98,318.56 -210.52,416.94 -140.25,98.62 -338.11,147.68 -593.01,147.68 -473.97,0 -748.67,-164.49 -824.2,-494.06l255.47 -51.26c29.47,117.09 92.02,202.8 187.65,257.64 95.88,54.83 225.96,82.14 390.75,82.14 170.27,0 301.72,-29.22 394.32,-87.66 92.56,-58.4 138.59,-144.37 138.59,-257.64 0,-63.38 -14.32,-114.89 -43.25,-154.57 -29.22,-39.68 -69.73,-72.22 -122.36,-98.12 -52.34,-25.61 -114.89,-47.4 -187.65,-64.75 -72.76,-17.64 -153.2,-36.36 -241.65,-56.78 -102.23,-22.87 -187.65,-45.45 -256.26,-67.49 -68.36,-22.04 -124.85,-45.45 -168.93,-69.73 -44.37,-24.53 -82.1,-50.43 -113.23,-77.95 -46.03,-42.46 -81.31,-91.77 -105.84,-147.72 -24.24,-56.2 -36.36,-120.7 -36.36,-193.42 0,-166.44 63.63,-294.86 191.22,-385.23 127.3,-90.11 310.01,-135.31 547.27,-135.31 220.98,0 389.92,33.87 506.76,101.4 116.84,67.78 198.94,182.96 245.8,346.09l-259.58 45.49c-28.39,-103.06 -82.93,-178.02 -162.87,-224.3 -80.19,-46.57 -190.94,-69.73 -332.85,-69.73 -155.44,0 -274.19,25.65 -356.04,77.16 -82.1,51.51 -122.9,128.42 -122.9,230.65 0,59.77 15.73,109.37 47.4,148.51 31.96,39.14 77.7,71.93 137.51,98.66 56.2,26.73 175.24,60.35 357.67,100.86 74.42,17.35 148.26,35.53 220.73,54.54 72.76,18.76 141.37,42.42 205.83,71.1 37.77,15.44 73.84,34.7 108.29,57.32 34.74,22.33 64.75,48.48 90.65,77.99 34.74,38.56 62.3,84.01 82.14,136.65 19.55,52.38 29.47,114.1 29.47,184.91z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
BIN
Logo DRESSED FOR SUCCESS/Знак/.DS_Store
vendored
BIN
Logo DRESSED FOR SUCCESS/Знак/.DS_Store
vendored
Binary file not shown.
BIN
Logo DRESSED FOR SUCCESS/Название/.DS_Store
vendored
BIN
Logo DRESSED FOR SUCCESS/Название/.DS_Store
vendored
Binary file not shown.
BIN
Logo DRESSED FOR SUCCESS/Основная версия/.DS_Store
vendored
BIN
Logo DRESSED FOR SUCCESS/Основная версия/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
112
README.md
112
README.md
@ -1,112 +0,0 @@
|
||||
# Dressed for Success - Интернет-магазин одежды
|
||||
|
||||
## О проекте
|
||||
|
||||
Интернет-магазин модной одежды Dressed for Success, созданный с использованием современных технологий:
|
||||
|
||||
- **Фронтенд**: Next.js, React, TypeScript, Tailwind CSS
|
||||
- **Бэкенд**: FastAPI, SQLAlchemy, PostgreSQL
|
||||
- **Развертывание**: Docker, Docker Compose
|
||||
|
||||
## Требования
|
||||
|
||||
Для запуска проекта вам потребуются:
|
||||
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
## Запуск проекта
|
||||
|
||||
### 1. Клонирование репозитория
|
||||
|
||||
```bash
|
||||
git clone https://github.com/username/dressed_for_success_store.git
|
||||
cd dressed_for_success_store
|
||||
```
|
||||
|
||||
### 2. Запуск через Docker Compose
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Это запустит:
|
||||
- Бэкенд на порту 8000
|
||||
- Фронтенд на порту 3000
|
||||
- PostgreSQL на порту 5432
|
||||
|
||||
### 3. Доступ к приложению
|
||||
|
||||
- **Фронтенд**: http://localhost:3000
|
||||
- **API бэкенда**: http://localhost:8000/api
|
||||
- **Документация API**: http://localhost:8000/docs
|
||||
|
||||
## Остановка проекта
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
Для удаления томов (данных базы данных и загруженных файлов):
|
||||
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Разработка
|
||||
|
||||
### Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── backend/ # Бэкенд на FastAPI
|
||||
│ ├── app/ # Код приложения
|
||||
│ ├── uploads/ # Загружаемые файлы
|
||||
│ └── requirements.txt # Зависимости Python
|
||||
│
|
||||
├── frontend/ # Фронтенд на Next.js
|
||||
│ ├── app/ # Код Next.js приложения
|
||||
│ ├── components/ # React компоненты
|
||||
│ ├── lib/ # Библиотеки и утилиты
|
||||
│ └── public/ # Статические файлы
|
||||
│
|
||||
├── docker-compose.yml # Конфигурация Docker Compose
|
||||
├── Dockerfile.backend # Dockerfile для бэкенда
|
||||
└── Dockerfile.frontend # Dockerfile для фронтенда
|
||||
```
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
### Фронтенд
|
||||
|
||||
Основные переменные окружения для фронтенда (файл `.env.local` или `.env.docker`):
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000/api
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_DEBUG=false
|
||||
NEXT_PUBLIC_MOCK_API=false
|
||||
```
|
||||
|
||||
### Бэкенд
|
||||
|
||||
Основные переменные окружения для бэкенда (файл `.env` или `.env.docker`):
|
||||
|
||||
```
|
||||
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db
|
||||
SECRET_KEY=supersecretkey
|
||||
DEBUG=0
|
||||
UPLOAD_DIRECTORY=/app/uploads
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
|
||||
[MIT License](LICENSE)
|
||||
|
||||
|
||||
|
||||
# Сначала получаем SSL-сертификат
|
||||
./init-letsencrypt.sh ваш-домен.ru
|
||||
|
||||
# Затем запускаем сервисы
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
BIN
backend/.DS_Store
vendored
BIN
backend/.DS_Store
vendored
Binary file not shown.
13
backend/.env
13
backend/.env
@ -1,13 +0,0 @@
|
||||
# Настройки базы данных
|
||||
DATABASE_URL=postgresql://gen_user:F%2BgEEiP3h7yB6d@93.183.81.86:5432/shop_db
|
||||
|
||||
# Настройки безопасности
|
||||
SECRET_KEY=supersecretkey
|
||||
DEBUG=1
|
||||
|
||||
# Настройки загрузки файлов
|
||||
UPLOAD_DIRECTORY=uploads
|
||||
|
||||
# Настройки Meilisearch
|
||||
MEILISEARCH_URL=http://localhost:7700
|
||||
MEILISEARCH_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM
|
||||
@ -1,14 +0,0 @@
|
||||
# Настройки базы данных
|
||||
# DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db
|
||||
|
||||
# Настройки приложения
|
||||
DEBUG=0
|
||||
SECRET_KEY=supersecretkey
|
||||
# UPLOAD_DIRECTORY=/app/uploads
|
||||
|
||||
# Настройки CORS
|
||||
FRONTEND_URL=http://frontend:3000
|
||||
|
||||
# Настройки Meilisearch
|
||||
MEILISEARCH_URL=http://meilisearch:7700
|
||||
MEILISEARCH_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM
|
||||
@ -1,26 +0,0 @@
|
||||
# Используем официальный образ с Uvicorn+Gunicorn, оптимизированный для FastAPI
|
||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY ./app ./app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Точка входа: стандартный CMD из базового образа запустит Gunicorn+Uvicorn
|
||||
|
||||
|
||||
|
||||
# FROM python:3.10-slim
|
||||
|
||||
# WORKDIR /app
|
||||
|
||||
# COPY requirements.txt .
|
||||
|
||||
# RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# # Для разработки код монтируется через volumes, а в продакшн-билде можно добавить COPY . /app
|
||||
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] # hot-reload
|
||||
@ -60,7 +60,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = postgresql://gen_user:F%%2BgEEiP3h7yB6d@93.183.81.86:5432/shop_db
|
||||
sqlalchemy.url = postgresql://postgres:postgres@localhost:5434/shop_db
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
BIN
backend/alembic/.DS_Store
vendored
BIN
backend/alembic/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
@ -1,42 +0,0 @@
|
||||
"""add_new_order
|
||||
|
||||
Revision ID: 7192b0707277
|
||||
Revises: 9773b0186faa
|
||||
Create Date: 2025-04-27 01:56:31.170476
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '7192b0707277'
|
||||
down_revision: Union[str, None] = '9773b0186faa'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('orders', sa.Column('user_info_json', sa.JSON(), nullable=True))
|
||||
op.add_column('orders', sa.Column('delivery_method', sa.String(), nullable=True))
|
||||
op.add_column('orders', sa.Column('city', sa.String(), nullable=True))
|
||||
op.add_column('orders', sa.Column('delivery_address', sa.String(), nullable=True))
|
||||
op.add_column('orders', sa.Column('cdek_info', sa.JSON(), nullable=True))
|
||||
op.add_column('orders', sa.Column('courier_info', sa.JSON(), nullable=True))
|
||||
op.add_column('orders', sa.Column('items_json', sa.JSON(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('orders', 'items_json')
|
||||
op.drop_column('orders', 'courier_info')
|
||||
op.drop_column('orders', 'cdek_info')
|
||||
op.drop_column('orders', 'delivery_address')
|
||||
op.drop_column('orders', 'city')
|
||||
op.drop_column('orders', 'delivery_method')
|
||||
op.drop_column('orders', 'user_info_json')
|
||||
# ### end Alembic commands ###
|
||||
@ -1,57 +0,0 @@
|
||||
"""Add size table and update product structure
|
||||
|
||||
Revision ID: 9773b0186faa
|
||||
Revises: ef40913679bd
|
||||
Create Date: 2025-03-16 20:30:39.057254
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '9773b0186faa'
|
||||
down_revision: Union[str, None] = 'ef40913679bd'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('sizes',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('code', sa.String(), nullable=False),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('code')
|
||||
)
|
||||
op.create_index(op.f('ix_sizes_id'), 'sizes', ['id'], unique=False)
|
||||
op.add_column('product_variants', sa.Column('size_id', sa.Integer(), nullable=False))
|
||||
op.create_foreign_key(None, 'product_variants', 'sizes', ['size_id'], ['id'])
|
||||
op.drop_column('product_variants', 'discount_price')
|
||||
op.drop_column('product_variants', 'name')
|
||||
op.drop_column('product_variants', 'price')
|
||||
op.add_column('products', sa.Column('price', sa.Float(), nullable=False))
|
||||
op.add_column('products', sa.Column('discount_price', sa.Float(), nullable=True))
|
||||
op.add_column('products', sa.Column('care_instructions', sa.JSON(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('products', 'care_instructions')
|
||||
op.drop_column('products', 'discount_price')
|
||||
op.drop_column('products', 'price')
|
||||
op.add_column('product_variants', sa.Column('price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False))
|
||||
op.add_column('product_variants', sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False))
|
||||
op.add_column('product_variants', sa.Column('discount_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
|
||||
op.drop_constraint(None, 'product_variants', type_='foreignkey')
|
||||
op.drop_column('product_variants', 'size_id')
|
||||
op.drop_index(op.f('ix_sizes_id'), table_name='sizes')
|
||||
op.drop_table('sizes')
|
||||
# ### end Alembic commands ###
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,30 +0,0 @@
|
||||
"""add_new_order_
|
||||
|
||||
Revision ID: f89a59b0e814
|
||||
Revises: 7192b0707277
|
||||
Create Date: 2025-04-27 02:49:23.818786
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'f89a59b0e814'
|
||||
down_revision: Union[str, None] = '7192b0707277'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('orders', sa.Column('payment_method', sa.Enum('CREDIT_CARD', 'PAYPAL', 'BANK_TRANSFER', 'CASH_ON_DELIVERY', 'SBP', 'CARD', name='paymentmethod'), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('orders', 'payment_method')
|
||||
# ### end Alembic commands ###
|
||||
BIN
backend/app/.DS_Store
vendored
BIN
backend/app/.DS_Store
vendored
Binary file not shown.
@ -1,13 +0,0 @@
|
||||
CDEK_LOGIN=q8AQtmLL7kPg6TuDo1eB2uqelJS4tHn2
|
||||
CDEK_PASSWORD=RmAmgvSgSl1yirlz9QupbzOJVqhCxcP5
|
||||
# CDEK_BASE_URL=https://api.cdek.ru/v2
|
||||
CDEK_BASE_URL=https://api.edu.cdek.ru/v2
|
||||
|
||||
MINIO_ENDPOINT_URL = http://45.129.128.113:9000
|
||||
MINIO_ACCESS_KEY = ZIK_DressedForSuccess
|
||||
MINIO_SECRET_KEY = ZIK_DressedForSuccess_/////ZIK_DressedForSuccess_!
|
||||
MINIO_BUCKET_NAME = dressedforsuccess
|
||||
MINIO_USE_SSL = false
|
||||
|
||||
MEILISEARCH_KEY = dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM
|
||||
MEILISEARCH_URL = http://localhost:7700
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,68 +1,25 @@
|
||||
import os
|
||||
from pydantic_settings import BaseSettings
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
# Загружаем переменные окружения из .env файла
|
||||
load_dotenv()
|
||||
|
||||
# Базовые настройки
|
||||
API_PREFIX = "/api"
|
||||
DEBUG = False
|
||||
|
||||
# Настройки безопасности
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "supersecretkey") # Для JWT
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 дней
|
||||
|
||||
# Настройки базы данных
|
||||
# DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db")
|
||||
|
||||
# Настройки почты
|
||||
MAIL_USERNAME = os.getenv("MAIL_USERNAME", "test@example.com")
|
||||
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD", "test_password")
|
||||
MAIL_FROM = os.getenv("MAIL_FROM", "noreply@example.com")
|
||||
MAIL_PORT = int(os.getenv("MAIL_PORT", "587"))
|
||||
MAIL_SERVER = os.getenv("MAIL_SERVER", "smtp.example.com")
|
||||
MAIL_TLS = True
|
||||
MAIL_SSL = False
|
||||
|
||||
# Настройки загрузки файлов (старые, для информации)
|
||||
# UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "uploads")
|
||||
ALLOWED_UPLOAD_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "webp"}
|
||||
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10 МБ
|
||||
|
||||
# Настройки MinIO/S3
|
||||
MINIO_ENDPOINT_URL = os.getenv("MINIO_ENDPOINT_URL", "http://localhost:9000")
|
||||
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin")
|
||||
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin")
|
||||
MINIO_BUCKET_NAME = os.getenv("MINIO_BUCKET_NAME", "images")
|
||||
MINIO_USE_SSL = os.getenv("MINIO_USE_SSL", "false").lower() == "true"
|
||||
|
||||
# Настройки корзины
|
||||
CART_EXPIRATION_DAYS = 30 # Срок хранения корзины
|
||||
|
||||
# Настройки API
|
||||
MAX_PAGE_SIZE = 100 # Максимальный размер страницы для пагинации
|
||||
|
||||
# Базовый URL фронтенда
|
||||
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000")
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Основные настройки приложения
|
||||
APP_NAME: str = "Интернет-магазин API"
|
||||
APP_VERSION: str = "0.1.0"
|
||||
APP_DESCRIPTION: str = "API для интернет-магазина на FastAPI"
|
||||
DEBUG: bool = False
|
||||
|
||||
|
||||
# Настройки базы данных
|
||||
DATABASE_URL: str = "postgresql://gen_user:F%2BgEEiP3h7yB6d@93.183.81.86:5432/shop_db"
|
||||
|
||||
DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5434/shop_db")
|
||||
|
||||
# Настройки безопасности
|
||||
SECRET_KEY: str = SECRET_KEY
|
||||
ALGORITHM: str = ALGORITHM
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-for-jwt-please-change-in-production")
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30*60*24
|
||||
|
||||
# Настройки CORS
|
||||
CORS_ORIGINS: list = [
|
||||
"http://localhost",
|
||||
@ -70,44 +27,27 @@ class Settings(BaseSettings):
|
||||
"http://localhost:8000",
|
||||
"http://localhost:8080",
|
||||
]
|
||||
|
||||
|
||||
# Настройки для загрузки файлов
|
||||
UPLOAD_DIRECTORY: str = "uploads"
|
||||
MAX_UPLOAD_SIZE: int = MAX_UPLOAD_SIZE
|
||||
ALLOWED_UPLOAD_EXTENSIONS: list = list(ALLOWED_UPLOAD_EXTENSIONS)
|
||||
|
||||
# Настройки MinIO/S3
|
||||
MINIO_ENDPOINT_URL: str = MINIO_ENDPOINT_URL
|
||||
MINIO_ACCESS_KEY: str = MINIO_ACCESS_KEY
|
||||
MINIO_SECRET_KEY: str = MINIO_SECRET_KEY
|
||||
MINIO_BUCKET_NAME: str = MINIO_BUCKET_NAME
|
||||
MINIO_USE_SSL: bool = MINIO_USE_SSL
|
||||
|
||||
MAX_UPLOAD_SIZE: int = 5 * 1024 * 1024 # 5 MB
|
||||
ALLOWED_UPLOAD_EXTENSIONS: list = ["jpg", "jpeg", "png", "gif", "webp"]
|
||||
|
||||
# Настройки для платежных систем (пример)
|
||||
PAYMENT_GATEWAY_API_KEY: str = os.getenv("PAYMENT_GATEWAY_API_KEY", "")
|
||||
PAYMENT_GATEWAY_SECRET: str = os.getenv("PAYMENT_GATEWAY_SECRET", "")
|
||||
|
||||
# Настройки для CDEK API
|
||||
CDEK_LOGIN: str = os.getenv("CDEK_LOGIN", "cdek-login")
|
||||
CDEK_PASSWORD: str = os.getenv("CDEK_PASSWORD", "cdek-pass")
|
||||
CDEK_BASE_URL: str = os.getenv("CDEK_BASE_URL", "https://api.cdek.ru/v2")
|
||||
|
||||
|
||||
# Настройки для отправки email (пример)
|
||||
SMTP_SERVER: str = MAIL_SERVER
|
||||
SMTP_PORT: int = MAIL_PORT
|
||||
SMTP_USERNAME: str = MAIL_USERNAME
|
||||
SMTP_PASSWORD: str = MAIL_PASSWORD
|
||||
EMAIL_FROM: str = MAIL_FROM
|
||||
|
||||
# Настройки Meilisearch
|
||||
MEILISEARCH_URL: str = os.getenv("MEILISEARCH_URL", "http://0.0.0.0:7700")
|
||||
MEILISEARCH_KEY: str = os.getenv("MEILISEARCH_KEY", "masterKey")
|
||||
|
||||
|
||||
SMTP_SERVER: str = os.getenv("SMTP_SERVER", "")
|
||||
SMTP_PORT: int = int(os.getenv("SMTP_PORT", "587"))
|
||||
SMTP_USERNAME: str = os.getenv("SMTP_USERNAME", "")
|
||||
SMTP_PASSWORD: str = os.getenv("SMTP_PASSWORD", "")
|
||||
EMAIL_FROM: str = os.getenv("EMAIL_FROM", "noreply@example.com")
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
|
||||
# Создаем экземпляр настроек
|
||||
settings = Settings()
|
||||
settings = Settings()
|
||||
@ -12,7 +12,7 @@ from sqlalchemy.orm import Session
|
||||
from app.config import settings
|
||||
|
||||
# Настройка SQLAlchemy
|
||||
SQLALCHEMY_DATABASE_URL = "postgresql://gen_user:F%2BgEEiP3h7yB6d@93.183.81.86:5432/shop_db"
|
||||
SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL
|
||||
|
||||
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
@ -1,57 +1,23 @@
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from app.config import settings
|
||||
from app.routers import router
|
||||
from app.core import Base, engine, SessionLocal
|
||||
from app.services import meilisearch_service
|
||||
from app.scripts.sync_meilisearch import sync_products, sync_categories, sync_collections, sync_sizes
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
from app.core import Base, engine
|
||||
|
||||
# Создаем таблицы в базе данных
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Инициализируем Meilisearch
|
||||
logging.info("Инициализация Meilisearch...")
|
||||
try:
|
||||
# Инициализируем индексы
|
||||
meilisearch_service.initialize_indexes()
|
||||
|
||||
# Создаем сессию базы данных
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Синхронизируем данные с Meilisearch
|
||||
sync_categories(db)
|
||||
sync_collections(db)
|
||||
sync_sizes(db)
|
||||
sync_products(db)
|
||||
logging.info("Синхронизация с Meilisearch завершена успешно")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при синхронизации данных с Meilisearch: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при инициализации Meilisearch: {str(e)}")
|
||||
|
||||
yield
|
||||
|
||||
# Создаем экземпляр приложения FastAPI
|
||||
app = FastAPI(
|
||||
title="Dressed for Success API",
|
||||
description="API для интернет-магазина одежды",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# Настраиваем CORS
|
||||
@ -74,10 +40,14 @@ async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
|
||||
# Подключаем роутеры
|
||||
app.include_router(router, prefix="/api")
|
||||
|
||||
# Создаем директорию для загрузок, если она не существует
|
||||
uploads_dir = Path(settings.UPLOAD_DIRECTORY)
|
||||
uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
# Монтируем статические файлы
|
||||
app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_DIRECTORY), name="uploads")
|
||||
|
||||
# Корневой маршрут
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Добро пожаловать в API интернет-магазина Dressed for Success"}
|
||||
return {"message": "Добро пожаловать в API интернет-магазина Dressed for Success"}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey, DateTime, Text, JSON
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey, DateTime, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
@ -37,20 +37,6 @@ class Category(Base):
|
||||
products = relationship("Product", back_populates="category")
|
||||
|
||||
|
||||
class Size(Base):
|
||||
__tablename__ = "sizes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False) # S, M, L, XL и т.д.
|
||||
code = Column(String, nullable=False, unique=True) # Уникальный код размера
|
||||
description = Column(String, nullable=True) # Дополнительная информация
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Отношения
|
||||
product_variants = relationship("ProductVariant", back_populates="size")
|
||||
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = "products"
|
||||
|
||||
@ -58,9 +44,6 @@ class Product(Base):
|
||||
name = Column(String, nullable=False)
|
||||
slug = Column(String, unique=True, index=True, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
price = Column(Float, nullable=False) # Базовая цена продукта
|
||||
discount_price = Column(Float, nullable=True) # Цена со скидкой
|
||||
care_instructions = Column(JSON, nullable=True) # Инструкции по уходу и другие детали
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
collection_id = Column(Integer, ForeignKey("collections.id"))
|
||||
@ -81,16 +64,17 @@ class ProductVariant(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||
size_id = Column(Integer, ForeignKey("sizes.id"), nullable=False)
|
||||
sku = Column(String, unique=True, nullable=False) # артикул
|
||||
stock = Column(Integer, default=0) # количество на складе
|
||||
name = Column(String, nullable=False)
|
||||
sku = Column(String, unique=True, nullable=False) # артикул
|
||||
price = Column(Float, nullable=False)
|
||||
discount_price = Column(Float, nullable=True)
|
||||
stock = Column(Integer, default=0)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Отношения
|
||||
product = relationship("Product", back_populates="variants")
|
||||
size = relationship("Size", back_populates="product_variants")
|
||||
|
||||
|
||||
class ProductImage(Base):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey, DateTime, Text, Enum, JSON
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey, DateTime, Text, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import enum
|
||||
@ -20,8 +20,6 @@ class PaymentMethod(str, enum.Enum):
|
||||
PAYPAL = "paypal"
|
||||
BANK_TRANSFER = "bank_transfer"
|
||||
CASH_ON_DELIVERY = "cash_on_delivery"
|
||||
SBP = "sbp"
|
||||
CARD = "card"
|
||||
|
||||
|
||||
class CartItem(Base):
|
||||
@ -46,28 +44,9 @@ class Order(Base):
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
status = Column(Enum(OrderStatus), default=OrderStatus.PENDING)
|
||||
total_amount = Column(Float, nullable=False)
|
||||
|
||||
# JSON-поле с информацией о пользователе (для резервного копирования)
|
||||
user_info_json = Column(JSON, nullable=True)
|
||||
|
||||
# Информация о доставке
|
||||
delivery_method = Column(String, nullable=True) # cdek, courier
|
||||
city = Column(String, nullable=True)
|
||||
delivery_address = Column(String, nullable=True) # Форматированный адрес
|
||||
cdek_info = Column(JSON, nullable=True) # Информация о доставке CDEK
|
||||
courier_info = Column(JSON, nullable=True) # Информация о курьерской доставке
|
||||
|
||||
# Старые поля (для обратной совместимости)
|
||||
shipping_address_id = Column(Integer, ForeignKey("user_addresses.id"), nullable=True)
|
||||
|
||||
# Информация об оплате
|
||||
payment_method = Column(Enum(PaymentMethod), nullable=True)
|
||||
payment_details = Column(Text, nullable=True)
|
||||
|
||||
# JSON-поле со списком заказанных товаров (для резервного копирования)
|
||||
items_json = Column(JSON, nullable=True)
|
||||
|
||||
# Дополнительная информация
|
||||
tracking_number = Column(String, nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@ -91,4 +70,4 @@ class OrderItem(Base):
|
||||
|
||||
# Отношения
|
||||
order = relationship("Order", back_populates="items")
|
||||
variant = relationship("ProductVariant")
|
||||
variant = relationship("ProductVariant")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -5,14 +5,13 @@ from typing import List, Optional, Dict, Any
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from app.models.catalog_models import Category, Product, ProductVariant, ProductImage, Collection, Size
|
||||
from app.models.catalog_models import Category, Product, ProductVariant, ProductImage, Collection
|
||||
from app.schemas.catalog_schemas import (
|
||||
CategoryCreate, CategoryUpdate,
|
||||
ProductCreate, ProductUpdate,
|
||||
ProductVariantCreate, ProductVariantUpdate,
|
||||
ProductImageCreate, ProductImageUpdate,
|
||||
CollectionCreate, CollectionUpdate,
|
||||
SizeCreate, SizeUpdate
|
||||
CollectionCreate, CollectionUpdate
|
||||
)
|
||||
|
||||
|
||||
@ -28,7 +27,7 @@ def generate_slug(name: str) -> str:
|
||||
slug = re.sub(r'-+', '-', slug)
|
||||
# Удаляем дефисы в начале и конце
|
||||
slug = slug.strip('-')
|
||||
|
||||
|
||||
return slug
|
||||
|
||||
|
||||
@ -42,16 +41,16 @@ def get_collection_by_slug(db: Session, slug: str) -> Optional[Collection]:
|
||||
|
||||
|
||||
def get_collections(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
is_active: Optional[bool] = True
|
||||
) -> List[Collection]:
|
||||
query = db.query(Collection)
|
||||
|
||||
|
||||
if is_active is not None:
|
||||
query = query.filter(Collection.is_active == is_active)
|
||||
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
@ -59,14 +58,14 @@ def create_collection(db: Session, collection: CollectionCreate) -> Collection:
|
||||
# Если slug не предоставлен, генерируем его из имени
|
||||
if not collection.slug:
|
||||
collection.slug = generate_slug(collection.name)
|
||||
|
||||
|
||||
# Проверяем, что коллекция с таким slug не существует
|
||||
if get_collection_by_slug(db, collection.slug):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Коллекция с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Создаем новую коллекцию
|
||||
db_collection = Collection(
|
||||
name=collection.name,
|
||||
@ -74,7 +73,7 @@ def create_collection(db: Session, collection: CollectionCreate) -> Collection:
|
||||
description=collection.description,
|
||||
is_active=collection.is_active
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_collection)
|
||||
db.commit()
|
||||
@ -95,10 +94,10 @@ def update_collection(db: Session, collection_id: int, collection: CollectionUpd
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Коллекция не найдена"
|
||||
)
|
||||
|
||||
|
||||
# Обновляем только предоставленные поля
|
||||
update_data = collection.dict(exclude_unset=True)
|
||||
|
||||
|
||||
# Если slug изменяется, проверяем его уникальность
|
||||
if "slug" in update_data and update_data["slug"] != db_collection.slug:
|
||||
if get_collection_by_slug(db, update_data["slug"]):
|
||||
@ -106,7 +105,7 @@ def update_collection(db: Session, collection_id: int, collection: CollectionUpd
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Коллекция с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Если имя изменяется и slug не предоставлен, генерируем новый slug
|
||||
if "name" in update_data and "slug" not in update_data:
|
||||
update_data["slug"] = generate_slug(update_data["name"])
|
||||
@ -116,11 +115,11 @@ def update_collection(db: Session, collection_id: int, collection: CollectionUpd
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Коллекция с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Применяем обновления
|
||||
for key, value in update_data.items():
|
||||
setattr(db_collection, key, value)
|
||||
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_collection)
|
||||
@ -140,14 +139,14 @@ def delete_collection(db: Session, collection_id: int) -> bool:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Коллекция не найдена"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, есть ли у коллекции продукты
|
||||
if db.query(Product).filter(Product.collection_id == collection_id).count() > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Нельзя удалить коллекцию, у которой есть продукты"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.delete(db_collection)
|
||||
db.commit()
|
||||
@ -170,18 +169,18 @@ def get_category_by_slug(db: Session, slug: str) -> Optional[Category]:
|
||||
|
||||
|
||||
def get_categories(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
parent_id: Optional[int] = None
|
||||
) -> List[Category]:
|
||||
query = db.query(Category)
|
||||
|
||||
|
||||
if parent_id is not None:
|
||||
query = query.filter(Category.parent_id == parent_id)
|
||||
else:
|
||||
query = query.filter(Category.parent_id == None)
|
||||
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
@ -189,21 +188,21 @@ def create_category(db: Session, category: CategoryCreate) -> Category:
|
||||
# Если slug не предоставлен, генерируем его из имени
|
||||
if not category.slug:
|
||||
category.slug = generate_slug(category.name)
|
||||
|
||||
|
||||
# Проверяем, что категория с таким slug не существует
|
||||
if get_category_by_slug(db, category.slug):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Категория с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, что родительская категория существует, если указана
|
||||
if category.parent_id and not get_category(db, category.parent_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Родительская категория не найдена"
|
||||
)
|
||||
|
||||
|
||||
# Создаем новую категорию
|
||||
db_category = Category(
|
||||
name=category.name,
|
||||
@ -212,7 +211,7 @@ def create_category(db: Session, category: CategoryCreate) -> Category:
|
||||
parent_id=category.parent_id,
|
||||
is_active=category.is_active
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_category)
|
||||
db.commit()
|
||||
@ -233,10 +232,10 @@ def update_category(db: Session, category_id: int, category: CategoryUpdate) ->
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Категория не найдена"
|
||||
)
|
||||
|
||||
|
||||
# Обновляем только предоставленные поля
|
||||
update_data = category.dict(exclude_unset=True)
|
||||
|
||||
|
||||
# Если slug изменяется, проверяем его уникальность
|
||||
if "slug" in update_data and update_data["slug"] != db_category.slug:
|
||||
if get_category_by_slug(db, update_data["slug"]):
|
||||
@ -244,7 +243,7 @@ def update_category(db: Session, category_id: int, category: CategoryUpdate) ->
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Категория с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Если имя изменяется и slug не предоставлен, генерируем новый slug
|
||||
if "name" in update_data and "slug" not in update_data:
|
||||
update_data["slug"] = generate_slug(update_data["name"])
|
||||
@ -254,25 +253,25 @@ def update_category(db: Session, category_id: int, category: CategoryUpdate) ->
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Категория с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, что родительская категория существует, если указана
|
||||
if "parent_id" in update_data and update_data["parent_id"] and not get_category(db, update_data["parent_id"]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Родительская категория не найдена"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, что категория не становится своим собственным родителем
|
||||
if "parent_id" in update_data and update_data["parent_id"] == category_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Категория не может быть своим собственным родителем"
|
||||
)
|
||||
|
||||
|
||||
# Применяем обновления
|
||||
for key, value in update_data.items():
|
||||
setattr(db_category, key, value)
|
||||
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_category)
|
||||
@ -292,21 +291,21 @@ def delete_category(db: Session, category_id: int) -> bool:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Категория не найдена"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, есть ли у категории подкатегории
|
||||
if db.query(Category).filter(Category.parent_id == category_id).count() > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Нельзя удалить категорию, у которой есть подкатегории"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, есть ли у категории продукты
|
||||
if db.query(Product).filter(Product.category_id == category_id).count() > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Нельзя удалить категорию, у которой есть продукты"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.delete(db_category)
|
||||
db.commit()
|
||||
@ -329,52 +328,50 @@ def get_product_by_slug(db: Session, slug: str) -> Optional[Product]:
|
||||
|
||||
|
||||
def get_products(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
category_id: Optional[int] = None,
|
||||
collection_id: Optional[int] = None,
|
||||
search: Optional[str] = None,
|
||||
min_price: Optional[float] = None,
|
||||
max_price: Optional[float] = None,
|
||||
is_active: Optional[bool] = True
|
||||
is_active: Optional[bool] = True,
|
||||
include_variants: Optional[bool] = False
|
||||
) -> List[Product]:
|
||||
query = db.query(Product)
|
||||
|
||||
|
||||
# Применяем фильтры
|
||||
if category_id:
|
||||
query = query.filter(Product.category_id == category_id)
|
||||
|
||||
|
||||
if collection_id:
|
||||
query = query.filter(Product.collection_id == collection_id)
|
||||
|
||||
|
||||
if search:
|
||||
query = query.filter(Product.name.ilike(f"%{search}%"))
|
||||
|
||||
|
||||
if is_active is not None:
|
||||
query = query.filter(Product.is_active == is_active)
|
||||
|
||||
|
||||
# Фильтрация по цене теперь должна быть через варианты продукта
|
||||
if min_price is not None or max_price is not None:
|
||||
query = query.join(ProductVariant)
|
||||
|
||||
|
||||
if min_price is not None:
|
||||
query = query.filter(ProductVariant.price >= min_price)
|
||||
|
||||
|
||||
if max_price is not None:
|
||||
query = query.filter(ProductVariant.price <= max_price)
|
||||
|
||||
|
||||
products = query.offset(skip).limit(limit).all()
|
||||
|
||||
# Всегда загружаем варианты и изображения для каждого продукта
|
||||
for product in products:
|
||||
# Загружаем варианты с размерами
|
||||
variants = db.query(ProductVariant).join(Size, ProductVariant.size_id == Size.id).filter(ProductVariant.product_id == product.id).all()
|
||||
product.variants = variants
|
||||
|
||||
# Загружаем изображения
|
||||
product.images = db.query(ProductImage).filter(ProductImage.product_id == product.id).all()
|
||||
|
||||
|
||||
# Если нужно включить варианты, загружаем их для каждого продукта
|
||||
if include_variants:
|
||||
for product in products:
|
||||
product.variants = db.query(ProductVariant).filter(ProductVariant.product_id == product.id).all()
|
||||
product.images = db.query(ProductImage).filter(ProductImage.product_id == product.id).all()
|
||||
|
||||
return products
|
||||
|
||||
|
||||
@ -382,50 +379,48 @@ def create_product(db: Session, product: ProductCreate) -> Product:
|
||||
# Если slug не предоставлен, генерируем его из имени
|
||||
if not product.slug:
|
||||
product.slug = generate_slug(product.name)
|
||||
|
||||
# Проверяем, что slug уникален
|
||||
if db.query(Product).filter(Product.slug == product.slug).first():
|
||||
|
||||
# Проверяем, что продукт с таким slug не существует
|
||||
if get_product_by_slug(db, product.slug):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Продукт с slug '{product.slug}' уже существует"
|
||||
detail="Продукт с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, что категория существует
|
||||
if not get_category(db, product.category_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Категория с ID {product.category_id} не найдена"
|
||||
detail="Категория не найдена"
|
||||
)
|
||||
|
||||
# Проверяем, что коллекция существует, если она указана
|
||||
|
||||
# Проверяем, что коллекция существует, если указана
|
||||
if product.collection_id and not get_collection(db, product.collection_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Коллекция с ID {product.collection_id} не найдена"
|
||||
detail="Коллекция не найдена"
|
||||
)
|
||||
|
||||
|
||||
# Создаем новый продукт
|
||||
db_product = Product(
|
||||
name=product.name,
|
||||
slug=product.slug,
|
||||
description=product.description,
|
||||
price=product.price,
|
||||
discount_price=product.discount_price,
|
||||
care_instructions=product.care_instructions,
|
||||
is_active=product.is_active,
|
||||
category_id=product.category_id,
|
||||
collection_id=product.collection_id
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_product)
|
||||
db.commit()
|
||||
db.refresh(db_product)
|
||||
return db_product
|
||||
except IntegrityError as e:
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при создании продукта: {str(e)}"
|
||||
detail="Ошибка при создании продукта"
|
||||
)
|
||||
|
||||
|
||||
@ -434,72 +429,57 @@ def update_product(db: Session, product_id: int, product: ProductUpdate) -> Prod
|
||||
if not db_product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Продукт с ID {product_id} не найден"
|
||||
detail="Продукт не найден"
|
||||
)
|
||||
|
||||
|
||||
# Обновляем только предоставленные поля
|
||||
update_data = product.dict(exclude_unset=True)
|
||||
|
||||
# Если slug изменяется, проверяем его уникальность
|
||||
if product.slug is not None and product.slug != db_product.slug:
|
||||
if db.query(Product).filter(Product.slug == product.slug).first():
|
||||
if "slug" in update_data and update_data["slug"] != db_product.slug:
|
||||
if get_product_by_slug(db, update_data["slug"]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Продукт с slug '{product.slug}' уже существует"
|
||||
detail="Продукт с таким slug уже существует"
|
||||
)
|
||||
|
||||
# Если имя изменяется и slug не предоставлен, обновляем slug
|
||||
if product.name is not None and product.name != db_product.name and product.slug is None:
|
||||
product.slug = generate_slug(product.name)
|
||||
# Проверяем, что новый slug уникален
|
||||
if db.query(Product).filter(Product.slug == product.slug).first():
|
||||
|
||||
# Если имя изменяется и slug не предоставлен, генерируем новый slug
|
||||
if "name" in update_data and "slug" not in update_data:
|
||||
update_data["slug"] = generate_slug(update_data["name"])
|
||||
# Проверяем уникальность сгенерированного slug
|
||||
if get_product_by_slug(db, update_data["slug"]) and get_product_by_slug(db, update_data["slug"]).id != product_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Продукт с slug '{product.slug}' уже существует"
|
||||
detail="Продукт с таким slug уже существует"
|
||||
)
|
||||
|
||||
# Проверяем, что категория существует, если она изменяется
|
||||
if product.category_id is not None and product.category_id != db_product.category_id:
|
||||
if not get_category(db, product.category_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Категория с ID {product.category_id} не найдена"
|
||||
)
|
||||
|
||||
# Проверяем, что коллекция существует, если она изменяется
|
||||
if product.collection_id is not None and product.collection_id != db_product.collection_id:
|
||||
if product.collection_id and not get_collection(db, product.collection_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Коллекция с ID {product.collection_id} не найдена"
|
||||
)
|
||||
|
||||
# Обновляем поля
|
||||
if product.name is not None:
|
||||
db_product.name = product.name
|
||||
if product.slug is not None:
|
||||
db_product.slug = product.slug
|
||||
if product.description is not None:
|
||||
db_product.description = product.description
|
||||
if product.price is not None:
|
||||
db_product.price = product.price
|
||||
if product.discount_price is not None:
|
||||
db_product.discount_price = product.discount_price
|
||||
if product.care_instructions is not None:
|
||||
db_product.care_instructions = product.care_instructions
|
||||
if product.is_active is not None:
|
||||
db_product.is_active = product.is_active
|
||||
if product.category_id is not None:
|
||||
db_product.category_id = product.category_id
|
||||
if product.collection_id is not None:
|
||||
db_product.collection_id = product.collection_id
|
||||
|
||||
|
||||
# Проверяем, что категория существует, если указана
|
||||
if "category_id" in update_data and update_data["category_id"] and not get_category(db, update_data["category_id"]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Категория не найдена"
|
||||
)
|
||||
|
||||
# Проверяем, что коллекция существует, если указана
|
||||
if "collection_id" in update_data and update_data["collection_id"] and not get_collection(db, update_data["collection_id"]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Коллекция не найдена"
|
||||
)
|
||||
|
||||
# Применяем обновления
|
||||
for key, value in update_data.items():
|
||||
setattr(db_product, key, value)
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_product)
|
||||
return db_product
|
||||
except IntegrityError as e:
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при обновлении продукта: {str(e)}"
|
||||
detail="Ошибка при обновлении продукта"
|
||||
)
|
||||
|
||||
|
||||
@ -510,36 +490,16 @@ def delete_product(db: Session, product_id: int) -> bool:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Продукт не найден"
|
||||
)
|
||||
|
||||
# Получаем ID всех вариантов продукта
|
||||
variant_ids = [variant.id for variant in db_product.variants]
|
||||
|
||||
# Проверяем, используются ли варианты в корзинах
|
||||
from app.models.order_models import CartItem
|
||||
cart_items_count = db.query(CartItem).filter(CartItem.variant_id.in_(variant_ids)).count()
|
||||
if cart_items_count > 0:
|
||||
# Удаляем все элементы корзины, связанные с вариантами продукта
|
||||
db.query(CartItem).filter(CartItem.variant_id.in_(variant_ids)).delete(synchronize_session=False)
|
||||
|
||||
# Проверяем, используются ли варианты в заказах
|
||||
from app.models.order_models import OrderItem
|
||||
order_items_count = db.query(OrderItem).filter(OrderItem.variant_id.in_(variant_ids)).count()
|
||||
if order_items_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Невозможно удалить продукт, так как его варианты используются в {order_items_count} заказах"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
# Удаляем продукт (варианты и изображения удалятся автоматически благодаря cascade)
|
||||
db.delete(db_product)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при удалении продукта: {str(e)}"
|
||||
detail="Ошибка при удалении продукта"
|
||||
)
|
||||
|
||||
|
||||
@ -549,64 +509,45 @@ def get_product_variant(db: Session, variant_id: int) -> Optional[ProductVariant
|
||||
|
||||
|
||||
def get_product_variants(db: Session, product_id: int) -> List[ProductVariant]:
|
||||
# Загружаем варианты с размерами
|
||||
return db.query(ProductVariant).join(Size, ProductVariant.size_id == Size.id).filter(ProductVariant.product_id == product_id).all()
|
||||
return db.query(ProductVariant).filter(ProductVariant.product_id == product_id).all()
|
||||
|
||||
|
||||
def create_product_variant(db: Session, variant: ProductVariantCreate) -> ProductVariant:
|
||||
# Проверяем, что продукт существует
|
||||
db_product = get_product(db, variant.product_id)
|
||||
if not db_product:
|
||||
if not get_product(db, variant.product_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Продукт с ID {variant.product_id} не найден"
|
||||
detail="Продукт не найден"
|
||||
)
|
||||
|
||||
# Проверяем, что размер существует
|
||||
db_size = get_size(db, variant.size_id)
|
||||
if not db_size:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Размер с ID {variant.size_id} не найден"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, что вариант с таким SKU не существует
|
||||
existing_variant = db.query(ProductVariant).filter(ProductVariant.sku == variant.sku).first()
|
||||
if existing_variant:
|
||||
if db.query(ProductVariant).filter(ProductVariant.sku == variant.sku).first():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Вариант продукта с SKU {variant.sku} уже существует"
|
||||
detail="Вариант с таким SKU уже существует"
|
||||
)
|
||||
|
||||
# Проверяем, что вариант с таким размером для этого продукта не существует
|
||||
existing_size_variant = db.query(ProductVariant).filter(
|
||||
ProductVariant.product_id == variant.product_id,
|
||||
ProductVariant.size_id == variant.size_id
|
||||
).first()
|
||||
if existing_size_variant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Вариант продукта с размером ID {variant.size_id} уже существует для этого продукта"
|
||||
)
|
||||
|
||||
|
||||
# Создаем новый вариант продукта
|
||||
db_variant = ProductVariant(
|
||||
product_id=variant.product_id,
|
||||
size_id=variant.size_id,
|
||||
name=variant.name,
|
||||
sku=variant.sku,
|
||||
price=variant.price,
|
||||
discount_price=variant.discount_price,
|
||||
stock=variant.stock,
|
||||
is_active=variant.is_active
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_variant)
|
||||
db.commit()
|
||||
db.refresh(db_variant)
|
||||
return db_variant
|
||||
except IntegrityError as e:
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при создании варианта продукта: {str(e)}"
|
||||
detail="Ошибка при создании варианта продукта"
|
||||
)
|
||||
|
||||
|
||||
@ -615,73 +556,41 @@ def update_product_variant(db: Session, variant_id: int, variant: ProductVariant
|
||||
if not db_variant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Вариант продукта с ID {variant_id} не найден"
|
||||
detail="Вариант продукта не найден"
|
||||
)
|
||||
|
||||
# Проверяем, что продукт существует, если ID продукта изменяется
|
||||
if variant.product_id is not None and variant.product_id != db_variant.product_id:
|
||||
db_product = get_product(db, variant.product_id)
|
||||
if not db_product:
|
||||
|
||||
# Обновляем только предоставленные поля
|
||||
update_data = variant.dict(exclude_unset=True)
|
||||
|
||||
# Если product_id изменяется, проверяем, что продукт существует
|
||||
if "product_id" in update_data and update_data["product_id"] != db_variant.product_id:
|
||||
if not get_product(db, update_data["product_id"]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Продукт с ID {variant.product_id} не найден"
|
||||
detail="Продукт не найден"
|
||||
)
|
||||
|
||||
# Проверяем, что размер существует, если ID размера изменяется
|
||||
if variant.size_id is not None and variant.size_id != db_variant.size_id:
|
||||
db_size = get_size(db, variant.size_id)
|
||||
if not db_size:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Размер с ID {variant.size_id} не найден"
|
||||
)
|
||||
|
||||
# Проверяем, что вариант с таким размером для этого продукта не существует
|
||||
product_id = variant.product_id if variant.product_id is not None else db_variant.product_id
|
||||
existing_size_variant = db.query(ProductVariant).filter(
|
||||
ProductVariant.product_id == product_id,
|
||||
ProductVariant.size_id == variant.size_id,
|
||||
ProductVariant.id != variant_id
|
||||
).first()
|
||||
if existing_size_variant:
|
||||
|
||||
# Если SKU изменяется, проверяем его уникальность
|
||||
if "sku" in update_data and update_data["sku"] != db_variant.sku:
|
||||
if db.query(ProductVariant).filter(ProductVariant.sku == update_data["sku"]).first():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Вариант продукта с размером ID {variant.size_id} уже существует для этого продукта"
|
||||
detail="Вариант с таким SKU уже существует"
|
||||
)
|
||||
|
||||
# Проверяем, что SKU уникален, если он изменяется
|
||||
if variant.sku is not None and variant.sku != db_variant.sku:
|
||||
existing_variant = db.query(ProductVariant).filter(
|
||||
ProductVariant.sku == variant.sku,
|
||||
ProductVariant.id != variant_id
|
||||
).first()
|
||||
if existing_variant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Вариант продукта с SKU {variant.sku} уже существует"
|
||||
)
|
||||
|
||||
# Обновляем поля
|
||||
if variant.product_id is not None:
|
||||
db_variant.product_id = variant.product_id
|
||||
if variant.size_id is not None:
|
||||
db_variant.size_id = variant.size_id
|
||||
if variant.sku is not None:
|
||||
db_variant.sku = variant.sku
|
||||
if variant.stock is not None:
|
||||
db_variant.stock = variant.stock
|
||||
if variant.is_active is not None:
|
||||
db_variant.is_active = variant.is_active
|
||||
|
||||
|
||||
# Применяем обновления
|
||||
for key, value in update_data.items():
|
||||
setattr(db_variant, key, value)
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_variant)
|
||||
return db_variant
|
||||
except IntegrityError as e:
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при обновлении варианта продукта: {str(e)}"
|
||||
detail="Ошибка при обновлении варианта продукта"
|
||||
)
|
||||
|
||||
|
||||
@ -692,7 +601,7 @@ def delete_product_variant(db: Session, variant_id: int) -> bool:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Вариант продукта не найден"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.delete(db_variant)
|
||||
db.commit()
|
||||
@ -721,14 +630,14 @@ def create_product_image(db: Session, image: ProductImageCreate) -> ProductImage
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Продукт не найден"
|
||||
)
|
||||
|
||||
|
||||
# Если изображение отмечено как основное, сбрасываем флаг у других изображений
|
||||
if image.is_primary:
|
||||
db.query(ProductImage).filter(
|
||||
ProductImage.product_id == image.product_id,
|
||||
ProductImage.is_primary == True
|
||||
).update({"is_primary": False})
|
||||
|
||||
|
||||
# Создаем новое изображение продукта
|
||||
db_image = ProductImage(
|
||||
product_id=image.product_id,
|
||||
@ -736,7 +645,7 @@ def create_product_image(db: Session, image: ProductImageCreate) -> ProductImage
|
||||
alt_text=image.alt_text,
|
||||
is_primary=image.is_primary
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_image)
|
||||
db.commit()
|
||||
@ -750,27 +659,24 @@ def create_product_image(db: Session, image: ProductImageCreate) -> ProductImage
|
||||
)
|
||||
|
||||
|
||||
def update_product_image(db: Session, image_id: int, image: ProductImageUpdate) -> ProductImage:
|
||||
def update_product_image(db: Session, image_id: int, is_primary: bool) -> ProductImage:
|
||||
db_image = get_product_image(db, image_id)
|
||||
if not db_image:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Изображение продукта не найдено"
|
||||
)
|
||||
|
||||
|
||||
# Если изображение отмечается как основное, сбрасываем флаг у других изображений
|
||||
if image.is_primary and not db_image.is_primary:
|
||||
if is_primary and not db_image.is_primary:
|
||||
db.query(ProductImage).filter(
|
||||
ProductImage.product_id == db_image.product_id,
|
||||
ProductImage.is_primary == True
|
||||
).update({"is_primary": False})
|
||||
|
||||
# Обновляем поля
|
||||
if image.alt_text is not None:
|
||||
db_image.alt_text = image.alt_text
|
||||
if image.is_primary is not None:
|
||||
db_image.is_primary = image.is_primary
|
||||
|
||||
|
||||
# Обновляем флаг
|
||||
db_image.is_primary = is_primary
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_image)
|
||||
@ -790,7 +696,7 @@ def delete_product_image(db: Session, image_id: int) -> bool:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Изображение продукта не найдено"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.delete(db_image)
|
||||
db.commit()
|
||||
@ -800,110 +706,4 @@ def delete_product_image(db: Session, image_id: int) -> bool:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Ошибка при удалении изображения продукта"
|
||||
)
|
||||
|
||||
|
||||
# Функции для работы с размерами
|
||||
def get_size(db: Session, size_id: int) -> Optional[Size]:
|
||||
return db.query(Size).filter(Size.id == size_id).first()
|
||||
|
||||
|
||||
def get_size_by_code(db: Session, code: str) -> Optional[Size]:
|
||||
return db.query(Size).filter(Size.code == code).first()
|
||||
|
||||
|
||||
def get_sizes(db: Session, skip: int = 0, limit: int = 100) -> List[Size]:
|
||||
return db.query(Size).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
def create_size(db: Session, size: SizeCreate) -> Size:
|
||||
# Проверяем, что размер с таким кодом не существует
|
||||
existing_size = get_size_by_code(db, size.code)
|
||||
if existing_size:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Размер с кодом {size.code} уже существует"
|
||||
)
|
||||
|
||||
db_size = Size(
|
||||
name=size.name,
|
||||
code=size.code,
|
||||
description=size.description
|
||||
)
|
||||
|
||||
try:
|
||||
db.add(db_size)
|
||||
db.commit()
|
||||
db.refresh(db_size)
|
||||
return db_size
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при создании размера: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
def update_size(db: Session, size_id: int, size: SizeUpdate) -> Size:
|
||||
db_size = get_size(db, size_id)
|
||||
if not db_size:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Размер с ID {size_id} не найден"
|
||||
)
|
||||
|
||||
# Проверяем, что код размера уникален, если он изменяется
|
||||
if size.code and size.code != db_size.code:
|
||||
existing_size = get_size_by_code(db, size.code)
|
||||
if existing_size:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Размер с кодом {size.code} уже существует"
|
||||
)
|
||||
|
||||
# Обновляем поля
|
||||
if size.name is not None:
|
||||
db_size.name = size.name
|
||||
if size.code is not None:
|
||||
db_size.code = size.code
|
||||
if size.description is not None:
|
||||
db_size.description = size.description
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_size)
|
||||
return db_size
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при обновлении размера: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
def delete_size(db: Session, size_id: int) -> bool:
|
||||
db_size = get_size(db, size_id)
|
||||
if not db_size:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Размер с ID {size_id} не найден"
|
||||
)
|
||||
|
||||
# Проверяем, используется ли размер в вариантах продуктов
|
||||
variants_with_size = db.query(ProductVariant).filter(ProductVariant.size_id == size_id).count()
|
||||
if variants_with_size > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Невозможно удалить размер, так как он используется в {variants_with_size} вариантах продуктов"
|
||||
)
|
||||
|
||||
try:
|
||||
db.delete(db_size)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при удалении размера: {str(e)}"
|
||||
)
|
||||
)
|
||||
@ -21,7 +21,7 @@ def generate_slug(title: str) -> str:
|
||||
slug = re.sub(r'-+', '-', slug)
|
||||
# Удаляем дефисы в начале и конце
|
||||
slug = slug.strip('-')
|
||||
|
||||
|
||||
return slug
|
||||
|
||||
|
||||
@ -35,16 +35,16 @@ def get_page_by_slug(db: Session, slug: str) -> Optional[Page]:
|
||||
|
||||
|
||||
def get_pages(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
published_only: bool = True
|
||||
) -> List[Page]:
|
||||
query = db.query(Page)
|
||||
|
||||
|
||||
if published_only:
|
||||
query = query.filter(Page.is_published == True)
|
||||
|
||||
|
||||
return query.order_by(Page.title).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
@ -52,14 +52,14 @@ def create_page(db: Session, page: PageCreate) -> Page:
|
||||
# Если slug не предоставлен, генерируем его из заголовка
|
||||
if not page.slug:
|
||||
page.slug = generate_slug(page.title)
|
||||
|
||||
|
||||
# Проверяем, что страница с таким slug не существует
|
||||
if get_page_by_slug(db, page.slug):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Страница с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Создаем новую страницу
|
||||
db_page = Page(
|
||||
title=page.title,
|
||||
@ -69,7 +69,7 @@ def create_page(db: Session, page: PageCreate) -> Page:
|
||||
meta_description=page.meta_description,
|
||||
is_published=page.is_published
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_page)
|
||||
db.commit()
|
||||
@ -90,10 +90,10 @@ def update_page(db: Session, page_id: int, page: PageUpdate) -> Page:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Страница не найдена"
|
||||
)
|
||||
|
||||
|
||||
# Обновляем только предоставленные поля
|
||||
update_data = page.model_dump(exclude_unset=True)
|
||||
|
||||
update_data = page.dict(exclude_unset=True)
|
||||
|
||||
# Если slug изменяется, проверяем его уникальность
|
||||
if "slug" in update_data and update_data["slug"] != db_page.slug:
|
||||
if get_page_by_slug(db, update_data["slug"]):
|
||||
@ -101,7 +101,7 @@ def update_page(db: Session, page_id: int, page: PageUpdate) -> Page:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Страница с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Если заголовок изменяется и slug не предоставлен, генерируем новый slug
|
||||
if "title" in update_data and "slug" not in update_data:
|
||||
update_data["slug"] = generate_slug(update_data["title"])
|
||||
@ -111,11 +111,11 @@ def update_page(db: Session, page_id: int, page: PageUpdate) -> Page:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Страница с таким slug уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Применяем обновления
|
||||
for key, value in update_data.items():
|
||||
setattr(db_page, key, value)
|
||||
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_page)
|
||||
@ -135,7 +135,7 @@ def delete_page(db: Session, page_id: int) -> bool:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Страница не найдена"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.delete(db_page)
|
||||
db.commit()
|
||||
@ -152,7 +152,7 @@ def delete_page(db: Session, page_id: int) -> bool:
|
||||
def log_analytics_event(db: Session, log: AnalyticsLogCreate) -> AnalyticsLog:
|
||||
# Создаем новую запись аналитики
|
||||
db_log = AnalyticsLog(
|
||||
user_id=log.user_id, # Может быть None для неавторизованных пользователей
|
||||
user_id=log.user_id,
|
||||
event_type=log.event_type,
|
||||
page_url=log.page_url,
|
||||
product_id=log.product_id,
|
||||
@ -162,7 +162,7 @@ def log_analytics_event(db: Session, log: AnalyticsLogCreate) -> AnalyticsLog:
|
||||
referrer=log.referrer,
|
||||
additional_data=log.additional_data
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_log)
|
||||
db.commit()
|
||||
@ -177,9 +177,9 @@ def log_analytics_event(db: Session, log: AnalyticsLogCreate) -> AnalyticsLog:
|
||||
|
||||
|
||||
def get_analytics_logs(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
event_type: Optional[str] = None,
|
||||
user_id: Optional[int] = None,
|
||||
product_id: Optional[int] = None,
|
||||
@ -188,31 +188,31 @@ def get_analytics_logs(
|
||||
end_date: Optional[datetime] = None
|
||||
) -> List[AnalyticsLog]:
|
||||
query = db.query(AnalyticsLog)
|
||||
|
||||
|
||||
# Применяем фильтры
|
||||
if event_type:
|
||||
query = query.filter(AnalyticsLog.event_type == event_type)
|
||||
|
||||
|
||||
if user_id:
|
||||
query = query.filter(AnalyticsLog.user_id == user_id)
|
||||
|
||||
|
||||
if product_id:
|
||||
query = query.filter(AnalyticsLog.product_id == product_id)
|
||||
|
||||
|
||||
if category_id:
|
||||
query = query.filter(AnalyticsLog.category_id == category_id)
|
||||
|
||||
|
||||
if start_date:
|
||||
query = query.filter(AnalyticsLog.created_at >= start_date)
|
||||
|
||||
|
||||
if end_date:
|
||||
query = query.filter(AnalyticsLog.created_at <= end_date)
|
||||
|
||||
|
||||
return query.order_by(AnalyticsLog.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
def get_analytics_report(
|
||||
db: Session,
|
||||
db: Session,
|
||||
period: str = "day",
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None
|
||||
@ -229,44 +229,44 @@ def get_analytics_report(
|
||||
start_date = datetime.utcnow() - timedelta(days=365)
|
||||
else:
|
||||
start_date = datetime.utcnow() - timedelta(days=30) # По умолчанию 30 дней
|
||||
|
||||
|
||||
if not end_date:
|
||||
end_date = datetime.utcnow()
|
||||
|
||||
|
||||
# Получаем все события за указанный период
|
||||
logs = db.query(AnalyticsLog).filter(
|
||||
AnalyticsLog.created_at >= start_date,
|
||||
AnalyticsLog.created_at <= end_date
|
||||
).all()
|
||||
|
||||
|
||||
# Подсчитываем статистику
|
||||
total_visits = len(logs)
|
||||
unique_visitors = len(set([log.ip_address for log in logs if log.ip_address]))
|
||||
|
||||
|
||||
# Подсчитываем просмотры страниц
|
||||
page_views = {}
|
||||
for log in logs:
|
||||
if log.event_type == "page_view" and log.page_url:
|
||||
page_views[log.page_url] = page_views.get(log.page_url, 0) + 1
|
||||
|
||||
|
||||
# Подсчитываем просмотры продуктов
|
||||
product_views = {}
|
||||
for log in logs:
|
||||
if log.event_type == "product_view" and log.product_id:
|
||||
product_id = str(log.product_id)
|
||||
product_views[product_id] = product_views.get(product_id, 0) + 1
|
||||
|
||||
|
||||
# Подсчитываем добавления в корзину
|
||||
cart_additions = sum(1 for log in logs if log.event_type == "add_to_cart")
|
||||
|
||||
|
||||
# Подсчитываем заказы и выручку
|
||||
orders_count = sum(1 for log in logs if log.event_type == "order_created")
|
||||
|
||||
|
||||
# Для расчета выручки и среднего чека нам нужны данные о заказах
|
||||
# В данном примере мы просто используем заглушки
|
||||
revenue = 0
|
||||
average_order_value = 0
|
||||
|
||||
|
||||
# Формируем отчет
|
||||
report = {
|
||||
"period": period,
|
||||
@ -281,5 +281,5 @@ def get_analytics_report(
|
||||
"revenue": revenue,
|
||||
"average_order_value": average_order_value
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
return report
|
||||
@ -1,15 +1,13 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException, status
|
||||
from typing import List, Optional, Dict, Any, Union, Tuple
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from app.models.order_models import CartItem, Order, OrderItem, OrderStatus, PaymentMethod
|
||||
from app.models.catalog_models import Product, ProductImage, ProductVariant, Size
|
||||
from app.models.catalog_models import Product, ProductImage, ProductVariant
|
||||
from app.models.user_models import User, UserAddress
|
||||
from app.schemas.order_schemas import CartItemCreate, CartItemUpdate, OrderCreate, OrderUpdate, OrderCreateNew
|
||||
from app.repositories import user_repo
|
||||
from app.schemas.order_schemas import CartItemCreate, CartItemUpdate, OrderCreate, OrderUpdate
|
||||
|
||||
|
||||
# Функции для работы с корзиной
|
||||
@ -36,7 +34,7 @@ def create_cart_item(db: Session, cart_item: CartItemCreate, user_id: int) -> Ca
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Вариант продукта не найден"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, что продукт активен
|
||||
product = db.query(Product).filter(Product.id == variant.product_id).first()
|
||||
if not product or not product.is_active:
|
||||
@ -44,7 +42,7 @@ def create_cart_item(db: Session, cart_item: CartItemCreate, user_id: int) -> Ca
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Продукт не активен или не найден"
|
||||
)
|
||||
|
||||
|
||||
# Проверяем, есть ли уже такой товар в корзине
|
||||
existing_item = get_cart_item_by_variant(db, user_id, cart_item.variant_id)
|
||||
if existing_item:
|
||||
@ -60,14 +58,14 @@ def create_cart_item(db: Session, cart_item: CartItemCreate, user_id: int) -> Ca
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Ошибка при обновлении элемента корзины"
|
||||
)
|
||||
|
||||
|
||||
# Создаем новый элемент корзины
|
||||
db_cart_item = CartItem(
|
||||
user_id=user_id,
|
||||
variant_id=cart_item.variant_id,
|
||||
quantity=cart_item.quantity
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_cart_item)
|
||||
db.commit()
|
||||
@ -86,20 +84,20 @@ def update_cart_item(db: Session, cart_item_id: int, cart_item: CartItemUpdate,
|
||||
CartItem.id == cart_item_id,
|
||||
CartItem.user_id == user_id
|
||||
).first()
|
||||
|
||||
|
||||
if not db_cart_item:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Элемент корзины не найден или не принадлежит пользователю"
|
||||
)
|
||||
|
||||
|
||||
# Обновляем только предоставленные поля
|
||||
update_data = cart_item.model_dump(exclude_unset=True)
|
||||
|
||||
update_data = cart_item.dict(exclude_unset=True)
|
||||
|
||||
# Применяем обновления
|
||||
for key, value in update_data.items():
|
||||
setattr(db_cart_item, key, value)
|
||||
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_cart_item)
|
||||
@ -117,13 +115,13 @@ def delete_cart_item(db: Session, cart_item_id: int, user_id: int) -> bool:
|
||||
CartItem.id == cart_item_id,
|
||||
CartItem.user_id == user_id
|
||||
).first()
|
||||
|
||||
|
||||
if not db_cart_item:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Элемент корзины не найден или не принадлежит пользователю"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.delete(db_cart_item)
|
||||
db.commit()
|
||||
@ -154,211 +152,43 @@ def get_order(db: Session, order_id: int) -> Optional[Order]:
|
||||
return db.query(Order).filter(Order.id == order_id).first()
|
||||
|
||||
|
||||
def create_order_new(db: Session, order: OrderCreateNew, user_id: Optional[int] = None) -> Order:
|
||||
"""
|
||||
Создает новый заказ на основе новой структуры данных.
|
||||
Если пользователь не авторизован (user_id=None), создает нового пользователя.
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
order: Данные для создания заказа
|
||||
user_id: ID пользователя (None, если пользователь не авторизован)
|
||||
|
||||
Returns:
|
||||
Созданный заказ
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла ошибка при создании заказа
|
||||
"""
|
||||
# Подготовим информацию о пользователе для сохранения в JSON
|
||||
user_info_dict = {
|
||||
"first_name": order.user_info.first_name,
|
||||
"last_name": order.user_info.last_name,
|
||||
"email": order.user_info.email,
|
||||
"phone": order.user_info.phone
|
||||
}
|
||||
|
||||
# Если пользователь не авторизован, создаем нового пользователя или находим существующего
|
||||
user_created_message = None
|
||||
if user_id is None:
|
||||
try:
|
||||
user, is_new, message = user_repo.find_or_create_user_from_order(db, user_info_dict)
|
||||
user_id = user.id
|
||||
user_created_message = message
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при создании пользователя: {str(e)}"
|
||||
)
|
||||
|
||||
# Подготовим информацию о товарах для сохранения в JSON
|
||||
items_json = []
|
||||
for item in order.items:
|
||||
# Получаем информацию о варианте и продукте
|
||||
variant = db.query(ProductVariant).filter(ProductVariant.id == item.variant_id).first()
|
||||
if not variant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Вариант товара с ID {item.variant_id} не найден"
|
||||
)
|
||||
|
||||
product = db.query(Product).filter(Product.id == variant.product_id).first()
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Продукт для варианта с ID {item.variant_id} не найден"
|
||||
)
|
||||
|
||||
# Получаем размер для варианта
|
||||
size = db.query(Size).filter(Size.id == variant.size_id).first()
|
||||
size_name = size.name if size else "Unknown"
|
||||
|
||||
# Получаем основное изображение продукта
|
||||
image = db.query(ProductImage).filter(
|
||||
ProductImage.product_id == product.id,
|
||||
ProductImage.is_primary == True
|
||||
).first()
|
||||
|
||||
if not image:
|
||||
# Если нет основного изображения, берем первое доступное
|
||||
image = db.query(ProductImage).filter(
|
||||
ProductImage.product_id == product.id
|
||||
).first()
|
||||
|
||||
image_url = image.image_url if image else None
|
||||
|
||||
# Добавляем информацию о товаре в JSON
|
||||
items_json.append({
|
||||
"product_id": product.id,
|
||||
"variant_id": variant.id,
|
||||
"quantity": item.quantity,
|
||||
"price": item.price,
|
||||
"product_name": product.name,
|
||||
"variant_name": size_name,
|
||||
"product_image": image_url,
|
||||
"slug": product.slug
|
||||
})
|
||||
|
||||
# Преобразуем строковое значение payment_method в значение перечисления PaymentMethod
|
||||
payment_method_str = order.payment_method
|
||||
|
||||
# Прямое сопоставление с известными значениями
|
||||
if payment_method_str == "sbp":
|
||||
payment_method = PaymentMethod.SBP
|
||||
elif payment_method_str == "card":
|
||||
payment_method = PaymentMethod.CARD
|
||||
else:
|
||||
# Если не нашли соответствия, используем значение по умолчанию
|
||||
payment_method = PaymentMethod.CARD
|
||||
# Логируем ошибку
|
||||
print(f"Неизвестный метод оплаты: {order.payment_method}. Используем значение по умолчанию: {payment_method}")
|
||||
|
||||
print(f"Метод оплаты: {payment_method} {type(payment_method)}")
|
||||
print(f"Тип payment_method_str: {type(payment_method_str)}")
|
||||
print(f"payment_method_str: {payment_method.value}")
|
||||
payment_method_str = payment_method.value
|
||||
print(f"payment_method_str: {payment_method_str} {type(payment_method_str)}")
|
||||
payment_method = payment_method_str
|
||||
|
||||
# Создаем новый заказ
|
||||
new_order = Order(
|
||||
user_id=user_id,
|
||||
status=OrderStatus.PENDING,
|
||||
|
||||
# JSON с информацией о пользователе
|
||||
user_info_json=user_info_dict,
|
||||
|
||||
# Информация о доставке
|
||||
delivery_method=order.delivery.method,
|
||||
city=order.delivery.address.city,
|
||||
delivery_address=order.delivery.address.formatted_address or f"{order.delivery.address.city}, {order.delivery.address.street} {order.delivery.address.house}",
|
||||
|
||||
# Информация о CDEK
|
||||
cdek_info=order.delivery.cdek_info.model_dump() if order.delivery.cdek_info else None,
|
||||
|
||||
# JSON с информацией о товарах
|
||||
items_json=items_json,
|
||||
|
||||
# Информация об оплате
|
||||
payment_method=payment_method,
|
||||
notes=order.comment,
|
||||
|
||||
# Общая сумма заказа (будет обновлена после добавления товаров)
|
||||
total_amount=0
|
||||
)
|
||||
|
||||
db.add(new_order)
|
||||
db.flush() # Получаем ID заказа
|
||||
|
||||
# Добавляем товары в заказ через разводную таблицу
|
||||
for item in order.items:
|
||||
# Создаем элемент заказа
|
||||
order_item = OrderItem(
|
||||
order_id=new_order.id,
|
||||
variant_id=item.variant_id,
|
||||
quantity=item.quantity,
|
||||
price=item.price
|
||||
)
|
||||
db.add(order_item)
|
||||
new_order.total_amount += item.price * item.quantity
|
||||
|
||||
# Проверяем, были ли добавлены элементы заказа
|
||||
if new_order.total_amount == 0:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Не удалось создать заказ: корзина пуста или товары недоступны"
|
||||
)
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(new_order)
|
||||
|
||||
# Добавляем информацию о создании пользователя в заказ
|
||||
if user_created_message:
|
||||
new_order.user_created_message = user_created_message
|
||||
|
||||
return new_order
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при создании заказа: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
def get_user_orders(db: Session, user_id: int, skip: int = 0, limit: int = 100) -> List[Order]:
|
||||
return db.query(Order).filter(Order.user_id == user_id).order_by(Order.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
def get_all_orders(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[OrderStatus] = None
|
||||
) -> List[Order]:
|
||||
query = db.query(Order)
|
||||
|
||||
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
|
||||
|
||||
return query.order_by(Order.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
def create_order(db: Session, order: OrderCreate, user_id: Optional[int] = None) -> Order:
|
||||
def create_order(db: Session, order: OrderCreate, user_id: int) -> Order:
|
||||
# Проверяем, что адрес доставки существует и принадлежит пользователю, если указан
|
||||
print(f"Начало создания заказа для пользователя {user_id}")
|
||||
print(f"Данные заказа: {order}")
|
||||
|
||||
if order.shipping_address_id:
|
||||
address = db.query(UserAddress).filter(
|
||||
UserAddress.id == order.shipping_address_id,
|
||||
UserAddress.user_id == user_id
|
||||
).first()
|
||||
|
||||
|
||||
if not address:
|
||||
print(f"Адрес доставки {order.shipping_address_id} не найден для пользователя {user_id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Указанный адрес доставки не найден"
|
||||
)
|
||||
|
||||
print(f"Адрес доставки найден: {address}")
|
||||
|
||||
# Создаем новый заказ
|
||||
new_order = Order(
|
||||
user_id=user_id,
|
||||
@ -368,21 +198,25 @@ def create_order(db: Session, order: OrderCreate, user_id: Optional[int] = None)
|
||||
notes=order.notes,
|
||||
total_amount=0 # Будет обновлено после добавления товаров
|
||||
)
|
||||
|
||||
|
||||
db.add(new_order)
|
||||
db.flush() # Получаем ID заказа
|
||||
|
||||
|
||||
print(f"Создан заказ с ID: {new_order.id}")
|
||||
|
||||
# Получаем элементы корзины пользователя
|
||||
cart_items = []
|
||||
|
||||
|
||||
# Если указаны конкретные элементы корзины
|
||||
if order.cart_items:
|
||||
print(f"Используем указанные элементы корзины: {order.cart_items}")
|
||||
cart_items = db.query(CartItem).filter(
|
||||
CartItem.id.in_(order.cart_items),
|
||||
CartItem.user_id == user_id
|
||||
).all()
|
||||
# Если указаны прямые элементы заказа
|
||||
elif order.items:
|
||||
print(f"Используем прямые элементы заказа: {order.items}")
|
||||
# Создаем элементы заказа напрямую
|
||||
for item_data in order.items:
|
||||
# Проверяем, что вариант существует
|
||||
@ -393,173 +227,118 @@ def create_order(db: Session, order: OrderCreate, user_id: Optional[int] = None)
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Вариант товара с ID {item_data.variant_id} не найден"
|
||||
)
|
||||
|
||||
# Получаем продукт для варианта
|
||||
product = db.query(Product).filter(Product.id == variant.product_id).first()
|
||||
if not product:
|
||||
|
||||
# Создаем элемент заказа
|
||||
order_item = OrderItem(
|
||||
order_id=new_order.id,
|
||||
product_id=variant.product_id,
|
||||
variant_id=variant.id,
|
||||
quantity=item_data.quantity,
|
||||
price=item_data.price
|
||||
)
|
||||
db.add(order_item)
|
||||
new_order.total_amount += order_item.price * order_item.quantity
|
||||
# Иначе используем все элементы корзины пользователя
|
||||
else:
|
||||
print(f"Используем все элементы корзины пользователя")
|
||||
cart_items = db.query(CartItem).filter(CartItem.user_id == user_id).all()
|
||||
|
||||
# Если используем элементы корзины
|
||||
if cart_items:
|
||||
print(f"Найдено {len(cart_items)} элементов корзины")
|
||||
# Создаем элементы заказа из элементов корзины
|
||||
for cart_item in cart_items:
|
||||
# Получаем вариант товара и его цену
|
||||
variant = db.query(ProductVariant).filter(ProductVariant.id == cart_item.variant_id).first()
|
||||
if not variant:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Продукт для варианта с ID {item_data.variant_id} не найден"
|
||||
detail=f"Вариант товара с ID {cart_item.variant_id} не найден"
|
||||
)
|
||||
|
||||
# Определяем цену для товара (используем discount_price если есть, иначе price)
|
||||
price = product.discount_price if product.discount_price and product.discount_price > 0 else product.price
|
||||
|
||||
|
||||
# Определяем цену (используем скидочную цену, если она есть)
|
||||
price = variant.discount_price if variant.discount_price else variant.price
|
||||
|
||||
# Создаем элемент заказа
|
||||
order_item = OrderItem(
|
||||
order_id=new_order.id,
|
||||
variant_id=variant.id,
|
||||
quantity=item_data.quantity,
|
||||
price=price
|
||||
)
|
||||
db.add(order_item)
|
||||
new_order.total_amount += price * item_data.quantity
|
||||
# Иначе используем все элементы корзины пользователя
|
||||
else:
|
||||
cart_items = db.query(CartItem).filter(CartItem.user_id == user_id).all()
|
||||
|
||||
# Если используем элементы корзины
|
||||
if cart_items:
|
||||
# Создаем элементы заказа из элементов корзины
|
||||
for cart_item in cart_items:
|
||||
# Получаем вариант продукта
|
||||
variant = db.query(ProductVariant).filter(ProductVariant.id == cart_item.variant_id).first()
|
||||
if not variant:
|
||||
continue
|
||||
|
||||
# Получаем продукт
|
||||
product = db.query(Product).filter(Product.id == variant.product_id).first()
|
||||
if not product:
|
||||
continue
|
||||
|
||||
# Определяем цену для товара (используем discount_price если есть, иначе price)
|
||||
price = product.discount_price if product.discount_price and product.discount_price > 0 else product.price
|
||||
|
||||
# Создаем элемент заказа
|
||||
order_item = OrderItem(
|
||||
order_id=new_order.id,
|
||||
variant_id=cart_item.variant_id,
|
||||
quantity=cart_item.quantity,
|
||||
price=price
|
||||
)
|
||||
db.add(order_item)
|
||||
|
||||
# Обновляем общую сумму заказа
|
||||
new_order.total_amount += price * cart_item.quantity
|
||||
|
||||
# Очищаем корзину пользователя после создания заказа
|
||||
if cart_items and not order.cart_items:
|
||||
# Если используем все элементы корзины, очищаем всю корзину
|
||||
db.query(CartItem).filter(CartItem.user_id == user_id).delete()
|
||||
elif order.cart_items:
|
||||
# Если используем только выбранные элементы корзины, удаляем только их
|
||||
db.query(CartItem).filter(
|
||||
CartItem.id.in_([item.id for item in cart_items]),
|
||||
CartItem.user_id == user_id
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
# Проверяем, были ли добавлены элементы заказа
|
||||
if new_order.total_amount == 0:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Не удалось создать заказ: корзина пуста или товары недоступны"
|
||||
)
|
||||
|
||||
|
||||
# Удаляем элемент из корзины
|
||||
db.delete(cart_item)
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(new_order)
|
||||
print(f"Заказ успешно создан: {new_order.id}, общая сумма: {new_order.total_amount}")
|
||||
return new_order
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Ошибка при создании заказа: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при создании заказа: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
def update_order(db: Session, order_id: int, order_update: OrderUpdate, is_admin: bool = False) -> Order:
|
||||
"""
|
||||
Обновляет информацию о заказе.
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
order_id: ID заказа
|
||||
order_update: Данные для обновления
|
||||
is_admin: Флаг, указывающий, является ли пользователь администратором
|
||||
|
||||
Returns:
|
||||
Обновленный заказ
|
||||
|
||||
Raises:
|
||||
HTTPException: Если заказ не найден или нет прав на обновление
|
||||
"""
|
||||
# Получаем заказ
|
||||
order = db.query(Order).filter(Order.id == order_id).first()
|
||||
if not order:
|
||||
def update_order(db: Session, order_id: int, order: OrderUpdate, is_admin: bool = False) -> Order:
|
||||
db_order = get_order(db, order_id)
|
||||
if not db_order:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Заказ не найден"
|
||||
)
|
||||
|
||||
# Проверяем возможность обновления статуса
|
||||
if order_update.status:
|
||||
# Если заказ уже отменен или доставлен, нельзя менять его статус
|
||||
if order.status in [OrderStatus.CANCELLED, OrderStatus.DELIVERED, OrderStatus.REFUNDED]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Невозможно изменить статус заказа из {order.status}"
|
||||
)
|
||||
|
||||
# Если пользователь не админ, он может только отменить заказ
|
||||
if not is_admin and order_update.status != OrderStatus.CANCELLED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Пользователи могут только отменить заказ"
|
||||
)
|
||||
|
||||
# Если пользователь хочет отменить заказ, проверяем возможность отмены
|
||||
if order_update.status == OrderStatus.CANCELLED:
|
||||
if order.status not in [OrderStatus.PENDING, OrderStatus.PROCESSING]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Нельзя отменить заказ, который уже отправлен или доставлен"
|
||||
)
|
||||
|
||||
# Проверяем другие поля для обновления
|
||||
if not is_admin:
|
||||
# Обычные пользователи могут обновлять только статус (отмена)
|
||||
for field in ["shipping_address_id", "payment_method", "payment_details", "tracking_number"]:
|
||||
if getattr(order_update, field) is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Пользователи не могут изменять поле {field}"
|
||||
)
|
||||
|
||||
# Обновляем поля заказа
|
||||
update_data = order_update.model_dump(exclude_unset=True)
|
||||
|
||||
# Если указан адрес доставки, проверяем его существование
|
||||
if "shipping_address_id" in update_data:
|
||||
address = db.query(UserAddress).filter(UserAddress.id == update_data["shipping_address_id"]).first()
|
||||
|
||||
# Обычные пользователи могут только отменить заказ
|
||||
if not is_admin and order.status and order.status != OrderStatus.CANCELLED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Недостаточно прав для изменения статуса заказа"
|
||||
)
|
||||
|
||||
# Нельзя изменить статус заказа с CANCELLED или REFUNDED
|
||||
if db_order.status in [OrderStatus.CANCELLED, OrderStatus.REFUNDED] and order.status:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Нельзя изменить статус заказа, который уже {db_order.status}"
|
||||
)
|
||||
|
||||
# Обновляем только предоставленные поля
|
||||
update_data = order.dict(exclude_unset=True)
|
||||
|
||||
# Проверяем, что адрес доставки существует и принадлежит пользователю, если указан
|
||||
if "shipping_address_id" in update_data and update_data["shipping_address_id"]:
|
||||
address = db.query(UserAddress).filter(
|
||||
UserAddress.id == update_data["shipping_address_id"],
|
||||
UserAddress.user_id == db_order.user_id
|
||||
).first()
|
||||
if not address:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Указанный адрес доставки не найден"
|
||||
detail="Адрес доставки не найден или не принадлежит пользователю"
|
||||
)
|
||||
|
||||
|
||||
# Применяем обновления
|
||||
for key, value in update_data.items():
|
||||
setattr(order, key, value)
|
||||
|
||||
setattr(db_order, key, value)
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
return order
|
||||
except Exception as e:
|
||||
db.refresh(db_order)
|
||||
return db_order
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка при обновлении заказа: {str(e)}"
|
||||
detail="Ошибка при обновлении заказа"
|
||||
)
|
||||
|
||||
|
||||
@ -570,14 +349,14 @@ def delete_order(db: Session, order_id: int, is_admin: bool = False) -> bool:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Заказ не найден"
|
||||
)
|
||||
|
||||
|
||||
# Только администраторы могут удалять заказы
|
||||
if not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Недостаточно прав для удаления заказа"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.delete(db_order)
|
||||
db.commit()
|
||||
@ -592,82 +371,65 @@ def delete_order(db: Session, order_id: int, is_admin: bool = False) -> bool:
|
||||
|
||||
# Функции для получения детальной информации
|
||||
def get_cart_with_product_details(db: Session, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Получает корзину пользователя с детальной информацией о товарах.
|
||||
"""
|
||||
# Получаем элементы корзины пользователя
|
||||
cart_items = db.query(CartItem).filter(CartItem.user_id == user_id).all()
|
||||
cart_items = get_user_cart(db, user_id)
|
||||
result = []
|
||||
|
||||
for cart_item in cart_items:
|
||||
# Получаем вариант продукта
|
||||
variant = db.query(ProductVariant).filter(ProductVariant.id == cart_item.variant_id).first()
|
||||
|
||||
for item in cart_items:
|
||||
# Получаем информацию о варианте и продукте
|
||||
variant = db.query(ProductVariant).filter(ProductVariant.id == item.variant_id).first()
|
||||
if not variant:
|
||||
continue
|
||||
|
||||
# Получаем продукт
|
||||
|
||||
product = db.query(Product).filter(Product.id == variant.product_id).first()
|
||||
if not product:
|
||||
continue
|
||||
|
||||
# Получаем размер варианта
|
||||
size = db.query(Size).filter(Size.id == variant.size_id).first() if variant.size_id else None
|
||||
size_name = size.name if size else ''
|
||||
|
||||
|
||||
# Получаем основное изображение продукта
|
||||
product_image = None
|
||||
primary_image = db.query(ProductImage).filter(
|
||||
image = db.query(ProductImage).filter(
|
||||
ProductImage.product_id == product.id,
|
||||
ProductImage.is_primary == True
|
||||
).first()
|
||||
|
||||
if primary_image:
|
||||
product_image = primary_image.image_url
|
||||
else:
|
||||
# Если нет основного изображения, берем любое
|
||||
any_image = db.query(ProductImage).filter(ProductImage.product_id == product.id).first()
|
||||
if any_image:
|
||||
product_image = any_image.image_url
|
||||
|
||||
# Определяем цену товара (используем discount_price если есть, иначе price)
|
||||
price = product.discount_price if product.discount_price and product.discount_price > 0 else product.price
|
||||
|
||||
# Формируем элемент корзины с деталями
|
||||
cart_item_details = {
|
||||
"id": cart_item.id,
|
||||
"user_id": cart_item.user_id,
|
||||
"variant_id": cart_item.variant_id,
|
||||
"quantity": cart_item.quantity,
|
||||
"created_at": cart_item.created_at,
|
||||
"updated_at": cart_item.updated_at,
|
||||
|
||||
# Если нет основного изображения, берем первое доступное
|
||||
if not image:
|
||||
image = db.query(ProductImage).filter(
|
||||
ProductImage.product_id == product.id
|
||||
).first()
|
||||
|
||||
# Рассчитываем цену
|
||||
price = variant.discount_price if variant.discount_price else variant.price
|
||||
|
||||
# Формируем результат
|
||||
result.append({
|
||||
"id": item.id,
|
||||
"user_id": item.user_id,
|
||||
"variant_id": item.variant_id,
|
||||
"slug": product.slug,
|
||||
"quantity": item.quantity,
|
||||
"created_at": item.created_at,
|
||||
"updated_at": item.updated_at,
|
||||
"product_id": product.id,
|
||||
"product_name": product.name,
|
||||
"product_price": price,
|
||||
"product_image": product_image,
|
||||
"slug": product.slug,
|
||||
"variant_name": size_name,
|
||||
"total_price": price * cart_item.quantity
|
||||
}
|
||||
|
||||
result.append(cart_item_details)
|
||||
|
||||
"product_image": image.image_url if image else None,
|
||||
"variant_name": variant.name,
|
||||
"total_price": price * item.quantity
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_order_with_details(db: Session, order_id: int) -> Optional[Dict]:
|
||||
"""
|
||||
Получает заказ по ID с детальной информацией о товарах и адресе доставки.
|
||||
"""
|
||||
# Получаем заказ
|
||||
order = db.query(Order).filter(Order.id == order_id).first()
|
||||
def get_order_with_details(db: Session, order_id: int) -> Dict[str, Any]:
|
||||
order = get_order(db, order_id)
|
||||
if not order:
|
||||
return None
|
||||
|
||||
# Получаем информацию о пользователе
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Заказ не найден"
|
||||
)
|
||||
|
||||
# Получаем пользователя
|
||||
user = db.query(User).filter(User.id == order.user_id).first()
|
||||
user_email = user.email if user else None
|
||||
user_name = f"{user.first_name} {user.last_name}" if user and user.first_name and user.last_name else None
|
||||
|
||||
|
||||
# Получаем адрес доставки
|
||||
shipping_address = None
|
||||
if order.shipping_address_id:
|
||||
@ -680,113 +442,51 @@ def get_order_with_details(db: Session, order_id: int) -> Optional[Dict]:
|
||||
"city": address.city,
|
||||
"state": address.state,
|
||||
"postal_code": address.postal_code,
|
||||
"country": address.country,
|
||||
"is_default": address.is_default
|
||||
"country": address.country
|
||||
}
|
||||
|
||||
# Получаем элементы заказа с информацией о продуктах
|
||||
|
||||
# Получаем элементы заказа с деталями
|
||||
items = []
|
||||
order_items = db.query(OrderItem).filter(OrderItem.order_id == order.id).all()
|
||||
|
||||
for item in order_items:
|
||||
# Получаем вариант продукта
|
||||
for item in order.items:
|
||||
# Получаем информацию о варианте и продукте
|
||||
variant = db.query(ProductVariant).filter(ProductVariant.id == item.variant_id).first()
|
||||
variant_name = None
|
||||
size_name = None
|
||||
|
||||
if variant:
|
||||
# Получаем размер варианта
|
||||
size = db.query(Size).filter(Size.id == variant.size_id).first() if variant.size_id else None
|
||||
if size:
|
||||
size_name = size.name
|
||||
variant_name = f"{size.name}"
|
||||
|
||||
# Получаем информацию о продукте
|
||||
product = None
|
||||
product_name = "Удаленный продукт"
|
||||
product_image = None
|
||||
|
||||
if variant:
|
||||
product = db.query(Product).filter(Product.id == variant.product_id).first()
|
||||
|
||||
if product:
|
||||
product_name = product.name
|
||||
|
||||
# Получаем основное изображение продукта
|
||||
primary_image = db.query(ProductImage).filter(
|
||||
ProductImage.product_id == product.id,
|
||||
ProductImage.is_primary == True
|
||||
).first()
|
||||
|
||||
if primary_image:
|
||||
product_image = primary_image.image_url
|
||||
else:
|
||||
# Если нет основного изображения, берем любое
|
||||
any_image = db.query(ProductImage).filter(
|
||||
ProductImage.product_id == product.id
|
||||
).first()
|
||||
|
||||
if any_image:
|
||||
product_image = any_image.image_url
|
||||
|
||||
# Добавляем информацию об элементе заказа
|
||||
if not variant:
|
||||
continue
|
||||
|
||||
product = db.query(Product).filter(Product.id == variant.product_id).first()
|
||||
if not product:
|
||||
continue
|
||||
|
||||
# Формируем элемент заказа
|
||||
items.append({
|
||||
"id": item.id,
|
||||
"order_id": item.order_id,
|
||||
"variant_id": item.variant_id,
|
||||
"quantity": item.quantity,
|
||||
"price": item.price,
|
||||
"product_id": variant.product_id if variant else None,
|
||||
"product_name": product_name,
|
||||
"product_image": product_image,
|
||||
"variant_name": variant_name,
|
||||
"size": size_name,
|
||||
"created_at": item.created_at,
|
||||
"product_id": product.id,
|
||||
"product_name": product.name,
|
||||
"variant_name": variant.name,
|
||||
"total_price": item.price * item.quantity
|
||||
})
|
||||
|
||||
# Получаем информацию о пользователе из JSON
|
||||
user_info = order.user_info_json or {}
|
||||
|
||||
|
||||
# Формируем результат
|
||||
result = {
|
||||
"id": order.id,
|
||||
"user_id": order.user_id,
|
||||
"status": order.status,
|
||||
"total_amount": order.total_amount,
|
||||
|
||||
# Информация о пользователе из JSON
|
||||
"user_info_json": order.user_info_json,
|
||||
|
||||
# Извлекаем информацию о пользователе для отображения
|
||||
"first_name": user_info.get("first_name", ""),
|
||||
"last_name": user_info.get("last_name", ""),
|
||||
"email": user_info.get("email", user_email),
|
||||
"phone": user_info.get("phone", ""),
|
||||
"user_email": user_email, # Для обратной совместимости
|
||||
"user_name": user_name, # Для обратной совместимости
|
||||
|
||||
# Информация о доставке
|
||||
"delivery_method": order.delivery_method,
|
||||
"city": order.city,
|
||||
"delivery_address": order.delivery_address,
|
||||
"cdek_info": order.cdek_info,
|
||||
"courier_info": order.courier_info,
|
||||
|
||||
# Информация о товарах из JSON
|
||||
"items_json": order.items_json,
|
||||
|
||||
# Старые поля (для обратной совместимости)
|
||||
"shipping_address_id": order.shipping_address_id,
|
||||
"shipping_address": shipping_address,
|
||||
|
||||
# Дополнительная информация
|
||||
"payment_method": order.payment_method,
|
||||
"payment_details": order.payment_details,
|
||||
"tracking_number": order.tracking_number,
|
||||
"notes": order.notes,
|
||||
"created_at": order.created_at,
|
||||
"updated_at": order.updated_at,
|
||||
"user_email": user.email if user else None,
|
||||
"shipping_address": shipping_address,
|
||||
"items": items
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
@ -1,9 +1,7 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException, status
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
import secrets
|
||||
import string
|
||||
from typing import List, Optional
|
||||
|
||||
from app.models.user_models import User, UserAddress
|
||||
from app.schemas.user_schemas import UserCreate, UserUpdate, AddressCreate, AddressUpdate
|
||||
@ -16,11 +14,6 @@ def get_user(db: Session, user_id: int) -> Optional[User]:
|
||||
|
||||
|
||||
def get_user_by_email(db: Session, email: str) -> Optional[User]:
|
||||
# print(email)
|
||||
users = db.query(User).all()
|
||||
# print(users)
|
||||
# for user in users:
|
||||
# print(f"ID: {user.id}, Email: {user.email}, Name: {user.first_name} {user.last_name}")
|
||||
return db.query(User).filter(User.email == email).first()
|
||||
|
||||
|
||||
@ -35,7 +28,7 @@ def create_user(db: Session, user: UserCreate) -> User:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Пользователь с таким email уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Создаем нового пользователя
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = User(
|
||||
@ -47,7 +40,7 @@ def create_user(db: Session, user: UserCreate) -> User:
|
||||
is_active=user.is_active,
|
||||
is_admin=user.is_admin
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
@ -68,17 +61,17 @@ def update_user(db: Session, user_id: int, user: UserUpdate) -> User:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Пользователь не найден"
|
||||
)
|
||||
|
||||
|
||||
# Обновляем только предоставленные поля
|
||||
update_data = user.dict(exclude_unset=True)
|
||||
|
||||
|
||||
# Если предоставлен новый пароль, хешируем его
|
||||
if "password" in update_data and update_data["password"]:
|
||||
update_data["password"] = get_password_hash(update_data.pop("password"))
|
||||
|
||||
|
||||
# Удаляем поле password_confirm, если оно есть
|
||||
update_data.pop("password_confirm", None)
|
||||
|
||||
|
||||
# Проверяем уникальность email, если он изменяется
|
||||
if "email" in update_data and update_data["email"] != db_user.email:
|
||||
if get_user_by_email(db, update_data["email"]):
|
||||
@ -86,11 +79,11 @@ def update_user(db: Session, user_id: int, user: UserUpdate) -> User:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Пользователь с таким email уже существует"
|
||||
)
|
||||
|
||||
|
||||
# Применяем обновления
|
||||
for key, value in update_data.items():
|
||||
setattr(db_user, key, value)
|
||||
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
@ -110,7 +103,7 @@ def delete_user(db: Session, user_id: int) -> bool:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Пользователь не найден"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.delete(db_user)
|
||||
db.commit()
|
||||
@ -132,57 +125,6 @@ def authenticate_user(db: Session, email: str, password: str) -> Optional[User]:
|
||||
return user
|
||||
|
||||
|
||||
def find_or_create_user_from_order(db: Session, user_info: Dict[str, Any]) -> Tuple[User, bool, str]:
|
||||
"""
|
||||
Ищет пользователя по email или создает нового на основе данных заказа.
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
user_info: Информация о пользователе из заказа
|
||||
|
||||
Returns:
|
||||
Кортеж (пользователь, создан_новый, сообщение)
|
||||
"""
|
||||
email = user_info.get("email")
|
||||
if not email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email обязателен для создания пользователя"
|
||||
)
|
||||
|
||||
# Ищем пользователя по email
|
||||
existing_user = get_user_by_email(db, email)
|
||||
if existing_user:
|
||||
return existing_user, False, "Пользователь с таким email уже существует"
|
||||
|
||||
# Генерируем случайный пароль
|
||||
password = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12))
|
||||
hashed_password = get_password_hash(password)
|
||||
|
||||
# Создаем нового пользователя
|
||||
new_user = User(
|
||||
email=email,
|
||||
password=hashed_password,
|
||||
first_name=user_info.get("first_name", ""),
|
||||
last_name=user_info.get("last_name", ""),
|
||||
phone=user_info.get("phone", ""),
|
||||
is_active=True,
|
||||
is_admin=False
|
||||
)
|
||||
|
||||
try:
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
return new_user, True, f"Создан новый пользователь с email {email}"
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Ошибка при создании пользователя"
|
||||
)
|
||||
|
||||
|
||||
# Функции для работы с адресами пользователей
|
||||
def get_address(db: Session, address_id: int) -> Optional[UserAddress]:
|
||||
return db.query(UserAddress).filter(UserAddress.id == address_id).first()
|
||||
@ -199,7 +141,7 @@ def create_address(db: Session, address: AddressCreate, user_id: int):
|
||||
UserAddress.user_id == user_id,
|
||||
UserAddress.is_default == True
|
||||
).update({"is_default": False})
|
||||
|
||||
|
||||
db_address = UserAddress(
|
||||
user_id=user_id,
|
||||
address_line1=address.address_line1,
|
||||
@ -210,7 +152,7 @@ def create_address(db: Session, address: AddressCreate, user_id: int):
|
||||
country=address.country,
|
||||
is_default=address.is_default
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.add(db_address)
|
||||
db.commit()
|
||||
@ -231,16 +173,16 @@ def update_address(db: Session, address_id: int, address: AddressUpdate, user_id
|
||||
UserAddress.id == address_id,
|
||||
UserAddress.user_id == user_id
|
||||
).first()
|
||||
|
||||
|
||||
if not db_address:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Адрес не найден или не принадлежит пользователю"
|
||||
)
|
||||
|
||||
|
||||
# Обновляем только предоставленные поля
|
||||
update_data = address.dict(exclude_unset=True)
|
||||
|
||||
|
||||
# Если адрес становится дефолтным, сбрасываем дефолтный статус у других адресов пользователя
|
||||
if "is_default" in update_data and update_data["is_default"]:
|
||||
db.query(UserAddress).filter(
|
||||
@ -248,11 +190,11 @@ def update_address(db: Session, address_id: int, address: AddressUpdate, user_id
|
||||
UserAddress.id != address_id,
|
||||
UserAddress.is_default == True
|
||||
).update({"is_default": False})
|
||||
|
||||
|
||||
# Применяем обновления
|
||||
for key, value in update_data.items():
|
||||
setattr(db_address, key, value)
|
||||
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_address)
|
||||
@ -270,13 +212,13 @@ def delete_address(db: Session, address_id: int, user_id: int) -> bool:
|
||||
UserAddress.id == address_id,
|
||||
UserAddress.user_id == user_id
|
||||
).first()
|
||||
|
||||
|
||||
if not db_address:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Адрес не найден или не принадлежит пользователю"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
db.delete(db_address)
|
||||
db.commit()
|
||||
@ -298,10 +240,10 @@ def update_password(db: Session, user_id: int, new_password: str) -> bool:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Пользователь не найден"
|
||||
)
|
||||
|
||||
|
||||
hashed_password = get_password_hash(new_password)
|
||||
db_user.password = hashed_password
|
||||
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
return True
|
||||
@ -317,14 +259,14 @@ def create_password_reset_token(db: Session, user_id: int) -> str:
|
||||
"""Создает токен для сброса пароля"""
|
||||
import secrets
|
||||
import datetime
|
||||
|
||||
|
||||
# В реальном приложении здесь должна быть модель для токенов сброса пароля
|
||||
# Для примера просто генерируем случайный токен
|
||||
token = secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
# В реальном приложении сохраняем токен в базе данных с привязкой к пользователю
|
||||
# и временем истечения срока действия
|
||||
|
||||
|
||||
return token
|
||||
|
||||
|
||||
@ -332,7 +274,7 @@ def verify_password_reset_token(db: Session, token: str) -> Optional[int]:
|
||||
"""Проверяет токен сброса пароля и возвращает ID пользователя"""
|
||||
# В реальном приложении проверяем токен в базе данных
|
||||
# и его срок действия
|
||||
|
||||
|
||||
# Для примера просто возвращаем фиктивный ID пользователя
|
||||
# В реальном приложении это должна быть проверка в базе данных
|
||||
return 1 # Фиктивный ID пользователя
|
||||
return 1 # Фиктивный ID пользователя
|
||||
@ -8,7 +8,6 @@ from app.routers.order_router import order_router
|
||||
from app.routers.review_router import review_router
|
||||
from app.routers.content_router import content_router
|
||||
from app.routers.analytics_router import analytics_router
|
||||
from app.routers.delivery_router import delivery_router
|
||||
|
||||
# Создаем основной роутер
|
||||
router = APIRouter()
|
||||
@ -21,5 +20,4 @@ router.include_router(cart_router)
|
||||
router.include_router(order_router)
|
||||
router.include_router(review_router)
|
||||
router.include_router(content_router)
|
||||
router.include_router(analytics_router)
|
||||
router.include_router(delivery_router)
|
||||
router.include_router(analytics_router)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -16,12 +16,6 @@ async def add_to_cart_endpoint(
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Добавляет товар в корзину пользователя.
|
||||
|
||||
- **variant_id**: ID варианта продукта
|
||||
- **quantity**: Количество товара
|
||||
"""
|
||||
return services.add_to_cart(db, current_user.id, cart_item)
|
||||
|
||||
|
||||
@ -32,47 +26,19 @@ async def update_cart_item_endpoint(
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Обновляет количество товара в корзине.
|
||||
|
||||
- **cart_item_id**: ID элемента корзины
|
||||
- **quantity**: Новое количество товара
|
||||
"""
|
||||
return services.update_cart_item(db, current_user.id, cart_item_id, cart_item)
|
||||
|
||||
|
||||
@cart_router.delete("/items/{cart_item_id}", response_model=Dict[str, Any])
|
||||
async def remove_from_cart_endpoint(
|
||||
cart_item_id: int,
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Удаляет товар из корзины.
|
||||
|
||||
- **cart_item_id**: ID элемента корзины
|
||||
"""
|
||||
async def remove_from_cart_endpoint(cart_item_id: int, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
|
||||
return services.remove_from_cart(db, current_user.id, cart_item_id)
|
||||
|
||||
|
||||
@cart_router.delete("/clear", response_model=Dict[str, Any])
|
||||
async def clear_cart_endpoint(
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Очищает всю корзину пользователя."""
|
||||
async def clear_cart_endpoint(current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
|
||||
return services.clear_cart(db, current_user.id)
|
||||
|
||||
|
||||
@cart_router.get("/", response_model=Dict[str, Any])
|
||||
async def get_cart_endpoint(
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Получает содержимое корзины пользователя.
|
||||
|
||||
Возвращает список товаров в корзине с информацией о продуктах,
|
||||
общую сумму и количество товаров.
|
||||
"""
|
||||
async def get_cart_endpoint(current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
|
||||
return services.get_cart(db, current_user.id)
|
||||
@ -1,32 +1,23 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status, UploadFile, File, Form, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from fastapi.responses import JSONResponse
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from app.core import get_db, get_current_admin_user
|
||||
from app import services
|
||||
from app.schemas.catalog_schemas import (
|
||||
CategoryCreate, CategoryUpdate, Category, CategoryWithSubcategories,
|
||||
ProductCreate, ProductUpdate, Product, ProductWithDetails,
|
||||
CategoryCreate, CategoryUpdate, Category,
|
||||
ProductCreate, ProductUpdate, Product,
|
||||
ProductVariantCreate, ProductVariantUpdate, ProductVariant,
|
||||
ProductImageCreate, ProductImageUpdate, ProductImage,
|
||||
CollectionCreate, CollectionUpdate, Collection,
|
||||
SizeCreate, SizeUpdate, Size,
|
||||
ProductCreateComplete, ProductUpdateComplete
|
||||
CollectionCreate, CollectionUpdate, Collection
|
||||
)
|
||||
from app.models.user_models import User as UserModel
|
||||
from app.models.catalog_models import Category, Size, Collection
|
||||
from app.repositories.catalog_repo import get_products, get_product_by_slug
|
||||
|
||||
# Роутер для каталога
|
||||
catalog_router = APIRouter(prefix="/catalog", tags=["Каталог"])
|
||||
|
||||
#########################
|
||||
# Маршруты для коллекций #
|
||||
#########################
|
||||
|
||||
# Маршруты для коллекций
|
||||
@catalog_router.post("/collections", response_model=Dict[str, Any])
|
||||
async def create_collection_endpoint(collection: CollectionCreate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
|
||||
return services.create_collection(db, collection)
|
||||
@ -43,69 +34,9 @@ async def delete_collection_endpoint(collection_id: int, current_user: UserModel
|
||||
|
||||
|
||||
@catalog_router.get("/collections", response_model=Dict[str, Any])
|
||||
async def get_collections_endpoint(
|
||||
*,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# Формируем фильтры для Meilisearch
|
||||
filters = []
|
||||
async def get_collections_endpoint(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
return services.get_collections(db, skip, limit)
|
||||
|
||||
if is_active is not None:
|
||||
filters.append(f"is_active = {str(is_active).lower()}")
|
||||
|
||||
# Используем Meilisearch для поиска коллекций
|
||||
from app.services import meilisearch_service
|
||||
|
||||
# Объединяем фильтры в строку
|
||||
filter_str = " AND ".join(filters) if filters else None
|
||||
|
||||
# Выполняем поиск в Meilisearch
|
||||
result = meilisearch_service.search_collections(
|
||||
query=search or "",
|
||||
filters=filter_str,
|
||||
limit=limit,
|
||||
offset=skip
|
||||
)
|
||||
|
||||
# Если поиск в Meilisearch успешен, возвращаем результаты
|
||||
if result["success"]:
|
||||
return {
|
||||
"success": True,
|
||||
"collections": result["collections"],
|
||||
"total": result["total"]
|
||||
}
|
||||
|
||||
# Если поиск в Meilisearch не удался, используем старый метод
|
||||
logging.warning(f"Meilisearch search failed: {result.get('error')}. Falling back to database search.")
|
||||
|
||||
# Получаем коллекции из базы данных
|
||||
collections_db = db.query(Collection).all()
|
||||
|
||||
# Преобразуем коллекции в список словарей
|
||||
from app.schemas.catalog_schemas import Collection as CollectionSchema
|
||||
collections_list = [CollectionSchema.model_validate(collection).model_dump() for collection in collections_db]
|
||||
|
||||
# Синхронизируем коллекции с Meilisearch для будущих запросов
|
||||
try:
|
||||
from app.scripts.sync_meilisearch import sync_collections
|
||||
sync_collections(db)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to sync collections with Meilisearch: {str(e)}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"collections": collections_list,
|
||||
"total": len(collections_list)
|
||||
}
|
||||
|
||||
|
||||
#########################
|
||||
# Маршруты для категорий #
|
||||
#########################
|
||||
|
||||
@catalog_router.post("/categories", response_model=Dict[str, Any])
|
||||
async def create_category_endpoint(category: CategoryCreate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
|
||||
@ -122,313 +53,71 @@ async def delete_category_endpoint(category_id: int, current_user: UserModel = D
|
||||
return services.delete_category(db, category_id)
|
||||
|
||||
|
||||
@catalog_router.get("/categories", response_model=Dict[str, Any])
|
||||
async def get_categories_endpoint(
|
||||
*,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None,
|
||||
parent_id: Optional[int] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# Формируем фильтры для Meilisearch
|
||||
filters = []
|
||||
@catalog_router.get("/categories", response_model=List[Dict[str, Any]])
|
||||
async def get_categories_tree(db: Session = Depends(get_db)):
|
||||
return services.get_category_tree(db)
|
||||
|
||||
if parent_id is not None:
|
||||
filters.append(f"parent_id = {parent_id}")
|
||||
|
||||
if is_active is not None:
|
||||
filters.append(f"is_active = {str(is_active).lower()}")
|
||||
|
||||
# Используем Meilisearch для поиска категорий
|
||||
from app.services import meilisearch_service
|
||||
|
||||
# Объединяем фильтры в строку
|
||||
filter_str = " AND ".join(filters) if filters else None
|
||||
|
||||
# Выполняем поиск в Meilisearch
|
||||
result = meilisearch_service.search_categories(
|
||||
query=search or "",
|
||||
filters=filter_str,
|
||||
limit=limit,
|
||||
offset=skip
|
||||
)
|
||||
|
||||
# Добавляем логирование для отладки
|
||||
logging.info(f"Meilisearch search result: {result}")
|
||||
|
||||
# Если поиск в Meilisearch успешен, возвращаем результаты
|
||||
if result["success"] and result["categories"]:
|
||||
return {
|
||||
"success": True,
|
||||
"categories": result["categories"],
|
||||
"total": result["total"]
|
||||
}
|
||||
|
||||
# Если поиск в Meilisearch не удался, используем старый метод
|
||||
logging.warning(f"Meilisearch search failed: {result.get('error')}. Falling back to database search.")
|
||||
|
||||
# Получаем категории из базы данных
|
||||
categories = db.query(Category).all()
|
||||
|
||||
# Преобразуем категории в список словарей
|
||||
from app.schemas.catalog_schemas import Category as CategorySchema
|
||||
categories_list = [CategorySchema.model_validate(category).model_dump() for category in categories]
|
||||
|
||||
# Синхронизируем категории с Meilisearch для будущих запросов
|
||||
try:
|
||||
from app.scripts.sync_meilisearch import sync_categories
|
||||
sync_categories(db)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to sync categories with Meilisearch: {str(e)}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"categories": categories_list,
|
||||
"total": len(categories_list)
|
||||
}
|
||||
|
||||
|
||||
#########################
|
||||
# Маршруты для продуктов #
|
||||
#########################
|
||||
|
||||
@catalog_router.post("/products", response_model=Dict[str, Any])
|
||||
async def create_product_endpoint(product: ProductCreate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
|
||||
# Используем синхронную версию для создания продукта
|
||||
return services.create_product(db, product)
|
||||
|
||||
|
||||
@catalog_router.put("/products/{product_id}", response_model=Dict[str, Any])
|
||||
async def update_product_endpoint(product_id: int, product: ProductUpdate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
|
||||
# Используем синхронную версию для обновления продукта
|
||||
return services.update_product(db, product_id, product)
|
||||
|
||||
|
||||
@catalog_router.delete("/products/{product_id}", response_model=Dict[str, Any])
|
||||
async def delete_product_endpoint(product_id: int, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
|
||||
# Используем синхронную версию для удаления продукта
|
||||
logging.warning(f"Удаление продукта с ID {product_id} в маршруте")
|
||||
result = services.delete_product(db, product_id)
|
||||
|
||||
# Если удаление не удалось и есть сообщение об ошибке, возвращаем соответствующий статус ошибки
|
||||
if not result.get("success") and "error" in result:
|
||||
if "заказах" in result.get("error", ""):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=result["error"])
|
||||
elif "не найден" in result.get("error", ""):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=result["error"])
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=result["error"])
|
||||
|
||||
return result
|
||||
return services.delete_product(db, product_id)
|
||||
|
||||
|
||||
@catalog_router.get("/products/{product_id}", response_model=Dict[str, Any])
|
||||
async def get_product_details_endpoint(*, product_id: int, db: Session = Depends(get_db)):
|
||||
# Сначала пробуем найти продукт в Meilisearch
|
||||
from app.services import meilisearch_service
|
||||
|
||||
# Используем поиск по ID
|
||||
result = meilisearch_service.get_product(product_id)
|
||||
|
||||
# Если продукт найден в Meilisearch, возвращаем его
|
||||
if result["success"]:
|
||||
return {
|
||||
"success": True,
|
||||
"product": result["product"]
|
||||
}
|
||||
|
||||
# Если продукт не найден в Meilisearch, используем старый метод
|
||||
logging.warning(f"Meilisearch search failed: {result.get('error')}. Falling back to database search.")
|
||||
|
||||
# Проверяем существование продукта
|
||||
from app.models.catalog_models import Product
|
||||
product = db.query(Product).filter(Product.id == product_id).first()
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Продукт с ID {product_id} не найден"
|
||||
)
|
||||
|
||||
# Получаем детали продукта
|
||||
product_details = services.get_product_details(db, product_id)
|
||||
|
||||
# Синхронизируем продукт с Meilisearch для будущих запросов
|
||||
try:
|
||||
from app.scripts.sync_meilisearch import sync_products
|
||||
sync_products(db)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to sync products with Meilisearch: {str(e)}")
|
||||
|
||||
return {"success": True, "product": product_details}
|
||||
async def get_product_details_endpoint(product_id: int, db: Session = Depends(get_db)):
|
||||
return services.get_product_details(db, product_id)
|
||||
|
||||
|
||||
@catalog_router.get("/products/slug/{slug}", response_model=Dict[str, Any])
|
||||
async def get_product_by_slug_endpoint(*, slug: str, db: Session = Depends(get_db)):
|
||||
# Сначала пробуем найти продукт в Meilisearch
|
||||
from app.services import meilisearch_service
|
||||
|
||||
# Используем поиск по slug
|
||||
result = meilisearch_service.get_product_by_slug(slug)
|
||||
|
||||
# Если продукт найден в Meilisearch, возвращаем его
|
||||
if result["success"]:
|
||||
return {
|
||||
"success": True,
|
||||
"product": result["product"]
|
||||
}
|
||||
|
||||
# Если продукт не найден в Meilisearch, используем старый метод
|
||||
logging.warning(f"Meilisearch search failed: {result.get('error')}. Falling back to database search.")
|
||||
|
||||
# Используем синхронную версию для получения продукта по slug
|
||||
async def get_product_by_slug_endpoint(slug: str, db: Session = Depends(get_db)):
|
||||
product = get_product_by_slug(db, slug)
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Продукт с slug {slug} не найден"
|
||||
detail="Продукт не найден"
|
||||
)
|
||||
|
||||
# Получаем детали продукта
|
||||
product_details = services.get_product_details(db, product.id)
|
||||
|
||||
# Синхронизируем продукт с Meilisearch для будущих запросов
|
||||
try:
|
||||
from app.scripts.sync_meilisearch import sync_products
|
||||
sync_products(db)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to sync products with Meilisearch: {str(e)}")
|
||||
|
||||
return {"success": True, "product": product_details}
|
||||
return services.get_product_details(db, product.id)
|
||||
|
||||
|
||||
@catalog_router.post("/products/{product_id}/variants", response_model=Dict[str, Any])
|
||||
async def add_product_variant_endpoint(
|
||||
product_id: int,
|
||||
variant: ProductVariantCreate,
|
||||
current_user: UserModel = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# Убедимся, что product_id в пути совпадает с product_id в данных
|
||||
if variant.product_id != product_id:
|
||||
variant.product_id = product_id
|
||||
async def add_product_variant_endpoint(product_id: int, variant: ProductVariantCreate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
|
||||
variant.product_id = product_id
|
||||
return services.add_product_variant(db, variant)
|
||||
|
||||
|
||||
@catalog_router.put("/variants/{variant_id}", response_model=Dict[str, Any])
|
||||
async def update_product_variant_endpoint(
|
||||
variant_id: int,
|
||||
variant: ProductVariantUpdate,
|
||||
current_user: UserModel = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
async def update_product_variant_endpoint(variant_id: int, variant: ProductVariantUpdate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
|
||||
return services.update_product_variant(db, variant_id, variant)
|
||||
|
||||
|
||||
@catalog_router.delete("/variants/{variant_id}", response_model=Dict[str, Any])
|
||||
async def delete_product_variant_endpoint(
|
||||
variant_id: int,
|
||||
current_user: UserModel = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
async def delete_product_variant_endpoint(variant_id: int, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
|
||||
return services.delete_product_variant(db, variant_id)
|
||||
|
||||
|
||||
@catalog_router.post("/products/{product_id}/images", description="Upload a product image")
|
||||
@catalog_router.post("/products/{product_id}/images", response_model=Dict[str, Any])
|
||||
async def upload_product_image_endpoint(
|
||||
product_id: int,
|
||||
is_primary: bool = Form(default=False),
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_admin_user)
|
||||
product_id: int,
|
||||
file: UploadFile = File(...),
|
||||
is_primary: bool = Form(False),
|
||||
current_user: UserModel = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Загружает изображение для продукта.
|
||||
|
||||
Args:
|
||||
product_id: ID продукта
|
||||
is_primary: Является ли изображение основным
|
||||
file: Загружаемый файл
|
||||
db: Сессия базы данных
|
||||
current_user: Текущий пользователь
|
||||
|
||||
Returns:
|
||||
Объект с флагом success и данными изображения:
|
||||
{
|
||||
"success": true,
|
||||
"image": {
|
||||
"id": int,
|
||||
"product_id": int,
|
||||
"image_url": str,
|
||||
"alt_text": str,
|
||||
"is_primary": bool,
|
||||
"created_at": datetime,
|
||||
"updated_at": datetime
|
||||
}
|
||||
}
|
||||
|
||||
В случае ошибки:
|
||||
{
|
||||
"success": false,
|
||||
"error": str
|
||||
}
|
||||
"""
|
||||
try:
|
||||
logging.info(f"Начало обработки загрузки изображения для продукта {product_id}")
|
||||
logging.info(f"Данные файла: имя={file.filename}, тип={file.content_type}, размер={file.size if hasattr(file, 'size') else 'unknown'}")
|
||||
logging.info(f"is_primary: {is_primary}")
|
||||
|
||||
# Удостоверяемся, что продукт существует
|
||||
from app.models.catalog_models import Product
|
||||
product = db.query(Product).filter(Product.id == product_id).first()
|
||||
if not product:
|
||||
error_msg = f"Продукт с ID {product_id} не найден"
|
||||
logging.error(error_msg)
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"success": False, "error": error_msg}
|
||||
)
|
||||
|
||||
# Используем сервис для загрузки изображения
|
||||
logging.info("Вызов сервиса upload_product_image")
|
||||
result = services.upload_product_image(
|
||||
db, product_id, file, is_primary, alt_text="")
|
||||
|
||||
logging.info(f"Результат загрузки изображения: {result}")
|
||||
|
||||
# Возвращаем успешный ответ с данными изображения
|
||||
return {
|
||||
"success": True,
|
||||
"image": result
|
||||
}
|
||||
except HTTPException as http_exc:
|
||||
# Обрабатываем HTTP-исключения
|
||||
logging.error(f"HTTP ошибка при загрузке изображения: {http_exc.detail}, код: {http_exc.status_code}")
|
||||
return JSONResponse(
|
||||
status_code=http_exc.status_code,
|
||||
content={"success": False, "error": http_exc.detail}
|
||||
)
|
||||
except Exception as e:
|
||||
# Логируем ошибку с полным трейсбеком
|
||||
error_msg = f"Неожиданная ошибка при загрузке изображения: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
# Возвращаем ошибку с кодом 400
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"success": False, "error": str(e)}
|
||||
)
|
||||
return services.upload_product_image(db, product_id, file, is_primary)
|
||||
|
||||
|
||||
@catalog_router.put("/images/{image_id}", response_model=Dict[str, Any])
|
||||
async def update_product_image_endpoint(
|
||||
image_id: int,
|
||||
image: ProductImageUpdate,
|
||||
current_user: UserModel = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
async def update_product_image_endpoint(image_id: int, image: ProductImageUpdate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
|
||||
return services.update_product_image(db, image_id, image)
|
||||
|
||||
|
||||
@ -437,305 +126,19 @@ async def delete_product_image_endpoint(image_id: int, current_user: UserModel =
|
||||
return services.delete_product_image(db, image_id)
|
||||
|
||||
|
||||
@catalog_router.get("/products", response_model=Dict[str, Any])
|
||||
@catalog_router.get("/products", response_model=List[Product])
|
||||
async def get_products_endpoint(
|
||||
*,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
category_id: Optional[int] = None,
|
||||
category_ids: Optional[str] = None, # Добавляем параметр category_ids в виде строки с разделителями-запятыми
|
||||
collection_id: Optional[int] = None,
|
||||
collection_ids: Optional[str] = None, # Добавляем параметр collection_ids в виде строки с разделителями-запятыми
|
||||
search: Optional[str] = None,
|
||||
min_price: Optional[float] = None,
|
||||
max_price: Optional[float] = None,
|
||||
is_active: Optional[bool] = True,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[str] = "asc",
|
||||
size_ids: Optional[str] = None, # Параметр size_ids в виде строки с разделителями-запятыми
|
||||
sort: Optional[str] = None, # Параметр sort для прямой передачи опции сортировки
|
||||
include_variants: Optional[bool] = False,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# Формируем фильтры для Meilisearch
|
||||
filters = []
|
||||
|
||||
# Обработка фильтрации по категориям
|
||||
if category_ids:
|
||||
try:
|
||||
# Преобразуем строку с ID категорий в список
|
||||
category_ids_list = [int(cat_id.strip()) for cat_id in category_ids.split(',') if cat_id.strip()]
|
||||
if category_ids_list:
|
||||
# Формируем условие для фильтрации по категориям
|
||||
category_filter = " OR ".join([f"category_id = {cat_id}" for cat_id in category_ids_list])
|
||||
filters.append(f"({category_filter})")
|
||||
except ValueError as e:
|
||||
logging.warning(f"Некорректный формат category_ids: {category_ids}. Ошибка: {str(e)}")
|
||||
# Для обратной совместимости
|
||||
elif category_id is not None:
|
||||
filters.append(f"category_id = {category_id}")
|
||||
|
||||
# Обработка фильтрации по коллекциям
|
||||
if collection_ids:
|
||||
try:
|
||||
# Преобразуем строку с ID коллекций в список
|
||||
collection_ids_list = [int(coll_id.strip()) for coll_id in collection_ids.split(',') if coll_id.strip()]
|
||||
if collection_ids_list:
|
||||
# Формируем условие для фильтрации по коллекциям
|
||||
collection_filter = " OR ".join([f"collection_id = {coll_id}" for coll_id in collection_ids_list])
|
||||
filters.append(f"({collection_filter})")
|
||||
except ValueError as e:
|
||||
logging.warning(f"Некорректный формат collection_ids: {collection_ids}. Ошибка: {str(e)}")
|
||||
# Для обратной совместимости
|
||||
elif collection_id is not None:
|
||||
filters.append(f"collection_id = {collection_id}")
|
||||
|
||||
if is_active is not None:
|
||||
filters.append(f"is_active = {str(is_active).lower()}")
|
||||
|
||||
if min_price is not None:
|
||||
filters.append(f"price >= {min_price}")
|
||||
|
||||
if max_price is not None:
|
||||
filters.append(f"price <= {max_price}")
|
||||
|
||||
# Обработка фильтрации по размерам
|
||||
if size_ids:
|
||||
try:
|
||||
# Преобразуем строку с ID размеров в список
|
||||
size_ids_list = [int(size_id.strip()) for size_id in size_ids.split(',') if size_id.strip()]
|
||||
if size_ids_list:
|
||||
# Формируем условие для фильтрации по размерам
|
||||
size_filter = " OR ".join([f"size_ids = {size_id}" for size_id in size_ids_list])
|
||||
filters.append(f"({size_filter})")
|
||||
except ValueError as e:
|
||||
logging.warning(f"Некорректный формат size_ids: {size_ids}. Ошибка: {str(e)}")
|
||||
# Если формат некорректный, игнорируем этот фильтр
|
||||
|
||||
# Формируем параметры сортировки
|
||||
sort_param = None
|
||||
|
||||
# Если передан прямой параметр sort, используем его
|
||||
if sort:
|
||||
# Обрабатываем популярные варианты сортировки
|
||||
if sort == 'popular':
|
||||
sort_param = None # Используем сортировку по умолчанию
|
||||
elif sort == 'price_asc':
|
||||
sort_param = ['price:asc']
|
||||
elif sort == 'price_desc':
|
||||
sort_param = ['price:desc']
|
||||
elif sort == 'newest':
|
||||
sort_param = ['created_at:desc']
|
||||
else:
|
||||
# Если передан другой вариант, используем его напрямую
|
||||
sort_param = [sort]
|
||||
# Если передан sort_by, используем его
|
||||
elif sort_by:
|
||||
sort_param = [f"{sort_by}:{sort_order}"]
|
||||
|
||||
# Используем Meilisearch для поиска продуктов
|
||||
from app.services import meilisearch_service
|
||||
|
||||
# Объединяем фильтры в строку
|
||||
filter_str = " AND ".join(filters) if filters else None
|
||||
|
||||
# Выполняем поиск в Meilisearch
|
||||
result = meilisearch_service.search_products(
|
||||
query=search or "",
|
||||
filters=filter_str,
|
||||
sort=sort_param,
|
||||
limit=limit,
|
||||
offset=skip
|
||||
)
|
||||
|
||||
# Если поиск в Meilisearch успешен, возвращаем результаты
|
||||
if result["success"]:
|
||||
return {
|
||||
"success": True,
|
||||
"products": result["products"],
|
||||
"total": result["total"],
|
||||
"skip": skip,
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
# Если поиск в Meilisearch не удался, используем старый метод
|
||||
logging.warning(f"Meilisearch search failed: {result.get('error')}. Falling back to database search.")
|
||||
products = get_products(db, skip, limit, category_id, collection_id, search, min_price, max_price, is_active)
|
||||
products_list = [services.format_product(product) for product in products]
|
||||
|
||||
# Синхронизируем продукты с Meilisearch для будущих запросов
|
||||
try:
|
||||
from app.scripts.sync_meilisearch import sync_products
|
||||
sync_products(db)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to sync products with Meilisearch: {str(e)}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"products": products_list,
|
||||
"total": len(products_list),
|
||||
"skip": skip,
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
|
||||
# Маршруты для размеров
|
||||
@catalog_router.get("/sizes", response_model=Dict[str, Any])
|
||||
async def get_sizes(*,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Получить список всех размеров"""
|
||||
# Используем Meilisearch для поиска размеров
|
||||
from app.services import meilisearch_service
|
||||
|
||||
# Выполняем поиск в Meilisearch
|
||||
result = meilisearch_service.search_sizes(
|
||||
query=search or "",
|
||||
filters=None,
|
||||
limit=limit,
|
||||
offset=skip
|
||||
)
|
||||
|
||||
# Если поиск в Meilisearch успешен, возвращаем результаты
|
||||
if result["success"]:
|
||||
return {
|
||||
"success": True,
|
||||
"sizes": result["sizes"],
|
||||
"total": result["total"]
|
||||
}
|
||||
|
||||
# Если поиск в Meilisearch не удался, используем старый метод
|
||||
logging.warning(f"Meilisearch search failed: {result.get('error')}. Falling back to database search.")
|
||||
|
||||
# Получаем размеры из базы данных
|
||||
from app.models.catalog_models import Size
|
||||
sizes_db = db.query(Size).all()
|
||||
|
||||
# Преобразуем размеры в список словарей
|
||||
from app.schemas.catalog_schemas import Size as SizeSchema
|
||||
sizes_list = [SizeSchema.model_validate(size).model_dump() for size in sizes_db]
|
||||
|
||||
# Синхронизируем размеры с Meilisearch для будущих запросов
|
||||
try:
|
||||
from app.scripts.sync_meilisearch import sync_sizes
|
||||
sync_sizes(db)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to sync sizes with Meilisearch: {str(e)}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"sizes": sizes_list,
|
||||
"total": len(sizes_list)
|
||||
}
|
||||
|
||||
|
||||
@catalog_router.get("/sizes/{size_id}", response_model=Dict[str, Any])
|
||||
async def get_size(
|
||||
size_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Получить размер по ID"""
|
||||
# Пробуем получить размер из Meilisearch
|
||||
from app.services import meilisearch_service
|
||||
result = meilisearch_service.get_size(size_id)
|
||||
|
||||
if result["success"]:
|
||||
return {
|
||||
"success": True,
|
||||
"size": result["size"]
|
||||
}
|
||||
|
||||
# Если размер не найден в Meilisearch, получаем его из базы данных
|
||||
size = services.get_size(db, size_id)
|
||||
|
||||
# Преобразуем объект SQLAlchemy в словарь
|
||||
from app.schemas.catalog_schemas import Size as SizeSchema
|
||||
size_dict = SizeSchema.model_validate(size).model_dump()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"size": size_dict
|
||||
}
|
||||
|
||||
|
||||
@catalog_router.post("/sizes", response_model=Dict[str, Any], status_code=status.HTTP_201_CREATED)
|
||||
def create_size(
|
||||
size: SizeCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Создать новый размер"""
|
||||
new_size = services.create_size(db, size)
|
||||
|
||||
# Преобразуем объект SQLAlchemy в словарь
|
||||
from app.schemas.catalog_schemas import Size as SizeSchema
|
||||
size_dict = SizeSchema.model_validate(new_size).model_dump()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"size": size_dict
|
||||
}
|
||||
|
||||
|
||||
@catalog_router.put("/sizes/{size_id}", response_model=Dict[str, Any])
|
||||
def update_size(
|
||||
size_id: int,
|
||||
size: SizeUpdate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Обновить размер"""
|
||||
updated_size = services.update_size(db, size_id, size)
|
||||
|
||||
# Преобразуем объект SQLAlchemy в словарь
|
||||
from app.schemas.catalog_schemas import Size as SizeSchema
|
||||
size_dict = SizeSchema.model_validate(updated_size).model_dump()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"size": size_dict
|
||||
}
|
||||
|
||||
|
||||
@catalog_router.delete("/sizes/{size_id}", response_model=Dict[str, Any])
|
||||
def delete_size(
|
||||
size_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Удалить размер"""
|
||||
# Удаляем размер из базы данных
|
||||
success = services.delete_size(db, size_id)
|
||||
|
||||
# Если удаление прошло успешно, удаляем размер из Meilisearch
|
||||
if success:
|
||||
from app.services import meilisearch_service
|
||||
meilisearch_service.delete_size(size_id)
|
||||
|
||||
return {"success": success}
|
||||
|
||||
|
||||
# Маршруты для комплексного создания и обновления продуктов
|
||||
@catalog_router.post("/products/complete", response_model=Dict[str, Any])
|
||||
async def create_product_complete_endpoint(
|
||||
product: ProductCreateComplete,
|
||||
current_user: UserModel = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Создание продукта вместе с его вариантами и изображениями в одном запросе.
|
||||
"""
|
||||
return services.create_product_complete(db, product)
|
||||
|
||||
|
||||
@catalog_router.put("/products/{product_id}/complete", response_model=Dict[str, Any])
|
||||
async def update_product_complete_endpoint(
|
||||
product_id: int,
|
||||
product: ProductUpdateComplete,
|
||||
current_user: UserModel = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Обновление продукта вместе с его вариантами и изображениями в одном запросе.
|
||||
"""
|
||||
result = services.update_product_complete(db, product_id, product)
|
||||
return result
|
||||
products = get_products(db, skip, limit, category_id, collection_id, search, min_price, max_price, is_active, include_variants)
|
||||
# Преобразуем объекты SQLAlchemy в схемы Pydantic
|
||||
return [Product.model_validate(product) for product in products]
|
||||
@ -1,46 +0,0 @@
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
import logging
|
||||
from app.services import get_cdek_service
|
||||
from app.services.delivery_service import CDEKService
|
||||
|
||||
# Настройка логирования
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Создаем роутер для доставки
|
||||
delivery_router = APIRouter(
|
||||
prefix="/delivery",
|
||||
tags=["delivery"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
@delivery_router.post("/cdek")
|
||||
async def cdek_service_endpoint(request: Request, cdek_service: CDEKService = Depends(get_cdek_service)):
|
||||
"""
|
||||
Эндпоинт для обработки запросов виджета CDEK.
|
||||
Объединяет query-параметры и тело запроса, затем передает их в сервис CDEKService.
|
||||
|
||||
- **action**: Тип действия (offices, calculate)
|
||||
- Для action=offices: Параметры для получения списка офисов
|
||||
- Для action=calculate: Параметры для расчета стоимости доставки
|
||||
"""
|
||||
# Получаем query-параметры из URL
|
||||
query_params = dict(request.query_params)
|
||||
logger.info("CDEK эндпоинт: Получены query-параметры: %s", query_params)
|
||||
|
||||
try:
|
||||
# Пытаемся получить тело запроса как JSON
|
||||
body = await request.json()
|
||||
logger.info("CDEK эндпоинт: Получено тело запроса: %s", body)
|
||||
except Exception as e:
|
||||
logger.warning("CDEK эндпоинт: Не удалось получить JSON тело запроса: %s", str(e))
|
||||
body = {}
|
||||
|
||||
# Объединяем данные (приоритет у тела запроса)
|
||||
request_data = {**query_params, **body}
|
||||
logger.info("CDEK эндпоинт: Объединенные данные запроса: %s", request_data)
|
||||
|
||||
# Асинхронно обрабатываем запрос
|
||||
logger.debug("CDEK эндпоинт: Передача запроса в сервис CDEKService")
|
||||
response = await cdek_service.process(request_data)
|
||||
logger.info("CDEK эндпоинт: Получен ответ от сервиса, статус: %s", response.status_code)
|
||||
return response
|
||||
@ -1,133 +1,45 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Body
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
from app.core import get_db, get_current_active_user
|
||||
from app import services
|
||||
from app.schemas.order_schemas import OrderCreate, OrderUpdate, Order, OrderCreateNew
|
||||
from app.schemas.order_schemas import OrderCreate, OrderUpdate, Order
|
||||
from app.models.user_models import User as UserModel
|
||||
from app.models.order_models import OrderStatus
|
||||
from app.repositories.order_repo import get_all_orders, get_user_orders
|
||||
|
||||
# Роутер для заказов
|
||||
order_router = APIRouter(prefix="/orders", tags=["Заказы"])
|
||||
|
||||
@order_router.post("/", response_model=Dict[str, Any])
|
||||
async def create_order_endpoint(
|
||||
order: OrderCreate,
|
||||
# current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Создает новый заказ (старый формат).
|
||||
|
||||
- **shipping_address_id**: ID адреса доставки
|
||||
- **payment_method**: Способ оплаты
|
||||
- **notes**: Примечания к заказу (опционально)
|
||||
- **cart_items**: Список ID элементов корзины (опционально)
|
||||
- **items**: Прямые элементы заказа (опционально)
|
||||
"""
|
||||
return services.create_order(db, None, order)
|
||||
|
||||
|
||||
@order_router.post("/new", response_model=Dict[str, Any])
|
||||
async def create_order_new_endpoint(
|
||||
order: OrderCreateNew,
|
||||
db: Session = Depends(get_db),
|
||||
# current_user: Optional[UserModel] = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Создает новый заказ с новой структурой данных.
|
||||
Если пользователь не авторизован, создает нового пользователя на основе данных заказа.
|
||||
|
||||
- **user_info**: Информация о пользователе (имя, фамилия, email, телефон)
|
||||
- **delivery**: Информация о доставке (метод, адрес, информация о CDEK)
|
||||
- **items**: Товары в заказе
|
||||
- **payment_method**: Способ оплаты
|
||||
- **comment**: Комментарий к заказу (опционально)
|
||||
"""
|
||||
# Если пользователь авторизован, используем его ID, иначе передаем None
|
||||
user_id = None # current_user.id if current_user else None
|
||||
|
||||
# Проверяем наличие email в данных заказа
|
||||
if not order.user_info.email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email обязателен для оформления заказа"
|
||||
)
|
||||
|
||||
# Создаем заказ (если пользователь не авторизован, будет создан новый пользователь)
|
||||
return services.create_order_new(db, user_id, order)
|
||||
async def create_order_endpoint(order: OrderCreate, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
|
||||
return services.create_order(db, current_user.id, order)
|
||||
|
||||
|
||||
@order_router.get("/{order_id}", response_model=Dict[str, Any])
|
||||
async def get_order_endpoint(
|
||||
order_id: int,
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Получает информацию о заказе по ID.
|
||||
|
||||
- **order_id**: ID заказа
|
||||
"""
|
||||
async def get_order_endpoint(order_id: int, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
|
||||
return services.get_order(db, current_user.id, order_id, current_user.is_admin)
|
||||
|
||||
|
||||
@order_router.put("/{order_id}", response_model=Dict[str, Any])
|
||||
async def update_order_endpoint(
|
||||
order_id: int,
|
||||
order: OrderUpdate,
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Обновляет информацию о заказе.
|
||||
|
||||
- **order_id**: ID заказа
|
||||
- **status**: Новый статус заказа (опционально, только для админов)
|
||||
- **shipping_address_id**: ID нового адреса доставки (опционально, только для админов)
|
||||
- **payment_method**: Новый способ оплаты (опционально, только для админов)
|
||||
- **payment_details**: Детали оплаты (опционально, только для админов)
|
||||
- **tracking_number**: Номер отслеживания (опционально, только для админов)
|
||||
- **notes**: Примечания к заказу (опционально)
|
||||
"""
|
||||
async def update_order_endpoint(order_id: int, order: OrderUpdate, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
|
||||
return services.update_order(db, current_user.id, order_id, order, current_user.is_admin)
|
||||
|
||||
|
||||
@order_router.post("/{order_id}/cancel", response_model=Dict[str, Any])
|
||||
async def cancel_order_endpoint(
|
||||
order_id: int,
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Отменяет заказ.
|
||||
|
||||
- **order_id**: ID заказа
|
||||
"""
|
||||
async def cancel_order_endpoint(order_id: int, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
|
||||
return services.cancel_order(db, current_user.id, order_id)
|
||||
|
||||
|
||||
@order_router.get("/", response_model=List[Dict[str, Any]])
|
||||
async def get_orders_endpoint(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[OrderStatus] = None,
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
@order_router.get("/", response_model=List[Order])
|
||||
async def get_orders(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[str] = None,
|
||||
current_user: UserModel = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Получает список заказов пользователя.
|
||||
|
||||
- **skip**: Количество пропускаемых записей
|
||||
- **limit**: Максимальное количество записей
|
||||
- **status**: Фильтр по статусу заказа (опционально)
|
||||
"""
|
||||
# Преобразуем заказы в словари с детальной информацией
|
||||
):
|
||||
if current_user.is_admin:
|
||||
orders = services.order_repo.get_all_orders(db, skip, limit, status)
|
||||
return get_all_orders(db, skip, limit, status)
|
||||
else:
|
||||
orders = services.order_repo.get_user_orders(db, current_user.id, skip, limit)
|
||||
|
||||
# Получаем полную информацию о каждом заказе
|
||||
return [services.order_repo.get_order_with_details(db, order.id) for order in orders]
|
||||
return get_user_orders(db, current_user.id, skip, limit)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import Optional, List, Union, Dict, Any
|
||||
from typing import Optional, List, Union
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@ -62,37 +62,14 @@ class Category(CategoryBase):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Схемы для размеров
|
||||
class SizeBase(BaseModel):
|
||||
name: str
|
||||
code: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class SizeCreate(SizeBase):
|
||||
pass
|
||||
|
||||
|
||||
class SizeUpdate(SizeBase):
|
||||
name: Optional[str] = None
|
||||
code: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class Size(SizeBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Схемы для вариантов продуктов
|
||||
class ProductVariantBase(BaseModel):
|
||||
product_id: int
|
||||
size_id: int
|
||||
name: str
|
||||
sku: str
|
||||
price: float
|
||||
discount_price: Optional[float] = None
|
||||
stock: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
@ -101,10 +78,12 @@ class ProductVariantCreate(ProductVariantBase):
|
||||
pass
|
||||
|
||||
|
||||
class ProductVariantUpdate(BaseModel):
|
||||
class ProductVariantUpdate(ProductVariantBase):
|
||||
product_id: Optional[int] = None
|
||||
size_id: Optional[int] = None
|
||||
name: Optional[str] = None
|
||||
sku: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
discount_price: Optional[float] = None
|
||||
stock: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
@ -113,7 +92,6 @@ class ProductVariant(ProductVariantBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
size: Optional[Size] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@ -128,8 +106,7 @@ class ProductImageBase(BaseModel):
|
||||
|
||||
|
||||
class ProductImageCreate(ProductImageBase):
|
||||
id: Optional[int] = None # Опциональное поле id для обеспечения совместимости с фронтендом
|
||||
created_at: Optional[datetime] = None # Опциональное поле created_at
|
||||
pass
|
||||
|
||||
|
||||
class ProductImageUpdate(BaseModel):
|
||||
@ -146,14 +123,12 @@ class ProductImage(ProductImageBase):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
|
||||
# Схемы для продуктов
|
||||
class ProductBase(BaseModel):
|
||||
name: str
|
||||
slug: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: float
|
||||
discount_price: Optional[float] = None
|
||||
care_instructions: Optional[Dict[str, Any]] = None
|
||||
is_active: bool = True
|
||||
category_id: int
|
||||
collection_id: Optional[int] = None
|
||||
@ -163,13 +138,10 @@ class ProductCreate(ProductBase):
|
||||
pass
|
||||
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
class ProductUpdate(ProductBase):
|
||||
name: Optional[str] = None
|
||||
slug: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
discount_price: Optional[float] = None
|
||||
care_instructions: Optional[Dict[str, Any]] = None
|
||||
is_active: Optional[bool] = None
|
||||
category_id: Optional[int] = None
|
||||
collection_id: Optional[int] = None
|
||||
@ -184,8 +156,6 @@ class Product(ProductBase):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
populate_by_name = True
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# Расширенные схемы для отображения
|
||||
@ -208,49 +178,4 @@ class ProductWithDetails(Product):
|
||||
|
||||
|
||||
# Рекурсивное обновление для CategoryWithChildren
|
||||
CategoryWithSubcategories.update_forward_refs()
|
||||
|
||||
# Схемы для продуктов с вложенными объектами
|
||||
class ProductVariantCreateNested(BaseModel):
|
||||
size_id: int
|
||||
sku: str
|
||||
stock: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
class ProductImageCreateNested(BaseModel):
|
||||
image_url: str
|
||||
alt_text: Optional[str] = None
|
||||
is_primary: bool = False
|
||||
|
||||
class ProductCreateComplete(ProductBase):
|
||||
variants: Optional[List[ProductVariantCreateNested]] = []
|
||||
images: Optional[List[ProductImageCreateNested]] = []
|
||||
# Остальные поля наследуются из ProductBase
|
||||
|
||||
class ProductVariantUpdateNested(BaseModel):
|
||||
id: Optional[int] = None # Если id присутствует, обновляем существующий вариант
|
||||
size_id: Optional[int] = None
|
||||
sku: Optional[str] = None
|
||||
stock: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class ProductImageUpdateNested(BaseModel):
|
||||
id: Optional[int] = None # Если id присутствует, обновляем существующее изображение
|
||||
image_url: Optional[str] = None
|
||||
alt_text: Optional[str] = None
|
||||
is_primary: Optional[bool] = None
|
||||
|
||||
class ProductUpdateComplete(BaseModel):
|
||||
name: Optional[str] = None
|
||||
slug: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
discount_price: Optional[float] = None
|
||||
care_instructions: Optional[Dict[str, Any]] = None
|
||||
is_active: Optional[bool] = None
|
||||
category_id: Optional[int] = None
|
||||
collection_id: Optional[int] = None
|
||||
variants: Optional[List[ProductVariantUpdateNested]] = None
|
||||
images: Optional[List[ProductImageUpdateNested]] = None
|
||||
variants_to_remove: Optional[List[int]] = None # ID вариантов для удаления
|
||||
images_to_remove: Optional[List[int]] = None # ID изображений для удаления
|
||||
CategoryWithSubcategories.update_forward_refs()
|
||||
@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, field_validator
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
@ -62,107 +62,7 @@ class OrderItemWithProduct(OrderItem):
|
||||
variant_name: Optional[str] = None
|
||||
|
||||
|
||||
# Схемы для информации о пользователе
|
||||
class UserInfoBase(BaseModel):
|
||||
first_name: str
|
||||
last_name: Optional[str] = ""
|
||||
email: str
|
||||
phone: str
|
||||
|
||||
|
||||
# Схемы для адреса доставки
|
||||
class AddressBase(BaseModel):
|
||||
city: str
|
||||
street: Optional[str] = ""
|
||||
house: Optional[str] = ""
|
||||
apartment: Optional[str] = ""
|
||||
postal_code: Optional[str] = ""
|
||||
formatted_address: Optional[str] = ""
|
||||
|
||||
|
||||
# Схемы для информации о доставке CDEK
|
||||
class CDEKPvzInfo(BaseModel):
|
||||
city_code: Optional[int] = None
|
||||
city: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
postal_code: Optional[str] = None
|
||||
country_code: Optional[str] = None
|
||||
region: Optional[str] = None
|
||||
have_cashless: Optional[bool] = None
|
||||
have_cash: Optional[bool] = None
|
||||
allowed_cod: Optional[bool] = None
|
||||
is_dressing_room: Optional[bool] = None
|
||||
code: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
work_time: Optional[str] = None
|
||||
location: Optional[List[float]] = None
|
||||
weight_min: Optional[int] = None
|
||||
weight_max: Optional[int] = None
|
||||
dimensions: Optional[Any] = None
|
||||
|
||||
|
||||
class CDEKTariffInfo(BaseModel):
|
||||
tariff_code: Optional[int] = None
|
||||
tariff_name: Optional[str] = None
|
||||
tariff_description: Optional[str] = None
|
||||
delivery_mode: Optional[int] = None
|
||||
delivery_sum: Optional[int] = None
|
||||
period_min: Optional[int] = None
|
||||
period_max: Optional[int] = None
|
||||
calendar_min: Optional[int] = None
|
||||
calendar_max: Optional[int] = None
|
||||
delivery_date_range: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
class CDEKInfo(BaseModel):
|
||||
pvz: Optional[CDEKPvzInfo] = None
|
||||
tariff: Optional[CDEKTariffInfo] = None
|
||||
delivery_type: Optional[str] = None
|
||||
|
||||
|
||||
# Схемы для информации о доставке курьером
|
||||
class CourierInfo(BaseModel):
|
||||
geo_lat: Optional[str] = None
|
||||
geo_lon: Optional[str] = None
|
||||
fias_id: Optional[str] = None
|
||||
kladr_id: Optional[str] = None
|
||||
|
||||
|
||||
# Схемы для информации о доставке
|
||||
class DeliveryInfo(BaseModel):
|
||||
method: str # cdek или courier
|
||||
address: AddressBase
|
||||
cdek_info: Optional[CDEKInfo] = None
|
||||
|
||||
|
||||
# Схемы для элементов заказа в новом формате
|
||||
class OrderItemNew(BaseModel):
|
||||
product_id: int
|
||||
variant_id: int
|
||||
quantity: int
|
||||
price: float
|
||||
|
||||
|
||||
# Схемы для заказов в новом формате
|
||||
class OrderCreateNew(BaseModel):
|
||||
user_info: UserInfoBase
|
||||
delivery: DeliveryInfo
|
||||
items: List[OrderItemNew]
|
||||
payment_method: str
|
||||
comment: Optional[str] = ""
|
||||
|
||||
@field_validator('payment_method')
|
||||
def validate_payment_method(cls, v):
|
||||
# Проверяем, что значение payment_method соответствует одному из допустимых значений
|
||||
valid_methods = ["sbp", "card"]
|
||||
if v.lower() not in valid_methods:
|
||||
# Если не соответствует, преобразуем к значению по умолчанию
|
||||
return "card"
|
||||
return v.lower() # Возвращаем значение в нижнем регистре
|
||||
|
||||
|
||||
# Старые схемы для заказов (для обратной совместимости)
|
||||
# Схемы для заказов
|
||||
class OrderBase(BaseModel):
|
||||
shipping_address_id: Optional[int] = None
|
||||
payment_method: Optional[PaymentMethod] = None
|
||||
@ -182,35 +82,12 @@ class OrderUpdate(BaseModel):
|
||||
tracking_number: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
# Новые поля
|
||||
delivery_method: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
delivery_address: Optional[str] = None
|
||||
cdek_info: Optional[Dict[str, Any]] = None
|
||||
courier_info: Optional[Dict[str, Any]] = None
|
||||
user_info_json: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class Order(OrderBase):
|
||||
id: int
|
||||
user_id: int
|
||||
status: OrderStatus
|
||||
total_amount: float
|
||||
|
||||
# Информация о пользователе (из JSON)
|
||||
user_info_json: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Информация о доставке
|
||||
delivery_method: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
delivery_address: Optional[str] = None
|
||||
cdek_info: Optional[Dict[str, Any]] = None
|
||||
courier_info: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Информация о товарах (из JSON)
|
||||
items_json: Optional[List[Dict[str, Any]]] = None
|
||||
|
||||
# Дополнительная информация
|
||||
payment_details: Optional[str] = None
|
||||
tracking_number: Optional[str] = None
|
||||
created_at: datetime
|
||||
@ -242,25 +119,7 @@ class OrderWithDetails(BaseModel):
|
||||
user_id: int
|
||||
status: OrderStatus
|
||||
total_amount: float
|
||||
|
||||
# Информация о пользователе (из JSON)
|
||||
user_info_json: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Информация о доставке
|
||||
delivery_method: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
delivery_address: Optional[str] = None
|
||||
cdek_info: Optional[Dict[str, Any]] = None
|
||||
courier_info: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Информация о товарах (из JSON)
|
||||
items_json: Optional[List[Dict[str, Any]]] = None
|
||||
|
||||
# Старые поля (для обратной совместимости)
|
||||
shipping_address_id: Optional[int] = None
|
||||
shipping_address: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Дополнительная информация
|
||||
payment_method: Optional[PaymentMethod] = None
|
||||
payment_details: Optional[str] = None
|
||||
tracking_number: Optional[str] = None
|
||||
@ -268,4 +127,5 @@ class OrderWithDetails(BaseModel):
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
user_email: Optional[str] = None
|
||||
items: List[Dict[str, Any]] = []
|
||||
shipping_address: Optional[Dict[str, Any]] = None
|
||||
items: List[Dict[str, Any]] = []
|
||||
@ -1 +0,0 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1,254 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем родительскую директорию в sys.path, чтобы импортировать модули приложения
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core import SessionLocal, engine
|
||||
from app.models.catalog_models import Product, Category, Collection, Size, ProductVariant, ProductImage
|
||||
from app.services import meilisearch_service
|
||||
from app.schemas.catalog_schemas import Product as ProductSchema, Category as CategorySchema
|
||||
from app.schemas.catalog_schemas import Collection as CollectionSchema, Size as SizeSchema
|
||||
|
||||
# Настройка логгера
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_product_for_meilisearch(product, variants, images):
|
||||
"""
|
||||
Форматирует продукт для индексации в Meilisearch.
|
||||
"""
|
||||
# Преобразуем объект SQLAlchemy в словарь
|
||||
product_dict = {
|
||||
"id": product.id,
|
||||
"name": product.name,
|
||||
"slug": product.slug,
|
||||
"description": product.description,
|
||||
"price": product.price,
|
||||
"discount_price": product.discount_price,
|
||||
"care_instructions": product.care_instructions,
|
||||
"is_active": product.is_active,
|
||||
"category_id": product.category_id,
|
||||
"collection_id": product.collection_id,
|
||||
"created_at": product.created_at.isoformat() if product.created_at else None,
|
||||
"updated_at": product.updated_at.isoformat() if product.updated_at else None,
|
||||
"variants": [],
|
||||
"images": [],
|
||||
"size_ids": [] # Добавляем массив для хранения ID размеров
|
||||
}
|
||||
|
||||
# Добавляем варианты продукта
|
||||
for variant in variants:
|
||||
variant_dict = {
|
||||
"id": variant.id,
|
||||
"size_id": variant.size_id,
|
||||
"sku": variant.sku,
|
||||
"stock": variant.stock,
|
||||
"is_active": variant.is_active
|
||||
}
|
||||
|
||||
# Добавляем информацию о размере, если она доступна
|
||||
if variant.size:
|
||||
variant_dict["size"] = {
|
||||
"id": variant.size.id,
|
||||
"name": variant.size.name,
|
||||
"code": variant.size.code
|
||||
}
|
||||
|
||||
# Добавляем ID размера в массив size_ids для фильтрации
|
||||
if variant.size_id not in product_dict["size_ids"]:
|
||||
product_dict["size_ids"].append(variant.size_id)
|
||||
|
||||
product_dict["variants"].append(variant_dict)
|
||||
|
||||
# Добавляем изображения продукта
|
||||
for image in images:
|
||||
image_dict = {
|
||||
"id": image.id,
|
||||
"image_url": image.image_url,
|
||||
"alt_text": image.alt_text,
|
||||
"is_primary": image.is_primary
|
||||
}
|
||||
product_dict["images"].append(image_dict)
|
||||
|
||||
# Добавляем основное изображение в корень продукта для удобства
|
||||
if image.is_primary:
|
||||
product_dict["primary_image"] = image.image_url
|
||||
|
||||
return product_dict
|
||||
|
||||
|
||||
def format_category_for_meilisearch(category):
|
||||
"""
|
||||
Форматирует категорию для индексации в Meilisearch.
|
||||
"""
|
||||
return {
|
||||
"id": category.id,
|
||||
"name": category.name,
|
||||
"slug": category.slug,
|
||||
"description": category.description,
|
||||
"parent_id": category.parent_id,
|
||||
"is_active": category.is_active,
|
||||
"created_at": category.created_at.isoformat() if category.created_at else None,
|
||||
"updated_at": category.updated_at.isoformat() if category.updated_at else None
|
||||
}
|
||||
|
||||
|
||||
def format_collection_for_meilisearch(collection):
|
||||
"""
|
||||
Форматирует коллекцию для индексации в Meilisearch.
|
||||
"""
|
||||
return {
|
||||
"id": collection.id,
|
||||
"name": collection.name,
|
||||
"slug": collection.slug,
|
||||
"description": collection.description,
|
||||
"is_active": collection.is_active,
|
||||
"created_at": collection.created_at.isoformat() if collection.created_at else None,
|
||||
"updated_at": collection.updated_at.isoformat() if collection.updated_at else None
|
||||
}
|
||||
|
||||
|
||||
def format_size_for_meilisearch(size):
|
||||
"""
|
||||
Форматирует размер для индексации в Meilisearch.
|
||||
"""
|
||||
return {
|
||||
"id": size.id,
|
||||
"name": size.name,
|
||||
"code": size.code,
|
||||
"description": size.description,
|
||||
"created_at": size.created_at.isoformat() if size.created_at else None,
|
||||
"updated_at": size.updated_at.isoformat() if size.updated_at else None
|
||||
}
|
||||
|
||||
|
||||
def sync_products(db: Session):
|
||||
"""
|
||||
Синхронизирует все продукты с Meilisearch.
|
||||
"""
|
||||
logger.info("Syncing products...")
|
||||
|
||||
# Получаем все продукты из базы данных
|
||||
products = db.query(Product).all()
|
||||
|
||||
# Форматируем продукты для Meilisearch
|
||||
products_data = []
|
||||
for product in products:
|
||||
variants = db.query(ProductVariant).filter(ProductVariant.product_id == product.id).all()
|
||||
images = db.query(ProductImage).filter(ProductImage.product_id == product.id).all()
|
||||
product_data = format_product_for_meilisearch(product, variants, images)
|
||||
products_data.append(product_data)
|
||||
|
||||
# Синхронизируем продукты с Meilisearch
|
||||
success = meilisearch_service.sync_all_products(products_data)
|
||||
|
||||
if success:
|
||||
logger.info(f"Successfully synced {len(products_data)} products")
|
||||
else:
|
||||
logger.error("Failed to sync products")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def sync_categories(db: Session):
|
||||
"""
|
||||
Синхронизирует все категории с Meilisearch.
|
||||
"""
|
||||
logger.info("Syncing categories...")
|
||||
|
||||
# Получаем все категории из базы данных
|
||||
categories = db.query(Category).all()
|
||||
|
||||
# Форматируем категории для Meilisearch
|
||||
categories_data = [format_category_for_meilisearch(category) for category in categories]
|
||||
|
||||
# Синхронизируем категории с Meilisearch
|
||||
success = meilisearch_service.sync_all_categories(categories_data)
|
||||
|
||||
if success:
|
||||
logger.info(f"Successfully synced {len(categories_data)} categories")
|
||||
else:
|
||||
logger.error("Failed to sync categories")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def sync_collections(db: Session):
|
||||
"""
|
||||
Синхронизирует все коллекции с Meilisearch.
|
||||
"""
|
||||
logger.info("Syncing collections...")
|
||||
|
||||
# Получаем все коллекции из базы данных
|
||||
collections = db.query(Collection).all()
|
||||
|
||||
# Форматируем коллекции для Meilisearch
|
||||
collections_data = [format_collection_for_meilisearch(collection) for collection in collections]
|
||||
|
||||
# Синхронизируем коллекции с Meilisearch
|
||||
success = meilisearch_service.sync_all_collections(collections_data)
|
||||
|
||||
if success:
|
||||
logger.info(f"Successfully synced {len(collections_data)} collections")
|
||||
else:
|
||||
logger.error("Failed to sync collections")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def sync_sizes(db: Session):
|
||||
"""
|
||||
Синхронизирует все размеры с Meilisearch.
|
||||
"""
|
||||
logger.info("Syncing sizes...")
|
||||
|
||||
# Получаем все размеры из базы данных
|
||||
sizes = db.query(Size).all()
|
||||
|
||||
# Форматируем размеры для Meilisearch
|
||||
sizes_data = [format_size_for_meilisearch(size) for size in sizes]
|
||||
|
||||
# Синхронизируем размеры с Meilisearch
|
||||
success = meilisearch_service.sync_all_sizes(sizes_data)
|
||||
|
||||
if success:
|
||||
logger.info(f"Successfully synced {len(sizes_data)} sizes")
|
||||
else:
|
||||
logger.error("Failed to sync sizes")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Основная функция для синхронизации всех данных с Meilisearch.
|
||||
"""
|
||||
logger.info("Starting Meilisearch synchronization...")
|
||||
|
||||
# Инициализируем индексы в Meilisearch
|
||||
meilisearch_service.initialize_indexes()
|
||||
|
||||
# Создаем сессию базы данных
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# Синхронизируем все данные
|
||||
sync_categories(db)
|
||||
sync_collections(db)
|
||||
sync_sizes(db)
|
||||
sync_products(db)
|
||||
|
||||
logger.info("Meilisearch synchronization completed successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during Meilisearch synchronization: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -9,14 +9,12 @@ from app.services.catalog_service import (
|
||||
create_product, update_product, delete_product, get_product_details,
|
||||
add_product_variant, update_product_variant, delete_product_variant,
|
||||
upload_product_image, update_product_image, delete_product_image,
|
||||
create_collection, update_collection, delete_collection, get_collections,
|
||||
get_size, get_size_by_code, get_sizes, create_size, update_size, delete_size,
|
||||
create_product_complete, update_product_complete, format_product
|
||||
create_collection, update_collection, delete_collection, get_collections
|
||||
)
|
||||
|
||||
from app.services.order_service import (
|
||||
add_to_cart, update_cart_item, remove_from_cart, clear_cart, get_cart,
|
||||
create_order, create_order_new, get_order, update_order, cancel_order,
|
||||
create_order, get_order, update_order, cancel_order
|
||||
)
|
||||
|
||||
from app.services.review_service import (
|
||||
@ -26,10 +24,4 @@ from app.services.review_service import (
|
||||
from app.services.content_service import (
|
||||
create_page, update_page, delete_page, get_page_by_slug,
|
||||
log_event, get_analytics_report
|
||||
)
|
||||
|
||||
from app.services.delivery_service import get_cdek_service
|
||||
|
||||
|
||||
# Импорт репозиториев для прямого доступа
|
||||
from app.repositories import order_repo
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user