Finish basic API implementation

This commit is contained in:
2025-08-27 15:01:06 +09:00
parent fff9bce9e7
commit cc9edb9932
19 changed files with 3844 additions and 5 deletions

View File

@ -0,0 +1,240 @@
"""
Location checkin view with evaluation_value based interaction logic
"""
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views import View
import json
import logging
logger = logging.getLogger(__name__)
@method_decorator(csrf_exempt, name='dispatch')
@method_decorator(login_required, name='dispatch')
class LocationCheckinView(View):
"""
evaluation_valueに基づく拡張チェックイン処理
"""
def post(self, request):
"""
ロケーションチェックイン処理
Request body:
{
"location_id": int,
"latitude": float,
"longitude": float,
"photo": str (base64) - evaluation_value=1の場合必須,
"qr_code_data": str - evaluation_value=2の場合必須,
"quiz_answer": str - evaluation_value=2の場合必須
}
"""
try:
data = json.loads(request.body)
location_id = data.get('location_id')
user_lat = data.get('latitude')
user_lon = data.get('longitude')
if not all([location_id, user_lat, user_lon]):
return JsonResponse({
'success': False,
'error': 'location_id, latitude, longitude are required'
}, status=400)
# ロケーション取得
from .models import Location
try:
location = Location.objects.get(id=location_id)
except Location.DoesNotExist:
return JsonResponse({
'success': False,
'error': 'Location not found'
}, status=404)
# 距離チェック
if not self._is_within_checkin_radius(location, user_lat, user_lon):
return JsonResponse({
'success': False,
'error': 'Too far from location',
'required_radius': location.checkin_radius or 15.0
}, status=400)
# evaluation_valueに基づく要件検証
from .location_interaction import validate_interaction_requirements
validation_result = validate_interaction_requirements(location, data)
if not validation_result['valid']:
return JsonResponse({
'success': False,
'error': 'Interaction requirements not met',
'errors': validation_result['errors']
}, status=400)
# インタラクション処理
interaction_result = self._process_interaction(location, data)
# ポイント計算
from .location_interaction import get_point_calculation
point_info = get_point_calculation(location, interaction_result)
# チェックイン記録保存
checkin_record = self._save_checkin_record(
request.user, location, user_lat, user_lon,
interaction_result, point_info
)
# レスポンス
response_data = {
'success': True,
'checkin_id': checkin_record.id,
'points_awarded': point_info['points_awarded'],
'point_type': point_info['point_type'],
'message': point_info['message'],
'location_name': location.location_name,
'interaction_type': location.evaluation_value or "0",
}
# インタラクション結果の詳細を追加
if interaction_result:
response_data['interaction_result'] = interaction_result
return JsonResponse(response_data)
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': 'Invalid JSON data'
}, status=400)
except Exception as e:
logger.error(f"Checkin error: {e}")
return JsonResponse({
'success': False,
'error': 'Internal server error'
}, status=500)
def _is_within_checkin_radius(self, location, user_lat, user_lon):
"""チェックイン範囲内かどうかを判定"""
from math import radians, cos, sin, asin, sqrt
# ロケーションの座標を取得
if location.geom and location.geom.coords:
loc_lon, loc_lat = location.geom.coords[0][:2]
else:
loc_lat = location.latitude
loc_lon = location.longitude
if not all([loc_lat, loc_lon]):
return False
# Haversine公式で距離計算
def haversine(lon1, lat1, lon2, lat2):
lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
c = 2 * asin(sqrt(a))
r = 6371000 # 地球の半径(メートル)
return c * r
distance = haversine(loc_lon, loc_lat, user_lon, user_lat)
allowed_radius = location.checkin_radius or 15.0
return distance <= allowed_radius
def _process_interaction(self, location, data):
"""evaluation_valueに基づくインタラクション処理"""
evaluation_value = location.evaluation_value or "0"
result = {}
if evaluation_value == "1":
# 写真撮影処理
photo_data = data.get('photo')
if photo_data:
result['photo_saved'] = True
result['photo_filename'] = self._save_photo(photo_data, location)
elif evaluation_value == "2":
# QRコード + クイズ処理
qr_data = data.get('qr_code_data')
quiz_answer = data.get('quiz_answer')
if qr_data and quiz_answer:
result['qr_scanned'] = True
result['quiz_answer'] = quiz_answer
result['quiz_correct'] = self._check_quiz_answer(qr_data, quiz_answer)
return result
def _save_photo(self, photo_data, location):
"""写真データを保存(実装は要調整)"""
import base64
import os
from django.conf import settings
from datetime import datetime
try:
# Base64デコード
photo_binary = base64.b64decode(photo_data)
# ファイル名生成
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"checkin_{location.id}_{timestamp}.jpg"
# 保存先ディレクトリ
photo_dir = os.path.join(settings.MEDIA_ROOT, 'checkin_photos')
os.makedirs(photo_dir, exist_ok=True)
# ファイル保存
file_path = os.path.join(photo_dir, filename)
with open(file_path, 'wb') as f:
f.write(photo_binary)
return filename
except Exception as e:
logger.error(f"Photo save error: {e}")
return None
def _check_quiz_answer(self, qr_data, quiz_answer):
"""クイズ回答の正答チェック(実装は要調整)"""
# QRコードデータから正答を取得
# 実際の実装では、QRコードに含まれるクイズIDから正答を取得
try:
import json
qr_info = json.loads(qr_data)
correct_answer = qr_info.get('correct_answer', '').lower()
user_answer = quiz_answer.lower().strip()
return correct_answer == user_answer
except (json.JSONDecodeError, KeyError):
# QRデータの形式が不正な場合はデフォルトで不正解
return False
def _save_checkin_record(self, user, location, lat, lon, interaction_result, point_info):
"""チェックイン記録を保存"""
from .models import Useractions
from datetime import datetime
# Useractionsレコード作成/更新
checkin_record, created = Useractions.objects.get_or_create(
user=user,
location=location,
defaults={
'checkin': True,
'created_at': datetime.now(),
'last_updated_at': datetime.now()
}
)
if not created:
checkin_record.checkin = True
checkin_record.last_updated_at = datetime.now()
checkin_record.save()
return checkin_record