Files
ROLAC/docs/superpowers/specs/2026-06-20-line-notifications-design.md
Chris Chen aaaae09bd2 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 <noreply@anthropic.com>
2026-06-20 20:49:00 -07:00

230 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Line 通知/提醒模組 — 設計文件
- **日期**2026-06-20
- **狀態**:已核可(待實作計畫)
- **作者**Chris Chen + Claude
- **專案**ROLACRiver 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-codeZapier 等)**:資料外流、難做精準個人綁定。
- **通用通知平台一次做好**:對本規模過度投資。
---
## 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=groupIdName 待命名);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 / 上架條件成熟後評估)。