diff --git a/rog/management/commands/check_external_registrations.py b/rog/management/commands/check_external_registrations.py new file mode 100644 index 0000000..8b52ba8 --- /dev/null +++ b/rog/management/commands/check_external_registrations.py @@ -0,0 +1,114 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from rog.models import GpsLog + + +class Command(BaseCommand): + help = '外部システム登録の状況を確認します' + + def add_arguments(self, parser): + parser.add_argument( + '--event-code', + type=str, + help='特定のイベントコードでフィルタ' + ) + parser.add_argument( + '--status', + type=str, + choices=['pending', 'success', 'failed', 'retry'], + help='特定のステータスでフィルタ' + ) + parser.add_argument( + '--show-details', + action='store_true', + help='詳細情報を表示' + ) + + def handle(self, *args, **options): + event_code = options.get('event_code') + status = options.get('status') + show_details = options['show_details'] + + self.stdout.write( + self.style.SUCCESS('外部システム登録状況を確認しています...') + ) + + # フィルタ条件を構築 + queryset = GpsLog.objects.filter( + serial_number=-1, + cp_number="EXTERNAL_REG" + ) + + if event_code: + queryset = queryset.filter(event_code=event_code) + + if status: + queryset = queryset.filter(external_registration_status=status) + + # 統計情報 + total_count = queryset.count() + pending_count = queryset.filter(external_registration_status='pending').count() + success_count = queryset.filter(external_registration_status='success').count() + failed_count = queryset.filter(external_registration_status='failed').count() + retry_count = queryset.filter(external_registration_status='retry').count() + + self.stdout.write(f'\n=== 外部システム登録統計 ===') + self.stdout.write(f'合計記録数: {total_count}') + self.stdout.write(f'保留中: {pending_count}') + self.stdout.write(f'成功: {success_count}') + self.stdout.write(f'失敗: {failed_count}') + self.stdout.write(f'リトライ要求: {retry_count}') + + if show_details and queryset.exists(): + self.stdout.write(f'\n=== 詳細情報 ===') + for record in queryset.order_by('-create_at')[:20]: # 最新20件 + status_color = self.get_status_color(record.external_registration_status) + self.stdout.write( + f'[{record.create_at.strftime("%Y-%m-%d %H:%M")}] ' + f'ゼッケン: {record.zekken_number} | ' + f'イベント: {record.event_code} | ' + f'チーム: {record.team_name} | ' + f'ステータス: {status_color(record.external_registration_status)} | ' + f'試行回数: {record.external_registration_attempts}' + ) + if record.external_registration_error: + self.stdout.write(f' エラー: {record.external_registration_error[:100]}...') + + # リトライが必要な記録をハイライト + needs_retry = queryset.filter( + external_registration_status__in=['pending', 'retry'], + external_registration_attempts__lt=3 + ) + + if needs_retry.exists(): + self.stdout.write( + self.style.WARNING( + f'\n注意: {needs_retry.count()}件の記録がリトライを待機中です。' + ' `python manage.py retry_external_registrations` でリトライできます。' + ) + ) + + # 最大試行回数に達した記録をチェック + max_attempts_reached = queryset.filter( + external_registration_attempts__gte=3, + external_registration_status__in=['pending', 'failed', 'retry'] + ) + + if max_attempts_reached.exists(): + self.stdout.write( + self.style.ERROR( + f'\n警告: {max_attempts_reached.count()}件の記録が最大試行回数に達しています。' + ' 手動での確認が必要です。' + ) + ) + + def get_status_color(self, status): + """ステータスに応じた色付き表示を返す""" + if status == 'success': + return self.style.SUCCESS + elif status == 'failed': + return self.style.ERROR + elif status in ['pending', 'retry']: + return self.style.WARNING + else: + return lambda x: x diff --git a/rog/management/commands/retry_external_registrations.py b/rog/management/commands/retry_external_registrations.py new file mode 100644 index 0000000..2cda805 --- /dev/null +++ b/rog/management/commands/retry_external_registrations.py @@ -0,0 +1,84 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from rog.models import GpsLog + + +class Command(BaseCommand): + help = '失敗した外部システム登録をリトライします' + + def add_arguments(self, parser): + parser.add_argument( + '--max-attempts', + type=int, + default=3, + help='最大試行回数 (デフォルト: 3)' + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='実際の処理を実行せず、処理予定の記録のみを表示' + ) + + def handle(self, *args, **options): + max_attempts = options['max_attempts'] + dry_run = options['dry_run'] + + self.stdout.write( + self.style.SUCCESS(f'外部システム登録リトライ処理を開始します (最大試行回数: {max_attempts})') + ) + + # 保留中の外部システム登録を取得 + pending_records = GpsLog.get_pending_external_registrations().filter( + external_registration_attempts__lt=max_attempts + ) + + if not pending_records.exists(): + self.stdout.write( + self.style.WARNING('リトライが必要な記録が見つかりませんでした。') + ) + return + + self.stdout.write(f'リトライ対象の記録数: {pending_records.count()}') + + if dry_run: + self.stdout.write(self.style.WARNING('-- DRY RUN モード --')) + for record in pending_records: + self.stdout.write( + f' ゼッケン番号: {record.zekken_number}, ' + f'イベント: {record.event_code}, ' + f'チーム名: {record.team_name}, ' + f'試行回数: {record.external_registration_attempts}' + ) + return + + # 実際のリトライ処理 + result = GpsLog.retry_failed_external_registrations(max_attempts) + + self.stdout.write( + self.style.SUCCESS( + f'リトライ完了: 成功 {result["success_count"]}件, ' + f'失敗 {result["failed_count"]}件, ' + f'合計処理数 {result["total_processed"]}件' + ) + ) + + # 最大試行回数に達した記録があるかチェック + max_attempts_reached = GpsLog.get_pending_external_registrations().filter( + external_registration_attempts__gte=max_attempts + ) + + if max_attempts_reached.exists(): + self.stdout.write( + self.style.ERROR( + f'警告: {max_attempts_reached.count()}件の記録が最大試行回数に達しました。' + ' 手動での確認が必要です。' + ) + ) + + # 詳細を表示 + for record in max_attempts_reached[:5]: # 最初の5件のみ表示 + self.stdout.write( + f' ゼッケン番号: {record.zekken_number}, ' + f'イベント: {record.event_code}, ' + f'エラー: {record.external_registration_error}' + ) diff --git a/rog/migrations/0002_add_competition_status_fields.py b/rog/migrations/0002_add_competition_status_fields.py deleted file mode 100644 index e69de29..0000000 diff --git a/rog/migrations/0014_add_external_registration_fields_to_gpslog.py b/rog/migrations/0014_add_external_registration_fields_to_gpslog.py new file mode 100644 index 0000000..7378375 --- /dev/null +++ b/rog/migrations/0014_add_external_registration_fields_to_gpslog.py @@ -0,0 +1,58 @@ +# Generated manually on 2025-09-06 00:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rog', '0013_add_competition_status_fields'), + ] + + operations = [ + migrations.AddField( + model_name='gpslog', + name='external_registration_status', + field=models.CharField( + choices=[ + ('pending', 'Pending'), + ('success', 'Success'), + ('failed', 'Failed'), + ('retry', 'Retry Required') + ], + default='pending', + help_text='外部システム登録状況', + max_length=20 + ), + ), + migrations.AddField( + model_name='gpslog', + name='external_registration_attempts', + field=models.IntegerField(default=0, help_text='外部システム登録試行回数'), + ), + migrations.AddField( + model_name='gpslog', + name='external_registration_error', + field=models.TextField(blank=True, help_text='外部システム登録エラー詳細', null=True), + ), + migrations.AddField( + model_name='gpslog', + name='last_external_registration_attempt', + field=models.DateTimeField(blank=True, help_text='最後の外部システム登録試行時刻', null=True), + ), + migrations.AddField( + model_name='gpslog', + name='team_name', + field=models.CharField(blank=True, help_text='チーム名(外部システム登録用)', max_length=255, null=True), + ), + migrations.AddField( + model_name='gpslog', + name='category_name', + field=models.CharField(blank=True, help_text='カテゴリ名(外部システム登録用)', max_length=255, null=True), + ), + migrations.AddField( + model_name='gpslog', + name='user_password_hash', + field=models.TextField(blank=True, help_text='ユーザーパスワードハッシュ(外部システム登録用)', null=True), + ), + ] diff --git a/rog/models.py b/rog/models.py index 7245364..8e5c2ac 100755 --- a/rog/models.py +++ b/rog/models.py @@ -9,6 +9,7 @@ from django.contrib.gis.db import models from django.contrib.postgres.fields import ArrayField from django.utils import timezone from datetime import timedelta +from django.conf import settings try: from django.db.models import JSONField except ImportError: @@ -2006,6 +2007,27 @@ class GpsLog(models.Model): # ゴール記録用に追加 score = models.IntegerField(default=0, null=True, blank=True) scoreboard_url = models.URLField(blank=True, null=True) + + # 外部システム登録管理用フィールド + external_registration_status = models.CharField( + max_length=20, + choices=[ + ('pending', 'Pending'), + ('success', 'Success'), + ('failed', 'Failed'), + ('retry', 'Retry Required') + ], + default='pending', + help_text="外部システム登録状況" + ) + external_registration_attempts = models.IntegerField(default=0, help_text="外部システム登録試行回数") + external_registration_error = models.TextField(null=True, blank=True, help_text="外部システム登録エラー詳細") + last_external_registration_attempt = models.DateTimeField(null=True, blank=True, help_text="最後の外部システム登録試行時刻") + + # エントリー関連情報(外部システム登録用) + team_name = models.CharField(max_length=255, null=True, blank=True, help_text="チーム名(外部システム登録用)") + category_name = models.CharField(max_length=255, null=True, blank=True, help_text="カテゴリ名(外部システム登録用)") + user_password_hash = models.TextField(null=True, blank=True, help_text="ユーザーパスワードハッシュ(外部システム登録用)") class Meta: db_table = 'gps_information' @@ -2084,6 +2106,102 @@ class GpsLog(models.Model): return self.create_at return None + @classmethod + def record_external_registration_request(cls, entry): + """ + 外部システム登録要求を記録する + Entry作成時に外部システム登録が失敗した場合の情報を保存 + """ + return cls.objects.create( + serial_number=-1, # 外部システム登録要求を表す特別な値 + zekken_number=str(entry.zekken_number), + event_code=entry.event.event_name, + cp_number="EXTERNAL_REG", + team_name=entry.team.team_name, + category_name=entry.category.category_name, + user_password_hash=entry.team.owner.password, + external_registration_status='pending', + external_registration_attempts=0, + create_at=timezone.now(), + update_at=timezone.now(), + buy_flag=False, + colabo_company_memo="Entry registration - external system pending" + ) + + def update_external_registration_status(self, status, error_message=None): + """ + 外部システム登録状況を更新する + """ + self.external_registration_status = status + self.external_registration_attempts += 1 + self.last_external_registration_attempt = timezone.now() + + if error_message: + self.external_registration_error = error_message + + self.update_at = timezone.now() + self.save() + + def retry_external_registration(self): + """ + 外部システム登録をリトライする + """ + import requests + + api_url = f"{settings.FRONTEND_URL}/gifuroge/register_team" + headers = {"Content-Type": "application/x-www-form-urlencoded"} + data = { + "zekken_number": self.zekken_number, + "event_code": self.event_code, + "team_name": self.team_name, + "class_name": self.category_name, + "password": self.user_password_hash + } + + try: + response = requests.post(api_url, headers=headers, data=data, timeout=30) + response.raise_for_status() + self.update_external_registration_status('success') + return True + except requests.RequestException as e: + self.update_external_registration_status('failed', str(e)) + return False + + @classmethod + def get_pending_external_registrations(cls): + """ + 外部システム登録が保留中の記録を取得する + """ + return cls.objects.filter( + serial_number=-1, + cp_number="EXTERNAL_REG", + external_registration_status__in=['pending', 'retry'] + ).order_by('create_at') + + @classmethod + def retry_failed_external_registrations(cls, max_attempts=3): + """ + 失敗した外部システム登録を一括でリトライする + """ + pending_records = cls.get_pending_external_registrations().filter( + external_registration_attempts__lt=max_attempts + ) + + success_count = 0 + failed_count = 0 + + for record in pending_records: + if record.retry_external_registration(): + success_count += 1 + else: + failed_count += 1 + + return { + 'success_count': success_count, + 'failed_count': failed_count, + 'total_processed': success_count + failed_count + } + class Waypoint(models.Model): diff --git a/rog/views.py b/rog/views.py index cbbdd3b..1fcb28b 100755 --- a/rog/views.py +++ b/rog/views.py @@ -1694,7 +1694,7 @@ class EntryViewSet(viewsets.ModelViewSet): logger.info(f"team.owner = {team.owner}, event_name = {event_name}") logger.info(f"team = {team}") - # 外部システムの更新 + # 外部システムの更新を試行(失敗してもエントリー作成は継続) success = self.register_team( entry.zekken_number, event_name, @@ -1703,9 +1703,13 @@ class EntryViewSet(viewsets.ModelViewSet): team.owner.password ) if not success: - logger.error("Failed to register external system") - raise serializers.ValidationError("外部システムの更新に失敗しました。") - logger.info("External system registered successfully") + logger.warning("Failed to register external system, but entry was created successfully") + # 外部システム登録失敗をGpsLogに記録(後でリトライ可能) + from .models import GpsLog + GpsLog.record_external_registration_request(entry) + logger.info("External system registration request recorded in GpsLog for later retry") + else: + logger.info("External system registered successfully") except Exception as e: logger.exception(f"Error creating Entry: {str(e)}")