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

View File

@@ -1,87 +0,0 @@
"""
Management команда для обработки существующих изображений товаров, комплектов и категорий.
Создает все необходимые размеры (thumbnail, medium, large) для уже загруженных изображений.
Использование:
python manage.py process_images
python manage.py process_images --model ProductPhoto
python manage.py process_images --model ProductKitPhoto
python manage.py process_images --model ProductCategoryPhoto
"""
from django.core.management.base import BaseCommand
from products.models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
from products.utils.image_processor import ImageProcessor
class Command(BaseCommand):
help = "Обработка существующих изображений и создание всех необходимых размеров"
def add_arguments(self, parser):
parser.add_argument(
'--model',
type=str,
default=None,
help='Какую модель обрабатывать (ProductPhoto, ProductKitPhoto, ProductCategoryPhoto)',
)
def handle(self, *args, **options):
model_name = options.get('model')
models_to_process = []
if not model_name:
# Обрабатываем все модели
models_to_process = [
('ProductPhoto', ProductPhoto, 'products'),
('ProductKitPhoto', ProductKitPhoto, 'kits'),
('ProductCategoryPhoto', ProductCategoryPhoto, 'categories'),
]
else:
# Обрабатываем конкретную модель
if model_name == 'ProductPhoto':
models_to_process = [('ProductPhoto', ProductPhoto, 'products')]
elif model_name == 'ProductKitPhoto':
models_to_process = [('ProductKitPhoto', ProductKitPhoto, 'kits')]
elif model_name == 'ProductCategoryPhoto':
models_to_process = [('ProductCategoryPhoto', ProductCategoryPhoto, 'categories')]
else:
self.stdout.write(
self.style.ERROR(f'Неизвестная модель: {model_name}')
)
return
total_processed = 0
total_errors = 0
for model_display_name, model_class, base_path in models_to_process:
self.stdout.write(f'\nОбработка {model_display_name}...')
self.stdout.write('-' * 50)
photos = model_class.objects.filter(image__isnull=False).exclude(image='')
if not photos.exists():
self.stdout.write(self.style.WARNING(f'Нет изображений для обработки в {model_display_name}'))
continue
count = photos.count()
self.stdout.write(f'Найдено изображений: {count}')
for i, photo in enumerate(photos, 1):
try:
# Сохраняем фото - это вызовет обработку в методе save()
photo.save()
total_processed += 1
self.stdout.write(
self.style.SUCCESS(f'✓ [{i}/{count}] {photo} - OK')
)
except Exception as e:
total_errors += 1
self.stdout.write(
self.style.ERROR(f'✗ [{i}/{count}] {photo} - ОШИБКА: {str(e)}')
)
self.stdout.write('\n' + '=' * 50)
self.stdout.write(self.style.SUCCESS(f'Обработано: {total_processed}'))
if total_errors:
self.stdout.write(self.style.ERROR(f'Ошибок: {total_errors}'))
self.stdout.write('=' * 50)