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