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>
This commit is contained in:
@@ -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 / 上架條件成熟後評估)。
|
||||
Reference in New Issue
Block a user