обновлена структура. добавлен базовый бекенд

This commit is contained in:
Zikil 2025-02-28 20:03:23 +07:00
parent fbd240efec
commit f2214e2b9e
131 changed files with 3794 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
backend/.DS_Store vendored Normal file

Binary file not shown.

BIN
backend/app/.DS_Store vendored Normal file

Binary file not shown.

0
backend/app/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

53
backend/app/config.py Normal file
View File

@ -0,0 +1,53 @@
import os
from pydantic_settings import BaseSettings
from dotenv import load_dotenv
# Загружаем переменные окружения из .env файла
load_dotenv()
class Settings(BaseSettings):
# Основные настройки приложения
APP_NAME: str = "Интернет-магазин API"
APP_VERSION: str = "0.1.0"
APP_DESCRIPTION: str = "API для интернет-магазина на FastAPI"
# Настройки базы данных
DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5434/shop_db")
# Настройки безопасности
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
# Настройки CORS
CORS_ORIGINS: list = [
"http://localhost",
"http://localhost:3000",
"http://localhost:8000",
"http://localhost:8080",
]
# Настройки для загрузки файлов
UPLOAD_DIRECTORY: str = "uploads"
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", "")
# Настройки для отправки email (пример)
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()

102
backend/app/core.py Normal file
View File

@ -0,0 +1,102 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from passlib.context import CryptContext
from jose import JWTError, jwt
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, Union
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.config import settings
# Настройка SQLAlchemy
SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Зависимость для получения сессии БД
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Настройка безопасности
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
# Функции для работы с паролями
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
# Функции для работы с JWT токенами
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_token(token: str) -> Dict[str, Any]:
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Невалидный токен аутентификации",
headers={"WWW-Authenticate": "Bearer"},
)
# Функция для получения текущего пользователя
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Невалидные учетные данные",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = verify_token(token)
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
from app.repositories.user_repo import get_user_by_username
user = get_user_by_username(db, username)
if user is None:
raise credentials_exception
return user
# Функция для проверки активного пользователя
async def get_current_active_user(current_user = Depends(get_current_user)):
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Неактивный пользователь")
return current_user
# Функция для проверки прав администратора
async def get_current_admin_user(current_user = Depends(get_current_active_user)):
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Недостаточно прав для выполнения операции"
)
return current_user

57
backend/app/main.py Normal file
View File

@ -0,0 +1,57 @@
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import JSONResponse
import os
from pathlib import Path
from app.config import settings
from app.routers import router
from app.core import Base, engine
# Создаем таблицы в базе данных
Base.metadata.create_all(bind=engine)
# Создаем экземпляр приложения FastAPI
app = FastAPI(
title=settings.APP_NAME,
description=settings.APP_DESCRIPTION,
version=settings.APP_VERSION
)
# Настраиваем CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Обработчик исключений
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# В реальном приложении здесь должно быть логирование ошибок
return JSONResponse(
status_code=500,
content={"detail": "Внутренняя ошибка сервера"}
)
# Подключаем роутеры
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 интернет-магазина",
"docs_url": "/docs",
"redoc_url": "/redoc"
}

View File

View File

@ -0,0 +1,73 @@
from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey, DateTime, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core import Base
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
slug = Column(String, unique=True, index=True, nullable=False)
description = Column(Text, nullable=True)
parent_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
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())
# Отношения
parent = relationship("Category", remote_side=[id], backref="subcategories")
products = relationship("Product", back_populates="category")
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
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)
stock = Column(Integer, default=0)
is_active = Column(Boolean, default=True)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Отношения
category = relationship("Category", back_populates="products")
variants = relationship("ProductVariant", back_populates="product", cascade="all, delete-orphan")
reviews = relationship("Review", back_populates="product", cascade="all, delete-orphan")
images = relationship("ProductImage", backref="product", cascade="all, delete-orphan")
class ProductVariant(Base):
__tablename__ = "product_variants"
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
name = Column(String, nullable=False)
sku = Column(String, unique=True, nullable=False)
price_adjustment = Column(Float, default=0.0)
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")
class ProductImage(Base):
__tablename__ = "product_images"
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
image_url = Column(String, nullable=False)
alt_text = Column(String, nullable=True)
is_primary = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())

View File

@ -0,0 +1,40 @@
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Text, Boolean, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core import Base
class Page(Base):
__tablename__ = "pages"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
slug = Column(String, unique=True, index=True, nullable=False)
content = Column(Text, nullable=False)
meta_title = Column(String, nullable=True)
meta_description = Column(String, nullable=True)
is_published = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class AnalyticsLog(Base):
__tablename__ = "analytics_logs"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
event_type = Column(String, nullable=False) # page_view, product_view, add_to_cart, etc.
page_url = Column(String, nullable=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=True)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
ip_address = Column(String, nullable=True)
user_agent = Column(String, nullable=True)
referrer = Column(String, nullable=True)
additional_data = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Отношения
user = relationship("User")
product = relationship("Product")
category = relationship("Category")

View File

@ -0,0 +1,73 @@
from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey, DateTime, Text, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.core import Base
class OrderStatus(str, enum.Enum):
PENDING = "pending"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
REFUNDED = "refunded"
class PaymentMethod(str, enum.Enum):
CREDIT_CARD = "credit_card"
PAYPAL = "paypal"
BANK_TRANSFER = "bank_transfer"
CASH_ON_DELIVERY = "cash_on_delivery"
class CartItem(Base):
__tablename__ = "cart_items"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
variant_id = Column(Integer, ForeignKey("product_variants.id"), nullable=False)
quantity = Column(Integer, default=1)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Отношения
user = relationship("User", back_populates="cart_items")
variant = relationship("ProductVariant")
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
status = Column(Enum(OrderStatus), default=OrderStatus.PENDING)
total_amount = Column(Float, nullable=False)
shipping_address_id = Column(Integer, ForeignKey("user_addresses.id"), nullable=True)
payment_method = Column(Enum(PaymentMethod), nullable=True)
payment_details = Column(Text, nullable=True)
tracking_number = Column(String, nullable=True)
notes = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Отношения
user = relationship("User", back_populates="orders")
shipping_address = relationship("UserAddress")
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
class OrderItem(Base):
__tablename__ = "order_items"
id = Column(Integer, primary_key=True, index=True)
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
variant_id = Column(Integer, ForeignKey("product_variants.id"), nullable=False)
quantity = Column(Integer, default=1)
price = Column(Float, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Отношения
order = relationship("Order", back_populates="items")
variant = relationship("ProductVariant")

View File

@ -0,0 +1,24 @@
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime, Text, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core import Base
class Review(Base):
__tablename__ = "reviews"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
rating = Column(Integer, nullable=False) # От 1 до 5
title = Column(String, nullable=True)
comment = Column(Text, nullable=True)
is_verified_purchase = Column(Boolean, default=False)
is_approved = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Отношения
user = relationship("User", back_populates="reviews")
product = relationship("Product", back_populates="reviews")

View File

@ -0,0 +1,45 @@
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
phone = Column(String, unique=True, nullable=True)
password = Column(String, nullable=False)
first_name = Column(String, nullable=True)
last_name = Column(String, nullable=True)
is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Отношения
addresses = relationship("UserAddress", back_populates="user", cascade="all, delete-orphan")
cart_items = relationship("CartItem", back_populates="user", cascade="all, delete-orphan")
orders = relationship("Order", back_populates="user")
reviews = relationship("Review", back_populates="user", cascade="all, delete-orphan")
class UserAddress(Base):
__tablename__ = "user_addresses"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
address_line1 = Column(String, nullable=False)
address_line2 = Column(String, nullable=True)
city = Column(String, nullable=False)
state = Column(String, nullable=False)
postal_code = Column(String, nullable=False)
country = Column(String, nullable=False)
is_default = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Отношения
user = relationship("User", back_populates="addresses")

View File

View File

@ -0,0 +1,542 @@
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException, status
from typing import List, Optional, Dict, Any
import re
from datetime import datetime
from app.models.catalog_models import Category, Product, ProductVariant, ProductImage
from app.schemas.catalog_schemas import (
CategoryCreate, CategoryUpdate,
ProductCreate, ProductUpdate,
ProductVariantCreate, ProductVariantUpdate,
ProductImageCreate, ProductImageUpdate
)
# Вспомогательная функция для генерации slug
def generate_slug(name: str) -> str:
# Преобразуем в нижний регистр
slug = name.lower()
# Заменяем пробелы на дефисы
slug = re.sub(r'\s+', '-', slug)
# Удаляем все символы, кроме букв, цифр и дефисов
slug = re.sub(r'[^a-z0-9-]', '', slug)
# Удаляем повторяющиеся дефисы
slug = re.sub(r'-+', '-', slug)
# Удаляем дефисы в начале и конце
slug = slug.strip('-')
return slug
# Функции для работы с категориями
def get_category(db: Session, category_id: int) -> Optional[Category]:
return db.query(Category).filter(Category.id == category_id).first()
def get_category_by_slug(db: Session, slug: str) -> Optional[Category]:
return db.query(Category).filter(Category.slug == slug).first()
def get_categories(
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()
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,
slug=category.slug,
description=category.description,
parent_id=category.parent_id,
is_active=category.is_active
)
try:
db.add(db_category)
db.commit()
db.refresh(db_category)
return db_category
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при создании категории"
)
def update_category(db: Session, category_id: int, category: CategoryUpdate) -> Category:
db_category = get_category(db, category_id)
if not db_category:
raise HTTPException(
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"]):
raise HTTPException(
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"])
# Проверяем уникальность сгенерированного slug
if get_category_by_slug(db, update_data["slug"]) and get_category_by_slug(db, update_data["slug"]).id != category_id:
raise HTTPException(
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)
return db_category
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении категории"
)
def delete_category(db: Session, category_id: int) -> bool:
db_category = get_category(db, category_id)
if not db_category:
raise HTTPException(
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()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении категории"
)
# Функции для работы с продуктами
def get_product(db: Session, product_id: int) -> Optional[Product]:
return db.query(Product).filter(Product.id == product_id).first()
def get_product_by_slug(db: Session, slug: str) -> Optional[Product]:
return db.query(Product).filter(Product.slug == slug).first()
def get_products(
db: Session,
skip: int = 0,
limit: int = 100,
category_id: Optional[int] = None,
search: Optional[str] = None,
min_price: Optional[float] = None,
max_price: Optional[float] = None,
is_active: Optional[bool] = True
) -> List[Product]:
query = db.query(Product)
# Применяем фильтры
if category_id:
query = query.filter(Product.category_id == category_id)
if search:
query = query.filter(Product.name.ilike(f"%{search}%"))
if min_price is not None:
query = query.filter(Product.price >= min_price)
if max_price is not None:
query = query.filter(Product.price <= max_price)
if is_active is not None:
query = query.filter(Product.is_active == is_active)
return query.offset(skip).limit(limit).all()
def create_product(db: Session, product: ProductCreate) -> Product:
# Если slug не предоставлен, генерируем его из имени
if not product.slug:
product.slug = generate_slug(product.name)
# Проверяем, что продукт с таким slug не существует
if get_product_by_slug(db, product.slug):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Продукт с таким slug уже существует"
)
# Проверяем, что категория существует
if not get_category(db, product.category_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Категория не найдена"
)
# Создаем новый продукт
db_product = Product(
name=product.name,
slug=product.slug,
description=product.description,
price=product.price,
discount_price=product.discount_price,
stock=product.stock,
is_active=product.is_active,
category_id=product.category_id
)
try:
db.add(db_product)
db.commit()
db.refresh(db_product)
return db_product
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при создании продукта"
)
def update_product(db: Session, product_id: int, product: ProductUpdate) -> Product:
db_product = get_product(db, product_id)
if not db_product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Продукт не найден"
)
# Обновляем только предоставленные поля
update_data = product.dict(exclude_unset=True)
# Если slug изменяется, проверяем его уникальность
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="Продукт с таким slug уже существует"
)
# Если имя изменяется и 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="Продукт с таким slug уже существует"
)
# Проверяем, что категория существует, если указана
if "category_id" in update_data and not get_category(db, update_data["category_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:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении продукта"
)
def delete_product(db: Session, product_id: int) -> bool:
db_product = get_product(db, product_id)
if not db_product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Продукт не найден"
)
try:
db.delete(db_product)
db.commit()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении продукта"
)
# Функции для работы с вариантами продуктов
def get_product_variant(db: Session, variant_id: int) -> Optional[ProductVariant]:
return db.query(ProductVariant).filter(ProductVariant.id == variant_id).first()
def get_product_variants(db: Session, product_id: int) -> List[ProductVariant]:
return db.query(ProductVariant).filter(ProductVariant.product_id == product_id).all()
def create_product_variant(db: Session, variant: ProductVariantCreate) -> ProductVariant:
# Проверяем, что продукт существует
if not get_product(db, variant.product_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Продукт не найден"
)
# Проверяем, что вариант с таким SKU не существует
if db.query(ProductVariant).filter(ProductVariant.sku == variant.sku).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Вариант с таким SKU уже существует"
)
# Создаем новый вариант продукта
db_variant = ProductVariant(
product_id=variant.product_id,
name=variant.name,
sku=variant.sku,
price_adjustment=variant.price_adjustment,
stock=variant.stock,
is_active=variant.is_active
)
try:
db.add(db_variant)
db.commit()
db.refresh(db_variant)
return db_variant
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при создании варианта продукта"
)
def update_product_variant(db: Session, variant_id: int, variant: ProductVariantUpdate) -> ProductVariant:
db_variant = get_product_variant(db, variant_id)
if not db_variant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Вариант продукта не найден"
)
# Обновляем только предоставленные поля
update_data = variant.dict(exclude_unset=True)
# Если 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="Вариант с таким SKU уже существует"
)
# Применяем обновления
for key, value in update_data.items():
setattr(db_variant, key, value)
try:
db.commit()
db.refresh(db_variant)
return db_variant
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении варианта продукта"
)
def delete_product_variant(db: Session, variant_id: int) -> bool:
db_variant = get_product_variant(db, variant_id)
if not db_variant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Вариант продукта не найден"
)
try:
db.delete(db_variant)
db.commit()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении варианта продукта"
)
# Функции для работы с изображениями продуктов
def get_product_image(db: Session, image_id: int) -> Optional[ProductImage]:
return db.query(ProductImage).filter(ProductImage.id == image_id).first()
def get_product_images(db: Session, product_id: int) -> List[ProductImage]:
return db.query(ProductImage).filter(ProductImage.product_id == product_id).all()
def create_product_image(db: Session, image: ProductImageCreate) -> ProductImage:
# Проверяем, что продукт существует
if not get_product(db, image.product_id):
raise HTTPException(
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,
image_url=image.image_url,
alt_text=image.alt_text,
is_primary=image.is_primary
)
try:
db.add(db_image)
db.commit()
db.refresh(db_image)
return db_image
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при создании изображения продукта"
)
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 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})
# Обновляем флаг
db_image.is_primary = is_primary
try:
db.commit()
db.refresh(db_image)
return db_image
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении изображения продукта"
)
def delete_product_image(db: Session, image_id: int) -> bool:
db_image = get_product_image(db, image_id)
if not db_image:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Изображение продукта не найдено"
)
try:
db.delete(db_image)
db.commit()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении изображения продукта"
)

View File

@ -0,0 +1,285 @@
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException, status
from typing import List, Optional, Dict, Any
import re
from datetime import datetime, timedelta
from app.models.content_models import Page, AnalyticsLog
from app.schemas.content_schemas import PageCreate, PageUpdate, AnalyticsLogCreate
# Вспомогательная функция для генерации slug
def generate_slug(title: str) -> str:
# Преобразуем в нижний регистр
slug = title.lower()
# Заменяем пробелы на дефисы
slug = re.sub(r'\s+', '-', slug)
# Удаляем все символы, кроме букв, цифр и дефисов
slug = re.sub(r'[^a-z0-9-]', '', slug)
# Удаляем повторяющиеся дефисы
slug = re.sub(r'-+', '-', slug)
# Удаляем дефисы в начале и конце
slug = slug.strip('-')
return slug
# Функции для работы со страницами
def get_page(db: Session, page_id: int) -> Optional[Page]:
return db.query(Page).filter(Page.id == page_id).first()
def get_page_by_slug(db: Session, slug: str) -> Optional[Page]:
return db.query(Page).filter(Page.slug == slug).first()
def get_pages(
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()
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,
slug=page.slug,
content=page.content,
meta_title=page.meta_title,
meta_description=page.meta_description,
is_published=page.is_published
)
try:
db.add(db_page)
db.commit()
db.refresh(db_page)
return db_page
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при создании страницы"
)
def update_page(db: Session, page_id: int, page: PageUpdate) -> Page:
db_page = get_page(db, page_id)
if not db_page:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Страница не найдена"
)
# Обновляем только предоставленные поля
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"]):
raise HTTPException(
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"])
# Проверяем уникальность сгенерированного slug
if get_page_by_slug(db, update_data["slug"]) and get_page_by_slug(db, update_data["slug"]).id != page_id:
raise HTTPException(
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)
return db_page
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении страницы"
)
def delete_page(db: Session, page_id: int) -> bool:
db_page = get_page(db, page_id)
if not db_page:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Страница не найдена"
)
try:
db.delete(db_page)
db.commit()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении страницы"
)
# Функции для работы с аналитикой
def log_analytics_event(db: Session, log: AnalyticsLogCreate) -> AnalyticsLog:
# Создаем новую запись аналитики
db_log = AnalyticsLog(
user_id=log.user_id,
event_type=log.event_type,
page_url=log.page_url,
product_id=log.product_id,
category_id=log.category_id,
ip_address=log.ip_address,
user_agent=log.user_agent,
referrer=log.referrer,
additional_data=log.additional_data
)
try:
db.add(db_log)
db.commit()
db.refresh(db_log)
return db_log
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при логировании события"
)
def get_analytics_logs(
db: Session,
skip: int = 0,
limit: int = 100,
event_type: Optional[str] = None,
user_id: Optional[int] = None,
product_id: Optional[int] = None,
category_id: Optional[int] = None,
start_date: Optional[datetime] = None,
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,
period: str = "day",
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> Dict[str, Any]:
# Устанавливаем период по умолчанию, если не указан
if not start_date:
if period == "day":
start_date = datetime.utcnow() - timedelta(days=1)
elif period == "week":
start_date = datetime.utcnow() - timedelta(weeks=1)
elif period == "month":
start_date = datetime.utcnow() - timedelta(days=30)
elif period == "year":
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,
"start_date": start_date,
"end_date": end_date,
"total_visits": total_visits,
"unique_visitors": unique_visitors,
"page_views": page_views,
"product_views": product_views,
"cart_additions": cart_additions,
"orders_count": orders_count,
"revenue": revenue,
"average_order_value": average_order_value
}
return report

View File

@ -0,0 +1,533 @@
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException, status
from typing import List, Optional, Dict, Any
from datetime import datetime
from app.models.order_models import CartItem, Order, OrderItem, OrderStatus, PaymentMethod
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
# Функции для работы с корзиной
def get_cart_item(db: Session, cart_item_id: int) -> Optional[CartItem]:
return db.query(CartItem).filter(CartItem.id == cart_item_id).first()
def get_user_cart(db: Session, user_id: int) -> List[CartItem]:
return db.query(CartItem).filter(CartItem.user_id == user_id).all()
def get_cart_item_by_variant(db: Session, user_id: int, variant_id: int) -> Optional[CartItem]:
return db.query(CartItem).filter(
CartItem.user_id == user_id,
CartItem.variant_id == variant_id
).first()
def create_cart_item(db: Session, cart_item: CartItemCreate, user_id: int) -> CartItem:
# Проверяем, что вариант существует
variant = db.query(ProductVariant).filter(ProductVariant.id == cart_item.variant_id).first()
if not variant:
raise HTTPException(
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:
raise HTTPException(
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:
# Обновляем количество
existing_item.quantity += cart_item.quantity
try:
db.commit()
db.refresh(existing_item)
return existing_item
except Exception:
db.rollback()
raise HTTPException(
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()
db.refresh(db_cart_item)
return db_cart_item
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при добавлении товара в корзину"
)
def update_cart_item(db: Session, cart_item_id: int, cart_item: CartItemUpdate, user_id: int) -> CartItem:
db_cart_item = db.query(CartItem).filter(
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.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)
return db_cart_item
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении элемента корзины"
)
def delete_cart_item(db: Session, cart_item_id: int, user_id: int) -> bool:
db_cart_item = db.query(CartItem).filter(
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()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении элемента корзины"
)
def clear_cart(db: Session, user_id: int) -> bool:
try:
db.query(CartItem).filter(CartItem.user_id == user_id).delete()
db.commit()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при очистке корзины"
)
# Функции для работы с заказами
def get_order(db: Session, order_id: int) -> Optional[Order]:
return db.query(Order).filter(Order.id == order_id).first()
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,
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: int) -> 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:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Адрес доставки не найден или не принадлежит пользователю"
)
# Получаем элементы заказа
order_items = []
total_amount = 0
# Если указаны элементы корзины, используем их
if order.cart_items:
cart_items = db.query(CartItem).filter(
CartItem.id.in_(order.cart_items),
CartItem.user_id == user_id
).all()
if len(cart_items) != len(order.cart_items):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Некоторые элементы корзины не найдены или не принадлежат пользователю"
)
for cart_item in cart_items:
# Получаем вариант и продукт
variant = db.query(ProductVariant).filter(ProductVariant.id == cart_item.variant_id).first()
if not variant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Вариант продукта с ID {cart_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 {cart_item.variant_id} не найден"
)
# Рассчитываем цену
price = product.discount_price if product.discount_price else product.price
price += variant.price_adjustment
# Создаем элемент заказа
order_item = OrderItem(
variant_id=cart_item.variant_id,
quantity=cart_item.quantity,
price=price
)
order_items.append(order_item)
# Обновляем общую сумму
total_amount += price * cart_item.quantity
# Если указаны прямые элементы заказа, используем их
elif order.items:
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} не найден"
)
# Создаем элемент заказа
order_item = OrderItem(
variant_id=item.variant_id,
quantity=item.quantity,
price=item.price
)
order_items.append(order_item)
# Обновляем общую сумму
total_amount += item.price * item.quantity
else:
# Если не указаны ни элементы корзины, ни прямые элементы заказа, используем всю корзину пользователя
cart_items = get_user_cart(db, user_id)
if not cart_items:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Корзина пуста"
)
for cart_item in cart_items:
# Получаем вариант и продукт
variant = db.query(ProductVariant).filter(ProductVariant.id == cart_item.variant_id).first()
if not variant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Вариант продукта с ID {cart_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 {cart_item.variant_id} не найден"
)
# Рассчитываем цену
price = product.discount_price if product.discount_price else product.price
price += variant.price_adjustment
# Создаем элемент заказа
order_item = OrderItem(
variant_id=cart_item.variant_id,
quantity=cart_item.quantity,
price=price
)
order_items.append(order_item)
# Обновляем общую сумму
total_amount += price * cart_item.quantity
# Создаем заказ
db_order = Order(
user_id=user_id,
status=OrderStatus.PENDING,
total_amount=total_amount,
shipping_address_id=order.shipping_address_id,
payment_method=order.payment_method,
notes=order.notes
)
try:
# Добавляем заказ
db.add(db_order)
db.flush() # Получаем ID заказа, не фиксируя транзакцию
# Добавляем элементы заказа
for item in order_items:
item.order_id = db_order.id
db.add(item)
# Очищаем корзину, если заказ создан из корзины
if not order.items:
db.query(CartItem).filter(CartItem.user_id == user_id).delete()
db.commit()
db.refresh(db_order)
return db_order
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при создании заказа"
)
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 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="Адрес доставки не найден или не принадлежит пользователю"
)
# Применяем обновления
for key, value in update_data.items():
setattr(db_order, key, value)
try:
db.commit()
db.refresh(db_order)
return db_order
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении заказа"
)
def delete_order(db: Session, order_id: int, is_admin: bool = False) -> bool:
db_order = get_order(db, order_id)
if not db_order:
raise HTTPException(
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()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении заказа"
)
# Функции для получения детальной информации
def get_cart_with_product_details(db: Session, user_id: int) -> List[Dict[str, Any]]:
cart_items = get_user_cart(db, user_id)
result = []
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
# Получаем основное изображение продукта
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()
# Рассчитываем цену
price = product.discount_price if product.discount_price else product.price
price += variant.price_adjustment
# Формируем результат
result.append({
"id": item.id,
"user_id": item.user_id,
"variant_id": item.variant_id,
"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": image.image_url if image else None,
"variant_name": variant.name,
"variant_price_adjustment": variant.price_adjustment,
"total_price": price * item.quantity
})
return result
def get_order_with_details(db: Session, order_id: int) -> Dict[str, Any]:
order = get_order(db, order_id)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Заказ не найден"
)
# Получаем пользователя
user = db.query(User).filter(User.id == order.user_id).first()
# Получаем адрес доставки
shipping_address = None
if order.shipping_address_id:
address = db.query(UserAddress).filter(UserAddress.id == order.shipping_address_id).first()
if address:
shipping_address = {
"id": address.id,
"address_line1": address.address_line1,
"address_line2": address.address_line2,
"city": address.city,
"state": address.state,
"postal_code": address.postal_code,
"country": address.country
}
# Получаем элементы заказа с деталями
items = []
for item in order.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
# Формируем элемент заказа
items.append({
"id": item.id,
"order_id": item.order_id,
"variant_id": item.variant_id,
"quantity": item.quantity,
"price": item.price,
"created_at": item.created_at,
"product_id": product.id,
"product_name": product.name,
"variant_name": variant.name,
"total_price": item.price * item.quantity
})
# Формируем результат
result = {
"id": order.id,
"user_id": order.user_id,
"status": order.status,
"total_amount": order.total_amount,
"shipping_address_id": order.shipping_address_id,
"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

View File

@ -0,0 +1,264 @@
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException, status
from typing import List, Optional, Dict, Any
from app.models.review_models import Review
from app.models.user_models import User
from app.models.catalog_models import Product
from app.schemas.review_schemas import ReviewCreate, ReviewUpdate
def get_review(db: Session, review_id: int) -> Optional[Review]:
return db.query(Review).filter(Review.id == review_id).first()
def get_product_reviews(
db: Session,
product_id: int,
skip: int = 0,
limit: int = 100,
approved_only: bool = True
) -> List[Review]:
query = db.query(Review).filter(Review.product_id == product_id)
if approved_only:
query = query.filter(Review.is_approved == True)
return query.order_by(Review.created_at.desc()).offset(skip).limit(limit).all()
def get_user_reviews(db: Session, user_id: int, skip: int = 0, limit: int = 100) -> List[Review]:
return db.query(Review).filter(Review.user_id == user_id).order_by(Review.created_at.desc()).offset(skip).limit(limit).all()
def get_all_reviews(
db: Session,
skip: int = 0,
limit: int = 100,
approved_only: bool = False
) -> List[Review]:
query = db.query(Review)
if approved_only:
query = query.filter(Review.is_approved == True)
return query.order_by(Review.created_at.desc()).offset(skip).limit(limit).all()
def create_review(db: Session, review: ReviewCreate, user_id: int) -> Review:
# Проверяем, что продукт существует
product = db.query(Product).filter(Product.id == review.product_id).first()
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Продукт не найден"
)
# Проверяем, не оставлял ли пользователь уже отзыв на этот продукт
existing_review = db.query(Review).filter(
Review.user_id == user_id,
Review.product_id == review.product_id
).first()
if existing_review:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Вы уже оставили отзыв на этот продукт"
)
# Проверяем, покупал ли пользователь этот продукт (для verified_purchase)
# Это можно реализовать, проверив наличие завершенных заказов с этим продуктом
# Для простоты пока устанавливаем is_verified_purchase = False
is_verified_purchase = False
# Создаем отзыв
db_review = Review(
user_id=user_id,
product_id=review.product_id,
rating=review.rating,
title=review.title,
comment=review.comment,
is_verified_purchase=is_verified_purchase,
is_approved=False # По умолчанию отзыв требует одобрения
)
try:
db.add(db_review)
db.commit()
db.refresh(db_review)
return db_review
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при создании отзыва"
)
def update_review(db: Session, review_id: int, review: ReviewUpdate, user_id: int, is_admin: bool = False) -> Review:
db_review = get_review(db, review_id)
if not db_review:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Отзыв не найден"
)
# Проверяем права на редактирование отзыва
if not is_admin and db_review.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Недостаточно прав для редактирования этого отзыва"
)
# Обновляем только предоставленные поля
update_data = review.dict(exclude_unset=True)
# Обычные пользователи не могут менять статус одобрения
if not is_admin and "is_approved" in update_data:
del update_data["is_approved"]
# Применяем обновления
for key, value in update_data.items():
setattr(db_review, key, value)
try:
db.commit()
db.refresh(db_review)
return db_review
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении отзыва"
)
def delete_review(db: Session, review_id: int, user_id: int, is_admin: bool = False) -> bool:
db_review = get_review(db, review_id)
if not db_review:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Отзыв не найден"
)
# Проверяем права на удаление отзыва
if not is_admin and db_review.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Недостаточно прав для удаления этого отзыва"
)
try:
db.delete(db_review)
db.commit()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении отзыва"
)
def approve_review(db: Session, review_id: int) -> Review:
db_review = get_review(db, review_id)
if not db_review:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Отзыв не найден"
)
db_review.is_approved = True
try:
db.commit()
db.refresh(db_review)
return db_review
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при одобрении отзыва"
)
def get_review_with_user(db: Session, review_id: int) -> Dict[str, Any]:
review = get_review(db, review_id)
if not review:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Отзыв не найден"
)
# Получаем пользователя
user = db.query(User).filter(User.id == review.user_id).first()
return {
"id": review.id,
"user_id": review.user_id,
"product_id": review.product_id,
"rating": review.rating,
"title": review.title,
"comment": review.comment,
"is_verified_purchase": review.is_verified_purchase,
"is_approved": review.is_approved,
"created_at": review.created_at,
"updated_at": review.updated_at,
"user_username": user.username if user else "Неизвестный пользователь"
}
def get_product_rating(db: Session, product_id: int) -> Dict[str, Any]:
# Проверяем, что продукт существует
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Продукт не найден"
)
# Получаем все одобренные отзывы для продукта
reviews = db.query(Review).filter(
Review.product_id == product_id,
Review.is_approved == True
).all()
# Рассчитываем средний рейтинг и количество отзывов
total_reviews = len(reviews)
if total_reviews == 0:
return {
"product_id": product_id,
"average_rating": 0,
"total_reviews": 0,
"rating_distribution": {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0
}
}
# Рассчитываем распределение рейтингов
rating_distribution = {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0
}
total_rating = 0
for review in reviews:
total_rating += review.rating
rating_distribution[str(review.rating)] += 1
average_rating = total_rating / total_reviews
return {
"product_id": product_id,
"average_rating": round(average_rating, 1),
"total_reviews": total_reviews,
"rating_distribution": rating_distribution
}

View File

@ -0,0 +1,244 @@
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException, status
from typing import List, Optional
from app.models.user_models import User, UserAddress
from app.schemas.user_schemas import UserCreate, UserUpdate, AddressCreate, AddressUpdate
from app.core import get_password_hash, verify_password
# Функции для работы с пользователями
def get_user(db: Session, user_id: int) -> Optional[User]:
return db.query(User).filter(User.id == user_id).first()
def get_user_by_email(db: Session, email: str) -> Optional[User]:
return db.query(User).filter(User.email == email).first()
def get_user_by_username(db: Session, username: str) -> Optional[User]:
return db.query(User).filter(User.username == username).first()
def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[User]:
return db.query(User).offset(skip).limit(limit).all()
def create_user(db: Session, user: UserCreate) -> User:
# Проверяем, что пользователь с таким email или username не существует
if get_user_by_email(db, user.email):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Пользователь с таким email уже существует"
)
if get_user_by_username(db, user.username):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Пользователь с таким username уже существует"
)
# Создаем нового пользователя
hashed_password = get_password_hash(user.password)
db_user = User(
email=user.email,
username=user.username,
hashed_password=hashed_password,
is_active=user.is_active,
is_admin=user.is_admin
)
try:
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при создании пользователя"
)
def update_user(db: Session, user_id: int, user: UserUpdate) -> User:
db_user = get_user(db, user_id)
if not db_user:
raise HTTPException(
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["hashed_password"] = get_password_hash(update_data.pop("password"))
# Удаляем поле password_confirm, если оно есть
update_data.pop("password_confirm", None)
# Проверяем уникальность email и username, если они изменяются
if "email" in update_data and update_data["email"] != db_user.email:
if get_user_by_email(db, update_data["email"]):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Пользователь с таким email уже существует"
)
if "username" in update_data and update_data["username"] != db_user.username:
if get_user_by_username(db, update_data["username"]):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Пользователь с таким username уже существует"
)
# Применяем обновления
for key, value in update_data.items():
setattr(db_user, key, value)
try:
db.commit()
db.refresh(db_user)
return db_user
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении пользователя"
)
def delete_user(db: Session, user_id: int) -> bool:
db_user = get_user(db, user_id)
if not db_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
try:
db.delete(db_user)
db.commit()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении пользователя"
)
def authenticate_user(db: Session, username: str, password: str) -> Optional[User]:
user = get_user_by_username(db, username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
# Функции для работы с адресами пользователей
def get_address(db: Session, address_id: int) -> Optional[UserAddress]:
return db.query(UserAddress).filter(UserAddress.id == address_id).first()
def get_user_addresses(db: Session, user_id: int) -> List[UserAddress]:
return db.query(UserAddress).filter(UserAddress.user_id == user_id).all()
def create_address(db: Session, address: AddressCreate, user_id: int) -> UserAddress:
# Если новый адрес помечен как дефолтный, сбрасываем дефолтный статус у других адресов пользователя
if address.is_default:
db.query(UserAddress).filter(
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,
address_line2=address.address_line2,
city=address.city,
state=address.state,
postal_code=address.postal_code,
country=address.country,
is_default=address.is_default
)
try:
db.add(db_address)
db.commit()
db.refresh(db_address)
return db_address
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при создании адреса"
)
def update_address(db: Session, address_id: int, address: AddressUpdate, user_id: int) -> UserAddress:
db_address = db.query(UserAddress).filter(
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(
UserAddress.user_id == 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)
return db_address
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при обновлении адреса"
)
def delete_address(db: Session, address_id: int, user_id: int) -> bool:
db_address = db.query(UserAddress).filter(
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()
return True
except Exception:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ошибка при удалении адреса"
)

354
backend/app/routers.py Normal file
View File

@ -0,0 +1,354 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Query, Body, Request
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from typing import List, Optional, Dict, Any
from datetime import datetime
from app.core import get_db, get_current_user, get_current_active_user, get_current_admin_user
from app.services import (
register_user, login_user, get_user_profile, update_user_profile,
add_user_address, update_user_address, delete_user_address,
create_category, update_category, delete_category, get_category_tree,
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,
add_to_cart, update_cart_item, remove_from_cart, clear_cart, get_cart,
create_order, get_order, update_order, cancel_order,
create_review, update_review, delete_review, approve_review, get_product_reviews,
create_page, update_page, delete_page, get_page_by_slug,
log_event, get_analytics_report
)
from app.schemas.user_schemas import (
UserCreate, UserUpdate, User, AddressCreate, AddressUpdate, Address, Token
)
from app.schemas.catalog_schemas import (
CategoryCreate, CategoryUpdate, Category,
ProductCreate, ProductUpdate, Product,
ProductVariantCreate, ProductVariantUpdate, ProductVariant,
ProductImageCreate, ProductImageUpdate, ProductImage
)
from app.schemas.order_schemas import (
CartItemCreate, CartItemUpdate, CartItem, CartItemWithProduct,
OrderCreate, OrderUpdate, Order, OrderWithDetails
)
from app.schemas.review_schemas import (
ReviewCreate, ReviewUpdate, Review, ReviewWithUser
)
from app.schemas.content_schemas import (
PageCreate, PageUpdate, Page, AnalyticsLogCreate, AnalyticsLog, AnalyticsReport
)
from app.models.user_models import User as UserModel
# Создаем основной роутер
router = APIRouter()
# Роутеры для аутентификации и пользователей
auth_router = APIRouter(prefix="/auth", tags=["Аутентификация"])
@auth_router.post("/register", response_model=Dict[str, Any])
async def register(user: UserCreate, db: Session = Depends(get_db)):
return register_user(db, user)
@auth_router.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
result = login_user(db, form_data.username, form_data.password)
return result
user_router = APIRouter(prefix="/users", tags=["Пользователи"])
@user_router.get("/me", response_model=Dict[str, Any])
async def read_users_me(current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return get_user_profile(db, current_user.id)
@user_router.put("/me", response_model=Dict[str, Any])
async def update_user_me(user_data: UserUpdate, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return update_user_profile(db, current_user.id, user_data)
@user_router.post("/me/addresses", response_model=Dict[str, Any])
async def create_user_address(address: AddressCreate, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return add_user_address(db, current_user.id, address)
@user_router.put("/me/addresses/{address_id}", response_model=Dict[str, Any])
async def update_user_address_endpoint(address_id: int, address: AddressUpdate, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return update_user_address(db, current_user.id, address_id, address)
@user_router.delete("/me/addresses/{address_id}", response_model=Dict[str, Any])
async def delete_user_address_endpoint(address_id: int, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return delete_user_address(db, current_user.id, address_id)
@user_router.get("/{user_id}", response_model=Dict[str, Any])
async def read_user(user_id: int, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
return get_user_profile(db, user_id)
# Роутеры для каталога
catalog_router = APIRouter(prefix="/catalog", tags=["Каталог"])
@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)):
return create_category(db, category)
@catalog_router.put("/categories/{category_id}", response_model=Dict[str, Any])
async def update_category_endpoint(category_id: int, category: CategoryUpdate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
return update_category(db, category_id, category)
@catalog_router.delete("/categories/{category_id}", response_model=Dict[str, Any])
async def delete_category_endpoint(category_id: int, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
return delete_category(db, category_id)
@catalog_router.get("/categories", response_model=List[Dict[str, Any]])
async def get_categories_tree(db: Session = Depends(get_db)):
return get_category_tree(db)
@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 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 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)):
return 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)):
return 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)):
variant.product_id = product_id
return 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)):
return 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)):
return delete_product_variant(db, variant_id)
@catalog_router.post("/products/{product_id}/images", response_model=Dict[str, Any])
async def upload_product_image_endpoint(
product_id: int,
file: UploadFile = File(...),
is_primary: bool = Form(False),
current_user: UserModel = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
return 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)):
return update_product_image(db, image_id, image)
@catalog_router.delete("/images/{image_id}", response_model=Dict[str, Any])
async def delete_product_image_endpoint(image_id: int, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
return delete_product_image(db, image_id)
@catalog_router.get("/products", response_model=List[Product])
async def get_products(
skip: int = 0,
limit: int = 100,
category_id: Optional[int] = None,
search: Optional[str] = None,
min_price: Optional[float] = None,
max_price: Optional[float] = None,
is_active: Optional[bool] = True,
db: Session = Depends(get_db)
):
from app.repositories.catalog_repo import get_products
return get_products(db, skip, limit, category_id, search, min_price, max_price, is_active)
# Роутеры для корзины и заказов
cart_router = APIRouter(prefix="/cart", tags=["Корзина"])
@cart_router.post("/items", response_model=Dict[str, Any])
async def add_to_cart_endpoint(cart_item: CartItemCreate, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return add_to_cart(db, current_user.id, cart_item)
@cart_router.put("/items/{cart_item_id}", response_model=Dict[str, Any])
async def update_cart_item_endpoint(cart_item_id: int, cart_item: CartItemUpdate, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return 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)):
return 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)):
return 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)):
return get_cart(db, current_user.id)
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)):
return 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)):
return 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)):
return 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)):
return cancel_order(db, current_user.id, order_id)
@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)
):
if current_user.is_admin:
from app.repositories.order_repo import get_all_orders
return get_all_orders(db, skip, limit, status)
else:
from app.repositories.order_repo import get_user_orders
return get_user_orders(db, current_user.id, skip, limit)
# Роутеры для отзывов
review_router = APIRouter(prefix="/reviews", tags=["Отзывы"])
@review_router.post("/", response_model=Dict[str, Any])
async def create_review_endpoint(review: ReviewCreate, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return create_review(db, current_user.id, review)
@review_router.put("/{review_id}", response_model=Dict[str, Any])
async def update_review_endpoint(review_id: int, review: ReviewUpdate, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return update_review(db, current_user.id, review_id, review, current_user.is_admin)
@review_router.delete("/{review_id}", response_model=Dict[str, Any])
async def delete_review_endpoint(review_id: int, current_user: UserModel = Depends(get_current_active_user), db: Session = Depends(get_db)):
return delete_review(db, current_user.id, review_id, current_user.is_admin)
@review_router.post("/{review_id}/approve", response_model=Dict[str, Any])
async def approve_review_endpoint(review_id: int, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
return approve_review(db, review_id)
@review_router.get("/products/{product_id}", response_model=Dict[str, Any])
async def get_product_reviews_endpoint(product_id: int, skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
return get_product_reviews(db, product_id, skip, limit)
# Роутеры для информационных страниц
content_router = APIRouter(prefix="/content", tags=["Контент"])
@content_router.post("/pages", response_model=Dict[str, Any])
async def create_page_endpoint(page: PageCreate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
return create_page(db, page)
@content_router.put("/pages/{page_id}", response_model=Dict[str, Any])
async def update_page_endpoint(page_id: int, page: PageUpdate, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
return update_page(db, page_id, page)
@content_router.delete("/pages/{page_id}", response_model=Dict[str, Any])
async def delete_page_endpoint(page_id: int, current_user: UserModel = Depends(get_current_admin_user), db: Session = Depends(get_db)):
return delete_page(db, page_id)
@content_router.get("/pages/{slug}", response_model=Dict[str, Any])
async def get_page_endpoint(slug: str, db: Session = Depends(get_db)):
return get_page_by_slug(db, slug)
@content_router.get("/pages", response_model=List[Page])
async def get_pages(
skip: int = 0,
limit: int = 100,
published_only: bool = True,
current_user: Optional[UserModel] = Depends(get_current_user),
db: Session = Depends(get_db)
):
# Если пользователь не админ, показываем только опубликованные страницы
is_admin = current_user and current_user.is_admin
from app.repositories.content_repo import get_pages
return get_pages(db, skip, limit, published_only=(not is_admin) and published_only)
# Роутеры для аналитики
analytics_router = APIRouter(prefix="/analytics", tags=["Аналитика"])
@analytics_router.post("/events", response_model=Dict[str, Any])
async def log_event_endpoint(log: AnalyticsLogCreate, request: Request, db: Session = Depends(get_db)):
# Добавляем IP-адрес и User-Agent, если они не указаны
if not log.ip_address:
log.ip_address = request.client.host
if not log.user_agent:
user_agent = request.headers.get("user-agent")
if user_agent:
log.user_agent = user_agent
return log_event(db, log)
@analytics_router.get("/reports", response_model=Dict[str, Any])
async def get_analytics_report_endpoint(
period: str = "day",
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
current_user: UserModel = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
return get_analytics_report(db, period, start_date, end_date)
# Включаем все роутеры в основной роутер
router.include_router(auth_router)
router.include_router(user_router)
router.include_router(catalog_router)
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)

View File

View File

@ -0,0 +1,142 @@
from pydantic import BaseModel, Field, validator
from typing import Optional, List, Union
from datetime import datetime
# Схемы для категорий
class CategoryBase(BaseModel):
name: str
slug: Optional[str] = None
description: Optional[str] = None
parent_id: Optional[int] = None
is_active: bool = True
class CategoryCreate(CategoryBase):
pass
class CategoryUpdate(CategoryBase):
name: Optional[str] = None
slug: Optional[str] = None
description: Optional[str] = None
parent_id: Optional[int] = None
is_active: Optional[bool] = None
class Category(CategoryBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Схемы для продуктов
class ProductBase(BaseModel):
name: str
slug: Optional[str] = None
description: Optional[str] = None
price: float
discount_price: Optional[float] = None
stock: int = 0
is_active: bool = True
category_id: int
class ProductCreate(ProductBase):
pass
class ProductUpdate(ProductBase):
name: Optional[str] = None
slug: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
discount_price: Optional[float] = None
stock: Optional[int] = None
is_active: Optional[bool] = None
category_id: Optional[int] = None
class Product(ProductBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Схемы для вариантов продуктов
class ProductVariantBase(BaseModel):
product_id: int
name: str
sku: str
price_adjustment: float = 0.0
stock: int = 0
is_active: bool = True
class ProductVariantCreate(ProductVariantBase):
pass
class ProductVariantUpdate(ProductVariantBase):
product_id: Optional[int] = None
name: Optional[str] = None
sku: Optional[str] = None
price_adjustment: Optional[float] = None
stock: Optional[int] = None
is_active: Optional[bool] = None
class ProductVariant(ProductVariantBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Схемы для изображений продуктов
class ProductImageBase(BaseModel):
product_id: int
image_url: str
alt_text: Optional[str] = None
is_primary: bool = False
class ProductImageCreate(ProductImageBase):
pass
class ProductImageUpdate(BaseModel):
alt_text: Optional[str] = None
is_primary: Optional[bool] = None
class ProductImage(ProductImageBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Расширенные схемы для отображения
class CategoryWithSubcategories(Category):
subcategories: List['CategoryWithSubcategories'] = []
class ProductWithDetails(Product):
category: Category
variants: List[ProductVariant] = []
images: List[ProductImage] = []
# Рекурсивное обновление для CategoryWithChildren
CategoryWithSubcategories.update_forward_refs()

View File

@ -0,0 +1,75 @@
from pydantic import BaseModel
from typing import Optional, Dict, Any
from datetime import datetime
# Схемы для информационных страниц
class PageBase(BaseModel):
title: str
slug: str
content: str
meta_title: Optional[str] = None
meta_description: Optional[str] = None
is_published: bool = True
class PageCreate(PageBase):
pass
class PageUpdate(BaseModel):
title: Optional[str] = None
slug: Optional[str] = None
content: Optional[str] = None
meta_title: Optional[str] = None
meta_description: Optional[str] = None
is_published: Optional[bool] = None
class Page(PageBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
# Схемы для аналитики
class AnalyticsLogBase(BaseModel):
event_type: str
page_url: Optional[str] = None
product_id: Optional[int] = None
category_id: Optional[int] = None
ip_address: Optional[str] = None
user_agent: Optional[str] = None
referrer: Optional[str] = None
additional_data: Optional[Dict[str, Any]] = None
class AnalyticsLogCreate(AnalyticsLogBase):
user_id: Optional[int] = None
class AnalyticsLog(AnalyticsLogBase):
id: int
user_id: Optional[int] = None
created_at: datetime
class Config:
orm_mode = True
# Схемы для аналитических отчетов
class AnalyticsReport(BaseModel):
period: str # day, week, month, year
start_date: datetime
end_date: datetime
total_visits: int
unique_visitors: int
page_views: Dict[str, int]
product_views: Dict[str, int]
cart_additions: int
orders_count: int
revenue: float
average_order_value: float

View File

@ -0,0 +1,133 @@
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from datetime import datetime
from app.models.order_models import OrderStatus, PaymentMethod
# Схемы для элементов корзины
class CartItemBase(BaseModel):
variant_id: int
quantity: int = 1
class CartItemCreate(CartItemBase):
pass
class CartItemUpdate(BaseModel):
quantity: Optional[int] = None
class CartItem(CartItemBase):
id: int
user_id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class CartItemWithProduct(CartItem):
product_name: str
product_price: float
product_image: Optional[str] = None
variant_name: str
variant_price_adjustment: float
total_price: float
# Схемы для элементов заказа
class OrderItemBase(BaseModel):
variant_id: int
quantity: int = 1
price: float
class OrderItemCreate(OrderItemBase):
pass
class OrderItem(OrderItemBase):
id: int
order_id: int
created_at: datetime
class Config:
from_attributes = True
class OrderItemWithProduct(OrderItem):
product_name: str
variant_name: Optional[str] = None
# Схемы для заказов
class OrderBase(BaseModel):
shipping_address_id: Optional[int] = None
payment_method: Optional[PaymentMethod] = None
notes: Optional[str] = None
class OrderCreate(OrderBase):
cart_items: Optional[List[int]] = None # ID элементов корзины
items: Optional[List[OrderItemCreate]] = None # Прямые элементы заказа
class OrderUpdate(BaseModel):
status: Optional[OrderStatus] = None
shipping_address_id: Optional[int] = None
payment_method: Optional[PaymentMethod] = None
payment_details: Optional[str] = None
tracking_number: Optional[str] = None
notes: Optional[str] = None
class Order(OrderBase):
id: int
user_id: int
status: OrderStatus
total_amount: float
payment_details: Optional[str] = None
tracking_number: Optional[str] = None
created_at: datetime
updated_at: Optional[datetime] = None
items: List[OrderItem] = []
class Config:
from_attributes = True
# Расширенные схемы для отображения
class CartItemWithDetails(BaseModel):
id: int
user_id: int
variant_id: int
quantity: int
created_at: datetime
updated_at: Optional[datetime] = None
product_id: int
product_name: str
product_price: float
product_image: Optional[str] = None
variant_name: str
variant_price_adjustment: float
total_price: float
class OrderWithDetails(BaseModel):
id: int
user_id: int
status: OrderStatus
total_amount: float
shipping_address_id: Optional[int] = None
payment_method: Optional[PaymentMethod] = None
payment_details: Optional[str] = None
tracking_number: Optional[str] = None
notes: Optional[str] = None
created_at: datetime
updated_at: Optional[datetime] = None
user_email: Optional[str] = None
shipping_address: Optional[Dict[str, Any]] = None
items: List[Dict[str, Any]] = []

View File

@ -0,0 +1,37 @@
from pydantic import BaseModel, Field, validator
from typing import Optional
from datetime import datetime
class ReviewBase(BaseModel):
product_id: int
rating: int = Field(..., ge=1, le=5)
title: Optional[str] = None
comment: Optional[str] = None
class ReviewCreate(ReviewBase):
pass
class ReviewUpdate(BaseModel):
rating: Optional[int] = Field(None, ge=1, le=5)
title: Optional[str] = None
comment: Optional[str] = None
is_approved: Optional[bool] = None
class Review(ReviewBase):
id: int
user_id: int
is_verified_purchase: bool
is_approved: bool
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
class ReviewWithUser(Review):
user_username: str

View File

@ -0,0 +1,95 @@
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional, List
from datetime import datetime
# Базовые схемы для адреса
class AddressBase(BaseModel):
address_line1: str
address_line2: Optional[str] = None
city: str
state: str
postal_code: str
country: str
is_default: bool = False
class AddressCreate(AddressBase):
pass
class AddressUpdate(AddressBase):
address_line1: Optional[str] = None
city: Optional[str] = None
state: Optional[str] = None
postal_code: Optional[str] = None
country: Optional[str] = None
class Address(AddressBase):
id: int
user_id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
# Базовые схемы для пользователя
class UserBase(BaseModel):
email: EmailStr
username: str
is_active: bool = True
is_admin: bool = False
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
password_confirm: str
@validator('password_confirm')
def passwords_match(cls, v, values, **kwargs):
if 'password' in values and v != values['password']:
raise ValueError('Пароли не совпадают')
return v
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = None
is_active: Optional[bool] = None
is_admin: Optional[bool] = None
password: Optional[str] = Field(None, min_length=8)
password_confirm: Optional[str] = None
@validator('password_confirm')
def passwords_match(cls, v, values, **kwargs):
if 'password' in values and values['password'] is not None and v != values['password']:
raise ValueError('Пароли не совпадают')
return v
class User(UserBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
addresses: List[Address] = []
class Config:
orm_mode = True
class UserInDB(User):
hashed_password: str
# Схемы для аутентификации
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
user_id: Optional[int] = None

482
backend/app/services.py Normal file
View File

@ -0,0 +1,482 @@
from sqlalchemy.orm import Session
from fastapi import HTTPException, status, UploadFile
from typing import List, Optional, Dict, Any, Union
from datetime import datetime, timedelta
import os
import uuid
import shutil
from pathlib import Path
from app.config import settings
from app.core import create_access_token, get_password_hash, verify_password
from app.repositories import (
user_repo, catalog_repo, order_repo, review_repo, content_repo
)
from app.schemas.user_schemas import UserCreate, UserUpdate, AddressCreate, AddressUpdate, Token
from app.schemas.catalog_schemas import (
CategoryCreate, CategoryUpdate,
ProductCreate, ProductUpdate,
ProductVariantCreate, ProductVariantUpdate,
ProductImageCreate, ProductImageUpdate
)
from app.schemas.order_schemas import CartItemCreate, CartItemUpdate, OrderCreate, OrderUpdate
from app.schemas.review_schemas import ReviewCreate, ReviewUpdate
from app.schemas.content_schemas import PageCreate, PageUpdate, AnalyticsLogCreate
# Сервисы аутентификации и пользователей
def register_user(db: Session, user: UserCreate) -> Dict[str, Any]:
# Создаем пользователя
db_user = user_repo.create_user(db, user)
# Создаем токен доступа
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": db_user.username}, expires_delta=access_token_expires
)
return {
"user": db_user,
"access_token": access_token,
"token_type": "bearer"
}
def login_user(db: Session, username: str, password: str) -> Dict[str, Any]:
# Аутентифицируем пользователя
user = user_repo.authenticate_user(db, username, password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверное имя пользователя или пароль",
headers={"WWW-Authenticate": "Bearer"},
)
# Проверяем, что пользователь активен
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неактивный пользователь"
)
# Создаем токен доступа
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer"
}
def get_user_profile(db: Session, user_id: int) -> Dict[str, Any]:
user = user_repo.get_user(db, user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
# Получаем адреса пользователя
addresses = user_repo.get_user_addresses(db, user_id)
# Получаем заказы пользователя
orders = order_repo.get_user_orders(db, user_id)
# Получаем отзывы пользователя
reviews = review_repo.get_user_reviews(db, user_id)
return {
"user": user,
"addresses": addresses,
"orders": orders,
"reviews": reviews
}
def update_user_profile(db: Session, user_id: int, user_data: UserUpdate) -> Dict[str, Any]:
updated_user = user_repo.update_user(db, user_id, user_data)
return {"user": updated_user}
def add_user_address(db: Session, user_id: int, address: AddressCreate) -> Dict[str, Any]:
new_address = user_repo.create_address(db, address, user_id)
return {"address": new_address}
def update_user_address(db: Session, user_id: int, address_id: int, address: AddressUpdate) -> Dict[str, Any]:
updated_address = user_repo.update_address(db, address_id, address, user_id)
return {"address": updated_address}
def delete_user_address(db: Session, user_id: int, address_id: int) -> Dict[str, Any]:
success = user_repo.delete_address(db, address_id, user_id)
return {"success": success}
# Сервисы каталога
def create_category(db: Session, category: CategoryCreate) -> Dict[str, Any]:
new_category = catalog_repo.create_category(db, category)
return {"category": new_category}
def update_category(db: Session, category_id: int, category: CategoryUpdate) -> Dict[str, Any]:
updated_category = catalog_repo.update_category(db, category_id, category)
return {"category": updated_category}
def delete_category(db: Session, category_id: int) -> Dict[str, Any]:
success = catalog_repo.delete_category(db, category_id)
return {"success": success}
def get_category_tree(db: Session) -> List[Dict[str, Any]]:
# Получаем все категории верхнего уровня
root_categories = catalog_repo.get_categories(db, parent_id=None)
result = []
for category in root_categories:
# Рекурсивно получаем подкатегории
category_dict = {
"id": category.id,
"name": category.name,
"slug": category.slug,
"description": category.description,
"is_active": category.is_active,
"subcategories": _get_subcategories(db, category.id)
}
result.append(category_dict)
return result
def _get_subcategories(db: Session, parent_id: int) -> List[Dict[str, Any]]:
subcategories = catalog_repo.get_categories(db, parent_id=parent_id)
result = []
for category in subcategories:
category_dict = {
"id": category.id,
"name": category.name,
"slug": category.slug,
"description": category.description,
"is_active": category.is_active,
"subcategories": _get_subcategories(db, category.id)
}
result.append(category_dict)
return result
def create_product(db: Session, product: ProductCreate) -> Dict[str, Any]:
new_product = catalog_repo.create_product(db, product)
return {"product": new_product}
def update_product(db: Session, product_id: int, product: ProductUpdate) -> Dict[str, Any]:
updated_product = catalog_repo.update_product(db, product_id, product)
return {"product": updated_product}
def delete_product(db: Session, product_id: int) -> Dict[str, Any]:
success = catalog_repo.delete_product(db, product_id)
return {"success": success}
def get_product_details(db: Session, product_id: int) -> Dict[str, Any]:
product = catalog_repo.get_product(db, product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Продукт не найден"
)
# Получаем варианты продукта
variants = catalog_repo.get_product_variants(db, product_id)
# Получаем изображения продукта
images = catalog_repo.get_product_images(db, product_id)
# Получаем рейтинг продукта
rating = review_repo.get_product_rating(db, product_id)
# Получаем отзывы продукта
reviews = review_repo.get_product_reviews(db, product_id, limit=5)
return {
"product": product,
"variants": variants,
"images": images,
"rating": rating,
"reviews": reviews
}
def add_product_variant(db: Session, variant: ProductVariantCreate) -> Dict[str, Any]:
new_variant = catalog_repo.create_variant(db, variant)
return {"variant": new_variant}
def update_product_variant(db: Session, variant_id: int, variant: ProductVariantUpdate) -> Dict[str, Any]:
updated_variant = catalog_repo.update_variant(db, variant_id, variant)
return {"variant": updated_variant}
def delete_product_variant(db: Session, variant_id: int) -> Dict[str, Any]:
success = catalog_repo.delete_variant(db, variant_id)
return {"success": success}
def upload_product_image(db: Session, product_id: int, file: UploadFile, is_primary: bool = False) -> Dict[str, Any]:
# Проверяем, что продукт существует
product = catalog_repo.get_product(db, product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Продукт не найден"
)
# Проверяем расширение файла
file_extension = file.filename.split(".")[-1].lower()
if file_extension not in settings.ALLOWED_UPLOAD_EXTENSIONS:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Неподдерживаемый формат файла. Разрешены: {', '.join(settings.ALLOWED_UPLOAD_EXTENSIONS)}"
)
# Создаем директорию для загрузок, если она не существует
upload_dir = Path(settings.UPLOAD_DIRECTORY) / "products" / str(product_id)
upload_dir.mkdir(parents=True, exist_ok=True)
# Генерируем уникальное имя файла
unique_filename = f"{uuid.uuid4()}.{file_extension}"
file_path = upload_dir / unique_filename
# Сохраняем файл
with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# Создаем запись об изображении в БД
image_data = ProductImageCreate(
product_id=product_id,
image_url=f"/uploads/products/{product_id}/{unique_filename}",
alt_text=file.filename,
is_primary=is_primary
)
new_image = catalog_repo.create_image(db, image_data)
return {"image": new_image}
def update_product_image(db: Session, image_id: int, image: ProductImageUpdate) -> Dict[str, Any]:
updated_image = catalog_repo.update_image(db, image_id, image)
return {"image": updated_image}
def delete_product_image(db: Session, image_id: int) -> Dict[str, Any]:
# Получаем информацию об изображении перед удалением
image = catalog_repo.get_image(db, image_id)
if not image:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Изображение не найдено"
)
# Удаляем запись из БД
success = catalog_repo.delete_image(db, image_id)
# Удаляем файл с диска
if success:
try:
# Получаем путь к файлу из URL
file_path = Path(settings.UPLOAD_DIRECTORY) / image.image_url.lstrip("/uploads/")
if file_path.exists():
file_path.unlink()
except Exception:
# Если не удалось удалить файл, просто логируем ошибку
# В реальном приложении здесь должно быть логирование
pass
return {"success": success}
# Сервисы корзины и заказов
def add_to_cart(db: Session, user_id: int, cart_item: CartItemCreate) -> Dict[str, Any]:
new_cart_item = order_repo.create_cart_item(db, cart_item, user_id)
# Логируем событие добавления в корзину
log_data = AnalyticsLogCreate(
user_id=user_id,
event_type="add_to_cart",
product_id=cart_item.product_id,
additional_data={"quantity": cart_item.quantity}
)
content_repo.log_analytics_event(db, log_data)
return {"cart_item": new_cart_item}
def update_cart_item(db: Session, user_id: int, cart_item_id: int, cart_item: CartItemUpdate) -> Dict[str, Any]:
updated_cart_item = order_repo.update_cart_item(db, cart_item_id, cart_item, user_id)
return {"cart_item": updated_cart_item}
def remove_from_cart(db: Session, user_id: int, cart_item_id: int) -> Dict[str, Any]:
success = order_repo.delete_cart_item(db, cart_item_id, user_id)
return {"success": success}
def clear_cart(db: Session, user_id: int) -> Dict[str, Any]:
success = order_repo.clear_cart(db, user_id)
return {"success": success}
def get_cart(db: Session, user_id: int) -> Dict[str, Any]:
cart_items = order_repo.get_cart_with_product_details(db, user_id)
# Рассчитываем общую сумму корзины
total_amount = sum(item["total_price"] for item in cart_items)
return {
"items": cart_items,
"total_amount": total_amount,
"items_count": len(cart_items)
}
def create_order(db: Session, user_id: int, order: OrderCreate) -> Dict[str, Any]:
new_order = order_repo.create_order(db, order, user_id)
# Логируем событие создания заказа
log_data = AnalyticsLogCreate(
user_id=user_id,
event_type="order_created",
additional_data={"order_id": new_order.id, "total_amount": new_order.total_amount}
)
content_repo.log_analytics_event(db, log_data)
return {"order": new_order}
def get_order(db: Session, user_id: int, order_id: int, is_admin: bool = False) -> Dict[str, Any]:
# Получаем заказ с деталями
order_details = order_repo.get_order_with_details(db, order_id)
# Проверяем права доступа
if not is_admin and order_details["user_id"] != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Недостаточно прав для просмотра этого заказа"
)
return {"order": order_details}
def update_order(db: Session, user_id: int, order_id: int, order: OrderUpdate, is_admin: bool = False) -> Dict[str, Any]:
updated_order = order_repo.update_order(db, order_id, order, is_admin)
# Проверяем права доступа
if not is_admin and updated_order.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Недостаточно прав для обновления этого заказа"
)
return {"order": updated_order}
def cancel_order(db: Session, user_id: int, order_id: int) -> Dict[str, Any]:
# Отменяем заказ (обычный пользователь может только отменить заказ)
order_update = OrderUpdate(status="cancelled")
updated_order = order_repo.update_order(db, order_id, order_update, is_admin=False)
# Проверяем права доступа
if updated_order.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Недостаточно прав для отмены этого заказа"
)
return {"order": updated_order}
# Сервисы отзывов
def create_review(db: Session, user_id: int, review: ReviewCreate) -> Dict[str, Any]:
new_review = review_repo.create_review(db, review, user_id)
return {"review": new_review}
def update_review(db: Session, user_id: int, review_id: int, review: ReviewUpdate, is_admin: bool = False) -> Dict[str, Any]:
updated_review = review_repo.update_review(db, review_id, review, user_id, is_admin)
return {"review": updated_review}
def delete_review(db: Session, user_id: int, review_id: int, is_admin: bool = False) -> Dict[str, Any]:
success = review_repo.delete_review(db, review_id, user_id, is_admin)
return {"success": success}
def approve_review(db: Session, review_id: int) -> Dict[str, Any]:
approved_review = review_repo.approve_review(db, review_id)
return {"review": approved_review}
def get_product_reviews(db: Session, product_id: int, skip: int = 0, limit: int = 10) -> Dict[str, Any]:
reviews = review_repo.get_product_reviews(db, product_id, skip, limit)
# Получаем рейтинг продукта
rating = review_repo.get_product_rating(db, product_id)
return {
"reviews": reviews,
"rating": rating,
"total": rating["total_reviews"],
"skip": skip,
"limit": limit
}
# Сервисы информационных страниц
def create_page(db: Session, page: PageCreate) -> Dict[str, Any]:
new_page = content_repo.create_page(db, page)
return {"page": new_page}
def update_page(db: Session, page_id: int, page: PageUpdate) -> Dict[str, Any]:
updated_page = content_repo.update_page(db, page_id, page)
return {"page": updated_page}
def delete_page(db: Session, page_id: int) -> Dict[str, Any]:
success = content_repo.delete_page(db, page_id)
return {"success": success}
def get_page_by_slug(db: Session, slug: str) -> Dict[str, Any]:
page = content_repo.get_page_by_slug(db, slug)
if not page:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Страница не найдена"
)
return {"page": page}
# Сервисы аналитики
def log_event(db: Session, log: AnalyticsLogCreate) -> Dict[str, Any]:
new_log = content_repo.log_analytics_event(db, log)
return {"log": new_log}
def get_analytics_report(
db: Session,
period: str = "day",
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> Dict[str, Any]:
report = content_repo.get_analytics_report(db, period, start_date, end_date)
return {"report": report}

12
backend/requirements.txt Normal file
View File

@ -0,0 +1,12 @@
fastapi==0.95.1
uvicorn==0.22.0
sqlalchemy==2.0.12
pydantic==1.10.7
python-jose==3.3.0
passlib==1.7.4
python-multipart==0.0.6
email-validator==2.0.0
psycopg2-binary==2.9.6
alembic==1.10.4
python-dotenv==1.0.0
bcrypt==4.0.1

129
docker-compose.yml Normal file
View File

@ -0,0 +1,129 @@
version: '3.8'
services:
# backend:
# build:
# context: ./backend
# dockerfile: Dockerfile
# container_name: backend
# expose:
# - "8000"
# restart: always
# frontend:
# build:
# context: ./frontend
# dockerfile: Dockerfile
# container_name: frontend
# expose:
# - "3000"
# environment:
# - NODE_ENV=production
# restart: always
# networks:
# - sta_network
# nginx:
# image: nginx:latest
# container_name: nginx
# ports:
# - "80:80"
# volumes:
# - ./nginx/sta_test.conf:/etc/nginx/conf.d/sta_test.conf:ro
# depends_on:
# - backend
# - frontend
# restart: always
# networks:
# - sta_network
# backend:
# build:
# context: ./backend
# dockerfile: Dockerfile
# container_name: backend
# ports:
# - "8000:8000"
# expose:
# - "8000"
# environment:
# - DEBUG=1
# - DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5433/sta
# - IS_DOCKER=1
# depends_on:
# postgres:
# condition: service_healthy
# redis:
# condition: service_healthy
# networks:
# - sta_network
# # dns:
# # - 8.8.8.8
# # - 8.8.4.4
# # dns_opt:
# # - ndots:1
# # - timeout:3
# # - attempts:5
# # sysctls:
# # - net.ipv4.tcp_keepalive_time=60
# # - net.ipv4.tcp_keepalive_intvl=10
# # - net.ipv4.tcp_keepalive_probes=6
postgres:
image: postgres:15
environment:
POSTGRES_DB: shop_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5434:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
# redis:
# image: redis:7
# ports:
# - "6380:6379"
# healthcheck:
# test: ["CMD", "redis-cli", "ping"]
# interval: 5s
# timeout: 5s
# retries: 5
# networks:
# - sta_network
# elasticsearch:
# image: elasticsearch:8.17.2
# environment:
# - discovery.type=single-node
# - xpack.security.enabled=false
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
# ports:
# - "9200:9200"
# volumes:
# - elasticsearch_data:/usr/share/elasticsearch/data
# healthcheck:
# test: ["CMD", "curl", "-f", "http://localhost:9200"]
# interval: 10s
# timeout: 5s
# retries: 5
# networks:
# - sta_network
# networks:
# sta_network:
# name: sta_network
# driver: bridge
volumes:
postgres_data:
driver: local
# elasticsearch_data:
# driver: local

View File

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

Before

Width:  |  Height:  |  Size: 312 KiB

After

Width:  |  Height:  |  Size: 312 KiB

View File

Before

Width:  |  Height:  |  Size: 441 KiB

After

Width:  |  Height:  |  Size: 441 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Some files were not shown because too many files have changed in this diff Show More