diff --git a/docs/newsletter_integration_memo.md b/docs/newsletter_integration_memo.md index 25f899e..82b9197 100644 --- a/docs/newsletter_integration_memo.md +++ b/docs/newsletter_integration_memo.md @@ -1,6 +1,6 @@ # 電子報介接備忘錄(租戶端 / Wagtail) -最後更新:2026-02-11 +最後更新:2026-02-18 ## 1. 目標與前提 @@ -154,3 +154,270 @@ 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: ` +- `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 或事件回寫機制處理。 + +### 9.10 One-Click Token 模式(目前採用) + +目前採用:`MemberCenter token relay` + +流程: +1. CMS 發送 payload 時同時提供: + - `template.list_unsubscribe_url_template`(給 Send Engine 產生 `List-Unsubscribe` header) + 信內可點連結由 `body_html/body_text` 直接使用 `{{unsubscribe_token}}` 組 URL。 + 例如:`https://{cms}/u/unsubscribe?token={{unsubscribe_token}}` +2. Send Engine 於發送時向 Member Center 取得每位收件者的退訂 token,並替換 `{{unsubscribe_token}}`。 +3. 使用者點擊信內退訂(或 mailbox one-click)後,進入 CMS `/u/unsubscribe`。 +4. CMS 不自行驗簽 token,直接以 S2S 呼叫 Member Center `unsubscribe(token)` 完成退訂。 + +備註: +- 舊的 CMS 自簽 HMAC one-click token 保留工具函式作為備援,但主流程已切到 relay 模式。 + +## 10. 下一階段演進備忘(先記錄,待試用回饋後再排) + +說明: +- 本節為「規劃中」項目,先記錄方向,不立即實作。 +- 優先等待:電子報排版產出 + 實際 user 試用回饋。 + +### 10.1 編輯器能力演進 + +現況: +- 目前使用 Wagtail Draftail 作為 HTML 編輯器,已可插入圖片(圖庫選擇 / 上傳)。 + +待回饋後評估: +- 若編輯需求提升(table、欄位版型、拖拉區塊、進階 email 元件),再評估導入更完整的 newsletter editor。 +- 原則:先確認真實編輯痛點,再導入新編輯器,避免過早複雜化。 + +### 10.2 電子報樣式(CSS)策略 + +建議方向: +- 採「固定樣式基底 + 內容編輯」模式。 +- CSS 不建議只放 `