update APIs

This commit is contained in:
2025-09-04 19:25:14 +09:00
parent 32f860af41
commit e0543e2b4e
8 changed files with 759 additions and 64 deletions

View File

@ -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='最後のチェックイン時刻'),
),
]

View File

@ -750,6 +750,15 @@ class Entry(models.Model):
staff_privileges = models.BooleanField(default=False, help_text="スタッフ権限フラグ") staff_privileges = models.BooleanField(default=False, help_text="スタッフ権限フラグ")
can_access_private_events = 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 = [ VALIDATION_STATUS_CHOICES = [
('approved', 'Approved'), ('approved', 'Approved'),
('pending', 'Pending'), ('pending', 'Pending'),

View File

@ -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_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_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_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_test import test_gifuroge,practice
from .views_apis.api_supervisor import get_events_for_supervisor from .views_apis.api_supervisor import get_events_for_supervisor
from django.views.decorators.csrf import csrf_exempt 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('submit_qr_points', submit_qr_points, name='submit_qr_points'),
path('qr_points_status', qr_points_status, name='qr_points_status'), 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: if settings.DEBUG:

View File

@ -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)

View File

@ -5,7 +5,7 @@ from django.utils import timezone
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status 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 logging
import uuid import uuid
import os import os
@ -1089,18 +1089,19 @@ def get_checkin_list(request):
logger.info("get_checkin_list called") logger.info("get_checkin_list called")
# リクエストからパラメータを取得GET/POSTの両方に対応 # リクエストからパラメータを取得GET/POSTの両方に対応
# 両方のパラメータ名に対応
if request.method == 'GET': if request.method == 'GET':
zekken_number = request.GET.get('zekken') zekken_number = request.GET.get('zekken_number') or request.GET.get('zekken')
event_code = request.GET.get('event') event_code = request.GET.get('event_code') or request.GET.get('event')
else: # POST else: # POST
try: try:
data = json.loads(request.body) data = json.loads(request.body)
zekken_number = data.get('zekken') zekken_number = data.get('zekken_number') or data.get('zekken')
event_code = data.get('event') event_code = data.get('event_code') or data.get('event')
except: except:
data = request.POST data = request.POST
zekken_number = data.get('zekken') zekken_number = data.get('zekken_number') or data.get('zekken')
event_code = data.get('event') 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] Request method: {request.method}")
logger.info(f"[GET_CHECKIN_LIST] Parameters received - zekken: {zekken_number}, event: {event_code}") logger.info(f"[GET_CHECKIN_LIST] Parameters received - zekken: {zekken_number}, event: {event_code}")
@ -1134,6 +1135,21 @@ def get_checkin_list(request):
}, status=status.HTTP_404_NOT_FOUND) }, 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,
zekken_number=zekken_number
).first()
if not entry:
# team__zekken_numberでも試してみる
entry = Entry.objects.filter( entry = Entry.objects.filter(
event=event, event=event,
team__zekken_number=zekken_number team__zekken_number=zekken_number
@ -1141,17 +1157,61 @@ def get_checkin_list(request):
if not entry: if not entry:
logger.warning(f"Team with zekken number {zekken_number} not found in event: {event_code}") 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({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません" "message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# チェックイン記録を取得 # チェックイン記録を取得
checkpoints = GpsLog.objects.filter( logger.info(f"[GET_CHECKIN_LIST] Found entry: {entry.team.team_name} (zekken: {entry.zekken_number})")
zekken_number=entry.team.zekken_number,
# 複数のテーブルからチェックイン記録を取得
# GpsLogテーブルから
gps_checkpoints = GpsLog.objects.filter(
zekken_number=str(entry.zekken_number),
event_code=event_code event_code=event_code
).order_by('checkin_time') ).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 start_info = None
if hasattr(entry, 'start_info'): if hasattr(entry, 'start_info'):
@ -1170,20 +1230,21 @@ def get_checkin_list(request):
# チェックイン記録をシリアライズ # チェックイン記録をシリアライズ
checkpoint_list = [] checkpoint_list = []
for cp in checkpoints: for cp in all_checkpoints:
checkpoint_data = { checkpoint_data = {
"id": cp.id, "id": cp['id'],
"cp_number": cp.cp_number, "cp_number": cp['cp_number'],
"checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None, "checkin_time": cp['checkin_time'].strftime("%Y-%m-%d %H:%M:%S") if cp['checkin_time'] else None,
"image_url": cp.image_address, "image_url": cp['image_address'],
"is_service_checked": cp.is_service_checked if hasattr(cp, 'is_service_checked') else False "is_service_checked": cp['is_service_checked'],
"source": cp['source']
} }
# チェックポイントの得点情報を取得( Location2025 モデルがある場合) # チェックポイントの得点情報を取得( Location2025 モデルがある場合)
try: try:
event_cp = Location2025.objects.filter( event_cp = Location2025.objects.filter(
event_id=event.id, event_id=event.id,
cp_number=cp.cp_number cp_number=cp['cp_number']
).first() ).first()
if event_cp: if event_cp:

View File

@ -361,19 +361,27 @@ def start_from_rogapp(request):
with transaction.atomic(): with transaction.atomic():
# スタート情報をGpsLogとして登録 # スタート情報をGpsLogとして登録
logger.info(f"[START_API] Creating start record - ID: {request_id}") logger.info(f"[START_API] Creating start record - ID: {request_id}")
start_time = timezone.now()
start_info = GpsLog.objects.create( start_info = GpsLog.objects.create(
zekken_number=entry.zekken_number, zekken_number=entry.zekken_number,
event_code=event.event_name, event_code=event.event_name,
cp_number="START", cp_number="START",
serial_number=0, serial_number=0,
checkin_time=timezone.now(), checkin_time=start_time,
create_at=timezone.now(), create_at=start_time,
update_at=timezone.now(), update_at=start_time,
buy_flag=False, buy_flag=False,
colabo_company_memo="" 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] ✅ Start record created - ID: {request_id}, GpsLog ID: {start_info.id}")
logger.info(f"[START_API] ✅ Competition status updated - is_in_rog: True")
# 統計情報取得 # 統計情報取得
try: try:
@ -387,9 +395,20 @@ def start_from_rogapp(request):
response_data = { response_data = {
"status": "OK", "status": "OK",
"message": "スタート処理が完了しました", "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, "team_name": team_name,
"event_code": event_code, "event_code": event_code,
"start_time": start_info.checkin_time.strftime("%Y-%m-%d %H:%M:%S"),
"zekken_number": entry.zekken_number, "zekken_number": entry.zekken_number,
"entry_id": entry.id "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}") 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: if gps_coordinates or camera_metadata:
try: try:
@ -689,7 +715,13 @@ def checkin_from_rogapp(request):
"scoring_breakdown": scoring_breakdown, "scoring_breakdown": scoring_breakdown,
"validation_status": "pending", "validation_status": "pending",
"requires_manual_review": bool(gps_coordinates.get('accuracy', 0) > 10), # 10m以上は要審査 "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: except Exception as e:
@ -880,11 +912,25 @@ def goal_from_rogapp(request):
colabo_company_memo="" 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] ✅ 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({ return Response({
"status": "OK", "status": "OK",
"message": "ゴール処理が正常に完了しました", "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, "team_name": team_name,
"goal_time": goal_info.checkin_time.strftime("%Y-%m-%d %H:%M:%S"), "goal_time": goal_info.checkin_time.strftime("%Y-%m-%d %H:%M:%S"),
"score": score, "score": score,

View File

@ -49,6 +49,10 @@ def submit_qr_points(request):
longitude = request.data.get('longitude') longitude = request.data.get('longitude')
image_data = request.data.get('image') image_data = request.data.get('image')
cp_number = request.data.get('cp_number') 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:") 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} - 📍 Latitude: {latitude}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 🌍 Longitude: {longitude}") 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} - 🏁 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} - <EFBFBD> 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} - <20>📸 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} - 🌐 Client IP: {client_ip}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 👤 User: {user_info}") logger.info(f"[QR_SUBMIT] ID: {request_id} - 👤 User: {user_info}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 🔧 User Agent: {user_agent[:100]}...") 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: except Exception as e:
logger.warning(f"[QR_SUBMIT] Failed to log request data: {e}") logger.warning(f"[QR_SUBMIT] Failed to log request data: {e}")
# パラメータ検証 # パラメータ検証 - 実際のリクエストデータに基づく
if not all([event_code, team_name, qr_code_data]): # 基本的な必須パラメータ: event_code, team_name
logger.warning(f"[QR_SUBMIT] ❌ Missing required parameters - ID: {request_id}") # オプション: 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({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "イベントコード、チーム名、QRコードデータが必要です", "message": f"必須パラメータが不足しています: {', '.join(missing_params)}",
"missing_parameters": missing_params,
"request_id": request_id "request_id": request_id
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
@ -124,7 +141,9 @@ def submit_qr_points(request):
"request_id": request_id "request_id": request_id
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# QRコードデータの解析 # QRコードデータの解析(オプション)
qr_data = None
if qr_code_data:
try: try:
if isinstance(qr_code_data, str): if isinstance(qr_code_data, str):
# JSON文字列の場合はパース # JSON文字列の場合はパース
@ -139,36 +158,68 @@ def submit_qr_points(request):
logger.info(f"[QR_SUBMIT] 📱 Parsed QR data: {qr_data} - ID: {request_id}") logger.info(f"[QR_SUBMIT] 📱 Parsed QR data: {qr_data} - ID: {request_id}")
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.warning(f"[QR_SUBMIT] Invalid QR code data format: {e} - ID: {request_id}") logger.warning(f"[QR_SUBMIT] ⚠️ Invalid QR code data format: {e}, using as string - ID: {request_id}")
return Response({ qr_data = {"code": str(qr_code_data)}
"status": "ERROR",
"message": "QRコードデータの形式が正しくありません",
"request_id": request_id
}, status=status.HTTP_400_BAD_REQUEST)
# チェックポイント情報の取得cp_numberが指定されている場合 # チェックポイント情報の取得
location = None 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( location = Location2025.objects.filter(
event_id=event.id, event_id=event.id,
cp_number=cp_number cp_number=cp_number
).first() ).first()
if location: 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: else:
logger.warning(f"[QR_SUBMIT] ⚠️ Location not found for CP{cp_number} - ID: {request_id}") 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ポイント登録処理 # QRポイント登録処理
current_time = timezone.now() 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 = { checkin_data = {
'event': event, 'event': event,
'entry': entry, 'entry': entry,
'zekken_number': entry.zekken_number, 'zekken_number': entry.zekken_number,
'cp_number': cp_number or 0, # cp_numberが指定されていない場合は0 'cp_number': cp_number or (location.cp_number if location else 0),
'checkin_time': current_time, 'checkin_time': checkin_time,
'is_service_checked': True, # QRコードはサービスポイントとして扱う 'is_service_checked': True, # QRコードはサービスポイントとして扱う
} }
@ -178,9 +229,17 @@ def submit_qr_points(request):
checkin_data['longitude'] = float(longitude) checkin_data['longitude'] = float(longitude)
logger.info(f"[QR_SUBMIT] 🌍 GPS coordinates recorded: {latitude}, {longitude} - ID: {request_id}") 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'): if hasattr(GpsCheckin, 'qr_code_data'):
checkin_data['qr_code_data'] = qr_data checkin_data['qr_code_data'] = qr_metadata
# 画像データを格納URLまたはパスの場合 # 画像データを格納URLまたはパスの場合
if image_data: if image_data:
@ -191,11 +250,6 @@ def submit_qr_points(request):
# レコードを作成 # レコードを作成
gps_checkin = GpsCheckin.objects.create(**checkin_data) gps_checkin = GpsCheckin.objects.create(**checkin_data)
# ポイント計算
point_value = 0
if location:
point_value = location.cp_point or 0
# 成功レスポンス # 成功レスポンス
response_data = { response_data = {
"status": "OK", "status": "OK",
@ -205,9 +259,11 @@ def submit_qr_points(request):
"event_code": event_code, "event_code": event_code,
"team_name": team_name, "team_name": team_name,
"zekken_number": entry.zekken_number, "zekken_number": entry.zekken_number,
"cp_number": cp_number, "cp_number": cp_number or (location.cp_number if location else 0),
"point_value": point_value, "point_value": final_point_value,
"checkin_time": current_time.isoformat(), "location_id": location_id,
"checkin_time": checkin_time.isoformat(),
"qr_scan": qr_scan,
"qr_code_processed": True, "qr_code_processed": True,
"has_location": bool(latitude and longitude), "has_location": bool(latitude and longitude),
"has_image": bool(image_data), "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) return Response(response_data, status=status.HTTP_200_OK)

View File

@ -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日
作成者: システム開発チーム