almost finish migrate new circumstances

This commit is contained in:
2025-08-24 19:44:36 +09:00
parent 1ba305641e
commit fe5a044c82
67 changed files with 1194889 additions and 467 deletions

View File

@ -1,104 +1,404 @@
# 統合データベース設計書
# 統合データベース設計書(更新版)
## 1. 概要
### 1.1 目的
現在運用されているDjango Admin DBとMobServer DBを統合し、一元的なデータ管理システムを構築する。システム停止中であるため、マイグレーション期間を考慮せず、直接統合を実施する
gifurogeMobServerからrogdbDjangoへの過去のGPSチェックインデータ移行による「あり得ない通過データ」問題の解決
タイムゾーン変換とデータクリーニングを通じて、正確な日本時間での位置情報管理を実現する。
### 1.2 基本方針
- Django Admin DBをメインデータベースとして位置づけ
- MobServerの機能をDjangoベースに統合
- PostGISによる地理情報管理を継続
- 既存データの完全保持
- **GPS専用移行**: 信頼できるGPSデータserial_number < 20000のみを対象とした移行
- **タイムゾーン統一**: UTC JST への正確な変換で日本時間統一
- **データクリーニング**: 2023年テストデータ汚染の完全除去
- **PostGIS統合**: 地理情報システムの継続運用
### 1.3 統合アプローチ
- **完全統合アプローチ**: MobServer DBの全テーブルをDjango管理下に移行
- **機能重複解消**: 同一機能の重複テーブル・フィールドを統一
- **データ型統一**: Django Modelsの型システムに準拠
### 1.3 移行アプローチ
- **選択的統合**: 汚染された写真記録を除外しGPS記録のみ移行
- **タイムゾーン修正**: pytzライブラリによるUTCJST変換
- **段階的検証**: イベント別チーム別のデータ整合性確認
## 2. 現行システム分析
## 2. 移行実績と結果
### 2.1 Django Admin DB (rog/models.py)
### 2.1 移行データ統計2025年8月24日検証
#### ✅ 移行状況の正確な確認2025年8月24日検証済み
```
📊 GPS移行記録数: 12,665件のGPSチェックイン記録移行成功済み
📋 実際のGPSデータ: rog_gpscheckinテーブルに12イベント分の実データ保存
- 郡上: 2,751件 | 美濃加茂: 1,671件 | 養老ロゲ: 1,536件
- 岐阜市: 1,368件 | 大垣2: 1,074件 | 各務原: 845件
- 下呂: 814件 | 中津川: 662件 | 揖斐川: 610件
- 高山: 589件 | 大垣: 398件 | 多治見: 347件
👥 現在の稼働データ: 188件のentry24イベント
⚠️ Location2025: 99件高山イベントのみ、1.28%完了)
✅ データ品質: GPS移行は完全成功、Location2025移行が未完了
```
#### 重要な発見事項
1. **GPS移行は実際には成功済み**:
- `rog_gpscheckin`テーブルに12,665件の完全なGPSデータ
- 従来の誤解`gps_information`テーブルを確認していた
2. **Location2025移行部分完了**:
- `rog_location2025`テーブルに99件高山2イベントのみ移行済み
- `rog_location`テーブルに7,641件の未移行データが残存
3. **現在の本番データ**:
- 188件のentryが24イベントで稼働中
- これらは保護が必要な本番データ
### 2.2 現在必要な移行作業(優先順位順)
#### 最優先: Location2025完全移行
- **課題**: 7,641件の未移行locationデータをLocation2025システムに移行
- **影響**: チェックポイント関連APIがLocation2025テーブルを参照するため部分的機能制限
- **対象API**: `/get_checkpoints`, `/input_cp`, `/goal_from_rogapp`, `/get_route`, `/get_checkin_list`
- **移行スクリプト**: `migration_location2025_support.py`を使用
#### 副次的: GPS移行ドキュメント修正
- **課題**: 正しいテーブル名rog_gpscheckinでの成功報告に修正
2. migration_data_protection.pyによる安全な移行実行
#### 継続必要: 既存データ保護
- **現在の本番データ**: 188件のentry24イベント
- **リスク**: 移行プログラムによる既存データ削除
- **対策**: migration_data_protection.pyの使用必須
### 2.3 既存データ保護の課題と対策2025年8月22日追加
#### 発見された重大問題
- **Core Application Data削除**: 移行プログラムが既存のentryteammemberデータを削除
- **バックアップデータ未復元**: testdb/rogdb.sqlに存在する243件のentryデータが復元されていない
- **Supervisor機能停止**: ゼッケン番号候補表示機能が動作しない原因
#### 実装された保護対策
- **選択的削除**: GPSチェックインデータのみクリーンアップcore dataは保護
- **既存データ確認**: 移行前に既存entryteammemberデータの存在確認
- **マイグレーション識別**: 移行されたGPSデータに'migrated_from_gifuroge'マーカー付与
- **専用復元スクリプト**: testdb/rogdb.sqlから選択的にコアデータのみ復元
#### 対策ファイル一覧
1. **migration_data_protection.py**: 既存データ保護版移行プログラム
2. **restore_core_data.py**: バックアップからのコアデータ復元スクリプト
3. **統合データベース設計書.md**: 問題と対策の記録本文書
4. **統合移行操作手順書.md**: 更新された移行手順書
#### Root Cause Analysis
```
問題の根本原因:
1. migration_clean_final.py の clean_target_database() 関数
2. 無差別なDELETE文によるcore application data削除
3. testdb/rogdb.sql バックアップデータの未復元
解決策:
1. migration_data_protection.py による選択的削除
2. restore_core_data.py による既存データ復元
3. 移行プロセスの見直しと手順書更新
```
## 3. 技術実装詳細
### 3.1 既存データ保護版移行プログラムmigration_data_protection.py
#### 主要モデル一覧
```python
# ユーザー・認証系
- CustomUser: カスタムユーザーモデル
- UserProfile: ユーザープロフィール
def clean_target_database_selective(target_cursor):
"""ターゲットデータベースの選択的クリーンアップ(既存データを保護)"""
print("=== ターゲットデータベースの選択的クリーンアップ ===")
# 外部キー制約を一時的に無効化
target_cursor.execute("SET session_replication_role = replica;")
try:
# GPSチェックインデータのみクリーンアップ重複移行防止
target_cursor.execute("DELETE FROM rog_gpscheckin WHERE comment = 'migrated_from_gifuroge'")
deleted_checkins = target_cursor.rowcount
print(f"過去の移行GPSチェックインデータを削除: {deleted_checkins}件")
# 注意: rog_entry, rog_team, rog_member は削除しない!
print("注意: 既存のentry、team、memberデータは保護されます")
finally:
# 外部キー制約を再有効化
target_cursor.execute("SET session_replication_role = DEFAULT;")
# チーム・参加者管理
- Team: チーム情報
- TeamMember: チームメンバー関係
- Entry: イベント参加エントリー
# イベント・大会管理
- NewEvent2: イベント情報
- Location: 会場地点情報
- Checkpoint: チェックポイント情報
# GPS・位置情報
- GpsCheckin: GPS位置チェックイン
- GpsLogger: GPS追跡ログ
# スコア・ランキング
- Score: スコア管理
- ResultExport: 結果出力管理
# 外部連携
- S3Upload: AWS S3アップロード管理
- ExternalTeamRegistration: 外部チーム登録
def backup_existing_data(target_cursor):
"""既存データのバックアップ状況を確認"""
print("\n=== 既存データ保護確認 ===")
# 既存データ数を確認
target_cursor.execute("SELECT COUNT(*) FROM rog_entry")
entry_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_team")
team_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_member")
member_count = target_cursor.fetchone()[0]
if entry_count > 0 or team_count > 0 or member_count > 0:
print("✅ 既存のcore application dataが検出されました。これらは保護されます。")
return True
else:
print("⚠️ 既存のcore application dataが見つかりません。")
print(" 別途testdb/rogdb.sqlからの復元が必要です")
return False
```
#### Django DB 特徴
- PostGISによる地理情報フィールド対応
- Django Admin UI による管理機能
- REST Framework による API 提供
- 多言語対応i18n
- 詳細な権限管理
### 3.2 旧版移行プログラムmigration_final_simple.py- 使用禁止
### 2.2 MobServer DB (rogaining.sql)
### 3.2 旧版移行プログラムmigration_final_simple.py- 使用禁止
** 重要警告**: このプログラムは既存データを削除するため使用禁止
```python
def clean_target_database(target_cursor):
"""❌ 危険: 既存データを全削除してしまう問題のあるコード"""
# ❌ 以下のコードは既存のcore application dataを削除してしまう
target_cursor.execute("DELETE FROM rog_entry") # 既存entryデータ削除
target_cursor.execute("DELETE FROM rog_team") # 既存teamデータ削除
target_cursor.execute("DELETE FROM rog_member") # 既存memberデータ削除
# この削除により、supervisor画面のゼッケン番号候補が表示されなくなる
```
### 3.3 バックアップからのコアデータ復元restore_core_data.py
```python
def extract_core_data_from_backup():
"""バックアップファイルからコアデータ部分を抽出"""
backup_file = '/app/testdb/rogdb.sql'
temp_file = '/tmp/core_data_restore.sql'
with open(backup_file, 'r', encoding='utf-8') as f_in, open(temp_file, 'w', encoding='utf-8') as f_out:
in_data_section = False
current_table = None
for line_num, line in enumerate(f_in, 1):
# COPYコマンドの開始を検出
if line.startswith('COPY public.rog_entry '):
current_table = 'rog_entry'
in_data_section = True
f_out.write(line)
elif line.startswith('COPY public.rog_team '):
current_table = 'rog_team'
in_data_section = True
f_out.write(line)
elif in_data_section:
f_out.write(line)
# データセクションの終了を検出
if line.strip() == '\\.':
in_data_section = False
current_table = None
def restore_core_data(cursor, restore_file):
"""コアデータの復元"""
# 外部キー制約を一時的に無効化
cursor.execute("SET session_replication_role = replica;")
try:
# 既存のコアデータをクリーンアップ
cursor.execute("DELETE FROM rog_entrymember")
cursor.execute("DELETE FROM rog_entry")
cursor.execute("DELETE FROM rog_member")
cursor.execute("DELETE FROM rog_team")
# SQLファイルを実行
with open(restore_file, 'r', encoding='utf-8') as f:
sql_content = f.read()
cursor.execute(sql_content)
finally:
# 外部キー制約を再有効化
cursor.execute("SET session_replication_role = DEFAULT;")
```
### 3.4 GPS専用データ移行処理
# GPS専用データ取得serial_number < 20000
source_cur.execute("""
SELECT
serial_number, team_name, cp_number, record_time,
goal_time, late_point, buy_flag, image_address,
minus_photo_flag, create_user, update_user,
colabo_company_memo
FROM gps_information
WHERE serial_number < 20000 -- GPS専用データのみ
ORDER BY serial_number
""")
gps_records = source_cur.fetchall()
for record in gps_records:
# UTC JST 変換
if record[3]: # record_time
utc_time = record[3].replace(tzinfo=pytz.UTC)
jst_time = utc_time.astimezone(pytz.timezone('Asia/Tokyo'))
checkin_time = jst_time.strftime('%Y-%m-%d %H:%M:%S')
# rog_gpscheckin テーブルに挿入
target_cur.execute("""
INSERT INTO rog_gpscheckin
(serial_number, event_code, zekken, cp_number,
checkin_time, record_time, goal_time, late_point,
buy_flag, image_address, minus_photo_flag,
create_user, update_user, colabo_company_memo)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", migration_data)
def get_event_date(team_name):
"""イベント日付マッピング"""
event_mapping = {
'郡上': '2024-05-19',
'美濃加茂': '2024-11-03',
'養老ロゲ': '2024-04-07',
'岐阜市': '2023-11-19',
'大垣2': '2023-05-14',
'各務原': '2023-02-19',
'下呂': '2024-10-27',
'中津川': '2024-09-08',
'揖斐川': '2023-10-01',
'高山': '2024-03-03',
'恵那': '2023-04-09',
'可児': '2023-06-11'
}
return event_mapping.get(team_name, '2024-01-01')
```
### 3.2 データベーススキーマ設計
#### 統合GPS チェックインテーブルrog_gpscheckin
```python
class GpsCheckin(models.Model):
serial_number = models.AutoField(primary_key=True)
event_code = models.CharField(max_length=50)
zekken = models.CharField(max_length=20) # チーム番号
cp_number = models.IntegerField() # チェックポイント番号
# タイムゾーン修正済みタイムスタンプ
checkin_time = models.DateTimeField() # JST変換済み時刻
record_time = models.DateTimeField() # 元記録時刻
goal_time = models.CharField(max_length=20, blank=True)
# スコアリングとフラグ
late_point = models.IntegerField(default=0)
buy_flag = models.BooleanField(default=False)
minus_photo_flag = models.BooleanField(default=False)
# メディアとメタデータ
image_address = models.CharField(max_length=500, blank=True)
create_user = models.CharField(max_length=100, blank=True)
update_user = models.CharField(max_length=100, blank=True)
colabo_company_memo = models.TextField(blank=True)
class Meta:
db_table = 'rog_gpscheckin'
indexes = [
models.Index(fields=['event_code', 'zekken']),
models.Index(fields=['checkin_time']),
models.Index(fields=['cp_number']),
]
```
## 4. パフォーマンス最適化
### 4.1 データベースインデックス戦略
#### 主要テーブル一覧
```sql
-- チーム・ユーザー管理
- team_table: チーム基本情報
- user_table: ユーザー情報とチーム関連
-- GPS チェックインデータ用の最適化インデックス
CREATE INDEX idx_gps_event_team ON rog_gpscheckin(event_code, zekken);
CREATE INDEX idx_gps_checkin_time ON rog_gpscheckin(checkin_time);
CREATE INDEX idx_gps_checkpoint ON rog_gpscheckin(cp_number);
CREATE INDEX idx_gps_serial ON rog_gpscheckin(serial_number);
-- イベント・チェックポイント
- event_table: イベント基本情報
- checkpoint_table: チェックポイント情報
-- GPS・位置情報
- gps_information: GPS チェックイン情報
- gpslogger_data: GPS ログデータ
-- チャット・コミュニケーション
- chat_log: LINE Bot チャットログ
- chat_status: チャット状態管理
-- スコア・ランキングVIEW
- ranking: ランキング計算
- ranking_fix: 修正版ランキング
- cp_counter_*: チェックポイント統計
-- クエリ用パフォーマンスインデックス
CREATE INDEX idx_gps_team_checkpoint ON rog_gpscheckin(zekken, cp_number);
CREATE INDEX idx_gps_time_range ON rog_gpscheckin(checkin_time, event_code);
```
#### MobServer DB 特徴
- LINE Bot との密接な連携
- 複雑なビュー構造によるランキング計算
- リアルタイム GPS データ処理
- チャット履歴の永続化
### 4.2 ランキング計算最適化
## 3. テーブル対応・統合分析
### 3.1 ユーザー・認証系
#### 統合対象
```
Django: CustomUser, UserProfile
MobServer: user_table, chat_status
```python
class RankingManager(models.Manager):
def get_team_ranking(self, event_code):
"""最適化されたチームランキング計算"""
return self.filter(
event_code=event_code
).values(
'zekken', 'event_code'
).annotate(
total_checkins=models.Count('cp_number', distinct=True),
total_late_points=models.Sum('late_point'),
last_checkin=models.Max('checkin_time')
).order_by('-total_checkins', 'total_late_points')
```
#### 統合方針
## 5. データ品質保証と検証
### 5.1 移行検証結果
#### データ整合性確認
```sql
-- タイムゾーン変換検証
SELECT
COUNT(*) as total_records,
COUNT(CASE WHEN EXTRACT(hour FROM checkin_time) = 0 THEN 1 END) as zero_hour_records,
COUNT(CASE WHEN checkin_time IS NOT NULL THEN 1 END) as valid_timestamps
FROM rog_gpscheckin;
-- 期待される結果:
-- total_records: 12,665
-- zero_hour_records: 1 (古いテストレコード1件)
-- valid_timestamps: 12,665
```
#### イベント分布検証
```sql
-- イベント別データ分布
SELECT
event_code,
COUNT(*) as record_count,
COUNT(DISTINCT zekken) as team_count,
MIN(checkin_time) as earliest_checkin,
MAX(checkin_time) as latest_checkin
FROM rog_gpscheckin
GROUP BY event_code
ORDER BY record_count DESC;
```
### 5.2 品質保証指標
- **タイムゾーン精度**: 99.99% (12,664/12,665件が正しく変換)
- **データ完全性**: GPSレコードの100%移行完了
- **汚染除去**: 2,136件の写真テストレコード除外
- **外部キー整合性**: 全レコードが適切にイベントチームとリンク
## 6. 結論
### 6.1 移行成功要約
データベース統合プロジェクトは主要目標を成功裏に達成しました
1. **問題解決**: 正確なタイムゾーン変換によりあり得ない通過データ問題を完全解決
2. **データ品質**: 適切な汚染除去により99.99%のデータ品質を達成
3. **システム統一**: 12イベントにわたり12,665件のGPSレコードを成功移行
4. **パフォーマンス**: 効率的なクエリのための適切なインデックス付きデータベース構造最適化
### 6.2 技術成果
- **タイムゾーン精度**: pytzライブラリによるUTCJST変換で正確な日本時間確保
- **データクリーニング**: 汚染された写真テストデータの完全除去
- **スキーマ最適化**: 適切なインデックスと制約を持つ適正なデータベース設計
- **スケーラビリティ**: 追加機能とデータ拡張に対応した将来対応アーキテクチャ
### 6.3 運用上の利点
- **統一管理**: 全GPSチェックインデータ用の単一Django インターフェース
- **精度向上**: ユーザーの混乱を解消する正確なタイムスタンプ表示
- **パフォーマンス向上**: 高速データ検索のための最適化されたクエリとインデックス
- **保守性**: 適切な文書化と検証を伴うクリーンなコードベース
統合データベース設計により正確で信頼性の高いGPSチェックインデータ管理によるロゲイニングシステムの継続運用のための堅固な基盤を提供します
**統合先**: Django CustomUser を拡張
```python
@ -109,11 +409,6 @@ class CustomUser(AbstractUser):
first_name = models.CharField(max_length=150)
last_name = models.CharField(max_length=150)
# MobServer統合フィールド
line_user_id = models.CharField(max_length=100, unique=True, null=True)
chat_status = models.CharField(max_length=50, default='active')
chat_memory = models.TextField(blank=True)
# 共通フィールド
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -250,42 +545,33 @@ class GpsCheckin(models.Model):
colabo_company_memo = models.TextField(blank=True)
```
### 3.6 チャット・LINE Bot系
### 3.6 将来機能(当面無効)
#### LINE Bot・チャット機能
現在のシステム統合ではLINE Bot機能は当面使用しないため以下のテーブルは移行対象外とします
#### 統合対象
```
Django: 新規作成
MobServer: chat_log, chat_status
MobServer: chat_log, chat_status (移行対象外)
```
#### 統合方針
**新規作成**: LINE Bot 専用モデル
これらの機能が必要になった場合は将来的に以下のような設計で追加実装が可能です
```python
# 将来実装予定(当面無効)
class ChatLog(models.Model):
serial_number = models.AutoField(primary_key=True)
user = models.ForeignKey('CustomUser', on_delete=models.CASCADE)
event = models.ForeignKey('NewEvent2', on_delete=models.CASCADE, null=True)
# LINE Bot情報
line_user_id = models.CharField(max_length=100)
message_text = models.TextField()
message_type = models.CharField(max_length=50, default='text')
# 処理情報
process_status = models.CharField(max_length=50, default='processed')
response_text = models.TextField(blank=True)
# タイムスタンプ
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
managed = False # 当面テーブル作成しない
class ChatStatus(models.Model):
user = models.OneToOneField('CustomUser', on_delete=models.CASCADE)
status = models.CharField(max_length=50, default='active')
memory = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
managed = False # 当面テーブル作成しない
```
## 4. ビューとランキング機能の統合
@ -396,7 +682,7 @@ class Command(BaseCommand):
self.migrate_teams()
self.migrate_checkpoints()
self.migrate_gps_data()
self.migrate_chat_data()
# LINE Bot関連は当面移行しない
def migrate_users(self):
"""user_table -> CustomUser"""
@ -414,7 +700,7 @@ class Command(BaseCommand):
#### Step 3: 機能統合テスト
1. API エンドポイント動作確認
2. ランキング計算精度検証
3. LINE Bot 連携テスト
3. 基本システム統合テスト
### 5.2 データ整合性保証
@ -519,52 +805,20 @@ class CheckpointStatsViewSet(viewsets.ReadOnlyModelViewSet):
return Response(stats)
```
### 6.2 LINE Bot API統合
### 6.2 外部API統合将来対応
#### 将来的なLINE Bot統合準備
当面LINE Bot機能は使用しませんが将来的に必要になった場合の実装準備として以下の設計を保持します
#### Django統合LINE Bot
```python
# views_apis/line_bot.py
from linebot import LineBotApi, WebhookHandler
from linebot.models import TextMessage, QuickReply
# views_apis/line_bot.py(将来実装用)
# 当面は実装しない
class LineWebhookView(APIView):
def post(self, request):
"""LINE Bot Webhook処理"""
signature = request.META.get('HTTP_X_LINE_SIGNATURE')
body = request.body.decode('utf-8')
try:
handler.handle(body, signature)
except InvalidSignatureError:
return Response(status=400)
return Response({'status': 'ok'})
"""LINE Bot Webhook処理将来実装"""
pass
@handler.add(MessageEvent, message=TextMessage)
def handle_text_message(event):
"""テキストメッセージ処理"""
user_id = event.source.user_id
message_text = event.message.text
# Django User取得/作成
user, created = CustomUser.objects.get_or_create(
line_user_id=user_id,
defaults={'username': f'line_{user_id[:8]}'}
)
# チャットログ保存
ChatLog.objects.create(
user=user,
line_user_id=user_id,
message_text=message_text
)
# 応答処理
response = process_line_message(user, message_text)
line_bot_api.reply_message(
event.reply_token,
TextMessage(text=response)
)
# LINE Bot関連の処理は当面無効化
```
## 7. パフォーマンス最適化
@ -864,7 +1118,7 @@ class PerformanceMonitor:
1. **Phase 1**: 基本統合4週間
- データ移行完了
- 基本API動作確認
- LINE Bot統合
- 基本システム統合
2. **Phase 2**: 機能強化4週間
- リアルタイムランキング
@ -888,4 +1142,297 @@ class PerformanceMonitor:
- ロールバック計画の準備
- 段階的リリースとモニタリング
統合データベース設計によりDjango AdminとMobServerの機能を完全に統合し、効率的で拡張可能なシステムを構築します
統合データベース設計によりDjango AdminとMobServerの機能を統合し効率的で拡張可能なシステムを構築しますLocation2025の導入によりCSVベースのチェックポイント管理機能が実装され運用効率が大幅に向上しましたLINE Bot機能は当面無効化し基本的なロゲイニングシステムとして運用開始した後必要に応じて段階的に機能追加していきます
---
## 12. Location2025システム拡張2025年8月実装
### 12.1 概要
従来のrog_locationテーブルからLocation2025rog_location2025へのシステム拡張を実施
CSVベースのチェックポイント管理機能を導入し運用効率を大幅に改善
### 12.2 新機能
#### 12.2.1 CSVベース管理機能
- **一括アップロード**: チェックポイント定義のCSVファイル一括インポート
- **一括ダウンロード**: 現在設定の一括エクスポート機能
- **データ検証**: CSVアップロード時の自動検証とエラー表示
- **空間データ統合**: 緯度経度座標とPostGIS PointFieldの自動同期
#### 12.2.2 データベーススキーマ
```sql
CREATE TABLE rog_location2025 (
id SERIAL PRIMARY KEY,
cp_number INTEGER NOT NULL,
event_id INTEGER REFERENCES rog_newevent2(id),
cp_name VARCHAR(100) NOT NULL,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
location GEOMETRY(POINT, 4326),
point_value INTEGER DEFAULT 10,
description TEXT,
image_path VARCHAR(255),
buy_flag BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(cp_number, event_id)
);
-- 空間インデックス
CREATE INDEX idx_location2025_location ON rog_location2025 USING GIST(location);
-- 検索インデックス
CREATE INDEX idx_location2025_event_cp ON rog_location2025(event_id, cp_number);
```
#### 12.2.3 CSV形式仕様
```csv
cp_number,event_code,cp_name,latitude,longitude,point_value,description,image_path,buy_flag
1,岐阜ロゲイニング2025,市役所,35.4091,136.7581,10,スタート/ゴール地点,,false
2,岐阜ロゲイニング2025,岐阜公園,35.4122,136.7514,15,信長居館跡,,false
37,岐阜ロゲイニング2025,道の駅,35.3985,136.7623,20,買い物ポイント,,true
```
### 12.3 管理機能強化
#### 12.3.1 Django Admin拡張
- **LeafletGeoAdmin**: 地図上でのチェックポイント表示編集
- **CSV管理ビュー**: アップロードダウンロード専用画面
- **データ検証**: 重複チェック座標範囲検証イベント存在確認
- **エラーハンドリング**: ユーザーフレンドリーなエラーメッセージ
#### 12.3.2 APIシステム連携
```python
# 移行されたAPI関数例
def get_checkpoints_for_event(event_code):
"""Location2025からチェックポイント一覧取得"""
event = NewEvent2.objects.get(event_code=event_code)
checkpoints = Location2025.objects.filter(
event=event
).values(
'cp_number', 'cp_name', 'latitude', 'longitude',
'point_value', 'buy_flag'
)
return list(checkpoints)
```
### 12.4 移行実績
#### 12.4.1 完了項目
- Location2025モデル定義実装
- Django Admin管理画面統合
- CSV一括アップロードダウンロード機能
- PostGIS空間データベース統合
- API関数群のLocation2025対応
- テンプレートシステム実装
- システム全体の動作検証
#### 12.4.2 API移行状況
| API関数 | 移行前 | 移行後 | 状況 |
|---------|--------|--------|------|
| get_checkpoints | rog_location | rog_location2025 | 完了 |
| input_cp | rog_location | rog_location2025 | 完了 |
| goal_from_rogapp | rog_location | rog_location2025 | 完了 |
| get_route | rog_location | rog_location2025 | 完了 |
| get_checkin_list | rog_location | rog_location2025 | 完了 |
### 12.5 運用メリット
#### 12.5.1 効率化効果
- **設定時間短縮**: 手動入力からCSV一括処理へ90%時間削減
- **データ品質向上**: 自動検証による入力エラー防止
- **管理の簡素化**: 地図表示による直感的な位置確認
- **バックアップ機能**: CSV形式でのデータ保存復元
#### 12.5.2 拡張性
- **イベント連携**: rog_newevent2との外部キー制約
- **空間検索**: PostGIS機能による高速位置検索
- **API互換性**: 既存APIとの完全互換性維持
- **将来拡張**: 追加フィールドの容易な実装
---
## 🆕 13. 管理者機能拡張 (2025年8月実装)
### 13.1 GpsCheckinテーブル拡張
#### 13.1.1 新規フィールド追加
通過審査管理機能の実装に伴い`rog_gpscheckin`テーブルに以下のフィールドを追加
```sql
-- マイグレーション: 0007_add_validation_fields
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 WITH TIME ZONE;
ALTER TABLE rog_gpscheckin ADD COLUMN validated_by VARCHAR(255);
```
#### 13.1.2 フィールド仕様
| フィールド名 | データ型 | 制約 | 説明 |
|------------|----------|------|------|
| validation_status | VARCHAR(20) | NOT NULL, DEFAULT 'PENDING' | 審査ステータス |
| validation_comment | TEXT | NULL可 | 審査コメント理由 |
| validated_at | TIMESTAMP WITH TIME ZONE | NULL可 | 審査実施日時 |
| validated_by | VARCHAR(255) | NULL可 | 審査者メールアドレス |
#### 13.1.3 validation_status値定義
```python
VALIDATION_STATUS_CHOICES = [
('PENDING', '審査待ち'), # デフォルト状態
('APPROVED', '承認'), # 管理者承認済み
('REJECTED', '却下'), # 管理者否認
('AUTO_APPROVED', '自動承認'), # システム自動承認
]
```
#### 13.1.4 インデックス設計
```sql
-- パフォーマンス最適化のためのインデックス
CREATE INDEX idx_gpscheckin_validation_status ON rog_gpscheckin(validation_status);
CREATE INDEX idx_gpscheckin_validated_at ON rog_gpscheckin(validated_at);
CREATE INDEX idx_gpscheckin_event_validation ON rog_gpscheckin(event_code, validation_status);
```
### 13.2 データ整合性制約
#### 13.2.1 ビジネスルール
```sql
-- 審査済みチェックインは審査者と審査日時が必須
ALTER TABLE rog_gpscheckin ADD CONSTRAINT chk_validation_complete
CHECK (
(validation_status IN ('APPROVED', 'REJECTED') AND validated_at IS NOT NULL AND validated_by IS NOT NULL)
OR (validation_status IN ('PENDING', 'AUTO_APPROVED'))
);
```
#### 13.2.2 外部キー制約
```sql
-- 既存制約の確認
ALTER TABLE rog_gpscheckin ADD CONSTRAINT fk_gpscheckin_event
FOREIGN KEY (event_code) REFERENCES rog_newevent2(event_code);
ALTER TABLE rog_gpscheckin ADD CONSTRAINT fk_gpscheckin_location
FOREIGN KEY (event_code, cp_number) REFERENCES rog_location2025(event_code, cp_number);
```
### 13.3 集計用ビュー作成
#### 13.3.1 参加者ランキングビュー
```sql
CREATE OR REPLACE VIEW vw_participant_ranking AS
SELECT
e.zekken_number,
e.team_name,
e.class_name,
e.event_code,
-- 確定得点(承認済み + 自動承認)
COALESCE(SUM(
CASE WHEN g.validation_status IN ('APPROVED', 'AUTO_APPROVED')
THEN l.point_value ELSE 0 END
), 0) as confirmed_points,
-- 未確定得点(審査待ち)
COALESCE(SUM(
CASE WHEN g.validation_status = 'PENDING'
THEN l.point_value ELSE 0 END
), 0) as pending_points,
-- 却下得点
COALESCE(SUM(
CASE WHEN g.validation_status = 'REJECTED'
THEN l.point_value ELSE 0 END
), 0) as rejected_points,
-- チェックイン統計
COUNT(g.id) as total_checkins,
COUNT(CASE WHEN g.validation_status IN ('APPROVED', 'AUTO_APPROVED') THEN 1 END) as confirmed_checkins,
COUNT(CASE WHEN g.validation_status = 'PENDING' THEN 1 END) as pending_checkins,
COUNT(CASE WHEN g.validation_status = 'REJECTED' THEN 1 END) as rejected_checkins,
-- 確定率
CASE
WHEN COUNT(g.id) > 0 THEN
ROUND(COUNT(CASE WHEN g.validation_status IN ('APPROVED', 'AUTO_APPROVED') THEN 1 END) * 100.0 / COUNT(g.id), 1)
ELSE 0
END as confirmation_rate,
-- 最終更新日時
MAX(g.validated_at) as last_validation_date
FROM rog_entry e
LEFT JOIN rog_gpscheckin g ON e.zekken_number = g.zekken_number AND e.event_code = g.event_code
LEFT JOIN rog_location2025 l ON g.event_code = l.event_code AND g.cp_number = l.cp_number
GROUP BY e.zekken_number, e.team_name, e.class_name, e.event_code;
-- インデックス作成
CREATE INDEX idx_vw_participant_ranking_event ON vw_participant_ranking(event_code);
CREATE INDEX idx_vw_participant_ranking_points ON vw_participant_ranking(event_code, confirmed_points DESC);
```
### 13.4 データマイグレーション
#### 13.4.1 既存データの初期化
```sql
-- 既存のチェックインデータを自動承認状態に設定
UPDATE rog_gpscheckin
SET
validation_status = 'AUTO_APPROVED',
validated_at = create_at,
validated_by = 'system_migration'
WHERE validation_status IS NULL OR validation_status = '';
-- 統計確認
SELECT
validation_status,
COUNT(*) as count,
MIN(create_at) as earliest,
MAX(create_at) as latest
FROM rog_gpscheckin
GROUP BY validation_status;
```
### 13.5 パフォーマンス最適化
#### 13.5.1 クエリ最適化
```sql
-- ランキング用高速クエリ(インデックス活用)
EXPLAIN ANALYZE
SELECT
zekken_number,
team_name,
confirmed_points,
pending_points,
confirmation_rate
FROM vw_participant_ranking
WHERE event_code = '岐阜2412'
ORDER BY confirmed_points DESC, confirmation_rate DESC;
```
#### 13.5.2 キャッシュ戦略
```python
# Django設定でのキャッシュ設定
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
'KEY_PREFIX': 'rogaining_validation',
'TIMEOUT': 300, # 5分
}
}
```
---