new #1
BIN
backend/.DS_Store
vendored
Normal file
BIN
backend/app/.DS_Store
vendored
Normal file
0
backend/app/__init__.py
Normal file
BIN
backend/app/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/app/__pycache__/config.cpython-310.pyc
Normal file
BIN
backend/app/__pycache__/core.cpython-310.pyc
Normal file
BIN
backend/app/__pycache__/main.cpython-310.pyc
Normal file
BIN
backend/app/__pycache__/routers.cpython-310.pyc
Normal file
BIN
backend/app/__pycache__/services.cpython-310.pyc
Normal file
53
backend/app/config.py
Normal 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
@ -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
@ -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"
|
||||
}
|
||||
0
backend/app/models/__init__.py
Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/app/models/__pycache__/catalog_models.cpython-310.pyc
Normal file
BIN
backend/app/models/__pycache__/content_models.cpython-310.pyc
Normal file
BIN
backend/app/models/__pycache__/order_models.cpython-310.pyc
Normal file
BIN
backend/app/models/__pycache__/review_models.cpython-310.pyc
Normal file
BIN
backend/app/models/__pycache__/user_models.cpython-310.pyc
Normal file
73
backend/app/models/catalog_models.py
Normal 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())
|
||||
40
backend/app/models/content_models.py
Normal 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")
|
||||
73
backend/app/models/order_models.py
Normal 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")
|
||||
24
backend/app/models/review_models.py
Normal 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")
|
||||
45
backend/app/models/user_models.py
Normal 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")
|
||||
0
backend/app/repositories/__init__.py
Normal file
BIN
backend/app/repositories/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/app/repositories/__pycache__/order_repo.cpython-310.pyc
Normal file
BIN
backend/app/repositories/__pycache__/review_repo.cpython-310.pyc
Normal file
BIN
backend/app/repositories/__pycache__/user_repo.cpython-310.pyc
Normal file
542
backend/app/repositories/catalog_repo.py
Normal 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="Ошибка при удалении изображения продукта"
|
||||
)
|
||||
285
backend/app/repositories/content_repo.py
Normal 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
|
||||
533
backend/app/repositories/order_repo.py
Normal 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
|
||||
264
backend/app/repositories/review_repo.py
Normal 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
|
||||
}
|
||||
244
backend/app/repositories/user_repo.py
Normal 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
@ -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)
|
||||
0
backend/app/schemas/__init__.py
Normal file
BIN
backend/app/schemas/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/app/schemas/__pycache__/catalog_schemas.cpython-310.pyc
Normal file
BIN
backend/app/schemas/__pycache__/content_schemas.cpython-310.pyc
Normal file
BIN
backend/app/schemas/__pycache__/order_schemas.cpython-310.pyc
Normal file
BIN
backend/app/schemas/__pycache__/review_schemas.cpython-310.pyc
Normal file
BIN
backend/app/schemas/__pycache__/user_schemas.cpython-310.pyc
Normal file
142
backend/app/schemas/catalog_schemas.py
Normal 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()
|
||||
75
backend/app/schemas/content_schemas.py
Normal 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
|
||||
133
backend/app/schemas/order_schemas.py
Normal 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]] = []
|
||||
37
backend/app/schemas/review_schemas.py
Normal 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
|
||||
95
backend/app/schemas/user_schemas.py
Normal 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
@ -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
@ -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
@ -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
|
||||
0
.gitignore → frontend/.gitignore
vendored
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 312 KiB After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 441 KiB After Width: | Height: | Size: 441 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.0 MiB |