add: Django Custom Command (1) проверка и перегенерация offer_code (нужно при смене соли)
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
Django management команда для перегенерации и проверки s_offer_code офферов.
|
||||
|
||||
ВАЖНО: При смене OFFER_HASHIDS_SALT нужно обновить ВСЕ коды сразу!
|
||||
Иначе старые коды перестанут работать и QR-коды станут невалидными.
|
||||
|
||||
Примеры использования:
|
||||
# 1. Проверить все коды (декодирование и валидация)
|
||||
python manage.py regenerate_offer_codes --check
|
||||
|
||||
# 2. Проверить и показать некорректные коды
|
||||
python manage.py regenerate_offer_codes --check --verbose
|
||||
|
||||
# 3. Обновить только некорректные коды
|
||||
python manage.py regenerate_offer_codes --fix-broken
|
||||
|
||||
# 4. Обновить ВСЕ коды (при смене соли)
|
||||
python manage.py regenerate_offer_codes
|
||||
|
||||
# 5. Пробный запуск (ничего не сохранять)
|
||||
python manage.py regenerate_offer_codes --dry-run
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from hashids import Hashids
|
||||
from lpon_site.settings import OFFER_HASHIDS_SALT, OFFER_HASHIDS_MIN_LENGTH
|
||||
from frontend.models import TbOffer
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Перегенерирует, проверяет и восстанавливает s_offer_code офферов.
|
||||
"""
|
||||
help = 'Перегенерирует/проверяет s_offer_code для офферов на основе текущего OFFER_HASHIDS_SALT'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Добавляем опциональные аргументы"""
|
||||
parser.add_argument(
|
||||
'--check',
|
||||
action='store_true',
|
||||
help='Проверить корректность всех кодов (декодировать обратно в id)',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--fix-broken',
|
||||
action='store_true',
|
||||
help='Обновить только коды, которые не декодируются правильно',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Показать, что будет изменено, но не сохранять',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action='store_true',
|
||||
help='Показывать подробный прогресс для каждого оффера',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Основная логика команды"""
|
||||
check_mode = options['check']
|
||||
fix_broken = options['fix_broken']
|
||||
dry_run = options['dry_run']
|
||||
verbose = options['verbose']
|
||||
|
||||
# Инициализируем Hashids
|
||||
hashids = Hashids(salt=OFFER_HASHIDS_SALT, min_length=OFFER_HASHIDS_MIN_LENGTH)
|
||||
|
||||
# Получаем все офферы
|
||||
queryset = TbOffer.objects.all()
|
||||
total_count = queryset.count()
|
||||
|
||||
if total_count == 0:
|
||||
self.stdout.write(self.style.WARNING('Нет офферов для обработки'))
|
||||
return
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'Найдено офферов: {total_count}')
|
||||
)
|
||||
|
||||
# ===== РЕЖИМ 1: ПРОВЕРКА КОДОВ =====
|
||||
if check_mode:
|
||||
self._check_codes(queryset, hashids, verbose, total_count)
|
||||
return
|
||||
|
||||
# ===== РЕЖИМ 2: ОБНОВЛЕНИЕ ТОЛЬКО НЕКОРРЕКТНЫХ =====
|
||||
if fix_broken:
|
||||
self._fix_broken_codes(queryset, hashids, dry_run, verbose, total_count)
|
||||
return
|
||||
|
||||
# ===== РЕЖИМ 3: ОБНОВЛЕНИЕ ВСЕХ КОДОВ =====
|
||||
self._regenerate_all_codes(queryset, hashids, dry_run, verbose, total_count)
|
||||
|
||||
def _check_codes(self, queryset, hashids, verbose, total_count):
|
||||
"""
|
||||
Режим проверки: декодируем все коды обратно и проверяем, что получается исходный id.
|
||||
"""
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(self.style.SUCCESS('РЕЖИМ ПРОВЕРКИ КОДОВ'))
|
||||
self.stdout.write('=' * 70)
|
||||
|
||||
broken_count = 0
|
||||
valid_count = 0
|
||||
broken_offers = []
|
||||
|
||||
for idx, offer in enumerate(queryset.iterator(chunk_size=1000), 1):
|
||||
try:
|
||||
# Пытаемся декодировать код обратно в ID
|
||||
decoded_ids = hashids.decode(offer.s_offer_code)
|
||||
|
||||
if not decoded_ids or decoded_ids[0] != offer.id:
|
||||
# Код не декодируется или декодируется в неправильный ID
|
||||
broken_count += 1
|
||||
broken_offers.append({
|
||||
'id': offer.id,
|
||||
'code': offer.s_offer_code,
|
||||
'decoded': decoded_ids[0] if decoded_ids else None,
|
||||
'reason': 'Неверный ID' if decoded_ids else 'Не декодируется'
|
||||
})
|
||||
|
||||
if verbose:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f' [{idx}/{total_count}] ID={offer.id} [err] код "{offer.s_offer_code}" '
|
||||
f'декодируется в {decoded_ids[0] if decoded_ids else "ОШИБКА"}'
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Код корректный
|
||||
valid_count += 1
|
||||
if verbose:
|
||||
self.stdout.write(
|
||||
f' [{idx}/{total_count}] ID={offer.id} [ok!] код "{offer.s_offer_code}"'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
broken_count += 1
|
||||
broken_offers.append({
|
||||
'id': offer.id,
|
||||
'code': offer.s_offer_code,
|
||||
'error': str(e)
|
||||
})
|
||||
if verbose:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f' [{idx}/{total_count}] ID={offer.id} [err] код "{offer.s_offer_code}" ОШИБКА: {str(e)}'
|
||||
)
|
||||
)
|
||||
|
||||
# Показываем прогресс каждые 1000
|
||||
if idx % 1000 == 0:
|
||||
self.stdout.write(f'Проверено: {idx}/{total_count}')
|
||||
|
||||
# Отчет
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(self.style.SUCCESS('ИТОГОВЫЙ ОТЧЕТ ПРОВЕРКИ'))
|
||||
self.stdout.write('=' * 70)
|
||||
self.stdout.write(self.style.SUCCESS(f'[ok!] Корректных кодов: {valid_count}'))
|
||||
|
||||
if broken_count > 0:
|
||||
self.stdout.write(self.style.ERROR(f'[err] Некорректных кодов: {broken_count}'))
|
||||
|
||||
if broken_count <= 20:
|
||||
# Показываем все если мало
|
||||
self.stdout.write('\nДетали некорректных кодов:')
|
||||
for item in broken_offers:
|
||||
if 'error' in item:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f" ID={item['id']}, код='{item['code']}', ошибка: {item['error']}"
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f" ID={item['id']}, код='{item['code']}', "
|
||||
f"декодируется в {item['decoded']}, причина: {item['reason']}"
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(f'\nПервые 20 некорректных кодов:')
|
||||
for item in broken_offers[:20]:
|
||||
if 'error' in item:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f" ID={item['id']}, код='{item['code']}', ошибка: {item['error']}"
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f" ID={item['id']}, код='{item['code']}', "
|
||||
f"декодируется в {item['decoded']}, причина: {item['reason']}"
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS('Все коды корректны! [ok!]'))
|
||||
|
||||
self.stdout.write(f'\nОбщее количество: {valid_count + broken_count}')
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
'\nДля обновления некорректных кодов используйте: '
|
||||
'--fix-broken'
|
||||
)
|
||||
)
|
||||
|
||||
def _fix_broken_codes(self, queryset, hashids, dry_run, verbose, total_count):
|
||||
"""
|
||||
Режим исправления: обновляем только те коды которые не декодируются правильно.
|
||||
"""
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(self.style.SUCCESS('РЕЖИМ ИСПРАВЛЕНИЯ НЕКОРРЕКТНЫХ КОДОВ'))
|
||||
self.stdout.write('=' * 70)
|
||||
|
||||
updated = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('!! DRY-RUN: изменения НЕ будут сохранены !!'))
|
||||
|
||||
for idx, offer in enumerate(queryset.iterator(chunk_size=1000), 1):
|
||||
try:
|
||||
# Проверяем корректность текущего кода
|
||||
decoded_ids = hashids.decode(offer.s_offer_code)
|
||||
|
||||
if decoded_ids and decoded_ids[0] == offer.id:
|
||||
# Код корректный - пропускаем
|
||||
skipped += 1
|
||||
if verbose:
|
||||
self.stdout.write(f' [{idx}/{total_count}] ID={offer.id} [ok!] пропущен (корректный)')
|
||||
continue
|
||||
|
||||
# Код некорректный - обновляем
|
||||
new_code = hashids.encode(offer.id)
|
||||
|
||||
if verbose:
|
||||
self.stdout.write(
|
||||
f' [{idx}/{total_count}] ID={offer.id}: '
|
||||
f'"{offer.s_offer_code}" → "{new_code}"'
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
# Обновляем напрямую (без вызова save() чтобы не было валидации)
|
||||
TbOffer.objects.filter(id=offer.id).update(s_offer_code=new_code)
|
||||
updated += 1
|
||||
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f' [{idx}/{total_count}] ID={offer.id} [err] код "{offer.s_offer_code}" ОШИБКА: {str(e)}'
|
||||
)
|
||||
)
|
||||
|
||||
# Показываем прогресс каждые 1000
|
||||
if idx % 1000 == 0:
|
||||
self.stdout.write(f'Обработано: {idx}/{total_count}')
|
||||
|
||||
# Отчет
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(self.style.SUCCESS('ИТОГОВЫЙ ОТЧЕТ ИСПРАВЛЕНИЯ'))
|
||||
self.stdout.write('=' * 70)
|
||||
self.stdout.write(f'Пропущено (корректные): {skipped}')
|
||||
|
||||
if not dry_run:
|
||||
self.stdout.write(self.style.SUCCESS(f'Обновлено: {updated}'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(f'Было бы обновлено: {updated} (dry-run)'))
|
||||
|
||||
if failed > 0:
|
||||
self.stdout.write(self.style.ERROR(f'Ошибок: {failed}'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS('Ошибок: 0'))
|
||||
|
||||
self.stdout.write(f'\nОбщее количество обработано: {skipped + updated + failed}')
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('Изменения НЕ были сохранены (dry-run)'))
|
||||
|
||||
def _regenerate_all_codes(self, queryset, hashids, dry_run, verbose, total_count):
|
||||
"""
|
||||
Режим полного обновления: перегенерируем ВСЕ коды.
|
||||
ВНИМАНИЕ: Используется при смене OFFER_HASHIDS_SALT!
|
||||
"""
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(self.style.ERROR('РЕЖИМ ПОЛНОГО ОБНОВЛЕНИЯ ВСЕХ КОДОВ'))
|
||||
self.stdout.write('=' * 70)
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
'ВНИМАНИЕ! Это обновит ВСЕ коды. Старые коды и QR-коды перестанут работать!\n'
|
||||
'Используется только при смене OFFER_HASHIDS_SALT на сервере.'
|
||||
)
|
||||
)
|
||||
|
||||
updated = 0
|
||||
unchanged = 0
|
||||
failed = 0
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('!! DRY-RUN: изменения НЕ будут сохранены !!'))
|
||||
|
||||
for idx, offer in enumerate(queryset.iterator(chunk_size=1000), 1):
|
||||
try:
|
||||
# Генерируем новый код
|
||||
new_code = hashids.encode(offer.id)
|
||||
|
||||
# Проверяем изменился ли
|
||||
if offer.s_offer_code == new_code:
|
||||
unchanged += 1
|
||||
if verbose:
|
||||
self.stdout.write(f' [{idx}/{total_count}] ID={offer.id}: без изменений')
|
||||
else:
|
||||
if verbose:
|
||||
self.stdout.write(
|
||||
f' [{idx}/{total_count}] ID={offer.id}: '
|
||||
f'"{offer.s_offer_code}" → "{new_code}"'
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
# Обновляем напрямую в БД
|
||||
TbOffer.objects.filter(id=offer.id).update(s_offer_code=new_code)
|
||||
updated += 1
|
||||
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f' [{idx}/{total_count}] ID={offer.id} [err] код "{offer.s_offer_code}" ОШИБКА: {str(e)}'
|
||||
)
|
||||
)
|
||||
|
||||
# Показываем прогресс каждые 1000
|
||||
if idx % 1000 == 0:
|
||||
self.stdout.write(f'Обработано: {idx}/{total_count}')
|
||||
|
||||
# Отчет
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(self.style.SUCCESS('ИТОГОВЫЙ ОТЧЕТ ПОЛНОГО ОБНОВЛЕНИЯ'))
|
||||
self.stdout.write('=' * 70)
|
||||
self.stdout.write(f'Без изменений: {unchanged}')
|
||||
|
||||
if not dry_run:
|
||||
self.stdout.write(self.style.SUCCESS(f'Обновлено: {updated}'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(f'Было бы обновлено: {updated} (dry-run)'))
|
||||
|
||||
if failed > 0:
|
||||
self.stdout.write(self.style.ERROR(f'Ошибок: {failed}'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS('Ошибок: 0'))
|
||||
|
||||
# Параметры
|
||||
self.stdout.write(f'\nПараметры Hashids:')
|
||||
self.stdout.write(f' Соль: {OFFER_HASHIDS_SALT[:16]}...')
|
||||
self.stdout.write(f' Минимальная длина: {OFFER_HASHIDS_MIN_LENGTH}')
|
||||
|
||||
self.stdout.write(f'\nОбщее количество обработано: {unchanged + updated + failed}')
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('Изменения НЕ были сохранены (dry-run)'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS('Команда завершена успешно! ✓'))
|
||||
Reference in New Issue
Block a user