アーキテクチャ
パッケージ構成
cmd/server/ エントリーポイント(main.go)— 各コンポーネントの組み立てのみ
internal/config/ 環境変数からの設定読み込み
internal/discordapi/ Discord REST API クライアント・Ed25519署名検証・型定義
internal/httpserver/ HTTPルーティングとInteractionのディスパッチ(ビジネスロジックは持たない)
internal/keymanager/ 鍵管理機能の本体
internal/sqlitedb/ 共有SQLiteコネクションのオープン処理internal/keymanager が実質的なアプリケーション本体で、以下を1パッケージに集約しています。
service.go—/key/backdate/backdate-fixコマンドとボタンのハンドラcommands.go— スラッシュコマンド定義・ボタンメッセージの構築sheets.go— Google スプレッドシートへの読み書きロジックalerts.go/alertstore.go— 返却期限超過アラートのスケジューリングと永続化status.go— 常設ステータスメッセージの更新、ログチャンネルへの投稿notify.go— Microsoft Teams Webhook通知envpersist.go—.envファイルへの安全な書き戻しtimeparse.go—HHMM/MMDD形式の入力パースとJST時刻の扱い
リクエストフロー
- Discord →
POST /(Interaction、Ed25519署名付き) httpserverが署名を検証- その場で
type: 5(deferred response,flags: 64でephemeral)をDiscordへ即時返却 go s.dispatch(interaction)で別goroutineに処理を渡し、ここから先は非同期keymanager.Serviceが Google Sheets から現在の貸出状況を取得- Sheets へ打刻を追記(月別シート+年別ログシート)
- 鍵取りであれば返却期限アラートをスケジュール
- ログチャンネルへ操作ログを投稿
- 常設ステータスメッセージを編集(初回のみ新規作成)
- deferred response を最終結果の文言に編集して確定応答
なぜ deferred response + goroutine なのか
Discord は Interaction 受信から 3秒以内 に応答しないとタイムアウトになります。しかし実処理(スプレッドシートの読み書き、複数のDiscord API呼び出し)は3秒を超えうるため、handleInteraction はまず type 5(deferred response, ephemeral)を即座に返し、実際の処理は go s.dispatch(interaction) でバックグラウンドに逃がします。ゴルーチン内の dispatch は recover() でパニックを捕捉し、プロセス全体がクラッシュしないようにしています。処理が終わったら EditOriginalInteractionResponse で最終結果に書き換えます。
なぜ「操作ログ」と「deferred response」が両方あるのか
deferred response の編集内容は 本人にしか見えない(ephemeral)確認用メッセージです。一方、誰が・いつ鍵を取った/返したかという記録は、全員に見える形でログチャンネルへの投稿と常設ステータスメッセージの更新によって共有されます。この2つは役割が異なるため、常にセットで呼ばれます。
データの持ち場
| データ | 保存先 | 理由 |
|---|---|---|
| 打刻の記録(誰が・いつ・何をしたか) | Google スプレッドシート | 人が閲覧・集計しやすい正の記録として |
| 返却期限アラートの予定 | SQLite(pending_alerts テーブル) | プロセス再起動後も期限超過チェックを再開できるように |
| 常設ステータスメッセージのID | .env(STATUS_MESSAGE_ID) | 再起動後も同じメッセージを編集し続けるように |
詳細は スプレッドシートのレイアウト と 返却期限アラート を参照してください。