# 電子報介接備忘錄（租戶端 / 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 或事件回寫機制處理。

### 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 不建議只放 `<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 快速檢視模式（若使用者回饋有需要）。

## 10. 電子報「類型化編輯」設計分析與實作計劃（2026-05-13）

需求摘要（已確認）：
- 建立電子報時，先選「電子報類型」再進入編輯。
- 每種電子報類型有預設 template，但允許使用者改模板。
- 既有類型：`通用電子報`（自由編輯內容）。
- 新增類型：`每週新聞電子報`（選文章組版）。
- `每週新聞`除模板與文章選擇外，還要有「單則新聞 HTML 樣式」：
  - 系統提供預設值。
  - 每封電子報可覆寫（通常不改，必要時可改）。
- 預覽功能必須保留，且要反映「套模板後最終結果」。
- 後續新增定型電子報，接受「需要改程式」。

### 10.1 可行性結論

可行，且建議採「類型策略（type strategy）」實作：
- 資料層只新增最少欄位承載「類型 + 類型專屬設定」。
- 組版/預覽/發送統一走同一組 renderer 介面，依類型分派。
- 後續加新類型時，不改既有類型流程，僅新增一個 renderer + 後台欄位配置。

### 10.2 目標操作流程（UX）

1. 使用者在 Snippet 列表按「新增電子報」。
2. 第一步顯示「選擇電子報類型」頁：
   - `general` 通用電子報
   - `weekly_news` 每週新聞電子報
3. 選類型後進入對應編輯畫面（或同畫面動態顯示對應欄位）。
4. 系統帶入該類型預設 template（可改）。
5. `weekly_news` 類型需額外完成：
   - 選文章（可多選、可排序）
   - 單則新聞樣式（預設值 + 可覆寫）
6. 預覽按鈕顯示最終結果（文章內容 + 單則樣式 + 外層模板）。
7. 發送時使用相同 renderer，避免預覽與實際寄送不一致。

### 10.3 資料模型規劃（最小變更）

建議在 `NewsletterCampaign` 新增：
- `campaign_type`（CharField）
  - choices: `general`, `weekly_news`
  - default: `general`
- `item_template_html`（TextField, blank=True）
  - 用於 `weekly_news` 單則新聞樣式覆寫
- `content_config`（JSONField, default=dict, blank=True）
  - 先用於存 `weekly_news` 文章清單與排序資訊
  - 範例：
    - `{"article_page_ids":[123,456,789]}`

建議在 `NewsletterTemplate` 新增（可選，但推薦）：
- `template_type`（CharField）
  - 用於限制模板類型對應（避免 weekly 套到不相容模板）
  - 若先求快可不加，先用通用模板 + placeholder 規範。

建議新增「類型預設設定」來源（二選一）：
1. 快速版：在程式常數定義每種類型預設值（包括預設 item template）。
2. 長期版：加 `NewsletterTypeSettings`（Wagtail setting/snippet）供後台改預設值。

### 10.4 渲染與發送策略（核心）

新增 renderer 介面（概念）：
- `build_campaign_body_html(campaign) -> str`
- `build_campaign_body_text(campaign) -> str`
- `build_preview_html(campaign_or_payload) -> str`

類型分派：
- `general`：
  - 直接沿用既有 `html_template` / `text_template` 流程。
- `weekly_news`：
  - 讀 `content_config.article_page_ids`
  - 載入文章（標題、連結、摘要、封面等）
  - 以 `item_template_html` 逐筆渲染並串接成 `email_body`
  - 再套外層 `newsletter_template.template_html`

注意：
- 預覽 API 與發送 job 必須共用同一 renderer。
- 不把套完外層模板的結果存回 DB（維持既有原則：預覽/發送當下組裝）。

### 10.5 後台表單與互動規劃

#### A. 建立流程（先選類型）
- 做法 1（推薦）：客製 `CreateView`，先顯示 `campaign_type`，提交後導到 edit。
- 做法 2：進 create 頁先顯示全欄位，但用 JS 依 `campaign_type` 切換可見區塊。

#### B. `general` 欄位
- 顯示：`subject_template`, `html_template`, `text_template`, `newsletter_template`
- 隱藏：`weekly_news` 文章選擇區與 item template（或只讀）

#### C. `weekly_news` 欄位
- 顯示：
  - `newsletter_template`
  - 文章選擇器（多選 + 排序）
  - `item_template_html`（有預設值，可覆寫）
  - （可選）`subject_template`（若空可由 template 預設 subject 帶入）
- 隱藏：
  - 自由編輯 `html_template`（或改成唯讀顯示最終組版來源）

### 10.6 預覽功能規劃

新增/調整 preview endpoint：
- 接收 `campaign_type` 與對應 payload。
- `weekly_news` 預覽時直接用目前畫面文章清單 + item template 組版，不必先存檔。
- 回傳最終 HTML（已套外層模板、已替換示例變數）。

### 10.7 每週新聞（Weekly）預設單則樣式建議

預設 `item_template_html` 建議包含：
- `{{article_title}}`
- `{{article_url}}`
- `{{article_cover_url}}`
- `{{article_intro}}`
- `{{article_date}}`

建議 fallback 規則：
- 無封面圖：隱藏 `<img>` 區塊或改用預設圖。
- 無摘要：顯示標題 + 連結即可。

### 10.8 實作步驟（建議）

1. Model + migration
- `NewsletterCampaign` 新增 `campaign_type`, `item_template_html`, `content_config`。

2. Admin form / panels
- 先選類型流程（create view）+ 欄位顯示切換。
- `weekly_news` 文章選擇與排序 UI（第一版可先用簡單多選）。

3. Renderer
- 抽出 `general` / `weekly_news` 共用入口。
- 串入 preview API 與 send scheduler。

4. Preview
- 擴充現有 preview compose 邏輯，使其依 `campaign_type` 分派。

5. Send
- `_build_send_job_payload` 改為依 renderer 組 body，再送 Send Engine。

6. 測試
- `general` 回歸測試（不可壞）。
- `weekly_news` 組版、預覽、發送一致性測試。

### 10.9 風險與控管

風險：
- 類型欄位加入後，既有 campaign 資料相容性。
- preview 與 send 使用不同邏輯導致內容不一致。
- 文章多選排序 UI 複雜度偏高。

控管：
- 既有資料 migration 一律補 `campaign_type='general'`。
- renderer 單一路徑，preview/send 共用。
- `weekly_news` 第一版先做可用，再迭代進階排序體驗。

### 10.10 後續新增定型電子報的擴充規範

每新增一種類型至少變更：
1. `campaign_type` choices
2. 該類型後台欄位配置
3. 該類型 renderer
4. 該類型 preview payload 驗證

此規範符合「新增定型電子報需要改程式」的產品決策，且可保持改動局部化。
