feat: Добавить систему мультитенантности с регистрацией магазинов

Реализована полноценная система мультитенантности на базе django-tenants.
Каждый магазин получает изолированную схему БД и поддомен.

Основные компоненты:

Django-tenants интеграция:
- Модели Client (тенант) и Domain в приложении tenants/
- Разделение на SHARED_APPS и TENANT_APPS
- Public schema для общей админки
- Tenant schemas для изолированных данных магазинов

Система регистрации магазинов:
- Публичная форма регистрации на /register/
- Модель TenantRegistration для заявок со статусами (pending/approved/rejected)
- Валидация schema_name (латиница, 3-63 символа, уникальность)
- Проверка на зарезервированные имена (admin, api, www и т.д.)
- Админ-панель для модерации заявок с кнопками активации/отклонения

Система подписок:
- Модель Subscription с планами (триал 90 дней, месяц, квартал, год)
- Автоматическое создание триальной подписки при активации
- Методы is_expired() и days_left() для проверки статуса
- Цветовая индикация в админке (зеленый/оранжевый/красный)

Приложения:
- tenants/ - управление тенантами, регистрация, подписки
- shops/ - точки магазинов/самовывоза (tenant app)
- Обновлены миграции для всех приложений

Утилиты:
- switch_to_tenant.py - переключение между схемами тенантов
- Обновлены image_processor и image_service

Конфигурация:
- urls_public.py - роуты для public schema (админка + регистрация)
- urls.py - роуты для tenant schemas (магазины)
- requirements.txt - добавлены django-tenants, django-environ, phonenumber-field

Документация:
- DJANGO_TENANTS_SETUP.md - настройка мультитенантности
- TENANT_REGISTRATION_GUIDE.md - руководство по регистрации
- QUICK_START.md - быстрый старт
- START_HERE.md - общая документация

Использование:
1. Пользователь: http://localhost:8000/register/ → заполняет форму
2. Админ: http://localhost:8000/admin/ → активирует заявку
3. Результат: http://{schema_name}.localhost:8000/ - готовый магазин

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-27 19:13:10 +03:00
parent 4b44624f86
commit 097d4ea304
43 changed files with 3186 additions and 553 deletions

142
myproject/shops/models.py Normal file
View File

@@ -0,0 +1,142 @@
from django.db import models
from phonenumber_field.modelfields import PhoneNumberField
class Shop(models.Model):
"""
Модель магазина/пункта самовывоза для цветочного магазина в Минске.
"""
name = models.CharField(
max_length=200,
verbose_name="Название магазина"
)
# Адрес магазина
street = models.CharField(
max_length=255,
verbose_name="Улица"
)
building_number = models.CharField(
max_length=20,
verbose_name="Номер здания"
)
district = models.CharField(
max_length=100,
blank=True,
null=True,
verbose_name="Район",
help_text="Район в Минске"
)
# Контактная информация
phone = PhoneNumberField(
verbose_name="Телефон",
help_text="Контактный телефон магазина"
)
email = models.EmailField(
blank=True,
null=True,
verbose_name="Email"
)
# Режим работы
opening_time = models.TimeField(
verbose_name="Время открытия",
help_text="Время начала работы магазина"
)
closing_time = models.TimeField(
verbose_name="Время закрытия",
help_text="Время окончания работы магазина"
)
working_days = models.CharField(
max_length=100,
default="Пн-Вс",
verbose_name="Рабочие дни",
help_text="Например: Пн-Пт, Пн-Вс, Пн-Сб"
)
# Статусы и настройки
is_active = models.BooleanField(
default=True,
verbose_name="Активен",
help_text="Работает ли магазин в данный момент"
)
is_pickup_point = models.BooleanField(
default=True,
verbose_name="Пункт самовывоза",
help_text="Доступен ли магазин для самовывоза заказов"
)
# Дополнительная информация
description = models.TextField(
blank=True,
null=True,
verbose_name="Описание",
help_text="Дополнительная информация о магазине"
)
delivery_instructions = models.TextField(
blank=True,
null=True,
verbose_name="Инструкции для клиентов",
help_text="Как найти магазин, где припарковаться и т.д."
)
# Координаты для карты (опционально)
latitude = models.DecimalField(
max_digits=9,
decimal_places=6,
null=True,
blank=True,
verbose_name="Широта",
help_text="Координаты для отображения на карте"
)
longitude = models.DecimalField(
max_digits=9,
decimal_places=6,
null=True,
blank=True,
verbose_name="Долгота",
help_text="Координаты для отображения на карте"
)
# Временные метки
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата создания"
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name="Дата обновления"
)
class Meta:
verbose_name = "Магазин"
verbose_name_plural = "Магазины"
indexes = [
models.Index(fields=['is_active']),
models.Index(fields=['is_pickup_point']),
models.Index(fields=['district']),
]
ordering = ['name']
def __str__(self):
return f"{self.name} ({self.full_address})"
@property
def full_address(self):
"""Полный адрес магазина"""
return f"{self.street}, {self.building_number}"
@property
def working_hours(self):
"""Форматированный режим работы"""
return f"{self.working_days}: {self.opening_time.strftime('%H:%M')} - {self.closing_time.strftime('%H:%M')}"