From aaaae09bd2b3fbb99c591e0508117723978ae859 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Sat, 20 Jun 2026 20:49:00 -0700 Subject: [PATCH] Add Line notifications module design spec Phase 1: Line Messaging API channel with webhook binding (individual + group), manual send-now, history, and binding/group admin UI. Scheduled sends and event triggers deferred to phases 2-3; IMessageChannel seam left for future PWA/WeChat channels. Co-Authored-By: Claude Opus 4.8 --- .../2026-06-20-line-notifications-design.md | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-20-line-notifications-design.md diff --git a/docs/superpowers/specs/2026-06-20-line-notifications-design.md b/docs/superpowers/specs/2026-06-20-line-notifications-design.md new file mode 100644 index 0000000..f514363 --- /dev/null +++ b/docs/superpowers/specs/2026-06-20-line-notifications-design.md @@ -0,0 +1,229 @@ +# Line 通知/提醒模組 — 設計文件 + +- **日期**:2026-06-20 +- **狀態**:已核可(待實作計畫) +- **作者**:Chris Chen + Claude +- **專案**:ROLAC(River Of Life Christian Church 教會管理系統) + +--- + +## 1. 背景與目標 + +教會需要一個發送「提醒」的管道,介於 Email(太正式)與簡訊 SMS(不適合)之間。 +需求是:**有時發到特定群組、有時發給個人**。 + +決策摘要(brainstorming 結論): + +- **平台**:第一階段做 **Line**(Arcadia 台裔會友圈使用率最高,Messaging API 文件成熟,個人+群組皆支援)。 +- **原生 Capacitor App 推播延後**:教會 501(c)(3) 尚未核准、App Store 上架對 50–100 人規模成本過高;之後再評估 PWA Web Push 或原生推播。 +- **綁定**:個人與群組**兩者都要**。 +- **觸發**:手動即時、排程定時、事件觸發**三者都要**,但**分階段**實作(先手動)。 +- **多通道未來性**:發送層抽 `IMessageChannel` 介面,未來 PWA/WeChat 各加一個實作即可,本階段不過度建置(YAGNI)。 + +### 為什麼不是其他方案 + +- **LINE Notify**:已於 2025 年停止服務,不可用。 +- **WhatsApp**:Business API 不支援程式化發送到「群組」,與「發特定群組」需求衝突。 +- **WeChat**:個人推播 48 小時視窗限制嚴、群組幾乎不可行;之後通道再評估。 +- **第三方/no-code(Zapier 等)**:資料外流、難做精準個人綁定。 +- **通用通知平台一次做好**:對本規模過度投資。 + +--- + +## 2. 關鍵技術現實(限制條件) + +1. **Bot 無法主動亂發訊息**:要發給個人,必須先取得對方的 Line `userId`,而此 id 只能在對方**加官方帳號好友 / 對 bot 說話 / 把 bot 拉進群**時,透過 webhook 取得。無法手動輸入。 +2. **Webhook 需要公開 HTTPS endpoint**:生產環境 nginx 終止 TLS、API 在後(`UseForwardedHeaders` 已設定),故 webhook 走 `https://<網域>/api/line/webhook`,**無需新基礎建設**,只需 nginx 加路由。 +3. **推播額度**:Line 官方帳號免費方案每月有則數上限(數百則)。**reply(對方剛互動的視窗內回覆)免費,push(主動推播)才計額度**。綁定流程走 reply,省額度。事件觸發大量發送時需留意額度。 +4. **Line 要求 webhook 快速回 200**:非 200 會觸發重送,故事件處理要快速 ACK、重活非同步化或設計為冪等。 + +--- + +## 3. 架構總覽 + +新增 **Messaging(通知)模組**。 + +``` +Angular (UserPortal) Line Platform + ├─ 撰寫+發送頁 ──┐ │ + ├─ 發送歷史頁 │ HTTPS (JWT) │ webhook (signed) + ├─ 會友綁定面板 │ │ + └─ 群組管理頁 ───┤ ▼ + ▼ LineWebhookController (匿名, 驗簽) + NotificationsController ──┐ │ follow/message/join/leave + (admin [Authorize]) │ ▼ + ├──► NotificationService ──► IMessageChannel + │ (收件人解析 + 稽核) └─ LineMessageChannel + │ (Push / Reply REST) + ▼ + AppDbContext (PostgreSQL) +``` + +- **對外推播**:`NotificationService` → `IMessageChannel.PushToUser/PushToGroup` → Line Push API。 +- **對內接收**:`LineWebhookController` 驗簽後分派事件,處理綁定與群組註冊(多用 reply,省額度)。 + +--- + +## 4. 資料模型(新實體) + +全部繼承既有 `SoftDeleteEntity` 慣例(除非另註);命名與欄位對齊風格沿用現有實體。 + +### `MemberChannelBinding` +會友與通道帳號的綁定(採獨立表,未來多通道不需改 `Member`)。 + +| 欄位 | 型別 | 說明 | +|---|---|---| +| Id | int | PK | +| MemberId | int FK → Member | | +| Channel | string | "line"(未來 "wechat"/"webpush") | +| ExternalId | string | Line userId | +| BoundAt | DateTime | 綁定時間 | + +- 唯一索引:`(MemberId, Channel)`、`(Channel, ExternalId)`。 + +### `LineBindingCode` +短效綁定碼(會友在 Line 輸入此碼以完成綁定)。 + +| 欄位 | 型別 | 說明 | +|---|---|---| +| Id | int | PK | +| Code | string | 短碼(避免易混淆字元) | +| MemberId | int FK → Member | | +| ExpiresAt | DateTime | 過期時間(建議 15 分鐘) | +| ConsumedAt | DateTime? | 已使用時間(null = 未用) | + +### `MessagingGroup` +bot 被拉進的 Line 群組。 + +| 欄位 | 型別 | 說明 | +|---|---|---| +| Id | int | PK | +| Channel | string | "line" | +| ExternalId | string | Line groupId | +| Name | string? | 管理員命名(join 時先空白待命名) | +| IsActive | bool | leave 事件時設 false | +| RegisteredAt | DateTime | | + +- 唯一索引:`(Channel, ExternalId)`。 + +### `NotificationLog` +每筆發送的稽核記錄。 + +| 欄位 | 型別 | 說明 | +|---|---|---| +| Id | int | PK | +| Channel | string | "line" | +| TargetType | string | "user" / "group" | +| TargetExternalId | string | 實際送達的 Line id | +| MemberId | int? FK | 個人發送時填 | +| MessagingGroupId | int? FK | 群組發送時填 | +| Body | string | 訊息內容 | +| Status | string | "sent" / "failed" | +| Error | string? | 失敗原因 | +| SentByUserId | string | 發送者(JWT sub claim,注意 `?? "sub"` fallback) | +| SentAt | DateTime | | + +### `ScheduledNotification`(Phase 2 預留,本階段不建表) +Body、收件人規格(memberIds/groupIds 或分群)、RunAtUtc、Recurrence、Status。 + +--- + +## 5. 元件與流程 + +### `IMessageChannel` / `LineMessageChannel` +- `PushToUserAsync(externalId, text)` +- `PushToGroupAsync(externalId, text)` +- `ReplyAsync(replyToken, text)` +- 封裝 Line REST 呼叫;`HttpClient` 經 `IHttpClientFactory`;token 讀 config。 +- 回傳結果含成功/失敗,供 `NotificationService` 寫 log。 + +### `LineWebhookController`(路由 `/api/line/webhook`,匿名) +1. 讀**原始 body**,以 `Line:ChannelSecret` 計算 HMAC-SHA256,與 `X-Line-Signature` 比對;不符回 400。 +2. 快速回 200,事件分派: + - **follow**(加好友):記 userId(暫存或直接回覆);reply 綁定說明(請輸入綁定碼)。 + - **message**(文字):比對有效(未過期、未使用)`LineBindingCode` → 建立 `MemberChannelBinding`、標記 code 已用、reply 綁定成功;無對應碼則 reply 說明文字。 + - **join**(拉進群):建一筆 `MessagingGroup`(ExternalId=groupId,Name 待命名);reply 提示管理員到後台命名。 + - **leave**:對應 `MessagingGroup` 設 `IsActive=false`。 + +### `NotificationService` +- `SendNowAsync(body, memberIds[], groupIds[], sentByUserId)`: + 1. 解析收件人 → 已綁定會友的 `ExternalId` + 啟用群組的 `ExternalId`。 + 2. 對每個目標呼叫對應 `IMessageChannel` 方法。 + 3. 每筆寫 `NotificationLog`(成功/失敗)。 + 4. 回傳彙總(成功數、失敗數、失敗明細)。 +- `GenerateBindingCodeAsync(memberId)`:產生短效碼。 +- 收件人/群組查詢、綁定列表/解除。 + +### `NotificationsController`(admin `[Authorize]`) +- `POST /api/notifications/send` — 立即發送(body + memberIds[] + groupIds[])。 +- `GET /api/notifications/recipients` — 可選收件人(已綁定會友 + 啟用群組)。 +- `GET /api/notifications/history` — `NotificationLog` 分頁。 +- `POST /api/notifications/members/{id}/line-binding-code` — 產生綁定碼。 +- `GET /api/notifications/groups` / `PUT .../groups/{id}` — 群組列表/改名/啟停用。 + +### 設定(config) +- `Line:ChannelAccessToken`、`Line:ChannelSecret`(user-secrets / appsettings,勿入版控)。 +- CORS 不受影響(webhook 為 server-to-server)。 +- DI:於 [Program.cs](API/ROLAC.API/Program.cs) 以 scoped 註冊 `IMessageChannel→LineMessageChannel`、`INotificationService→NotificationService`;`AddHttpClient`。 + +--- + +## 6. 前端(Angular) + +依慣例掛進 **UserPortalComponent** 側欄(`financeNavItems` 模式 + `getPageTitle`),新 `notifications` feature: + +- **撰寫+發送頁**:文字框 + 收件人多選(已綁定會友 + 群組)+ 送出。Kendo 元件、Tailwind 排版(`grid grid-cols-1 md:grid-cols-2`,preflight off)。Kendo DropdownList/MultiSelect 記得 `[valuePrimitive]="true"`。送出後顯示成功/失敗彙總。 +- **發送歷史頁**:Kendo 表格列 `NotificationLog`;列點選看明細,動作放右鍵 context menu(依慣例)。 +- **會友 Line 綁定面板**:會員明細頁加區塊,顯示綁定狀態;「產生綁定碼」按鈕 + 操作說明(加官方帳號 → 輸入此碼)。 +- **群組管理頁**:列出已註冊 Line 群組,可命名 / 啟停用。 +- 新增 `OfferingEntry` 之外的對應 API service(`notification-api.service.ts`)。 + +--- + +## 7. 錯誤處理 + +- **Webhook 簽章不符** → 400 並記錄;合法事件一律快速回 200(避免 Line 重送)。 +- **推播失敗**(對方封鎖官方帳號、id 失效、**額度用罄**)→ 寫入 `NotificationLog`(status=failed + error),並把失敗筆數回報管理員 UI。 +- **綁定碼**:過期 / 已使用 / 不存在 → reply 友善說明;重複綁定 → 更新既有 binding 而非報錯。 +- **額度**:之後階段可加每月推播計數器;本階段先以 log 觀察。 + +--- + +## 8. 測試(沿用既有 xUnit 慣例,`-c Release`/`--output` 建置) + +- 簽章驗證(HMAC-SHA256 正/負案例)。 +- 綁定碼比對(有效 / 過期 / 已使用 / 不存在)。 +- 收件人解析(含未綁定會友被略過、停用群組被略過)。 +- Line payload 組裝(mock `HttpClient`/`HttpMessageHandler`)。 +- Webhook 事件分派(follow / message / join / leave 樣本 payload)。 +- `NotificationService.SendNowAsync` 成功/部分失敗的彙總與 log 寫入。 + +--- + +## 9. 分階段範圍 + +### Phase 1(本次實作) +Line 通道 + webhook 綁定(個人+群組)+ 手動立即發送 + 發送歷史 + 會友綁定 UI + 群組管理 UI。 + +### Phase 2 +排程定時發送:`ScheduledNotification` 實體 + 背景 worker(`BackgroundService` 或 Hangfire/Quartz,實作計畫時決定)。 + +### Phase 3 +事件觸發(生日 / 活動報名確認 / 奉獻收據)整合既有模組;多通道擴充(PWA Web Push、WeChat)各加一個 `IMessageChannel` 實作。 + +--- + +## 10. 使用者需先準備(非程式) + +- 申請 **Line Official Account + Messaging API channel**,取得 **Channel access token** 與 **Channel secret**。 +- 在 nginx 設定,把 `/api/line/webhook` 對外路由到 API;於 Line 後台填入 webhook URL 並啟用。 +- 將 token/secret 放入 API 設定(user-secrets / appsettings,勿入版控)。 + +--- + +## 11. 待解 / 未涵蓋(Out of scope,留待後續) + +- 訊息範本與圖文訊息(Phase 1 僅純文字)。 +- 多語訊息(會友 `LanguagePreference` 可於後續用於範本選擇)。 +- 推播額度自動監控與告警。 +- 原生 App 推播 / PWA Web Push(待 501c3 / 上架條件成熟後評估)。