Fix entry registration
This commit is contained in:
114
rog/management/commands/check_external_registrations.py
Normal file
114
rog/management/commands/check_external_registrations.py
Normal file
@ -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
|
||||||
84
rog/management/commands/retry_external_registrations.py
Normal file
84
rog/management/commands/retry_external_registrations.py
Normal file
@ -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}'
|
||||||
|
)
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
118
rog/models.py
118
rog/models.py
@ -9,6 +9,7 @@ from django.contrib.gis.db import models
|
|||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from django.conf import settings
|
||||||
try:
|
try:
|
||||||
from django.db.models import JSONField
|
from django.db.models import JSONField
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -2007,6 +2008,27 @@ class GpsLog(models.Model):
|
|||||||
score = models.IntegerField(default=0, null=True, blank=True)
|
score = models.IntegerField(default=0, null=True, blank=True)
|
||||||
scoreboard_url = models.URLField(blank=True, null=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:
|
class Meta:
|
||||||
db_table = 'gps_information'
|
db_table = 'gps_information'
|
||||||
# 複合主キーの設定
|
# 複合主キーの設定
|
||||||
@ -2084,6 +2106,102 @@ class GpsLog(models.Model):
|
|||||||
return self.create_at
|
return self.create_at
|
||||||
return None
|
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):
|
class Waypoint(models.Model):
|
||||||
|
|||||||
10
rog/views.py
10
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.owner = {team.owner}, event_name = {event_name}")
|
||||||
logger.info(f"team = {team}")
|
logger.info(f"team = {team}")
|
||||||
|
|
||||||
# 外部システムの更新
|
# 外部システムの更新を試行(失敗してもエントリー作成は継続)
|
||||||
success = self.register_team(
|
success = self.register_team(
|
||||||
entry.zekken_number,
|
entry.zekken_number,
|
||||||
event_name,
|
event_name,
|
||||||
@ -1703,8 +1703,12 @@ class EntryViewSet(viewsets.ModelViewSet):
|
|||||||
team.owner.password
|
team.owner.password
|
||||||
)
|
)
|
||||||
if not success:
|
if not success:
|
||||||
logger.error("Failed to register external system")
|
logger.warning("Failed to register external system, but entry was created successfully")
|
||||||
raise serializers.ValidationError("外部システムの更新に失敗しました。")
|
# 外部システム登録失敗を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")
|
logger.info("External system registered successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error creating Entry: {str(e)}")
|
logger.exception(f"Error creating Entry: {str(e)}")
|
||||||
|
|||||||
Reference in New Issue
Block a user