aaaae09bd2
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>
11 KiB
11 KiB
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. 關鍵技術現實(限制條件)
- Bot 無法主動亂發訊息:要發給個人,必須先取得對方的 Line
userId,而此 id 只能在對方加官方帳號好友 / 對 bot 說話 / 把 bot 拉進群時,透過 webhook 取得。無法手動輸入。 - Webhook 需要公開 HTTPS endpoint:生產環境 nginx 終止 TLS、API 在後(
UseForwardedHeaders已設定),故 webhook 走https://<網域>/api/line/webhook,無需新基礎建設,只需 nginx 加路由。 - 推播額度:Line 官方帳號免費方案每月有則數上限(數百則)。reply(對方剛互動的視窗內回覆)免費,push(主動推播)才計額度。綁定流程走 reply,省額度。事件觸發大量發送時需留意額度。
- 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,匿名)
- 讀原始 body,以
Line:ChannelSecret計算 HMAC-SHA256,與X-Line-Signature比對;不符回 400。 - 快速回 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):- 解析收件人 → 已綁定會友的
ExternalId+ 啟用群組的ExternalId。 - 對每個目標呼叫對應
IMessageChannel方法。 - 每筆寫
NotificationLog(成功/失敗)。 - 回傳彙總(成功數、失敗數、失敗明細)。
- 解析收件人 → 已綁定會友的
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 以 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 / 上架條件成熟後評估)。