Files
rogaining_srv/詳細機能設計書-外部連携.md
2025-08-20 19:15:19 +09:00

15 KiB
Raw Blame History

詳細機能設計書 - 外部システム連携

1. 概要

本文書は、ロゲイニング大会管理システムの外部システム連携機能について詳細に記述したものです。

システム構成

  • Django RESTフレームワーク: メインAPIPython
  • 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

目的: 外部ロゲイニングシステムにチーム情報を登録する

リクエスト仕様:

POST /register_team
Content-Type: application/json

{
  "zekken_number": "string",    # ゼッケン番号
  "team_name": "string",        # チーム名
  "event_code": "string",       # イベントコード
  "class_name": "string",       # 参加クラス
  "member_count": "integer",    # メンバー数
  "password": "string"          # チーム認証パスワード
}

レスポンス仕様:

{
  "status": "OK|ERROR",
  "message": "処理結果メッセージ",
  "team_id": "登録されたチームID",
  "timestamp": "2024-01-01T12:00:00Z"
}

処理フロー:

  1. リクエストパラメータの検証
  2. team_tableからチーム情報の取得
  3. 外部システムへの登録データ送信
  4. 登録結果の確認とレスポンス返却

コード実装詳細

# 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

目的: 外部システムに登録済みのチーム名を更新する

リクエスト仕様:

POST /update_team_name
Content-Type: application/json

{
  "zekken_number": "string",    # ゼッケン番号
  "new_team_name": "string",    # 新しいチーム名
  "event_code": "string"        # イベントコード
}

レスポンス仕様:

{
  "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通知
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 非同期処理による大量生成

# キューイング処理
$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 設定情報

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 ファイルアップロード機能

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接続設定

def set_dbname
  return "gifuroge"
end

# データベース接続
@pgconn = UserPostgres.new
dbname = set_dbname()
@pgconn.connectPg("localhost", "mobilous", 0, dbname)

6.2 主要テーブル操作

team_table操作

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操作

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 画像リサイズ処理

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 エラーコード体系

# エラーコード一覧
# 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 例外処理パターン

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 認証・認可

チーム認証

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

代理人認証

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インジェクション対策

# パラメータのエスケープ処理
if team_name.include?("'")
  team_name.gsub!("'", "''")
end

where = "team_name = '#{team_name}' AND event_code = '#{event_code}'"

9.3 CORS設定

def crossdomain
  headers 'Access-Control-Allow-Origin' => '*'
end

def headjson
  headers 'Content-Type' => 'application/json; charset=utf-8'
end

10. ログ・監視

10.1 チャットログ

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 キューエラーログ

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 データベース接続管理

# 接続の確実な切断
@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 画像処理の最適化

# サムネイル生成のバッチ処理
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 キューイング最適化

# 非同期処理による負荷分散
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 バックアップ戦略

# データベースバックアップ
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 ヘルスチェック

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 多言語対応

  • 国際大会対応のため多言語化
  • タイムゾーン対応の強化

この詳細機能設計書により、外部システム連携の実装、運用、保守に必要な全ての技術的詳細が文書化されています。