- Added one-click unsubscribe functionality with token generation and verification. - Introduced a new model for tracking one-click unsubscribe audits. - Enhanced newsletter campaign management with the ability to send campaigns immediately. - Implemented a scheduler for dispatching due newsletter campaigns. - Updated views and templates to support one-click unsubscribe and campaign previews. - Added management commands for running the newsletter scheduler. - Removed obsolete SSL certificate file. - Updated entrypoint script to handle different application roles.
408 lines
13 KiB
Markdown
408 lines
13 KiB
Markdown
# 電子報介接備忘錄(租戶端 / Wagtail)
|
||
|
||
最後更新:2026-02-18
|
||
|
||
## 1. 目標與前提
|
||
|
||
本備忘錄用於整理租戶端(Wagtail)接下來的實作方向,重點是先完成可運行流程,再逐步補完細節。
|
||
|
||
## 2. 已確認決策(本次會議結論)
|
||
|
||
1. Member Center 雖為共用平台,但目前只有一個租戶。
|
||
2. 現階段「發確認信」與「訂閱/退訂頁面」先做在 Wagtail。
|
||
3. Wagtail 與 Member Center 的溝通以 API 為主。
|
||
4. SMTP relay 僅負責寄出一次,重送、token 安全、簽章等由其他系統或工具負責,不屬 Wagtail 責任。
|
||
5. 工作大項與順序固定為:
|
||
1. 準備工作(電子報系統設定)
|
||
2. 訂閱流程
|
||
3. 退訂流程
|
||
4. 電子報 app
|
||
5. 發信流程(排程)
|
||
|
||
## 3. 工作大項與執行順序(含範圍)
|
||
|
||
## 3.1 準備工作(電子報系統設定,第一優先)
|
||
|
||
範圍:
|
||
- 建立 Member Center 連線設定(base URL、tenant_id、list_id、API timeout 等)。
|
||
- 建立 Send Engine 連線設定(base URL、OAuth scope、API timeout 等)。
|
||
- 建立一般發信設定(SMTP relay、寄件者名稱/信箱、reply-to、預設編碼等)。
|
||
- 建立模板設定:
|
||
- 訂閱確認信 template
|
||
- 訂閱確認(成功)頁面 template
|
||
- 取消訂閱頁面 template
|
||
|
||
重點:
|
||
- 設定集中管理,避免散落在程式碼或多處後台欄位。
|
||
- 區分「系統連線設定」與「內容模板設定」,便於權限與維運。
|
||
|
||
完成標準(DoD):
|
||
- 可在單一設定入口完成上述設定並通過基本驗證。
|
||
- 模板可被後續訂閱/退訂流程直接引用。
|
||
|
||
## 3.2 訂閱流程(第二優先)
|
||
|
||
範圍:
|
||
- 在站台頁面區塊(暫定 Footer)提供 email 訂閱入口。
|
||
- 後端接收 email,呼叫 Member Center 訂閱 API。
|
||
- 由 Wagtail 送出訂閱確認信(透過 SMTP relay)。
|
||
- 使用者點信內連結回到 Wagtail 確認頁。
|
||
- 確認頁自動呼叫 Member Center 確認 API。
|
||
|
||
重點:
|
||
- 流程中 token 需一路帶入與驗證。
|
||
- 確認信內容採 HTML(可附純文字 fallback)。
|
||
|
||
完成標準(DoD):
|
||
- 可從站台成功發起訂閱。
|
||
- 可收到確認信並完成確認。
|
||
- 確認成功/失敗頁有明確訊息與追蹤記錄。
|
||
|
||
## 3.3 退訂流程(第三優先)
|
||
|
||
範圍:
|
||
- 電子報底部提供退訂連結,導向 Wagtail 退訂頁。
|
||
- 進入退訂頁時,先向 Member Center 申請退訂 token。
|
||
- 使用者按確認退訂後,呼叫 Member Center 退訂 API 完成流程。
|
||
|
||
重點:
|
||
- token 不做長期保存。
|
||
- 頁面需可處理 token 失效/不存在等錯誤狀態。
|
||
|
||
完成標準(DoD):
|
||
- 使用者能從信件連結完成退訂。
|
||
- 退訂結果頁可正確呈現成功/失敗狀態。
|
||
|
||
## 3.4 電子報 app(第四優先)
|
||
|
||
範圍:
|
||
- 在 Wagtail 後台提供電子報內容管理能力。
|
||
- 提供 HTML 編輯器建立/編修內容。
|
||
- 支援可替換參數(例如 token、email)。
|
||
|
||
重點:
|
||
- 編輯器中的連結參數以佔位符表示,發送前由 backend 替換。
|
||
- 若有 Member Center API 相關連結需在內容中可配置,也採相同參數機制。
|
||
|
||
完成標準(DoD):
|
||
- 編輯器可存草稿與更新內容。
|
||
- 內容中參數可在送出前正確替換。
|
||
|
||
## 3.5 發信流程(排程,第五優先)
|
||
|
||
範圍:
|
||
- 可建立發送任務與排程時間。
|
||
- 排程前可持續編輯內容與調整時間。
|
||
- 到時觸發時:先取 Member Center auth,再將內容包固定 header/footer,送到 Send Engine。
|
||
|
||
重點:
|
||
- 「內容定稿」與「實際送出」拆開。
|
||
- 發送前的模板組裝由 Wagtail backend 負責。
|
||
|
||
完成標準(DoD):
|
||
- 排程到點可成功建立 Send Engine send job。
|
||
- 失敗可記錄原因並可人工重試。
|
||
|
||
## 4. 參數替換規則(先行約定)
|
||
|
||
建議佔位符格式(待實作時可再定版):
|
||
- `{{token}}`
|
||
- `{{email}}`
|
||
- `{{list_id}}`
|
||
- `{{tenant_id}}`
|
||
- `{{confirm_url}}`
|
||
- `{{unsubscribe_url}}`
|
||
|
||
替換時機:
|
||
- 發信前 backend 最後一步統一替換。
|
||
|
||
注意事項:
|
||
- URL 參數需做 URL encode。
|
||
- 缺少必要參數時,中止發送並記錄可追蹤錯誤。
|
||
|
||
## 5. URL 與系統歸屬標註規範(文件撰寫規則)
|
||
|
||
後續文件凡提到網址,必須明確標註「是哪個系統」:
|
||
|
||
1. Wagtail(租戶站台)網址
|
||
- 例:`https://{tenant-site}/newsletter/confirm?token=...`
|
||
- 用途:使用者互動頁、站台前台/後台頁面。
|
||
|
||
2. Member Center API 網址
|
||
- 例:`https://{member-center}/newsletter/subscribe`
|
||
- 用途:訂閱、確認、退訂 token、退訂等 API。
|
||
|
||
3. Send Engine API 網址
|
||
- 例:`https://{send-engine}/api/send-jobs`
|
||
- 用途:建立/查詢/取消發信任務。
|
||
|
||
4. SMTP Relay 連線端點
|
||
- 用途:Wagtail 寄送確認信。
|
||
- 備註:僅作為寄送通道,不承擔重送與安全機制。
|
||
|
||
## 6. 非 Wagtail 責任邊界(本階段)
|
||
|
||
以下不納入目前 Wagtail 工作範圍:
|
||
- SMTP 後續重送策略。
|
||
- token 簽章與高階安全策略。
|
||
- 跨系統風控與防濫用機制。
|
||
|
||
## 7. 下一步(實作啟動清單)
|
||
|
||
1. 先完成電子報系統設定(Member Center / Send Engine / SMTP / 模板)。
|
||
2. 再完成訂閱流程 API 串接與確認頁。
|
||
3. 再完成退訂頁與退訂 API 串接。
|
||
4. 建立電子報 app 與 HTML 編輯器資料模型。
|
||
5. 最後接排程任務與 Send Engine 發信。
|
||
|
||
## 8. Member Center API 實際規格(以 member_center 程式碼 / OpenAPI 為準)
|
||
|
||
資料來源(2026-02-17 比對):
|
||
- `../member_center/src/MemberCenter.Api/Controllers/NewsletterController.cs`
|
||
- `../member_center/docs/openapi.yaml`
|
||
|
||
### 8.1 Base URL 與路徑
|
||
|
||
- OpenAPI `servers.url` 為 `/api`,部署時實際呼叫通常為:
|
||
- `https://{member-center}/api/newsletter/...`
|
||
- 若部署已在 gateway 做 path rewrite,也可為:
|
||
- `https://{member-center}/newsletter/...`
|
||
- 租戶端必須以實際部署路徑設定 `member_center_base_url`。
|
||
|
||
### 8.2 Endpoint / Method / Request
|
||
|
||
1. 訂閱
|
||
- `POST /newsletter/subscribe`
|
||
- JSON body(必要):
|
||
- `list_id`(Guid)
|
||
- `email`(string)
|
||
- JSON body(可選):
|
||
- `preferences`(object)
|
||
- `source`(string)
|
||
- 回傳包含 `confirm_token`
|
||
|
||
2. 訂閱確認(雙重驗證)
|
||
- `GET /newsletter/confirm?token=...`
|
||
- 注意:**confirm 是 GET,不是 POST**
|
||
|
||
3. 單一名單退訂
|
||
- `POST /newsletter/unsubscribe`
|
||
- JSON body(必要):
|
||
- `token`(string)
|
||
|
||
4. 申請退訂 token
|
||
- `POST /newsletter/unsubscribe-token`
|
||
- JSON body(必要):
|
||
- `list_id`(Guid)
|
||
- `email`(string)
|
||
- 回傳:
|
||
- `unsubscribe_token`(string)
|
||
|
||
### 8.3 與租戶端目前實作對齊結果
|
||
|
||
- `subscribe`:已使用 `POST`。
|
||
- `confirm`:已改為 `GET + query token`(修正 HTTP 405 問題)。
|
||
- `unsubscribe`:已使用 `POST`,且 body 僅送 `token`。
|
||
- `unsubscribe-token`:已使用 `POST`,且 body 送 `list_id + email`。
|
||
|
||
## 9. List-Unsubscribe One-Click 規範(發信流程階段導入)
|
||
|
||
說明:
|
||
- 本節為「電子報 app + 排程發信」階段的目標規範。
|
||
- 已完成的訂閱/退訂流程維持現況,不回頭重做;待發信流程上線時再整合 one-click。
|
||
|
||
### 9.1 目標
|
||
|
||
- 提供電子報編輯與排程。
|
||
- 產生 `List-Unsubscribe` one-click header。
|
||
- 呼叫發信代理 API。
|
||
- 提供 unsubscribe endpoint(one-click + 人類點擊頁)。
|
||
|
||
### 9.2 Unsubscribe Token 設計
|
||
|
||
建議使用 JWT 或 HMAC token。
|
||
|
||
Token 內容建議:
|
||
- `subscriber_id`
|
||
- `list_id`
|
||
- `site_id`
|
||
- `exp`
|
||
- `nonce`(optional)
|
||
|
||
安全要求:
|
||
- 不需登入即可退訂。
|
||
- 必須可 idempotent。
|
||
- Token 需有過期時間。
|
||
- Token 必須簽章驗證。
|
||
|
||
### 9.3 Unsubscribe Endpoint
|
||
|
||
1. One-click endpoint
|
||
- `POST /u/unsubscribe`
|
||
- Request:`token`
|
||
- 流程:
|
||
- 驗證 token
|
||
- 呼叫會員平台 API
|
||
- 回傳 `200`
|
||
- 不可:
|
||
- 要求登入
|
||
- 要求二次確認
|
||
|
||
2. 人類點擊頁面
|
||
- `GET /u/unsubscribe?token=xxx`
|
||
- 流程:
|
||
- 驗證 token
|
||
- 顯示退訂成功頁面
|
||
|
||
### 9.4 發信 Header 規則
|
||
|
||
Newsletter 必須包含:
|
||
- `List-Unsubscribe: <https://domain/u/unsubscribe?token=xxx>`
|
||
- `List-Unsubscribe-Post: List-Unsubscribe=One-Click`
|
||
|
||
註記:
|
||
- Transactional email 不應包含 List-Unsubscribe。
|
||
|
||
### 9.5 與會員平台整合(one-click 目標介面)
|
||
|
||
呼叫 API:
|
||
- `POST /api/subscriptions/unsubscribe`
|
||
|
||
Request:
|
||
- `subscriber_id`
|
||
- `list_id`
|
||
- `source: "one_click"`
|
||
- `campaign_id`
|
||
|
||
回傳:
|
||
- `success`
|
||
- `already_unsubscribed`
|
||
|
||
### 9.6 測試案例
|
||
|
||
1. Token 驗證
|
||
- 正常 token -> 成功退訂
|
||
- 過期 token -> `400` 或 `410`
|
||
- 已退訂 -> `200`
|
||
- 簽章錯誤 -> `400`
|
||
|
||
2. Header 驗證
|
||
- Newsletter 發送時必定包含 `List-Unsubscribe`
|
||
- Transactional email 不包含 `List-Unsubscribe`
|
||
- Header 格式正確
|
||
|
||
### 9.7 安全與 UX
|
||
|
||
安全:
|
||
- Unsubscribe endpoint 不受 CSRF 限制
|
||
- Token 驗證必須 server-side
|
||
- 記錄 audit log
|
||
- 不顯示 `subscriber_id` 在 URL 明文
|
||
|
||
UX:
|
||
- 退訂成功頁面簡潔
|
||
- 提供重新訂閱入口
|
||
- 不強迫填問卷
|
||
|
||
### 9.8 目前實作狀態(Wagtail)
|
||
|
||
- 已提供 one-click endpoint:
|
||
- `POST /u/unsubscribe`(token 可由 query/body 提供)
|
||
- `GET /u/unsubscribe?token=...`(人類點擊頁)
|
||
- 已實作 token 驗證(HMAC + `exp`)與 idempotent 行為(重複退訂維持 200)。
|
||
- 已實作 server-side audit log(`OneClickUnsubscribeAudit`)。
|
||
- 已提供 one-click URL + Header 產生工具:
|
||
- `List-Unsubscribe`
|
||
- `List-Unsubscribe-Post`
|
||
|
||
### 9.9 Send Engine 發送介接(依 `../mass_mail_engine/docs/openapi.yaml` 對齊)
|
||
|
||
已對齊的 API:
|
||
1. 建立 send job
|
||
- `POST /api/send-jobs`
|
||
- request 以 `CreateSendJobRequest` 為主(`list_id`、`subject`、`body_html/body_text/template`)
|
||
- response 讀取:
|
||
- `send_job_id`
|
||
- `status`
|
||
|
||
2. 查詢 send job
|
||
- `GET /api/send-jobs/{id}`
|
||
- 若建立後狀態非最終態,持續輪詢直到最終態或超過輪詢上限。
|
||
|
||
狀態對應(Wagtail campaign):
|
||
- Send Engine `completed` -> campaign `sent`
|
||
- Send Engine `failed` / `cancelled` -> campaign `failed`
|
||
- 建立失敗 / 輪詢逾時 / 輪詢錯誤 -> campaign `failed`
|
||
|
||
Send Engine 最終態(terminal):
|
||
- `completed`
|
||
- `failed`
|
||
- `cancelled`
|
||
|
||
備註:
|
||
- 目前 dispatch 紀錄改為「每次送 job / 重試一次一筆」,不再是逐收件者一筆。
|
||
- 若要做投遞到單一收件者的最終監控(delivery/bounce/complaint),仍建議接 Send Engine webhook 或事件回寫機制處理。
|
||
|
||
## 10. 下一階段演進備忘(先記錄,待試用回饋後再排)
|
||
|
||
說明:
|
||
- 本節為「規劃中」項目,先記錄方向,不立即實作。
|
||
- 優先等待:電子報排版產出 + 實際 user 試用回饋。
|
||
|
||
### 10.1 編輯器能力演進
|
||
|
||
現況:
|
||
- 目前使用 Wagtail Draftail 作為 HTML 編輯器,已可插入圖片(圖庫選擇 / 上傳)。
|
||
|
||
待回饋後評估:
|
||
- 若編輯需求提升(table、欄位版型、拖拉區塊、進階 email 元件),再評估導入更完整的 newsletter editor。
|
||
- 原則:先確認真實編輯痛點,再導入新編輯器,避免過早複雜化。
|
||
|
||
### 10.2 電子報樣式(CSS)策略
|
||
|
||
建議方向:
|
||
- 採「固定樣式基底 + 內容編輯」模式。
|
||
- CSS 不建議只放 `<head><style>`,實務上需考慮 email client 相容性,通常會在發送前做 CSS inlining(將重要樣式轉為 inline style)。
|
||
- 可保留少量 head style(例如 media query),但核心排版建議以 inline 為主。
|
||
|
||
### 10.3 外框模板(Header / Footer / Shell)管理
|
||
|
||
目標:
|
||
- 提供可編輯的 newsletter shell(例如 `layout.html`),內含 head、header、footer 與內容插槽。
|
||
|
||
建議實作概念:
|
||
- 內容編輯區僅負責 body 內文。
|
||
- 發送時由 backend 組裝:`shell + content + one-click headers/links`。
|
||
- 預覽也走同一組裝流程,避免「預覽長得跟實際寄出不一致」。
|
||
|
||
### 10.4 退訂連結與個人化參數責任分工
|
||
|
||
共識方向:
|
||
- 退訂連結需與收件者身分綁定(每位收件者唯一 token / URL)。
|
||
- 內容模板需預留替換位(例如 `{{unsubscribe_url}}`)。
|
||
|
||
責任分工建議:
|
||
- CMS:產內容、產 placeholder、組裝信件與 one-click header。
|
||
- Send Engine:執行發送、接收投遞回報(bounce/complaint 等)。
|
||
- SES(或下游 provider):最終投遞與回執來源。
|
||
|
||
### 10.5 文章區塊直接引入
|
||
|
||
需求方向:
|
||
- 支援從既有文章中挑選區塊或全文片段匯入電子報。
|
||
|
||
建議:
|
||
- 第一階段可先做「選文章 -> 帶入標題/摘要/首圖/連結」。
|
||
- 第二階段再考慮「可編輯快照」或「保持動態同步」策略。
|
||
|
||
### 10.6 預覽能力
|
||
|
||
需求方向:
|
||
- 內文編輯預覽。
|
||
- 樣式(shell)編輯預覽。
|
||
- 能注入假資料(假收件者、假 token、假文章)看最終結果。
|
||
|
||
建議:
|
||
- 做「最終寄出 HTML 預覽」功能,預覽管線與正式發送使用同一套 renderer。
|
||
- 後續可加多裝置寬度與常見 email client 快速檢視模式(若使用者回饋有需要)。
|