innovedus_cms/docs/newsletter_integration_memo.md
Warren Chen b16ee811e3 feat: Add support for weekly newsletters with article selection and templates
- Introduced `campaign_type` field in `NewsletterCampaign` model to differentiate between general and weekly newsletters.
- Added `weekly_articles` ManyToMany field to associate articles with weekly newsletters.
- Implemented `item_template_html` for customizing the appearance of articles in weekly newsletters.
- Updated forms and views to handle new fields and validation logic for weekly newsletters.
- Created a new view for selecting newsletter types before creating a campaign.
- Enhanced the newsletter editor UI to show/hide fields based on the selected campaign type.
- Added JavaScript functionality for paginating article selections in the editor.
- Updated CSS for new UI components related to newsletter campaigns.
- Created a migration to add new fields to the `NewsletterCampaign` model.
- Added a template for the newsletter type selection page.
2026-05-14 18:17:35 +09:00

584 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 電子報介接備忘錄(租戶端 / 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 endpointone-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 驗證
此規範符合「新增定型電子報需要改程式」的產品決策,且可保持改動局部化。