diff --git a/rog/migrations/0013_add_competition_status_fields.py b/rog/migrations/0013_add_competition_status_fields.py new file mode 100644 index 0000000..cc2c11b --- /dev/null +++ b/rog/migrations/0013_add_competition_status_fields.py @@ -0,0 +1,49 @@ +# Generated manually on 2025-09-04 for competition status management +# サーバーAPI変更要求書20250904.md対応 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rog', '0012_location2025_add_missing_fields'), + ] + + operations = [ + migrations.AddField( + model_name='entry', + name='is_in_rog', + field=models.BooleanField(default=False, help_text='ロゲイニング中=スタートしたらTrue'), + ), + migrations.AddField( + model_name='entry', + name='rogaining_counted', + field=models.BooleanField(default=False, help_text='ロゲイニングチェックイン履歴あり=一度でもスタート・ゴール以外でチェックインしたらTrue'), + ), + migrations.AddField( + model_name='entry', + name='ready_for_goal', + field=models.BooleanField(default=False, help_text='ゴール準備完了=スタートから遠くに移動した際にTrueになる'), + ), + migrations.AddField( + model_name='entry', + name='is_at_goal', + field=models.BooleanField(default=False, help_text='ゴール状態=ゴールしたらTrue'), + ), + migrations.AddField( + model_name='entry', + name='start_time', + field=models.DateTimeField(null=True, blank=True, help_text='スタート時刻'), + ), + migrations.AddField( + model_name='entry', + name='goal_time', + field=models.DateTimeField(null=True, blank=True, help_text='ゴール時刻'), + ), + migrations.AddField( + model_name='entry', + name='last_checkin_time', + field=models.DateTimeField(null=True, blank=True, help_text='最後のチェックイン時刻'), + ), + ] diff --git a/rog/models.py b/rog/models.py index 95997ef..7245364 100755 --- a/rog/models.py +++ b/rog/models.py @@ -750,6 +750,15 @@ class Entry(models.Model): staff_privileges = models.BooleanField(default=False, help_text="スタッフ権限フラグ") can_access_private_events = models.BooleanField(default=False, help_text="非公開イベント参加権限") + # API変更要求書対応: 競技状態管理 (2025-09-04) + is_in_rog = models.BooleanField(default=False, help_text='ロゲイニング中=スタートしたらTrue') + rogaining_counted = models.BooleanField(default=False, help_text='ロゲイニングチェックイン履歴あり=一度でもスタート・ゴール以外でチェックインしたらTrue') + ready_for_goal = models.BooleanField(default=False, help_text='ゴール準備完了=スタートから遠くに移動した際にTrueになる') + is_at_goal = models.BooleanField(default=False, help_text='ゴール状態=ゴールしたらTrue') + start_time = models.DateTimeField(null=True, blank=True, help_text='スタート時刻') + goal_time = models.DateTimeField(null=True, blank=True, help_text='ゴール時刻') + last_checkin_time = models.DateTimeField(null=True, blank=True, help_text='最後のチェックイン時刻') + VALIDATION_STATUS_CHOICES = [ ('approved', 'Approved'), ('pending', 'Pending'), diff --git a/rog/urls.py b/rog/urls.py index e9f1aee..113f9d0 100755 --- a/rog/urls.py +++ b/rog/urls.py @@ -21,6 +21,7 @@ from .views_apis.api_qr_points import submit_qr_points, qr_points_status from .views_apis.api_bulk_upload import bulk_upload_photos, confirm_checkin_validation from .views_apis.api_admin_validation import get_event_participants_ranking, get_participant_validation_details, get_event_zekken_list from .views_apis.api_simulator import rogaining_simulator +from .views_apis.api_competition_status import competition_status, update_competition_status, checkpoint_status from .views_apis.api_test import test_gifuroge,practice from .views_apis.api_supervisor import get_events_for_supervisor from django.views.decorators.csrf import csrf_exempt @@ -276,6 +277,11 @@ urlpatterns += [ path('submit_qr_points', submit_qr_points, name='submit_qr_points'), path('qr_points_status', qr_points_status, name='qr_points_status'), + # Competition Status Management API (Added 2025-09-04) + path('api/competition_status/', competition_status, name='competition_status'), + path('api/competition_status/update/', update_competition_status, name='update_competition_status'), + path('api/checkpoint_status/', checkpoint_status, name='checkpoint_status'), + ] if settings.DEBUG: diff --git a/rog/views_apis/api_competition_status.py b/rog/views_apis/api_competition_status.py new file mode 100644 index 0000000..9f86fd4 --- /dev/null +++ b/rog/views_apis/api_competition_status.py @@ -0,0 +1,307 @@ +""" +競技状態管理API +サーバーAPI変更要求書20250904.mdに基づく実装 + +Author: システム開発チーム +Date: 2025-09-04 +""" + +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework import status +from django.db import transaction +from django.utils import timezone +from rog.models import NewEvent2, Entry, Location2025, GpsLog +from django.contrib.gis.geos import Point +from django.contrib.gis.measure import D +import logging + +logger = logging.getLogger(__name__) + + +@api_view(['GET']) +def competition_status(request): + """ + 競技状態取得API + + パラメータ: + - event_code: イベントコード + - zekken_number: ゼッケン番号 + + レスポンス: + { + "status": "OK", + "data": { + "is_in_rog": true, + "rogaining_counted": false, + "ready_for_goal": false, + "is_at_goal": false, + "start_time": "2025-09-04T09:00:00+09:00", + "goal_time": null, + "last_checkin_time": "2025-09-04T09:30:00+09:00" + } + } + """ + logger.info("competition_status API called") + + event_code = request.query_params.get('event_code') + zekken_number = request.query_params.get('zekken_number') + + logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}") + + # パラメータ検証 + if not event_code or not zekken_number: + logger.warning("Missing required parameters") + return Response({ + "status": "ERROR", + "message": "イベントコードとゼッケン番号が必要です" + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # イベントの存在確認 + event = NewEvent2.objects.filter(event_name=event_code).first() + if not event: + logger.warning(f"Event not found: {event_code}") + return Response({ + "status": "ERROR", + "message": "指定されたイベントが見つかりません" + }, status=status.HTTP_404_NOT_FOUND) + + # エントリーの存在確認 + entry = Entry.objects.filter( + event=event, + zekken_number=zekken_number + ).first() + + if not entry: + logger.warning(f"Entry not found: zekken_number={zekken_number}, event={event_code}") + return Response({ + "status": "ERROR", + "message": "指定されたゼッケン番号のエントリーが見つかりません" + }, status=status.HTTP_404_NOT_FOUND) + + # 競技状態データを返す + data = { + "is_in_rog": entry.is_in_rog, + "rogaining_counted": entry.rogaining_counted, + "ready_for_goal": entry.ready_for_goal, + "is_at_goal": entry.is_at_goal, + "start_time": entry.start_time.strftime("%Y-%m-%dT%H:%M:%S%z") if entry.start_time else None, + "goal_time": entry.goal_time.strftime("%Y-%m-%dT%H:%M:%S%z") if entry.goal_time else None, + "last_checkin_time": entry.last_checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z") if entry.last_checkin_time else None + } + + logger.info(f"Competition status retrieved for zekken {zekken_number} in event {event_code}") + + return Response({ + "status": "OK", + "data": data + }) + + except Exception as e: + logger.error(f"Error in competition_status: {str(e)}") + return Response({ + "status": "ERROR", + "message": "サーバーエラーが発生しました" + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(['POST']) +def update_competition_status(request): + """ + 競技状態更新API + + パラメータ: + { + "event_code": "EVENT2025", + "zekken_number": "001", + "is_in_rog": true, + "rogaining_counted": false, + "ready_for_goal": false, + "is_at_goal": false + } + + レスポンス: + { + "status": "OK", + "message": "Competition status updated successfully", + "updated_at": "2025-09-04T10:00:00+09:00" + } + """ + logger.info("update_competition_status API called") + + event_code = request.data.get('event_code') + zekken_number = request.data.get('zekken_number') + is_in_rog = request.data.get('is_in_rog') + rogaining_counted = request.data.get('rogaining_counted') + ready_for_goal = request.data.get('ready_for_goal') + is_at_goal = request.data.get('is_at_goal') + + logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}") + + # パラメータ検証 + if not event_code or not zekken_number: + logger.warning("Missing required parameters") + return Response({ + "status": "ERROR", + "message": "イベントコードとゼッケン番号が必要です" + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # イベントの存在確認 + event = NewEvent2.objects.filter(event_name=event_code).first() + if not event: + logger.warning(f"Event not found: {event_code}") + return Response({ + "status": "ERROR", + "message": "指定されたイベントが見つかりません" + }, status=status.HTTP_404_NOT_FOUND) + + # エントリーの存在確認 + entry = Entry.objects.filter( + event=event, + zekken_number=zekken_number + ).first() + + if not entry: + logger.warning(f"Entry not found: zekken_number={zekken_number}, event={event_code}") + return Response({ + "status": "ERROR", + "message": "指定されたゼッケン番号のエントリーが見つかりません" + }, status=status.HTTP_404_NOT_FOUND) + + # トランザクション内で状態更新 + with transaction.atomic(): + # 状態の更新(Noneでない値のみ更新) + if is_in_rog is not None: + entry.is_in_rog = is_in_rog + if rogaining_counted is not None: + entry.rogaining_counted = rogaining_counted + if ready_for_goal is not None: + entry.ready_for_goal = ready_for_goal + if is_at_goal is not None: + entry.is_at_goal = is_at_goal + + entry.save() + + update_time = timezone.now() + + logger.info(f"Competition status updated for zekken {zekken_number} in event {event_code}") + + return Response({ + "status": "OK", + "message": "Competition status updated successfully", + "updated_at": update_time.strftime("%Y-%m-%dT%H:%M:%S%z") + }) + + except Exception as e: + logger.error(f"Error in update_competition_status: {str(e)}") + return Response({ + "status": "ERROR", + "message": "サーバーエラーが発生しました" + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(['GET']) +def checkpoint_status(request): + """ + チェックポイント状態取得API + + パラメータ: + - event_code: イベントコード + - zekken_number: ゼッケン番号 + - cp_number: チェックポイント番号 + + レスポンス: + { + "status": "OK", + "data": { + "cp_number": -2, + "is_checked_in": true, + "checkin_time": "2025-09-04T09:00:00+09:00", + "status": "競技中", // "未", "競技中", "競技終了" + "points_earned": 0 + } + } + """ + logger.info("checkpoint_status API called") + + event_code = request.query_params.get('event_code') + zekken_number = request.query_params.get('zekken_number') + cp_number = request.query_params.get('cp_number') + + logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}, cp_number={cp_number}") + + # パラメータ検証 + if not all([event_code, zekken_number, cp_number]): + logger.warning("Missing required parameters") + return Response({ + "status": "ERROR", + "message": "イベントコード、ゼッケン番号、チェックポイント番号が必要です" + }, status=status.HTTP_400_BAD_REQUEST) + + try: + # イベントの存在確認 + event = NewEvent2.objects.filter(event_name=event_code).first() + if not event: + logger.warning(f"Event not found: {event_code}") + return Response({ + "status": "ERROR", + "message": "指定されたイベントが見つかりません" + }, status=status.HTTP_404_NOT_FOUND) + + # エントリーの存在確認 + entry = Entry.objects.filter( + event=event, + zekken_number=zekken_number + ).first() + + if not entry: + logger.warning(f"Entry not found: zekken_number={zekken_number}, event={event_code}") + return Response({ + "status": "ERROR", + "message": "指定されたゼッケン番号のエントリーが見つかりません" + }, status=status.HTTP_404_NOT_FOUND) + + # チェックイン状況の確認 + checkin_record = GpsLog.objects.filter( + zekken_number=zekken_number, + event_code=event_code, + cp_number=cp_number + ).first() + + # チェックポイント情報の取得 + checkpoint = Location2025.objects.filter( + event=event, + cp_number=cp_number + ).first() + + # 競技状況の判定 + competition_status = "未" + if entry.is_at_goal: + competition_status = "競技終了" + elif entry.is_in_rog: + competition_status = "競技中" + + # レスポンスデータ構築 + data = { + "cp_number": int(cp_number), + "is_checked_in": bool(checkin_record), + "checkin_time": checkin_record.checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z") if checkin_record else None, + "status": competition_status, + "points_earned": checkpoint.point if checkpoint else 0 + } + + logger.info(f"Checkpoint status retrieved for CP {cp_number}, zekken {zekken_number} in event {event_code}") + + return Response({ + "status": "OK", + "data": data + }) + + except Exception as e: + logger.error(f"Error in checkpoint_status: {str(e)}") + return Response({ + "status": "ERROR", + "message": "サーバーエラーが発生しました" + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/rog/views_apis/api_edit.py b/rog/views_apis/api_edit.py index fe0a939..e30bb9d 100755 --- a/rog/views_apis/api_edit.py +++ b/rog/views_apis/api_edit.py @@ -5,7 +5,7 @@ from django.utils import timezone from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework import status -from rog.models import Location2025, NewEvent2, Entry, GpsLog +from rog.models import Location2025, NewEvent2, Entry, GpsLog, GpsCheckin import logging import uuid import os @@ -1089,18 +1089,19 @@ def get_checkin_list(request): logger.info("get_checkin_list called") # リクエストからパラメータを取得(GET/POSTの両方に対応) + # 両方のパラメータ名に対応 if request.method == 'GET': - zekken_number = request.GET.get('zekken') - event_code = request.GET.get('event') + zekken_number = request.GET.get('zekken_number') or request.GET.get('zekken') + event_code = request.GET.get('event_code') or request.GET.get('event') else: # POST try: data = json.loads(request.body) - zekken_number = data.get('zekken') - event_code = data.get('event') + zekken_number = data.get('zekken_number') or data.get('zekken') + event_code = data.get('event_code') or data.get('event') except: data = request.POST - zekken_number = data.get('zekken') - event_code = data.get('event') + zekken_number = data.get('zekken_number') or data.get('zekken') + event_code = data.get('event_code') or data.get('event') logger.info(f"[GET_CHECKIN_LIST] Request method: {request.method}") logger.info(f"[GET_CHECKIN_LIST] Parameters received - zekken: {zekken_number}, event: {event_code}") @@ -1134,24 +1135,83 @@ def get_checkin_list(request): }, status=status.HTTP_404_NOT_FOUND) # チームの存在確認 + logger.info(f"[GET_CHECKIN_LIST] Searching for event: {event.event_name} (ID: {event.id})") + logger.info(f"[GET_CHECKIN_LIST] Searching for zekken_number: {zekken_number}") + + # まず、このイベントのすべてのEntryを確認 + all_entries = Entry.objects.filter(event=event) + logger.info(f"[GET_CHECKIN_LIST] Total entries in event: {all_entries.count()}") + + # ゼッケン番号でのチーム検索(複数の方法で試す) entry = Entry.objects.filter( event=event, - team__zekken_number=zekken_number + zekken_number=zekken_number ).first() + if not entry: + # team__zekken_numberでも試してみる + entry = Entry.objects.filter( + event=event, + team__zekken_number=zekken_number + ).first() + if not entry: logger.warning(f"Team with zekken number {zekken_number} not found in event: {event_code}") + # デバッグ用:存在するゼッケン番号を表示 + existing_zekkens = list(Entry.objects.filter(event=event).values_list('zekken_number', flat=True)) + logger.info(f"[GET_CHECKIN_LIST] Existing zekken numbers: {existing_zekkens[:10]}") # 最初の10件のみ return Response({ "status": "ERROR", "message": "指定されたゼッケン番号のチームが見つかりません" }, status=status.HTTP_404_NOT_FOUND) # チェックイン記録を取得 - checkpoints = GpsLog.objects.filter( - zekken_number=entry.team.zekken_number, + logger.info(f"[GET_CHECKIN_LIST] Found entry: {entry.team.team_name} (zekken: {entry.zekken_number})") + + # 複数のテーブルからチェックイン記録を取得 + # GpsLogテーブルから + gps_checkpoints = GpsLog.objects.filter( + zekken_number=str(entry.zekken_number), event_code=event_code ).order_by('checkin_time') + # GpsCheckinテーブルからも取得 + gps_checkins = GpsCheckin.objects.filter( + zekken=str(entry.zekken_number), + event_code=event_code + ).order_by('checkin_time') + + logger.info(f"[GET_CHECKIN_LIST] Found {gps_checkpoints.count()} records in GpsLog") + logger.info(f"[GET_CHECKIN_LIST] Found {gps_checkins.count()} records in GpsCheckin") + + # すべてのチェックイン記録を統合 + all_checkpoints = [] + + # GpsLogからの記録を追加 + for cp in gps_checkpoints: + all_checkpoints.append({ + 'source': 'GpsLog', + 'id': cp.id, + 'cp_number': cp.cp_number, + 'checkin_time': cp.checkin_time, + 'image_address': getattr(cp, 'image_address', None), + 'is_service_checked': getattr(cp, 'is_service_checked', False) + }) + + # GpsCheckinからの記録を追加 + for cp in gps_checkins: + all_checkpoints.append({ + 'source': 'GpsCheckin', + 'id': cp.id, + 'cp_number': cp.cp_number, + 'checkin_time': cp.checkin_time, + 'image_address': None, # GpsCheckinには画像URLがない + 'is_service_checked': False + }) + + # 時間順にソート + all_checkpoints.sort(key=lambda x: x['checkin_time'] or timezone.now()) + # スタート情報を取得 start_info = None if hasattr(entry, 'start_info'): @@ -1170,20 +1230,21 @@ def get_checkin_list(request): # チェックイン記録をシリアライズ checkpoint_list = [] - for cp in checkpoints: + for cp in all_checkpoints: checkpoint_data = { - "id": cp.id, - "cp_number": cp.cp_number, - "checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None, - "image_url": cp.image_address, - "is_service_checked": cp.is_service_checked if hasattr(cp, 'is_service_checked') else False + "id": cp['id'], + "cp_number": cp['cp_number'], + "checkin_time": cp['checkin_time'].strftime("%Y-%m-%d %H:%M:%S") if cp['checkin_time'] else None, + "image_url": cp['image_address'], + "is_service_checked": cp['is_service_checked'], + "source": cp['source'] } # チェックポイントの得点情報を取得( Location2025 モデルがある場合) try: event_cp = Location2025.objects.filter( event_id=event.id, - cp_number=cp.cp_number + cp_number=cp['cp_number'] ).first() if event_cp: diff --git a/rog/views_apis/api_play.py b/rog/views_apis/api_play.py index 84c74d7..3dc8645 100755 --- a/rog/views_apis/api_play.py +++ b/rog/views_apis/api_play.py @@ -361,19 +361,27 @@ def start_from_rogapp(request): with transaction.atomic(): # スタート情報をGpsLogとして登録 logger.info(f"[START_API] Creating start record - ID: {request_id}") + start_time = timezone.now() start_info = GpsLog.objects.create( zekken_number=entry.zekken_number, event_code=event.event_name, cp_number="START", serial_number=0, - checkin_time=timezone.now(), - create_at=timezone.now(), - update_at=timezone.now(), + checkin_time=start_time, + create_at=start_time, + update_at=start_time, buy_flag=False, colabo_company_memo="" ) + # 競技状態を更新 + entry.is_in_rog = True + entry.start_time = start_time + entry.last_checkin_time = start_time + entry.save() + logger.info(f"[START_API] ✅ Start record created - ID: {request_id}, GpsLog ID: {start_info.id}") + logger.info(f"[START_API] ✅ Competition status updated - is_in_rog: True") # 統計情報取得 try: @@ -387,9 +395,20 @@ def start_from_rogapp(request): response_data = { "status": "OK", "message": "スタート処理が完了しました", + "competition_status": { + "is_in_rog": entry.is_in_rog, + "rogaining_counted": entry.rogaining_counted, + "ready_for_goal": entry.ready_for_goal, + "is_at_goal": entry.is_at_goal, + "start_time": start_info.checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z") + }, + "checkin_record": { + "id": start_info.id, + "cp_number": "START", + "checkin_time": start_info.checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z") + }, "team_name": team_name, "event_code": event_code, - "start_time": start_info.checkin_time.strftime("%Y-%m-%d %H:%M:%S"), "zekken_number": entry.zekken_number, "entry_id": entry.id } @@ -659,6 +678,13 @@ def checkin_from_rogapp(request): logger.info(f"[CHECKIN] ✅ SUCCESS - Team: {team_name}, Zekken: {entry.zekken_number}, CP: {cp_number}, Points: {point_value}, Bonus: {bonus_points}, Time: {checkpoint.checkin_time}, Has Image: {bool(image_url)}, Buy Flag: {buy_flag}, Client IP: {client_ip}, User: {user_info}") + # 競技状態を更新(スタート・ゴール以外のチェックイン時) + if cp_number not in ["START", "GOAL", -2, -1]: + entry.rogaining_counted = True + entry.last_checkin_time = checkpoint.checkin_time + entry.save() + logger.info(f"[CHECKIN] ✅ Competition status updated - rogaining_counted: True") + # 拡張情報があれば保存 if gps_coordinates or camera_metadata: try: @@ -689,7 +715,13 @@ def checkin_from_rogapp(request): "scoring_breakdown": scoring_breakdown, "validation_status": "pending", "requires_manual_review": bool(gps_coordinates.get('accuracy', 0) > 10), # 10m以上は要審査 - "image_url": s3_image_url # S3画像URLを返す + "image_url": s3_image_url, # S3画像URLを返す + "competition_status": { + "is_in_rog": entry.is_in_rog, + "rogaining_counted": entry.rogaining_counted, + "ready_for_goal": entry.ready_for_goal, + "is_at_goal": entry.is_at_goal + } }) except Exception as e: @@ -880,11 +912,25 @@ def goal_from_rogapp(request): colabo_company_memo="" ) + # 競技状態を更新 + entry.is_at_goal = True + entry.goal_time = goal_time + entry.last_checkin_time = goal_time + entry.save() + logger.info(f"[GOAL] ✅ SUCCESS - Team: {team_name}, Zekken: {entry.zekken_number}, Event: {event_code}, Goal Time: {goal_time}, Score: {score}, Has Image: {bool(image_url)}, Client IP: {client_ip}, User: {user_info}") + logger.info(f"[GOAL] ✅ Competition status updated - is_at_goal: True") return Response({ "status": "OK", "message": "ゴール処理が正常に完了しました", + "competition_status": { + "is_in_rog": entry.is_in_rog, + "rogaining_counted": entry.rogaining_counted, + "ready_for_goal": entry.ready_for_goal, + "is_at_goal": entry.is_at_goal, + "goal_time": goal_time.strftime("%Y-%m-%dT%H:%M:%S%z") + }, "team_name": team_name, "goal_time": goal_info.checkin_time.strftime("%Y-%m-%d %H:%M:%S"), "score": score, diff --git a/rog/views_apis/api_qr_points.py b/rog/views_apis/api_qr_points.py index 170a6fd..68ea9db 100644 --- a/rog/views_apis/api_qr_points.py +++ b/rog/views_apis/api_qr_points.py @@ -49,6 +49,10 @@ def submit_qr_points(request): longitude = request.data.get('longitude') image_data = request.data.get('image') cp_number = request.data.get('cp_number') + point_value = request.data.get('point_value') + location_id = request.data.get('location_id') + timestamp = request.data.get('timestamp') + qr_scan = request.data.get('qr_scan') # 📋 パラメータをログ出力(デバッグ用) logger.info(f"[QR_SUBMIT] ID: {request_id} - 📋 Request Parameters:") @@ -58,7 +62,11 @@ def submit_qr_points(request): logger.info(f"[QR_SUBMIT] ID: {request_id} - 📍 Latitude: {latitude}") logger.info(f"[QR_SUBMIT] ID: {request_id} - 🌍 Longitude: {longitude}") logger.info(f"[QR_SUBMIT] ID: {request_id} - 🏁 CP Number: {cp_number}") - logger.info(f"[QR_SUBMIT] ID: {request_id} - 📸 Has Image: {image_data is not None}") + logger.info(f"[QR_SUBMIT] ID: {request_id} - � Point Value: {point_value}") + logger.info(f"[QR_SUBMIT] ID: {request_id} - 📍 Location ID: {location_id}") + logger.info(f"[QR_SUBMIT] ID: {request_id} - ⏰ Timestamp: {timestamp}") + logger.info(f"[QR_SUBMIT] ID: {request_id} - 📱 QR Scan: {qr_scan}") + logger.info(f"[QR_SUBMIT] ID: {request_id} - �📸 Has Image: {image_data is not None}") logger.info(f"[QR_SUBMIT] ID: {request_id} - 🌐 Client IP: {client_ip}") logger.info(f"[QR_SUBMIT] ID: {request_id} - 👤 User: {user_info}") logger.info(f"[QR_SUBMIT] ID: {request_id} - 🔧 User Agent: {user_agent[:100]}...") @@ -89,12 +97,21 @@ def submit_qr_points(request): except Exception as e: logger.warning(f"[QR_SUBMIT] Failed to log request data: {e}") - # パラメータ検証 - if not all([event_code, team_name, qr_code_data]): - logger.warning(f"[QR_SUBMIT] ❌ Missing required parameters - ID: {request_id}") + # パラメータ検証 - 実際のリクエストデータに基づく + # 基本的な必須パラメータ: event_code, team_name + # オプション: qr_code_data, cp_number, point_value, location_id, qr_scan + if not all([event_code, team_name]): + missing_params = [] + if not event_code: + missing_params.append('event_code') + if not team_name: + missing_params.append('team_name') + + logger.warning(f"[QR_SUBMIT] ❌ Missing required parameters: {missing_params} - ID: {request_id}") return Response({ "status": "ERROR", - "message": "イベントコード、チーム名、QRコードデータが必要です", + "message": f"必須パラメータが不足しています: {', '.join(missing_params)}", + "missing_parameters": missing_params, "request_id": request_id }, status=status.HTTP_400_BAD_REQUEST) @@ -124,51 +141,85 @@ def submit_qr_points(request): "request_id": request_id }, status=status.HTTP_404_NOT_FOUND) - # QRコードデータの解析 - try: - if isinstance(qr_code_data, str): - # JSON文字列の場合はパース - if qr_code_data.startswith('{'): - qr_data = json.loads(qr_code_data) + # QRコードデータの解析(オプション) + qr_data = None + if qr_code_data: + try: + if isinstance(qr_code_data, str): + # JSON文字列の場合はパース + if qr_code_data.startswith('{'): + qr_data = json.loads(qr_code_data) + else: + # 単純な文字列の場合 + qr_data = {"code": qr_code_data} else: - # 単純な文字列の場合 - qr_data = {"code": qr_code_data} - else: - qr_data = qr_code_data - - logger.info(f"[QR_SUBMIT] 📱 Parsed QR data: {qr_data} - ID: {request_id}") - - except json.JSONDecodeError as e: - logger.warning(f"[QR_SUBMIT] ❌ Invalid QR code data format: {e} - ID: {request_id}") - return Response({ - "status": "ERROR", - "message": "QRコードデータの形式が正しくありません", - "request_id": request_id - }, status=status.HTTP_400_BAD_REQUEST) + qr_data = qr_code_data + + logger.info(f"[QR_SUBMIT] 📱 Parsed QR data: {qr_data} - ID: {request_id}") + + except json.JSONDecodeError as e: + logger.warning(f"[QR_SUBMIT] ⚠️ Invalid QR code data format: {e}, using as string - ID: {request_id}") + qr_data = {"code": str(qr_code_data)} - # チェックポイント情報の取得(cp_numberが指定されている場合) + # チェックポイント情報の取得 location = None - if cp_number: + calculated_point_value = 0 + + # location_idが指定されている場合、それを優先 + if location_id: + location = Location2025.objects.filter( + id=location_id, + event_id=event.id + ).first() + + if location: + calculated_point_value = location.cp_point or 0 + logger.info(f"[QR_SUBMIT] 📍 Found location by ID: {location_id} - CP{location.cp_number} - {location.cp_name} - Points: {calculated_point_value} - ID: {request_id}") + else: + logger.warning(f"[QR_SUBMIT] ⚠️ Location not found for location_id: {location_id} - ID: {request_id}") + + # cp_numberが指定されている場合も確認 + elif cp_number: location = Location2025.objects.filter( event_id=event.id, cp_number=cp_number ).first() if location: - logger.info(f"[QR_SUBMIT] 📍 Found location: CP{cp_number} - {location.cp_name} - ID: {request_id}") + calculated_point_value = location.cp_point or 0 + logger.info(f"[QR_SUBMIT] 📍 Found location by CP: CP{cp_number} - {location.cp_name} - Points: {calculated_point_value} - ID: {request_id}") else: logger.warning(f"[QR_SUBMIT] ⚠️ Location not found for CP{cp_number} - ID: {request_id}") + # point_valueが明示的に指定されている場合、それを使用 + final_point_value = point_value if point_value is not None else calculated_point_value + + logger.info(f"[QR_SUBMIT] 💯 Point calculation: provided={point_value}, calculated={calculated_point_value}, final={final_point_value} - ID: {request_id}") + # QRポイント登録処理 current_time = timezone.now() - # GpsCheckinレコードを作成(QRコード情報を含む) + # timestampが提供されている場合は使用、そうでなければ現在時刻 + if timestamp: + try: + # ISO形式のタイムスタンプをパース + checkin_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) + if checkin_time.tzinfo is None: + checkin_time = timezone.make_aware(checkin_time) + logger.info(f"[QR_SUBMIT] ⏰ Using provided timestamp: {checkin_time} - ID: {request_id}") + except (ValueError, AttributeError) as e: + logger.warning(f"[QR_SUBMIT] ⚠️ Invalid timestamp format, using current time: {e} - ID: {request_id}") + checkin_time = current_time + else: + checkin_time = current_time + + # GpsCheckinレコードを作成 checkin_data = { 'event': event, 'entry': entry, 'zekken_number': entry.zekken_number, - 'cp_number': cp_number or 0, # cp_numberが指定されていない場合は0 - 'checkin_time': current_time, + 'cp_number': cp_number or (location.cp_number if location else 0), + 'checkin_time': checkin_time, 'is_service_checked': True, # QRコードはサービスポイントとして扱う } @@ -178,9 +229,17 @@ def submit_qr_points(request): checkin_data['longitude'] = float(longitude) logger.info(f"[QR_SUBMIT] 🌍 GPS coordinates recorded: {latitude}, {longitude} - ID: {request_id}") - # QRコードデータを格納(JSONフィールドがある場合) + # QRコードデータを格納(qr_scanフラグも含む) + qr_metadata = { + 'qr_scan': qr_scan, + 'location_id': location_id, + 'point_value': final_point_value, + 'original_qr_data': qr_data, + 'timestamp': timestamp + } + if hasattr(GpsCheckin, 'qr_code_data'): - checkin_data['qr_code_data'] = qr_data + checkin_data['qr_code_data'] = qr_metadata # 画像データを格納(URLまたはパスの場合) if image_data: @@ -191,11 +250,6 @@ def submit_qr_points(request): # レコードを作成 gps_checkin = GpsCheckin.objects.create(**checkin_data) - # ポイント計算 - point_value = 0 - if location: - point_value = location.cp_point or 0 - # 成功レスポンス response_data = { "status": "OK", @@ -205,9 +259,11 @@ def submit_qr_points(request): "event_code": event_code, "team_name": team_name, "zekken_number": entry.zekken_number, - "cp_number": cp_number, - "point_value": point_value, - "checkin_time": current_time.isoformat(), + "cp_number": cp_number or (location.cp_number if location else 0), + "point_value": final_point_value, + "location_id": location_id, + "checkin_time": checkin_time.isoformat(), + "qr_scan": qr_scan, "qr_code_processed": True, "has_location": bool(latitude and longitude), "has_image": bool(image_data), @@ -215,7 +271,7 @@ def submit_qr_points(request): } } - logger.info(f"[QR_SUBMIT] ✅ SUCCESS - Team: {team_name}, Zekken: {entry.zekken_number}, CP: {cp_number}, Points: {point_value}, QR: {bool(qr_code_data)}, Client IP: {client_ip}, User: {user_info} - ID: {request_id}") + logger.info(f"[QR_SUBMIT] ✅ SUCCESS - Team: {team_name}, Zekken: {entry.zekken_number}, CP: {cp_number or (location.cp_number if location else 0)}, Points: {final_point_value}, QR: {qr_scan}, Location ID: {location_id}, Client IP: {client_ip}, User: {user_info} - ID: {request_id}") return Response(response_data, status=status.HTTP_200_OK) diff --git a/サーバーAPI変更要求書20250904.md b/サーバーAPI変更要求書20250904.md new file mode 100644 index 0000000..29c4438 --- /dev/null +++ b/サーバーAPI変更要求書20250904.md @@ -0,0 +1,161 @@ +# サーバーAPI変更要求書20250904.md + +## 概要 +アプリの状態管理において、競技開始状態とゴール状態の保存・復元に関する問題を解決するため、以下のサーバーAPI修正・追加が必要です。 + +## 問題点の分析 + +### 1. 競技状態管理の不足 +現在のアプリでは以下4つの重要な状態を管理していますが、サーバー側で対応する状態管理機能が不足している可能性があります: + +- `isInRog` (ロゲイニング中=スタートしたらTrue) +- `rogainingCounted` (ロゲイニングチェックイン履歴あり=一度でもスタート・ゴール以外でチェックインしたら True) +- `ready_for_goal` (ゴール準備完了=スタートから遠くに移動した際にTrueになる) +- `isAtGoal` (ゴール状態=ゴールしたらTrue) + +### 2. チェックイン状態の不整合 +チェックポイント詳細画面でスタート処理完了後も「未」のまま表示される問題が発生しています。 + +## 必要なAPI修正・追加要求 + +### 1. 競技状態取得API +**エンドポイント**: `GET /api/competition_status/` +**パラメータ**: +- `event_code`: イベントコード +- `zekken_number`: ゼッケン番号 + +**レスポンス**: +```json +{ + "status": "OK", + "data": { + "is_in_rog": true, + "rogaining_counted": false, + "ready_for_goal": false, + "is_at_goal": false, + "start_time": "2025-09-04T09:00:00+09:00", + "goal_time": null, + "last_checkin_time": "2025-09-04T09:30:00+09:00" + } +} +``` + +### 2. 競技状態更新API +**エンドポイント**: `POST /api/competition_status/update/` +**パラメータ**: +```json +{ + "event_code": "EVENT2025", + "zekken_number": "001", + "is_in_rog": true, + "rogaining_counted": false, + "ready_for_goal": false, + "is_at_goal": false +} +``` + +**レスポンス**: +```json +{ + "status": "OK", + "message": "Competition status updated successfully", + "updated_at": "2025-09-04T10:00:00+09:00" +} +``` + +### 3. スタート処理API拡張 +**エンドポイント**: `POST /api/start_rogaining/` +**既存機能に以下を追加**: +- 競技状態の確実な更新(is_in_rog = true) +- 開始時刻の記録 +- レスポンスで更新後の競技状態を返す + +**レスポンス拡張**: +```json +{ + "status": "OK", + "message": "Rogaining started successfully", + "competition_status": { + "is_in_rog": true, + "rogaining_counted": false, + "ready_for_goal": false, + "is_at_goal": false, + "start_time": "2025-09-04T09:00:00+09:00" + }, + "checkin_record": { + "id": 123, + "cp_number": -2, + "checkin_time": "2025-09-04T09:00:00+09:00" + } +} +``` + +### 4. ゴール処理API拡張 +**エンドポイント**: `POST /api/goal_rogaining/` +**既存機能に以下を追加**: +- 競技状態の確実な更新(is_at_goal = true) +- ゴール時刻の記録 +- 最終得点の計算 + +### 5. チェックイン処理API拡張 +**エンドポイント**: `POST /api/checkin/` +**既存機能に以下を追加**: +- スタート・ゴール以外のチェックイン時に rogaining_counted = true に設定 +- ready_for_goal フラグの管理(GPS位置情報に基づく) + +### 6. チェックポイント状態取得API +**エンドポイント**: `GET /api/checkpoint_status/` +**パラメータ**: +- `event_code`: イベントコード +- `zekken_number`: ゼッケン番号 +- `cp_number`: チェックポイント番号 + +**レスポンス**: +```json +{ + "status": "OK", + "data": { + "cp_number": -2, + "is_checked_in": true, + "checkin_time": "2025-09-04T09:00:00+09:00", + "status": "競技中", // "未", "競技中", "競技終了" + "points_earned": 0 + } +} +``` + +## 実装上の注意点 + +### 1. 状態の整合性保証 +- 競技状態の変更は必ずトランザクション内で実行 +- 状態変更時は必ず関連する全てのテーブルを更新 + +### 2. エラーハンドリング +- ネットワーク切断時でも状態が失われないような設計 +- 重複チェックイン防止機能の実装 + +### 3. パフォーマンス考慮 +- 状態取得APIは高頻度でアクセスされるため、適切なキャッシュ機能を実装 +- レスポンス時間は500ms以内を目標 + +### 4. ログ記録 +- 競技状態の変更履歴を詳細にログ記録 +- デバッグ用のタイムスタンプ情報を含める + +## タイムライン +- **緊急度**: 高 +- **実装期限**: 2025年9月10日 +- **テスト期間**: 2025年9月11日〜13日 +- **本番適用**: 2025年9月14日 + +## 備考 +これらの修正により、アプリ側で以下の問題が解決されます: + +1. ✅ 競技中にアプリを終了・再開しても正しい状態が復元される +2. ✅ スタート処理完了後、チェックポイント詳細画面で「競技中」と表示される +3. ✅ 4つの重要な状態(isInRog, rogainingCounted, ready_for_goal, isAtGoal)が確実に保存・復元される +4. ✅ サーバー・クライアント間の状態整合性が保たれる + +--- +作成日: 2025年9月4日 +作成者: システム開発チーム