update APIs
This commit is contained in:
49
rog/migrations/0013_add_competition_status_fields.py
Normal file
49
rog/migrations/0013_add_competition_status_fields.py
Normal 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='最後のチェックイン時刻'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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'),
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
307
rog/views_apis/api_competition_status.py
Normal file
307
rog/views_apis/api_competition_status.py
Normal 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)
|
||||||
@ -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,24 +1135,83 @@ 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(
|
entry = Entry.objects.filter(
|
||||||
event=event,
|
event=event,
|
||||||
team__zekken_number=zekken_number
|
zekken_number=zekken_number
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
if not entry:
|
||||||
|
# team__zekken_numberでも試してみる
|
||||||
|
entry = Entry.objects.filter(
|
||||||
|
event=event,
|
||||||
|
team__zekken_number=zekken_number
|
||||||
|
).first()
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,51 +141,85 @@ 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コードデータの解析(オプション)
|
||||||
try:
|
qr_data = None
|
||||||
if isinstance(qr_code_data, str):
|
if qr_code_data:
|
||||||
# JSON文字列の場合はパース
|
try:
|
||||||
if qr_code_data.startswith('{'):
|
if isinstance(qr_code_data, str):
|
||||||
qr_data = json.loads(qr_code_data)
|
# JSON文字列の場合はパース
|
||||||
|
if qr_code_data.startswith('{'):
|
||||||
|
qr_data = json.loads(qr_code_data)
|
||||||
|
else:
|
||||||
|
# 単純な文字列の場合
|
||||||
|
qr_data = {"code": qr_code_data}
|
||||||
else:
|
else:
|
||||||
# 単純な文字列の場合
|
qr_data = qr_code_data
|
||||||
qr_data = {"code": qr_code_data}
|
|
||||||
else:
|
logger.info(f"[QR_SUBMIT] 📱 Parsed QR data: {qr_data} - ID: {request_id}")
|
||||||
qr_data = qr_code_data
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
logger.info(f"[QR_SUBMIT] 📱 Parsed QR data: {qr_data} - ID: {request_id}")
|
logger.warning(f"[QR_SUBMIT] ⚠️ Invalid QR code data format: {e}, using as string - ID: {request_id}")
|
||||||
|
qr_data = {"code": str(qr_code_data)}
|
||||||
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)
|
|
||||||
|
|
||||||
# チェックポイント情報の取得(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)
|
||||||
|
|
||||||
|
|||||||
161
サーバーAPI変更要求書20250904.md
Normal file
161
サーバーAPI変更要求書20250904.md
Normal 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日
|
||||||
|
作成者: システム開発チーム
|
||||||
Reference in New Issue
Block a user