Реализована полноценная система мультитенантности на базе 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>
146 lines
7.1 KiB
Python
146 lines
7.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Management команда для ручной активации заявки на регистрацию тенанта.
|
||
|
||
Использование:
|
||
python manage.py activate_registration <schema_name>
|
||
|
||
Пример:
|
||
python manage.py activate_registration mixflowers
|
||
"""
|
||
from django.core.management.base import BaseCommand, CommandError
|
||
from django.db import transaction
|
||
from django.utils import timezone
|
||
from tenants.models import TenantRegistration, Client, Domain, Subscription
|
||
|
||
|
||
class Command(BaseCommand):
|
||
help = 'Активировать заявку на регистрацию тенанта вручную'
|
||
|
||
def add_arguments(self, parser):
|
||
parser.add_argument(
|
||
'schema_name',
|
||
type=str,
|
||
help='Schema name заявки для активации'
|
||
)
|
||
parser.add_argument(
|
||
'--force',
|
||
action='store_true',
|
||
help='Принудительная активация (пересоздать тенант если уже существует)'
|
||
)
|
||
|
||
def handle(self, *args, **options):
|
||
schema_name = options['schema_name']
|
||
force = options.get('force', False)
|
||
|
||
try:
|
||
# Ищем заявку
|
||
registration = TenantRegistration.objects.get(schema_name=schema_name)
|
||
except TenantRegistration.DoesNotExist:
|
||
raise CommandError(f'Заявка с schema_name "{schema_name}" не найдена')
|
||
|
||
self.stdout.write(f'Найдена заявка: {registration.shop_name} ({registration.schema_name})')
|
||
self.stdout.write(f'Статус: {registration.get_status_display()}')
|
||
self.stdout.write(f'Email: {registration.owner_email}')
|
||
self.stdout.write('')
|
||
|
||
# Проверяем статус
|
||
if registration.status == TenantRegistration.STATUS_APPROVED and not force:
|
||
self.stdout.write(self.style.WARNING('Эта заявка уже была одобрена!'))
|
||
if registration.tenant:
|
||
self.stdout.write(f'Связанный тенант: {registration.tenant.name} (ID: {registration.tenant.id})')
|
||
else:
|
||
self.stdout.write(self.style.WARNING('Но тенант не был создан. Используйте --force для пересоздания'))
|
||
if not self._confirm('Продолжить с --force?'):
|
||
return
|
||
force = True
|
||
|
||
# Проверяем существование тенанта
|
||
existing_client = Client.objects.filter(schema_name=schema_name).first()
|
||
if existing_client and not force:
|
||
raise CommandError(
|
||
f'Тенант с schema_name "{schema_name}" уже существует (ID: {existing_client.id}). '
|
||
'Используйте --force для пересоздания (ОПАСНО: удалит все данные!)'
|
||
)
|
||
|
||
# Активируем
|
||
self.stdout.write('')
|
||
self.stdout.write(self.style.WARNING('НАЧИНАЮ АКТИВАЦИЮ...'))
|
||
self.stdout.write('')
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
# Если force - удаляем старый тенант
|
||
if force and existing_client:
|
||
self.stdout.write(self.style.WARNING(f'Удаление существующего тенанта {existing_client.id}...'))
|
||
# Удаляем подписку
|
||
try:
|
||
existing_client.subscription.delete()
|
||
except:
|
||
pass
|
||
# Удаляем домены
|
||
Domain.objects.filter(tenant=existing_client).delete()
|
||
# Удаляем тенант (это также удалит схему из PostgreSQL)
|
||
existing_client.delete()
|
||
self.stdout.write(self.style.SUCCESS('Старый тенант удален'))
|
||
|
||
# Создаем тенант
|
||
self.stdout.write(f'Создание тенанта: {registration.schema_name}')
|
||
client = Client.objects.create(
|
||
schema_name=registration.schema_name,
|
||
name=registration.shop_name,
|
||
owner_email=registration.owner_email,
|
||
owner_name=registration.owner_name,
|
||
phone=registration.phone,
|
||
is_active=True
|
||
)
|
||
self.stdout.write(self.style.SUCCESS(f'✓ Тенант создан (ID: {client.id})'))
|
||
|
||
# Создаем домен
|
||
domain_name = f"{registration.schema_name}.localhost"
|
||
self.stdout.write(f'Создание домена: {domain_name}')
|
||
domain = Domain.objects.create(
|
||
domain=domain_name,
|
||
tenant=client,
|
||
is_primary=True
|
||
)
|
||
self.stdout.write(self.style.SUCCESS(f'✓ Домен создан (ID: {domain.id})'))
|
||
|
||
# Создаем триальную подписку
|
||
self.stdout.write('Создание триальной подписки на 90 дней')
|
||
subscription = Subscription.create_trial(client)
|
||
self.stdout.write(self.style.SUCCESS(
|
||
f'✓ Подписка создана (ID: {subscription.id}), '
|
||
f'истекает: {subscription.expires_at.strftime("%Y-%m-%d")}'
|
||
))
|
||
|
||
# Обновляем заявку
|
||
registration.status = TenantRegistration.STATUS_APPROVED
|
||
registration.processed_at = timezone.now()
|
||
registration.processed_by = None # Активировано через команду
|
||
registration.tenant = client
|
||
registration.save()
|
||
self.stdout.write(self.style.SUCCESS('✓ Заявка обновлена'))
|
||
|
||
self.stdout.write('')
|
||
self.stdout.write(self.style.SUCCESS('=' * 60))
|
||
self.stdout.write(self.style.SUCCESS('АКТИВАЦИЯ ЗАВЕРШЕНА УСПЕШНО!'))
|
||
self.stdout.write(self.style.SUCCESS('=' * 60))
|
||
self.stdout.write('')
|
||
self.stdout.write(f'Магазин: {client.name}')
|
||
self.stdout.write(f'Schema: {client.schema_name}')
|
||
self.stdout.write(f'Домен: http://{domain_name}:8000/')
|
||
self.stdout.write(f'Подписка до: {subscription.expires_at.strftime("%Y-%m-%d")} ({subscription.days_left()} дней)')
|
||
self.stdout.write('')
|
||
|
||
except Exception as e:
|
||
self.stdout.write(self.style.ERROR(f'ОШИБКА: {str(e)}'))
|
||
import traceback
|
||
self.stdout.write(traceback.format_exc())
|
||
raise CommandError('Активация не удалась')
|
||
|
||
def _confirm(self, question):
|
||
"""Запрос подтверждения у пользователя"""
|
||
answer = input(f'{question} (yes/no): ')
|
||
return answer.lower() in ['yes', 'y', 'да']
|