# 詳細機能設計書 - 外部システム連携 ## 1. 概要 本文書は、ロゲイニング大会管理システムの外部システム連携機能について詳細に記述したものです。 ### システム構成 - **Django RESTフレームワーク**: メインAPI(Python) - **Ruby Sinatra Server**: 外部システム連携サーバー(MobServer_gifuroge.rb) - **AWS S3**: ファイルストレージ - **外部ロゲイニングシステム**: チーム登録情報との連携 ## 2. 外部連携アーキテクチャ ``` Django API → Ruby Server → External Rogaining System ↓ AWS S3 Storage ``` ### 2.1 通信フロー 1. Django APIが外部システム連携要求を受信 2. Ruby Sinatraサーバーにリクエスト転送 3. Ruby Serverから外部ロゲイニングシステムに情報送信 4. 処理結果をS3にアップロード 5. レスポンスをDjango API経由でクライアントに返却 ## 3. 外部連携API仕様 ### 3.1 チーム登録API #### POST /register_team **目的**: 外部ロゲイニングシステムにチーム情報を登録する **リクエスト仕様**: ```http POST /register_team Content-Type: application/json { "zekken_number": "string", # ゼッケン番号 "team_name": "string", # チーム名 "event_code": "string", # イベントコード "class_name": "string", # 参加クラス "member_count": "integer", # メンバー数 "password": "string" # チーム認証パスワード } ``` **レスポンス仕様**: ```json { "status": "OK|ERROR", "message": "処理結果メッセージ", "team_id": "登録されたチームID", "timestamp": "2024-01-01T12:00:00Z" } ``` **処理フロー**: 1. リクエストパラメータの検証 2. team_tableからチーム情報の取得 3. 外部システムへの登録データ送信 4. 登録結果の確認とレスポンス返却 #### コード実装詳細 ```ruby # MobServer_gifuroge.rbから抜粋 app.post '/register_team' do crossdomain headjson # パラメータ取得 zekken_number = params[:zekken_number] team_name = params[:team_name] event_code = params[:event_code] # チーム情報の検証 team_data = getTeamDataByZekken_number(zekken_number, event_code) if team_data['result'] == "ERROR" return { status: "ERROR", message: "チーム情報が見つかりません" }.to_json end # 外部システムへの登録処理 result = register_to_external_system(team_data) # 結果の返却 { status: result[:status], message: result[:message], team_id: result[:team_id], timestamp: DateTime.now.iso8601 }.to_json end ``` ### 3.2 チーム名更新API #### POST /update_team_name **目的**: 外部システムに登録済みのチーム名を更新する **リクエスト仕様**: ```http POST /update_team_name Content-Type: application/json { "zekken_number": "string", # ゼッケン番号 "new_team_name": "string", # 新しいチーム名 "event_code": "string" # イベントコード } ``` **レスポンス仕様**: ```json { "status": "OK|ERROR", "message": "更新結果メッセージ", "old_name": "変更前のチーム名", "new_name": "変更後のチーム名", "timestamp": "2024-01-01T12:00:00Z" } ``` ## 4. スコアボード生成・配信システム ### 4.1 スコアボード自動生成 #### 機能概要 - チーム毎のスコアボードを自動生成 - ExcelファイルからPDFに変換 - AWS S3への自動アップロード #### POST /generate_scoreboard **処理フロー**: 1. GPS情報とチェックポイント通過記録の取得 2. Excel形式でのスコアボード生成 3. PDF変換処理 4. S3へのアップロード 5. 外部システムへの配信URL通知 ```ruby def makeScoreboard(zekken_number, event_code, reprintF = false, budleF = false) # スコアボード生成処理 filepath = getScoreboardGeneral(zekken_number, event_code) if filepath == "no_data" return "no data error" end # PDF変換 command = "/home/mobilousInstance/jodconverter-cli-4.4.5-SNAPSHOT/bin/jodconverter-cli -o #{docpath_user}/#{pdffile}.xlsx #{docpath_user}/#{pdffile}.pdf" system(command) # S3アップロード aws_url = s3Uploader("#{event_code}/scoreboard", docpath_user, pdffile + '.pdf') # レポート情報の更新 if getFinalReport(zekken_number, event_code) == "no report" inputFinalReport(zekken_number, event_code, aws_url) else changeFinalReport(zekken_number, event_code, aws_url) end aws_url end ``` ### 4.2 非同期処理による大量生成 ```ruby # キューイング処理 $queue = Queue.new Thread.new do loop do begin item = JSON.parse($queue.pop) makeScoreboard(item["zekken_number"], item["event_code"], to_boolean(item["reprintF"]), false) result = removeQueueMemory() rescue => e # エラーログ記録 timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S") error_message = "#{timestamp}=> #{item} : #{e.message}\n" File.open(queue_error_log_base(), "a") do |file| file.write(error_message) end end end end ``` ## 5. AWS S3連携 ### 5.1 設定情報 ```ruby def s3_key return "AKIA6LVMTADSVEB5LZ2H" end def s3_Skey return "KIbm47dqVBxSmeHygrh5ENV1uXzJMc7fLnJOvtUm" end def s3_bucket return "sumasenrogaining" end def s3_region return "us-west-2" end def s3_domain return "https://sumasenrogaining.s3.us-west-2.amazonaws.com" end ``` ### 5.2 ファイルアップロード機能 ```ruby def s3Uploader(data_dir, filepath, filename) bucket = s3_bucket().freeze region = s3_region().freeze access_key = s3_key().freeze secret_key = s3_Skey().freeze aws_storage = S3_StorageUtil.new(access_key, secret_key, region, bucket, data_dir) aws_bucket = S3_FolderUtil.new("offlineRecog/work", aws_storage) originPath = filepath destPath = data_dir filename = filename aws_bucket.uploadFile(originPath, destPath, filename) aws_url = s3_domain() + '/' + data_dir + '/' + filename return aws_url end ``` ## 6. データベース連携 ### 6.1 PostgreSQL接続設定 ```ruby def set_dbname return "gifuroge" end # データベース接続 @pgconn = UserPostgres.new dbname = set_dbname() @pgconn.connectPg("localhost", "mobilous", 0, dbname) ``` ### 6.2 主要テーブル操作 #### team_table操作 ```ruby def getTeamDataByZekken_number(zekken_number, event_code) @pgconn = UserPostgres.new dbname = set_dbname() @pgconn.connectPg("localhost", "mobilous", 0, dbname) anytable = UserPostgresTable.new anytable.useTable(@pgconn, 'team_table') where = "zekken_number = '#{zekken_number}' AND event_code = '#{event_code}'" list = anytable.find2(where) result = {} list.each { |rec| result["zekken_number"] = rec["zekken_number"] result["team_name"] = rec["team_name"] result["class_name"] = rec["class_name"] } @pgconn.disconnect if result != {} && result != nil result["result"] = "OK" return result else result["result"] = "ERROR" return result end end ``` #### gps_information操作 ```ruby def getGpsInfo(zekken_number, event_code) @pgconn = UserPostgres.new dbname = set_dbname() @pgconn.connectPg("localhost", "mobilous", 0, dbname) anytable = UserPostgresTable.new anytable.useTable(@pgconn, 'gps_information') where = "zekken_number = '#{zekken_number}' AND event_code = '#{event_code}' ORDER BY serial_number" list = anytable.find2(where) result = [] count = 0 list.each { |rec| result[count] = {"cp_number" => rec['cp_number']} count += 1 } @pgconn.disconnect return result end ``` ## 7. 画像処理・サムネイル生成 ### 7.1 画像リサイズ処理 ```ruby def image_size_width return 150 end def image_size_height return 105 end # 画像処理実装 filename = rec["cp#{i}_ph"].split('/').last fullpath = docpath + '/' + filename # S3からの画像取得 fullpath2 = s3_domain + '/' + URI.encode_www_form_component(event_code, enc=nil) + '/' + zekken_number + '/' + filename URI.open(fullpath2) do |image| File.open(fullpath, 'w') do |file| file.write(image.read) end end # サムネイル生成 extension = filename.split('.').last filenameTh = filename.gsub(".#{extension}", "_th.#{extension}") imageOri = Magick::Image.read(fullpath).first imageTh = imageOri.scale(image_size_width(), image_size_height()) imageTh.write("temp/#{filenameTh}") nextpath = docpath + '/' + filenameTh FileUtils.mv("/var/www/temp/#{filenameTh}", nextpath) result["cp#{i}_ph"] = nextpath ``` ## 8. エラーハンドリング ### 8.1 エラーコード体系 ```ruby # エラーコード一覧 # STA-001 : パスワード入力のキャンセルに失敗した状態 # STA-002 : 処理には成功したがchat_statusのレコードデリートに失敗した状態 # STA-003 : 写真受付のキャンセルに失敗した状態 # USR-001 : LINEのユーザー名を取得できていない状態 # USR-002 : ログインに成功したが、user_tableのレコードアップデートに失敗した状態 # USR-003 : ログアウトの処理に失敗した状態 # DBS-001 : そのユーザーが何時間部門のユーザーなのかを認識できなくなった状態 def text_importantError(errorCode) return "エラーコード:#{errorCode}\nゼッケン番号とエラーコードを添えて、運営に問い合わせて下さい。" end ``` ### 8.2 例外処理パターン ```ruby begin ret = anytable.exec(sql) rescue => error p error return "UPDATE ERROR" end # データベース接続の確実な切断 @pgconn.disconnect # キューエラーハンドリング rescue => e timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S") item_value = defined?(item) ? item.to_s : "" error_message = "#{timestamp}=> #{item_value} : #{e.message}\n" File.open(queue_error_log_base(), "a") do |file| file.write(error_message) end end ``` ## 9. セキュリティ対策 ### 9.1 認証・認可 #### チーム認証 ```ruby def zekkenAuthorization(zekken, password) @pgconn = UserPostgres.new dbname = set_dbname() @pgconn.connectPg("localhost", "mobilous", 0, dbname) anytable = UserPostgresTable.new anytable.useTable(@pgconn, 'team_table') where = "zekken_number = '#{zekken}' AND password = '#{password}'" list = anytable.find2(where) result = {} if list == nil return { "zekken_number" => "ERROR" } end list.each { |rec| result["zekken_number"] = rec["zekken_number"] result["team_name"] = rec["team_name"] result["event_code"] = rec["event_code"] } @pgconn.disconnect if result != {} && result != nil result["result"] = "OK" return result else result["result"] = "ERROR" return result end end ``` #### 代理人認証 ```ruby def agentAuthorization(password) @pgconn = UserPostgres.new dbname = set_dbname() @pgconn.connectPg("localhost", "mobilous", 0, dbname) anytable = UserPostgresTable.new anytable.useTable(@pgconn, 'agent_table') where = "password = '#{password}'" list = anytable.find2(where) result = "" if list == nil return "ERROR" end list.each { |rec| result = rec["agent_id"] } @pgconn.disconnect if result != "" && result != nil return result else return "ERROR" end end ``` ### 9.2 SQLインジェクション対策 ```ruby # パラメータのエスケープ処理 if team_name.include?("'") team_name.gsub!("'", "''") end where = "team_name = '#{team_name}' AND event_code = '#{event_code}'" ``` ### 9.3 CORS設定 ```ruby def crossdomain headers 'Access-Control-Allow-Origin' => '*' end def headjson headers 'Content-Type' => 'application/json; charset=utf-8' end ``` ## 10. ログ・監視 ### 10.1 チャットログ ```ruby def chatLogger(userId, talker, type, detail) p "#{talker} : #{detail}" @pgconn = UserPostgres.new dbname = set_dbname() @pgconn.connectPg("localhost", "mobilous", 0, dbname) anytable = UserPostgresTable.new anytable.useTable(@pgconn, 'chat_log') title = ["userid", "talker", "message_type", "message_detail", "create_at"] record = {"userid" => userId, "talker" => talker, "message_type" => type, "message_detail" => detail, "create_at" => DateTime.now} sql = anytable.makeInsertKeySet(title, record) begin ret = anytable.exec(sql) rescue => error p error end @pgconn.disconnect return "OK" end ``` ### 10.2 キューエラーログ ```ruby def queue_error_log_base() return "/var/log/queue_error.log" end # エラーログ記録 timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S") error_message = "#{timestamp}=> #{item} : #{e.message}\n" File.open(queue_error_log_base(), "a") do |file| file.write(error_message) end ``` ## 11. パフォーマンス最適化 ### 11.1 データベース接続管理 ```ruby # 接続の確実な切断 @pgconn.disconnect # 接続プールの活用 def with_db_connection @pgconn = UserPostgres.new dbname = set_dbname() @pgconn.connectPg("localhost", "mobilous", 0, dbname) begin yield @pgconn ensure @pgconn.disconnect end end ``` ### 11.2 画像処理の最適化 ```ruby # サムネイル生成のバッチ処理 def batch_thumbnail_generation(image_list) image_list.each do |image_path| imageOri = Magick::Image.read(image_path).first imageTh = imageOri.scale(image_size_width(), image_size_height()) imageTh.write(thumbnail_path(image_path)) end end ``` ### 11.3 キューイング最適化 ```ruby # 非同期処理による負荷分散 Thread.new do loop do begin item = JSON.parse($queue.pop) process_queue_item(item) rescue => e log_queue_error(e, item) end end end ``` ## 12. 運用・保守 ### 12.1 バックアップ戦略 ```ruby # データベースバックアップ def backup_database(event_code) timestamp = Time.now.strftime("%Y%m%d_%H%M%S") backup_file = "backup_#{event_code}_#{timestamp}.sql" system("pg_dump #{set_dbname()} > #{backup_file}") # S3へのバックアップアップロード s3Uploader("backups", ".", backup_file) end ``` ### 12.2 ヘルスチェック ```ruby app.get '/health' do crossdomain headjson { status: "OK", timestamp: DateTime.now.iso8601, version: "1.0.0", database: check_database_connection(), s3: check_s3_connection() }.to_json end def check_database_connection() begin @pgconn = UserPostgres.new dbname = set_dbname() @pgconn.connectPg("localhost", "mobilous", 0, dbname) @pgconn.disconnect return "OK" rescue => e return "ERROR: #{e.message}" end end ``` ## 13. 今後の拡張計画 ### 13.1 マイクロサービス化 - Ruby ServerをDockerコンテナ化 - APIゲートウェイの導入 - サービス間通信の最適化 ### 13.2 リアルタイム機能 - WebSocketによるリアルタイム順位更新 - プッシュ通知機能の実装 ### 13.3 多言語対応 - 国際大会対応のため多言語化 - タイムゾーン対応の強化 --- この詳細機能設計書により、外部システム連携の実装、運用、保守に必要な全ての技術的詳細が文書化されています。 --- ## 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") ``` ---