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

11 KiB
Raw Permalink Blame History

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 年停止服務,不可用。
  • WhatsAppBusiness 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)
  • 對外推播NotificationServiceIMessageChannel.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

ScheduledNotificationPhase 2 預留,本階段不建表)

Body、收件人規格(memberIds/groupIds 或分群)、RunAtUtc、Recurrence、Status。


5. 元件與流程

IMessageChannel / LineMessageChannel

  • PushToUserAsync(externalId, text)
  • PushToGroupAsync(externalId, text)
  • ReplyAsync(replyToken, text)
  • 封裝 Line REST 呼叫;HttpClientIHttpClientFactorytoken 讀 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(拉進群):建一筆 MessagingGroupExternalId=groupIdName 待命名);reply 提示管理員到後台命名。
    • leave:對應 MessagingGroupIsActive=false

NotificationService

  • SendNowAsync(body, memberIds[], groupIds[], sentByUserId)
    1. 解析收件人 → 已綁定會友的 ExternalId + 啟用群組的 ExternalId
    2. 對每個目標呼叫對應 IMessageChannel 方法。
    3. 每筆寫 NotificationLog(成功/失敗)。
    4. 回傳彙總(成功數、失敗數、失敗明細)。
  • GenerateBindingCodeAsync(memberId):產生短效碼。
  • 收件人/群組查詢、綁定列表/解除。

NotificationsControlleradmin [Authorize]

  • POST /api/notifications/send — 立即發送(body + memberIds[] + groupIds[])。
  • GET /api/notifications/recipients — 可選收件人(已綁定會友 + 啟用群組)。
  • GET /api/notifications/historyNotificationLog 分頁。
  • POST /api/notifications/members/{id}/line-binding-code — 產生綁定碼。
  • GET /api/notifications/groups / PUT .../groups/{id} — 群組列表/改名/啟停用。

設定(config

  • Line:ChannelAccessTokenLine:ChannelSecretuser-secrets / appsettings,勿入版控)。
  • CORS 不受影響(webhook 為 server-to-server)。
  • DI:於 Program.cs 以 scoped 註冊 IMessageChannel→LineMessageChannelINotificationService→NotificationServiceAddHttpClient

6. 前端(Angular

依慣例掛進 UserPortalComponent 側欄(financeNavItems 模式 + getPageTitle),新 notifications feature

  • 撰寫+發送頁:文字框 + 收件人多選(已綁定會友 + 群組)+ 送出。Kendo 元件、Tailwind 排版(grid grid-cols-1 md:grid-cols-2preflight off)。Kendo DropdownList/MultiSelect 記得 [valuePrimitive]="true"。送出後顯示成功/失敗彙總。
  • 發送歷史頁Kendo 表格列 NotificationLog;列點選看明細,動作放右鍵 context menu(依慣例)。
  • 會友 Line 綁定面板:會員明細頁加區塊,顯示綁定狀態;「產生綁定碼」按鈕 + 操作說明(加官方帳號 → 輸入此碼)。
  • 群組管理頁:列出已註冊 Line 群組,可命名 / 啟停用。
  • 新增 OfferingEntry 之外的對應 API servicenotification-api.service.ts)。

7. 錯誤處理

  • Webhook 簽章不符 → 400 並記錄;合法事件一律快速回 200(避免 Line 重送)。
  • 推播失敗(對方封鎖官方帳號、id 失效、額度用罄)→ 寫入 NotificationLogstatus=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 實體 + 背景 workerBackgroundService 或 Hangfire/Quartz,實作計畫時決定)。

Phase 3

事件觸發(生日 / 活動報名確認 / 奉獻收據)整合既有模組;多通道擴充(PWA Web Push、WeChat)各加一個 IMessageChannel 實作。


10. 使用者需先準備(非程式)

  • 申請 Line Official Account + Messaging API channel,取得 Channel access tokenChannel 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 / 上架條件成熟後評估)。