initial setting at 20-Aug-2025
This commit is contained in:
891
統合データベース設計書.md
Normal file
891
統合データベース設計書.md
Normal file
@ -0,0 +1,891 @@
|
||||
# 統合データベース設計書
|
||||
|
||||
## 1. 概要
|
||||
|
||||
### 1.1 目的
|
||||
現在運用されているDjango Admin DBとMobServer DBを統合し、一元的なデータ管理システムを構築する。システム停止中であるため、マイグレーション期間を考慮せず、直接統合を実施する。
|
||||
|
||||
### 1.2 基本方針
|
||||
- Django Admin DBをメインデータベースとして位置づけ
|
||||
- MobServerの機能をDjangoベースに統合
|
||||
- PostGISによる地理情報管理を継続
|
||||
- 既存データの完全保持
|
||||
|
||||
### 1.3 統合アプローチ
|
||||
- **完全統合アプローチ**: MobServer DBの全テーブルをDjango管理下に移行
|
||||
- **機能重複解消**: 同一機能の重複テーブル・フィールドを統一
|
||||
- **データ型統一**: Django Modelsの型システムに準拠
|
||||
|
||||
## 2. 現行システム分析
|
||||
|
||||
### 2.1 Django Admin DB (rog/models.py)
|
||||
|
||||
#### 主要モデル一覧
|
||||
```python
|
||||
# ユーザー・認証系
|
||||
- CustomUser: カスタムユーザーモデル
|
||||
- UserProfile: ユーザープロフィール
|
||||
|
||||
# チーム・参加者管理
|
||||
- Team: チーム情報
|
||||
- TeamMember: チームメンバー関係
|
||||
- Entry: イベント参加エントリー
|
||||
|
||||
# イベント・大会管理
|
||||
- NewEvent2: イベント情報
|
||||
- Location: 会場・地点情報
|
||||
- Checkpoint: チェックポイント情報
|
||||
|
||||
# GPS・位置情報
|
||||
- GpsCheckin: GPS位置チェックイン
|
||||
- GpsLogger: GPS追跡ログ
|
||||
|
||||
# スコア・ランキング
|
||||
- Score: スコア管理
|
||||
- ResultExport: 結果出力管理
|
||||
|
||||
# 外部連携
|
||||
- S3Upload: AWS S3アップロード管理
|
||||
- ExternalTeamRegistration: 外部チーム登録
|
||||
```
|
||||
|
||||
#### Django DB 特徴
|
||||
- PostGISによる地理情報フィールド対応
|
||||
- Django Admin UI による管理機能
|
||||
- REST Framework による API 提供
|
||||
- 多言語対応(i18n)
|
||||
- 詳細な権限管理
|
||||
|
||||
### 2.2 MobServer DB (rogaining.sql)
|
||||
|
||||
#### 主要テーブル一覧
|
||||
```sql
|
||||
-- チーム・ユーザー管理
|
||||
- team_table: チーム基本情報
|
||||
- user_table: ユーザー情報とチーム関連
|
||||
|
||||
-- イベント・チェックポイント
|
||||
- event_table: イベント基本情報
|
||||
- checkpoint_table: チェックポイント情報
|
||||
|
||||
-- GPS・位置情報
|
||||
- gps_information: GPS チェックイン情報
|
||||
- gpslogger_data: GPS ログデータ
|
||||
|
||||
-- チャット・コミュニケーション
|
||||
- chat_log: LINE Bot チャットログ
|
||||
- chat_status: チャット状態管理
|
||||
|
||||
-- スコア・ランキング(VIEW)
|
||||
- ranking: ランキング計算
|
||||
- ranking_fix: 修正版ランキング
|
||||
- cp_counter_*: チェックポイント統計
|
||||
```
|
||||
|
||||
#### MobServer DB 特徴
|
||||
- LINE Bot との密接な連携
|
||||
- 複雑なビュー構造によるランキング計算
|
||||
- リアルタイム GPS データ処理
|
||||
- チャット履歴の永続化
|
||||
|
||||
## 3. テーブル対応・統合分析
|
||||
|
||||
### 3.1 ユーザー・認証系
|
||||
|
||||
#### 統合対象
|
||||
```
|
||||
Django: CustomUser, UserProfile
|
||||
MobServer: user_table, chat_status
|
||||
```
|
||||
|
||||
#### 統合方針
|
||||
**統合先**: Django CustomUser を拡張
|
||||
|
||||
```python
|
||||
class CustomUser(AbstractUser):
|
||||
# 既存フィールド
|
||||
username = models.CharField(max_length=150, unique=True)
|
||||
email = models.EmailField(unique=True)
|
||||
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)
|
||||
```
|
||||
|
||||
### 3.2 チーム管理系
|
||||
|
||||
#### 統合対象
|
||||
```
|
||||
Django: Team, TeamMember, Entry
|
||||
MobServer: team_table
|
||||
```
|
||||
|
||||
#### 統合方針
|
||||
**統合先**: Django Team モデルを拡張
|
||||
|
||||
```python
|
||||
class Team(models.Model):
|
||||
# 既存フィールド
|
||||
name = models.CharField(max_length=100)
|
||||
event = models.ForeignKey('NewEvent2', on_delete=models.CASCADE)
|
||||
category = models.CharField(max_length=50)
|
||||
|
||||
# MobServer統合フィールド
|
||||
zekken_number = models.CharField(max_length=20, unique=True) # team_table.zekken_number
|
||||
password = models.CharField(max_length=100) # team_table.password
|
||||
class_name = models.CharField(max_length=100) # team_table.class_name
|
||||
|
||||
# 地理情報
|
||||
location = models.PointField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['zekken_number', 'event']
|
||||
```
|
||||
|
||||
### 3.3 イベント管理系
|
||||
|
||||
#### 統合対象
|
||||
```
|
||||
Django: NewEvent2, Location
|
||||
MobServer: event_table
|
||||
```
|
||||
|
||||
#### 統合方針
|
||||
**統合先**: Django NewEvent2 を拡張
|
||||
|
||||
```python
|
||||
class NewEvent2(models.Model):
|
||||
# 既存フィールド
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
start_date = models.DateTimeField()
|
||||
end_date = models.DateTimeField()
|
||||
|
||||
# MobServer統合フィールド
|
||||
event_code = models.CharField(max_length=50, unique=True) # event_table.event_code
|
||||
start_time = models.CharField(max_length=20) # event_table.start_time
|
||||
event_day = models.CharField(max_length=20) # event_table.event_day
|
||||
|
||||
# 会場情報統合
|
||||
venue_location = models.PointField(null=True, blank=True)
|
||||
venue_address = models.CharField(max_length=500, blank=True)
|
||||
```
|
||||
|
||||
### 3.4 チェックポイント管理系
|
||||
|
||||
#### 統合対象
|
||||
```
|
||||
Django: Checkpoint
|
||||
MobServer: checkpoint_table
|
||||
```
|
||||
|
||||
#### 統合方針
|
||||
**統合先**: Django Checkpoint を拡張
|
||||
|
||||
```python
|
||||
class Checkpoint(models.Model):
|
||||
# MobServer完全統合
|
||||
cp_number = models.IntegerField() # checkpoint_table.cp_number
|
||||
event = models.ForeignKey('NewEvent2', on_delete=models.CASCADE)
|
||||
cp_name = models.CharField(max_length=200) # checkpoint_table.cp_name
|
||||
|
||||
# 位置情報(PostGIS対応)
|
||||
location = models.PointField() # latitude, longitude統合
|
||||
|
||||
# ポイント情報
|
||||
photo_point = models.IntegerField(default=0) # checkpoint_table.photo_point
|
||||
buy_point = models.IntegerField(default=0) # checkpoint_table.buy_point
|
||||
|
||||
# サンプル・メモ
|
||||
sample_photo = models.CharField(max_length=500, blank=True)
|
||||
colabo_company_memo = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['cp_number', 'event']
|
||||
```
|
||||
|
||||
### 3.5 GPS・位置情報系
|
||||
|
||||
#### 統合対象
|
||||
```
|
||||
Django: GpsCheckin, GpsLogger
|
||||
MobServer: gps_information, gpslogger_data
|
||||
```
|
||||
|
||||
#### 統合方針
|
||||
**統合先**: Django GpsCheckin を拡張
|
||||
|
||||
```python
|
||||
class GpsCheckin(models.Model):
|
||||
# MobServer統合
|
||||
serial_number = models.AutoField(primary_key=True)
|
||||
team = models.ForeignKey('Team', on_delete=models.CASCADE)
|
||||
checkpoint = models.ForeignKey('Checkpoint', on_delete=models.CASCADE)
|
||||
|
||||
# 画像・証拠
|
||||
image_address = models.CharField(max_length=500, blank=True)
|
||||
|
||||
# タイミング・ペナルティ
|
||||
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)
|
||||
|
||||
# 管理情報
|
||||
create_user = models.CharField(max_length=100, blank=True)
|
||||
update_user = models.CharField(max_length=100, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# コラボ・メモ
|
||||
colabo_company_memo = models.TextField(blank=True)
|
||||
```
|
||||
|
||||
### 3.6 チャット・LINE Bot系
|
||||
|
||||
#### 統合対象
|
||||
```
|
||||
Django: 新規作成
|
||||
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 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)
|
||||
```
|
||||
|
||||
## 4. ビューとランキング機能の統合
|
||||
|
||||
### 4.1 MobServer複雑ビュー分析
|
||||
|
||||
#### 主要ビュー
|
||||
```sql
|
||||
-- 基本ランキング
|
||||
ranking: チーム別総合ポイント計算
|
||||
ranking_fix: 修正版ランキング
|
||||
ranking_fix_for_fc_gifu: 地域特化版
|
||||
|
||||
-- チェックポイント統計
|
||||
cp_counter_*: クラス別チェックポイント通過統計
|
||||
cp_point_usage_summary: ポイント使用状況
|
||||
|
||||
-- GPS詳細
|
||||
gps_detail: GPS + チェックポイント結合
|
||||
gps_detail_fix: 修正版GPS詳細
|
||||
```
|
||||
|
||||
### 4.2 Django実装方針
|
||||
|
||||
#### カスタムマネージャー実装
|
||||
```python
|
||||
class RankingManager(models.Manager):
|
||||
def get_team_ranking(self, event):
|
||||
"""チーム別ランキング計算"""
|
||||
return self.filter(
|
||||
team__event=event
|
||||
).values(
|
||||
'team__zekken_number',
|
||||
'team__name',
|
||||
'team__class_name'
|
||||
).annotate(
|
||||
total_point=models.Sum(
|
||||
models.F('checkpoint__photo_point') +
|
||||
models.F('checkpoint__buy_point') -
|
||||
models.F('late_point')
|
||||
),
|
||||
cp_count=models.Count('checkpoint'),
|
||||
late_point_total=models.Sum('late_point')
|
||||
).order_by('-total_point')
|
||||
|
||||
def get_checkpoint_statistics(self, event, class_name=None):
|
||||
"""チェックポイント統計"""
|
||||
qs = self.filter(team__event=event)
|
||||
if class_name:
|
||||
qs = qs.filter(team__class_name=class_name)
|
||||
|
||||
return qs.values(
|
||||
'checkpoint__cp_number',
|
||||
'checkpoint__cp_name'
|
||||
).annotate(
|
||||
visit_count=models.Count('team', distinct=True)
|
||||
).order_by('checkpoint__cp_number')
|
||||
|
||||
class GpsCheckin(models.Model):
|
||||
# フィールド定義...
|
||||
|
||||
objects = RankingManager()
|
||||
```
|
||||
|
||||
#### キャッシュ戦略
|
||||
```python
|
||||
from django.core.cache import cache
|
||||
|
||||
class RankingService:
|
||||
@staticmethod
|
||||
def get_cached_ranking(event_id, class_name=None):
|
||||
cache_key = f"ranking_{event_id}_{class_name or 'all'}"
|
||||
ranking = cache.get(cache_key)
|
||||
|
||||
if ranking is None:
|
||||
ranking = GpsCheckin.objects.get_team_ranking(
|
||||
event_id=event_id
|
||||
)
|
||||
if class_name:
|
||||
ranking = ranking.filter(team__class_name=class_name)
|
||||
|
||||
cache.set(cache_key, list(ranking), 300) # 5分キャッシュ
|
||||
|
||||
return ranking
|
||||
```
|
||||
|
||||
## 5. データ移行戦略
|
||||
|
||||
### 5.1 移行手順
|
||||
|
||||
#### Step 1: Django Model拡張
|
||||
1. 既存Django Modelsにフィールド追加
|
||||
2. マイグレーションファイル生成・実行
|
||||
3. インデックス最適化
|
||||
|
||||
#### Step 2: データ移行スクリプト
|
||||
```python
|
||||
# management/commands/migrate_mobserver_data.py
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
import psycopg2
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
with transaction.atomic():
|
||||
self.migrate_users()
|
||||
self.migrate_events()
|
||||
self.migrate_teams()
|
||||
self.migrate_checkpoints()
|
||||
self.migrate_gps_data()
|
||||
self.migrate_chat_data()
|
||||
|
||||
def migrate_users(self):
|
||||
"""user_table -> CustomUser"""
|
||||
# MobServer DBからデータ取得
|
||||
# Django Modelに挿入
|
||||
pass
|
||||
|
||||
def migrate_teams(self):
|
||||
"""team_table -> Team"""
|
||||
pass
|
||||
|
||||
# 他の移行メソッド...
|
||||
```
|
||||
|
||||
#### Step 3: 機能統合テスト
|
||||
1. API エンドポイント動作確認
|
||||
2. ランキング計算精度検証
|
||||
3. LINE Bot 連携テスト
|
||||
|
||||
### 5.2 データ整合性保証
|
||||
|
||||
#### 制約条件維持
|
||||
```python
|
||||
class Team(models.Model):
|
||||
# 既存フィールド...
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['zekken_number', 'event'],
|
||||
name='unique_team_per_event'
|
||||
),
|
||||
models.CheckConstraint(
|
||||
check=models.Q(zekken_number__isnull=False),
|
||||
name='zekken_number_required'
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
#### 外部キー関係
|
||||
```python
|
||||
# MobServer の参照整合性をDjangoで再現
|
||||
class GpsCheckin(models.Model):
|
||||
team = models.ForeignKey(
|
||||
'Team',
|
||||
on_delete=models.CASCADE,
|
||||
db_constraint=True # 外部キー制約有効
|
||||
)
|
||||
checkpoint = models.ForeignKey(
|
||||
'Checkpoint',
|
||||
on_delete=models.CASCADE,
|
||||
db_constraint=True
|
||||
)
|
||||
```
|
||||
|
||||
## 6. API統合設計
|
||||
|
||||
### 6.1 Django REST Framework統合
|
||||
|
||||
#### シリアライザー統合
|
||||
```python
|
||||
class TeamSerializer(serializers.ModelSerializer):
|
||||
"""MobServer API互換性維持"""
|
||||
zekken_number = serializers.CharField()
|
||||
team_name = serializers.CharField(source='name')
|
||||
class_name = serializers.CharField()
|
||||
|
||||
# ランキング情報追加
|
||||
total_point = serializers.IntegerField(read_only=True)
|
||||
cp_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = [
|
||||
'zekken_number', 'team_name', 'class_name',
|
||||
'total_point', 'cp_count'
|
||||
]
|
||||
|
||||
class GpsCheckinSerializer(serializers.ModelSerializer):
|
||||
"""GPS チェックイン統合"""
|
||||
cp_number = serializers.IntegerField(source='checkpoint.cp_number')
|
||||
cp_name = serializers.CharField(source='checkpoint.cp_name')
|
||||
photo_point = serializers.IntegerField(source='checkpoint.photo_point')
|
||||
buy_point = serializers.IntegerField(source='checkpoint.buy_point')
|
||||
|
||||
class Meta:
|
||||
model = GpsCheckin
|
||||
fields = [
|
||||
'serial_number', 'cp_number', 'cp_name',
|
||||
'photo_point', 'buy_point', 'late_point',
|
||||
'buy_flag', 'goal_time', 'image_address'
|
||||
]
|
||||
```
|
||||
|
||||
#### ビューセット統合
|
||||
```python
|
||||
class RankingViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ランキングAPI - MobServer互換"""
|
||||
|
||||
def list(self, request):
|
||||
event_code = request.query_params.get('event_code')
|
||||
class_name = request.query_params.get('class_name')
|
||||
|
||||
# キャッシュされたランキング取得
|
||||
ranking = RankingService.get_cached_ranking(
|
||||
event_code, class_name
|
||||
)
|
||||
|
||||
serializer = TeamSerializer(ranking, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
class CheckpointStatsViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""チェックポイント統計API"""
|
||||
|
||||
def list(self, request):
|
||||
event_code = request.query_params.get('event_code')
|
||||
stats = GpsCheckin.objects.get_checkpoint_statistics(
|
||||
event_code
|
||||
)
|
||||
return Response(stats)
|
||||
```
|
||||
|
||||
### 6.2 LINE Bot API統合
|
||||
|
||||
#### Django統合LINE Bot
|
||||
```python
|
||||
# views_apis/line_bot.py
|
||||
from linebot import LineBotApi, WebhookHandler
|
||||
from linebot.models import TextMessage, QuickReply
|
||||
|
||||
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'})
|
||||
|
||||
@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)
|
||||
)
|
||||
```
|
||||
|
||||
## 7. パフォーマンス最適化
|
||||
|
||||
### 7.1 データベースインデックス
|
||||
|
||||
#### 統合DB最適化インデックス
|
||||
```sql
|
||||
-- 統合後の重要インデックス
|
||||
CREATE INDEX idx_team_event_zekken ON rog_team(event_id, zekken_number);
|
||||
CREATE INDEX idx_gps_team_checkpoint ON rog_gpscheckin(team_id, checkpoint_id);
|
||||
CREATE INDEX idx_gps_created_at ON rog_gpscheckin(created_at);
|
||||
CREATE INDEX idx_checkpoint_event_cp ON rog_checkpoint(event_id, cp_number);
|
||||
|
||||
-- PostGIS空間インデックス
|
||||
CREATE INDEX idx_checkpoint_location ON rog_checkpoint USING GIST(location);
|
||||
CREATE INDEX idx_team_location ON rog_team USING GIST(location);
|
||||
|
||||
-- ランキング計算高速化
|
||||
CREATE INDEX idx_gps_ranking ON rog_gpscheckin(team_id, buy_flag, late_point);
|
||||
```
|
||||
|
||||
### 7.2 キャッシュ戦略
|
||||
|
||||
#### Redis統合キャッシュ
|
||||
```python
|
||||
# settings.py
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django_redis.cache.RedisCache',
|
||||
'LOCATION': 'redis://127.0.0.1:6379/1',
|
||||
'OPTIONS': {
|
||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# キャッシュキー設計
|
||||
CACHE_KEYS = {
|
||||
'ranking': 'ranking:event_{event_id}:class_{class_name}',
|
||||
'checkpoint_stats': 'cp_stats:event_{event_id}',
|
||||
'team_detail': 'team:event_{event_id}:zekken_{zekken_number}',
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Celery非同期処理
|
||||
|
||||
#### ランキング計算の非同期化
|
||||
```python
|
||||
# tasks.py
|
||||
from celery import shared_task
|
||||
|
||||
@shared_task
|
||||
def calculate_ranking(event_id):
|
||||
"""ランキング非同期計算"""
|
||||
event = NewEvent2.objects.get(id=event_id)
|
||||
|
||||
# 全クラスのランキング計算
|
||||
for class_name in Team.objects.filter(event=event).values_list('class_name', flat=True).distinct():
|
||||
ranking = GpsCheckin.objects.get_team_ranking(event, class_name)
|
||||
|
||||
# キャッシュ更新
|
||||
cache_key = f"ranking_{event_id}_{class_name}"
|
||||
cache.set(cache_key, list(ranking), 3600)
|
||||
|
||||
return f"Ranking calculated for event {event_id}"
|
||||
|
||||
@shared_task
|
||||
def update_checkpoint_stats(event_id):
|
||||
"""チェックポイント統計更新"""
|
||||
stats = GpsCheckin.objects.get_checkpoint_statistics(event_id)
|
||||
cache_key = f"cp_stats_{event_id}"
|
||||
cache.set(cache_key, list(stats), 3600)
|
||||
```
|
||||
|
||||
## 8. セキュリティ・権限統合
|
||||
|
||||
### 8.1 Django認証統合
|
||||
|
||||
#### 統合認証システム
|
||||
```python
|
||||
class RogainingPermission(BasePermission):
|
||||
"""ロゲイニング専用権限"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# イベント主催者権限
|
||||
if hasattr(request.user, 'is_event_organizer'):
|
||||
return request.user.is_event_organizer
|
||||
|
||||
# チーム所属権限
|
||||
return request.user.teams.exists()
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# チーム関連オブジェクトの権限
|
||||
if hasattr(obj, 'team'):
|
||||
return obj.team.members.filter(user=request.user).exists()
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
### 8.2 API認証統合
|
||||
|
||||
#### トークン認証
|
||||
```python
|
||||
class APIKeyAuthentication(BaseAuthentication):
|
||||
"""API キー認証(MobServer互換)"""
|
||||
|
||||
def authenticate(self, request):
|
||||
api_key = request.META.get('HTTP_X_API_KEY')
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
try:
|
||||
# API キー検証
|
||||
user = CustomUser.objects.get(api_key=api_key)
|
||||
return (user, None)
|
||||
except CustomUser.DoesNotExist:
|
||||
raise AuthenticationFailed('Invalid API key')
|
||||
```
|
||||
|
||||
## 9. 監視・ログ統合
|
||||
|
||||
### 9.1 統合ログシステム
|
||||
|
||||
#### 構造化ログ
|
||||
```python
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class LoggingMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
start_time = time.time()
|
||||
|
||||
response = self.get_response(request)
|
||||
|
||||
logger.info(
|
||||
"api_request",
|
||||
method=request.method,
|
||||
path=request.path,
|
||||
status_code=response.status_code,
|
||||
duration=time.time() - start_time,
|
||||
user_id=request.user.id if request.user.is_authenticated else None
|
||||
)
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
### 9.2 メトリクス収集
|
||||
|
||||
#### Django統合メトリクス
|
||||
```python
|
||||
# monitoring/metrics.py
|
||||
from prometheus_client import Counter, Histogram
|
||||
|
||||
# メトリクス定義
|
||||
api_requests_total = Counter(
|
||||
'api_requests_total',
|
||||
'Total API requests',
|
||||
['method', 'endpoint', 'status']
|
||||
)
|
||||
|
||||
ranking_calculation_duration = Histogram(
|
||||
'ranking_calculation_duration_seconds',
|
||||
'Ranking calculation duration'
|
||||
)
|
||||
|
||||
def track_api_request(method, endpoint, status):
|
||||
api_requests_total.labels(
|
||||
method=method,
|
||||
endpoint=endpoint,
|
||||
status=status
|
||||
).inc()
|
||||
```
|
||||
|
||||
## 10. 運用・保守計画
|
||||
|
||||
### 10.1 データバックアップ戦略
|
||||
|
||||
#### 統合バックアップシステム
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup_integrated_db.sh
|
||||
|
||||
BACKUP_DIR="/backup/rogaining_integrated"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
# PostgreSQL + PostGIS バックアップ
|
||||
pg_dump \
|
||||
--host=localhost \
|
||||
--port=5432 \
|
||||
--username=admin \
|
||||
--dbname=rogaining_integrated \
|
||||
--format=custom \
|
||||
--file="${BACKUP_DIR}/rogaining_${DATE}.dump"
|
||||
|
||||
# S3アップロード
|
||||
aws s3 cp "${BACKUP_DIR}/rogaining_${DATE}.dump" \
|
||||
s3://rogaining-backups/db/
|
||||
|
||||
# 古いバックアップ削除(30日以上)
|
||||
find $BACKUP_DIR -name "*.dump" -mtime +30 -delete
|
||||
```
|
||||
|
||||
### 10.2 健全性チェック
|
||||
|
||||
#### データ整合性検証
|
||||
```python
|
||||
# management/commands/check_data_integrity.py
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
self.check_team_integrity()
|
||||
self.check_gps_integrity()
|
||||
self.check_ranking_consistency()
|
||||
|
||||
def check_team_integrity(self):
|
||||
"""チーム情報整合性確認"""
|
||||
# zekken_number重複チェック
|
||||
duplicates = Team.objects.values(
|
||||
'zekken_number', 'event'
|
||||
).annotate(
|
||||
count=Count('id')
|
||||
).filter(count__gt=1)
|
||||
|
||||
if duplicates.exists():
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'Team duplicates found: {duplicates}')
|
||||
)
|
||||
|
||||
def check_gps_integrity(self):
|
||||
"""GPS データ整合性確認"""
|
||||
# 孤児GPSレコードチェック
|
||||
orphan_gps = GpsCheckin.objects.filter(
|
||||
team__isnull=True
|
||||
).count()
|
||||
|
||||
if orphan_gps > 0:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'Orphan GPS records: {orphan_gps}')
|
||||
)
|
||||
```
|
||||
|
||||
### 10.3 パフォーマンス監視
|
||||
|
||||
#### 自動パフォーマンス監視
|
||||
```python
|
||||
# monitoring/performance.py
|
||||
class PerformanceMonitor:
|
||||
@staticmethod
|
||||
def check_slow_queries():
|
||||
"""遅いクエリの検出"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT query, calls, total_time, mean_time
|
||||
FROM pg_stat_statements
|
||||
WHERE mean_time > 1000 -- 1秒以上
|
||||
ORDER BY mean_time DESC
|
||||
LIMIT 10;
|
||||
""")
|
||||
|
||||
slow_queries = cursor.fetchall()
|
||||
if slow_queries:
|
||||
logger.warning("Slow queries detected", queries=slow_queries)
|
||||
|
||||
@staticmethod
|
||||
def check_cache_hit_rate():
|
||||
"""キャッシュヒット率確認"""
|
||||
hit_rate = cache.get_stats().get('hit_rate', 0)
|
||||
if hit_rate < 0.8: # 80%未満
|
||||
logger.warning(f"Low cache hit rate: {hit_rate}")
|
||||
```
|
||||
|
||||
## 11. 結論
|
||||
|
||||
### 11.1 統合効果
|
||||
|
||||
#### システム統一によるメリット
|
||||
1. **管理効率向上**: 単一のDjango Admin UIでの一元管理
|
||||
2. **開発効率向上**: 統一されたDjango開発環境
|
||||
3. **保守性向上**: コードベース統一とドキュメント整備
|
||||
4. **スケーラビリティ**: Django + Celery によるスケーラブルな非同期処理
|
||||
|
||||
#### データ管理改善
|
||||
1. **データ整合性**: Django ORMによる制約管理
|
||||
2. **バックアップ統一**: 単一データベースの包括的バックアップ
|
||||
3. **パフォーマンス**: 最適化されたインデックスとキャッシュ戦略
|
||||
|
||||
### 11.2 今後の拡張計画
|
||||
|
||||
#### 機能拡張ロードマップ
|
||||
1. **Phase 1**: 基本統合(4週間)
|
||||
- データ移行完了
|
||||
- 基本API動作確認
|
||||
- LINE Bot統合
|
||||
|
||||
2. **Phase 2**: 機能強化(4週間)
|
||||
- リアルタイムランキング
|
||||
- 高度な統計機能
|
||||
- モバイルアプリ対応
|
||||
|
||||
3. **Phase 3**: 運用最適化(4週間)
|
||||
- パフォーマンス調整
|
||||
- 監視システム構築
|
||||
- 自動化システム
|
||||
|
||||
### 11.3 リスク管理
|
||||
|
||||
#### 主要リスク項目
|
||||
1. **データ移行リスク**: 段階的移行とバックアップによる対応
|
||||
2. **パフォーマンスリスク**: 事前負荷テストとキャッシュ戦略
|
||||
3. **機能互換リスク**: API互換性維持とテストケース網羅
|
||||
|
||||
#### 対応策
|
||||
- 詳細な移行計画と検証手順
|
||||
- ロールバック計画の準備
|
||||
- 段階的リリースとモニタリング
|
||||
|
||||
統合データベース設計により、Django AdminとMobServerの機能を完全に統合し、効率的で拡張可能なシステムを構築します。
|
||||
Reference in New Issue
Block a user