almost finish migrate new circumstances
This commit is contained in:
375
詳細機能設計書-外部連携.md
375
詳細機能設計書-外部連携.md
@ -664,3 +664,378 @@ end
|
||||
---
|
||||
|
||||
この詳細機能設計書により、外部システム連携の実装、運用、保守に必要な全ての技術的詳細が文書化されています。
|
||||
|
||||
---
|
||||
|
||||
## 14. 🆕 管理者向け機能拡張 (2025年8月実装)
|
||||
|
||||
### 14.1 一括写真アップロード・自動チェックイン機能
|
||||
|
||||
#### 機能概要
|
||||
複数の写真を一括でアップロードし、EXIF情報から自動的にチェックイン処理を実行する機能です。
|
||||
|
||||
#### 技術仕様
|
||||
|
||||
**実装ファイル**: `rog/views_apis/api_bulk_upload.py`
|
||||
|
||||
**主要クラス・関数**:
|
||||
```python
|
||||
def bulk_upload_photos(request):
|
||||
"""
|
||||
一括写真アップロード処理
|
||||
|
||||
処理フロー:
|
||||
1. アップロードファイルの検証
|
||||
2. EXIF情報抽出(GPS座標、撮影時刻)
|
||||
3. 近接チェックポイント検索
|
||||
4. 自動チェックイン処理
|
||||
5. S3ストレージへの画像保存
|
||||
"""
|
||||
```
|
||||
|
||||
**依存ライブラリ**:
|
||||
- `piexif`: EXIF情報抽出
|
||||
- `PIL`: 画像処理
|
||||
- `boto3`: S3連携
|
||||
|
||||
**処理フロー図**:
|
||||
```
|
||||
写真アップロード → EXIF抽出 → GPS検証 → チェックポイント検索 → 自動チェックイン
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
ファイル検証 座標・時刻 位置精度確認 距離計算 DB記録
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
S3アップロード メタデータ タイムスタンプ ポイント照合 確定処理
|
||||
```
|
||||
|
||||
#### GPS精度検証ロジック
|
||||
```python
|
||||
def validate_gps_proximity(gps_coords, checkin_time, event_code):
|
||||
"""
|
||||
GPS座標の妥当性検証
|
||||
|
||||
検証項目:
|
||||
- チェックポイントとの距離(50m以内)
|
||||
- 撮影時刻の妥当性(イベント期間内)
|
||||
- 重複チェックイン防止(同一CP 30分以内)
|
||||
"""
|
||||
max_distance = 50 # メートル
|
||||
min_interval = 30 # 分
|
||||
```
|
||||
|
||||
#### エラーハンドリング
|
||||
```python
|
||||
class BulkUploadError(Exception):
|
||||
"""一括アップロード専用例外クラス"""
|
||||
pass
|
||||
|
||||
# エラーパターン
|
||||
- GPS情報なし: "GPS情報が見つかりません"
|
||||
- 距離超過: "チェックポイントから50m以上離れています"
|
||||
- 時刻異常: "撮影時刻がイベント期間外です"
|
||||
- ファイル形式: "対応していないファイル形式です"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 14.2 通過審査管理機能
|
||||
|
||||
#### 機能概要
|
||||
チェックインの確定・否認を管理し、審査状況を追跡する機能です。
|
||||
|
||||
#### データベース拡張
|
||||
**テーブル**: `rog_gpsCheckin`
|
||||
|
||||
**新規フィールド**:
|
||||
```sql
|
||||
ALTER TABLE rog_gpsCheckin ADD COLUMN validation_status VARCHAR(20) DEFAULT 'PENDING';
|
||||
ALTER TABLE rog_gpsCheckin ADD COLUMN validation_comment TEXT;
|
||||
ALTER TABLE rog_gpsCheckin ADD COLUMN validated_at TIMESTAMP;
|
||||
ALTER TABLE rog_gpsCheckin ADD COLUMN validated_by VARCHAR(255);
|
||||
```
|
||||
|
||||
**ステータス定義**:
|
||||
```python
|
||||
VALIDATION_STATUS_CHOICES = [
|
||||
('PENDING', '審査待ち'),
|
||||
('APPROVED', '承認'),
|
||||
('REJECTED', '却下'),
|
||||
('AUTO_APPROVED', '自動承認'),
|
||||
]
|
||||
```
|
||||
|
||||
#### API実装
|
||||
**実装ファイル**: `rog/views_apis/api_bulk_upload.py`
|
||||
|
||||
```python
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def confirm_checkin_validation(request):
|
||||
"""
|
||||
チェックイン確定・否認処理
|
||||
|
||||
パラメータ:
|
||||
- checkin_id: 対象チェックインID
|
||||
- action: APPROVED/REJECTED
|
||||
- comment: 審査コメント
|
||||
"""
|
||||
```
|
||||
|
||||
#### 審査ワークフロー
|
||||
```
|
||||
写真アップロード → AUTO_APPROVED → 管理者審査 → APPROVED/REJECTED
|
||||
↓ ↓ ↓ ↓
|
||||
自動処理 システム判定 手動確認 最終決定
|
||||
↓ ↓ ↓ ↓
|
||||
即座反映 暫定得点 審査待ち 確定得点
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 14.3 全参加者ランキング表示機能
|
||||
|
||||
#### 機能概要
|
||||
イベント全参加者の得点と審査状況をクラス別に一覧表示する機能です。
|
||||
|
||||
#### 実装仕様
|
||||
**実装ファイル**: `rog/views_apis/api_admin_validation.py`
|
||||
|
||||
**主要関数**:
|
||||
```python
|
||||
def get_event_participants_ranking(request):
|
||||
"""
|
||||
全参加者ランキング取得
|
||||
|
||||
集計項目:
|
||||
- 確定得点(APPROVED)
|
||||
- 未確定得点(PENDING)
|
||||
- 確定率(確定数/総数)
|
||||
- クラス別順位
|
||||
"""
|
||||
```
|
||||
|
||||
#### SQLクエリ最適化
|
||||
```sql
|
||||
SELECT
|
||||
e.zekken_number,
|
||||
e.team_name,
|
||||
e.class_name,
|
||||
SUM(CASE WHEN g.validation_status = 'APPROVED' THEN l.point_value ELSE 0 END) as confirmed_points,
|
||||
SUM(CASE WHEN g.validation_status = 'PENDING' THEN l.point_value ELSE 0 END) as pending_points,
|
||||
COUNT(g.id) as total_checkins,
|
||||
COUNT(CASE WHEN g.validation_status = 'APPROVED' THEN 1 END) as confirmed_checkins
|
||||
FROM rog_entry e
|
||||
LEFT JOIN rog_gpsCheckin g ON e.zekken_number = g.zekken_number
|
||||
LEFT JOIN rog_location2025 l ON g.cp_number = l.cp_number
|
||||
WHERE e.event_code = %s
|
||||
GROUP BY e.zekken_number, e.team_name, e.class_name
|
||||
ORDER BY confirmed_points DESC;
|
||||
```
|
||||
|
||||
#### キャッシュ戦略
|
||||
```python
|
||||
from django.core.cache import cache
|
||||
|
||||
def get_cached_ranking(event_code):
|
||||
"""
|
||||
ランキングデータのキャッシュ
|
||||
|
||||
キャッシュキー: f"ranking_{event_code}"
|
||||
有効期限: 300秒(5分)
|
||||
更新トリガー: チェックイン確定時
|
||||
"""
|
||||
cache_key = f"ranking_{event_code}"
|
||||
cached_data = cache.get(cache_key)
|
||||
|
||||
if cached_data is None:
|
||||
cached_data = calculate_ranking(event_code)
|
||||
cache.set(cache_key, cached_data, 300)
|
||||
|
||||
return cached_data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 14.4 管理画面UI拡張
|
||||
|
||||
#### フロントエンド実装
|
||||
**実装ファイル**: `supervisor/html/index.html`
|
||||
|
||||
**新機能**:
|
||||
1. **表示モード切り替え**: 個別表示 ⇄ ランキング表示
|
||||
2. **一括操作**: 確定・否認ボタン
|
||||
3. **ファイルアップロード**: ドラッグ&ドロップ対応
|
||||
4. **リアルタイム更新**: AJAX通信
|
||||
|
||||
#### JavaScript実装概要
|
||||
```javascript
|
||||
class AdminValidationManager {
|
||||
constructor() {
|
||||
this.apiBaseUrl = '/api';
|
||||
this.currentEventCode = '';
|
||||
this.displayMode = 'individual';
|
||||
}
|
||||
|
||||
// 一括写真アップロード
|
||||
async handleBulkPhotoUpload(files) {
|
||||
const formData = new FormData();
|
||||
files.forEach(file => formData.append('photos', file));
|
||||
formData.append('event_code', this.currentEventCode);
|
||||
|
||||
const response = await fetch(`${this.apiBaseUrl}/bulk-upload-photos/`, {
|
||||
method: 'POST',
|
||||
headers: this.getAuthHeaders(),
|
||||
body: formData
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// チェックイン確定・否認
|
||||
async confirmCheckin(checkinId, action) {
|
||||
const response = await fetch(`${this.apiBaseUrl}/confirm-checkin-validation/`, {
|
||||
method: 'POST',
|
||||
headers: this.getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
checkin_id: checkinId,
|
||||
action: action,
|
||||
comment: action === 'REJECTED' ? '手動確認により却下' : '手動確認により承認'
|
||||
})
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### レスポンシブデザイン
|
||||
```css
|
||||
/* 管理画面専用スタイル */
|
||||
.admin-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.ranking-table {
|
||||
overflow-x: auto;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.validation-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.validation-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 14.5 セキュリティ対策
|
||||
|
||||
#### 認証・認可
|
||||
```python
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
|
||||
def is_admin_user(user):
|
||||
"""管理者権限チェック"""
|
||||
return user.is_authenticated and user.is_superuser
|
||||
|
||||
@user_passes_test(is_admin_user)
|
||||
def admin_only_view(request):
|
||||
"""管理者専用ビュー"""
|
||||
pass
|
||||
```
|
||||
|
||||
#### ファイルアップロード検証
|
||||
```python
|
||||
def validate_upload_file(file):
|
||||
"""
|
||||
アップロードファイル検証
|
||||
|
||||
検証項目:
|
||||
- ファイルサイズ(10MB以下)
|
||||
- ファイル形式(JPEG/PNG)
|
||||
- EXIF情報の存在
|
||||
- マルウェアスキャン
|
||||
"""
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
ALLOWED_TYPES = ['image/jpeg', 'image/png']
|
||||
|
||||
if file.size > MAX_FILE_SIZE:
|
||||
raise ValidationError('ファイルサイズが制限を超えています')
|
||||
|
||||
if file.content_type not in ALLOWED_TYPES:
|
||||
raise ValidationError('対応していないファイル形式です')
|
||||
```
|
||||
|
||||
#### レート制限
|
||||
```python
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
|
||||
@ratelimit(key='user', rate='10/h', method=['POST'])
|
||||
def bulk_upload_photos(request):
|
||||
"""
|
||||
レート制限: 1時間あたり10回まで
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 14.6 監視・ログ機能
|
||||
|
||||
#### 操作ログ
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('admin_operations')
|
||||
|
||||
def log_admin_action(user, action, target, details=None):
|
||||
"""
|
||||
管理者操作ログ記録
|
||||
|
||||
ログ項目:
|
||||
- 操作者
|
||||
- 操作内容
|
||||
- 対象データ
|
||||
- タイムスタンプ
|
||||
- 詳細情報
|
||||
"""
|
||||
logger.info(f"Admin Action: {user.username} performed {action} on {target}", extra={
|
||||
'user_id': user.id,
|
||||
'action': action,
|
||||
'target': target,
|
||||
'details': details,
|
||||
'timestamp': timezone.now().isoformat()
|
||||
})
|
||||
```
|
||||
|
||||
#### パフォーマンス監視
|
||||
```python
|
||||
from django.db import connection
|
||||
from django.conf import settings
|
||||
|
||||
def monitor_query_performance():
|
||||
"""
|
||||
クエリパフォーマンス監視
|
||||
"""
|
||||
if settings.DEBUG:
|
||||
query_count = len(connection.queries)
|
||||
slow_queries = [q for q in connection.queries if float(q['time']) > 0.1]
|
||||
|
||||
if slow_queries:
|
||||
logger.warning(f"Slow queries detected: {len(slow_queries)} queries > 0.1s")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user