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