# 詳細機能設計書 - 外部システム連携 ## 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 多言語対応 - 国際大会対応のため多言語化 - タイムゾーン対応の強化 --- この詳細機能設計書により、外部システム連携の実装、運用、保守に必要な全ての技術的詳細が文書化されています。