# Windmill 通知ワークフロー連携仕様 > **作成日**: 2026-02-21 > **対象システム**: 白皇集落営農組合 統合システム (`shiraou.keinafarm.net`) > **目的**: 予約・実績の変更をLINEで管理者に通知するWindmillワークフローの実装仕様 --- ## 1. 概要 統合システム側が「変更履歴取得API」を提供する。 Windmillは定期的にこのAPIをポーリングし、変更があればLINEへ通知する。 ``` Windmill(定期実行) └→ GET https://shiraou.keinafarm.net/reservations/api/changes/?since=<前回実行時刻> └→ 変更一覧(予約・実績)を取得 └→ 変更があればLINE Messaging APIへ通知 └→ 前回実行時刻を更新 ``` --- ## 2. 変更履歴取得API ### エンドポイント ``` GET https://shiraou.keinafarm.net/reservations/api/changes/ ``` ### 認証 `X-API-Key` ヘッダーにAPIキーを指定する(統合システム管理者から取得)。 ``` X-API-Key: ``` APIキーが不正な場合は `401 Unauthorized` が返る。 ### クエリパラメータ | パラメータ | 型 | 必須 | 説明 | |-----------|-----|------|------| | `since` | ISO8601文字列 | 必須 | この日時以降の変更を取得する | **`since` の形式例**: - `2026-02-21T10:00:00` (ナイーブ、JSTとして扱われる) - `2026-02-21T10:00:00+09:00` (タイムゾーン付き、推奨) ### レスポンス(200 OK) ```json { "checked_at": "2026-02-21T12:00:00+09:00", "since": "2026-02-21T10:00:00+09:00", "reservations": [ { "operation": "create", "reservation_id": 123, "user_name": "田中太郎", "machine_name": "トラクター", "start_at": "2026-02-25T09:00:00+09:00", "end_at": "2026-02-25T12:00:00+09:00", "operated_at": "2026-02-21T11:30:00+09:00", "operator_name": "田中太郎", "reason": "" }, { "operation": "cancel", "reservation_id": 120, "user_name": "佐藤花子", "machine_name": "田植機", "start_at": "2026-02-22T08:00:00+09:00", "end_at": "2026-02-22T17:00:00+09:00", "operated_at": "2026-02-21T11:45:00+09:00", "operator_name": "佐藤花子", "reason": "" } ], "usages": [ { "operation": "update", "usage_id": 456, "user_name": "山田次郎", "machine_name": "コンバイン", "amount": 4.0, "unit": "時間", "start_at": "2026-02-20T08:00:00+09:00", "end_at": "2026-02-20T12:00:00+09:00", "operated_at": "2026-02-21T11:55:00+09:00", "operator_name": "管理者A", "reason": "記録ミスのため修正" } ] } ``` ### operation の値一覧 **予約(reservations)**: | 値 | 意味 | |----|------| | `create` | 予約が作成された | | `update` | 予約の日時・機械が変更された | | `cancel` | 予約がキャンセルされた | **実績(usages)**: | 値 | 意味 | |----|------| | `create` | 実績が登録された | | `update` | 実績が修正された | | `delete` | 実績が削除された | ### 変更なしの場合 `reservations` と `usages` が両方空配列になる。通知は不要。 ```json { "checked_at": "2026-02-21T12:05:00+09:00", "since": "2026-02-21T12:00:00+09:00", "reservations": [], "usages": [] } ``` ### エラーレスポンス | ステータス | 原因 | |-----------|------| | `401 Unauthorized` | APIキーが不正または未設定 | | `400 Bad Request` | `since` パラメータが欠落または不正な日時形式 | --- ## 3. Windmillワークフロー設計 ### 3.1 スケジュール - **実行間隔**: 5分毎(`*/5 * * * *`) - 農業機械の予約という用途上、数分の遅延は許容範囲 ### 3.2 状態管理(前回実行時刻) Windmillの **Resource** または **State Variable** に前回実行時刻を保存する。 ``` 変数名: last_checked_at 初期値: (初回実行時は現在時刻 - 10分 を使用) ``` ### 3.3 ワークフロー全体フロー(擬似コード) ```python # 1. 前回実行時刻を取得 last_checked = get_state("last_checked_at") or (now() - 10 minutes) # 2. 変更履歴を取得 response = GET "https://shiraou.keinafarm.net/reservations/api/changes/" params: { since: last_checked.isoformat() } headers: { "X-API-Key": NOTIFICATION_API_KEY } # 3. 変更があればLINEに通知 if response.reservations or response.usages: message = format_line_message(response) send_line_message(LINE_CHANNEL_ACCESS_TOKEN, LINE_USER_ID, message) # 4. 前回実行時刻を更新 set_state("last_checked_at", response.checked_at) ``` ### 3.4 LINEメッセージのフォーマット例 ```python def format_line_message(data): lines = ["📋 営農システム 変更通知\n"] for r in data["reservations"]: start = r["start_at"][:16].replace("T", " ") end = r["end_at"][:16].replace("T", " ") if r["operation"] == "create": icon = "🟢" label = "予約作成" elif r["operation"] == "update": icon = "🔵" label = "予約変更" elif r["operation"] == "cancel": icon = "🔴" label = "予約キャンセル" lines.append(f"{icon} {label}") lines.append(f" 機械: {r['machine_name']}") lines.append(f" 利用者: {r['user_name']}") lines.append(f" 日時: {start} 〜 {end}") if r["reason"]: lines.append(f" 理由: {r['reason']}") lines.append("") for u in data["usages"]: start = u["start_at"][:16].replace("T", " ") if u["operation"] == "create": icon = "🟢" label = "実績登録" elif u["operation"] == "update": icon = "🔵" label = "実績修正" elif u["operation"] == "delete": icon = "🔴" label = "実績削除" lines.append(f"{icon} {label}") lines.append(f" 機械: {u['machine_name']}") lines.append(f" 利用者: {u['user_name']}") lines.append(f" 利用量: {u['amount']}{u['unit']}") lines.append(f" 日: {start[:10]}") if u["reason"]: lines.append(f" 理由: {u['reason']}") lines.append("") return "\n".join(lines).strip() ``` **出力例**: ``` 📋 営農システム 変更通知 🟢 予約作成 機械: トラクター 利用者: 田中太郎 日時: 2026-02-25 09:00 〜 2026-02-25 12:00 🔴 予約キャンセル 機械: 田植機 利用者: 佐藤花子 日時: 2026-02-22 08:00 〜 2026-02-22 17:00 🔵 実績修正 機械: コンバイン 利用者: 山田次郎 利用量: 4.0時間 日: 2026-02-20 理由: 記録ミスのため修正 ``` --- ## 4. Windmill側の環境変数(シークレット) | 変数名 | 説明 | 設定場所 | |--------|------|---------| | `NOTIFICATION_API_KEY` | 統合システムのAPIキー | Windmill Secret | | `LINE_CHANNEL_ACCESS_TOKEN` | LINE Messaging API チャネルアクセストークン | Windmill Secret | | `LINE_USER_ID` / `LINE_GROUP_ID` | 通知先のユーザーIDまたはグループID | Windmill Secret | --- ## 5. 統合システム側の設定(django側の作業) 本番サーバーの `docker-compose.yml` に以下の環境変数を追加し、デプロイする。 ```yaml environment: - NOTIFICATION_API_KEY=<任意の強いランダム文字列> ``` **APIキーの生成例**: ```bash openssl rand -hex 32 ``` --- ## 6. 動作確認方法 ### curlで直接テスト ```bash # 変更なし(直近1分) curl -H "X-API-Key: <キー>" \ "https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-02-21T11:59:00%2B09:00" # 広い範囲で変更を取得(初期確認用) curl -H "X-API-Key: <キー>" \ "https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-01-01T00:00:00" # APIキーなし → 401が返ることを確認 curl "https://shiraou.keinafarm.net/reservations/api/changes/?since=2026-01-01T00:00:00" ``` ### ダッシュボードで変更確認 管理者アカウントで `https://shiraou.keinafarm.net/reservations/dashboard/` にアクセスすると、 直近30件の予約操作履歴・実績操作ログを確認できる。 Windmillワークフローのデバッグ時に「APIが何を返すべきか」の確認に使える。 --- ## 7. 注意事項 - `since` に渡す時刻は **前回の `checked_at`** を使う(`since` ではなく `checked_at` を保存すること) - 同一の変更が2回通知されないよう、状態管理を確実に行う - ワークフローがエラーで終了した場合、`last_checked_at` を更新しないようにすること(次回実行時に再取得される) - APIキーは定期的にローテーションすること(変更時は統合システム側の環境変数も同時に更新)