Skip to content

Preferences: SDD ​

PRD · Tests · trạng thái: 01-06 đã xây (05-06 từ 14.10.2026, cùng Nhịp thư); trang tuỳ chọn trong tokyo và chọn theo kênh chưa có

Bảng ​

Hai bảng, hai khoá khác nhau, có chủ ý:

BảngKhoáGiữ gì
email_preferences (migration 20260907b_email_preferences.sql)user_idHuỷ nhận TOÀN BỘ thư gắn kết: unsubscribed_at, source (link = người chọn thôi nhận, suppressed = địa chỉ không nhận được), updated_at. Không có dòng nghĩa là chưa từ chối; cố ý không seed. Migration 20260909c_backfill_suppressed.sql bù source='suppressed' từ các lượt hỏng trước đó.
email_audience_prefs (migration 20261114a_email_orchestrator.sql)email_key = lower(trim(email))Nhịp (auto/weekly/important/none), nhóm bị tắt (muted, CSV các mã hoc_tap, su_kien, bai_doc, tin_tuan, lam_quen; mã lạ bị bỏ khi đọc), promo_consent_at, câu trả lời thư hỏi lại người hệ cũ, consent_version, source. Khoá theo địa chỉ vì 57 người có hai id ở hai bảng người dùng. Thêm một nhóm mới thì dòng cũ không có nó trong CSV, tức người đã lưu tuỳ chọn mặc định NHẬN nhóm đó.

Vì sao không gộp: mọi vòng gửi cũ (daily.ts, cổng gửi) đọc huỷ nhận theo user_id, nên "Không nhận thư nào" vẫn ghi email_preferences cho MỌI id của địa chỉ; bảng mới chỉ thêm những lựa chọn mà bảng cũ không chứa được.

Đường đi ​

BướcFileGhi chú
Sinh link + headerunsubscribeToken(env, userId) và nhánh gắn kết trong sendEmail() ở api.conan.school/src/shared/email/send.tsLink huỷ: HMAC của user_id trần với EMAIL_UNSUB_SECRET (giữ nguyên để link trong thư cũ vẫn chạy). Link tuỳ chọn: HMAC của chuỗi có mục đích prefs đứng trước user_id (orchestrator/links.ts), nên chữ ký huỷ không dùng được cho trang tuỳ chọn và ngược lại. Chân thư: "Đổi nhịp nhận thư · Huỷ nhận thư". Thiếu bí mật thì thư gắn kết đi ra không có cả hai link lẫn List-Unsubscribe.
HuỷGET / POST /api/system/email/unsubscribe?u=...&t=... ở src/domains/system/email-orch.tsGET chỉ trả trang xác nhận có nút (đổi 14.10.2026: GET từng ghi ngay, và máy quét link huỷ hộ người ta). POST giữ đúng RFC 8058 cho Gmail một chạm: u, t từ query, không cookie, không Origin, trả 200 HTML không chuyển hướng; ghi hỏng thì 503. Ghi source='link'. Log email_unsubscribed.
Trang tuỳ chọnGET / POST /api/system/email/prefs?u=...&t=..., cùng fileGET hiện nhịp, năm nhóm tắt được (CATEGORY_OPTIONS, cùng thứ tự MUTABLE_CATEGORIES: Học tập, Sự kiện và buổi học, Bài đọc và câu chuyện, Tin tuần, Chuỗi thư làm quen; nhóm cuối thêm 14.10.2026 cho journey_day), ô Ưu đãi; đã huỷ thì hiện đúng sự thật là nhịp "Không nhận". POST lưu email_audience_prefs + PREFS_CONSENT_VERSION; none ghi huỷ cho mọi id; nhịp khác gỡ huỷ link (không gỡ suppressed).
Hỏi lại người hệ cũGET / POST /api/system/email/reconnect?u=...&a=...&t=... (a là yes hoặc no), cùng fileGET là trang xác nhận; POST "Có" là cách DUY NHẤT người hệ cũ được coi là đồng ý nhận thư thường; "Không" huỷ mọi id.
Chặn khi gửisendEmail()Thư gắn kết: unsubscribed_at → E_UNSUBSCRIBED; prefsBlock đọc email_audience_prefs theo địa chỉ → E_UNSUBSCRIBED (nhịp none, trả lời "Không") hoặc E_PREFS (nhóm tắt, important, weekly đã có thư trong ~7 ngày). Thi hành cho MỌI vòng gửi thư gắn kết, kể cả daily.ts.
Nhớ bị chặnsendEmail() khi nhận E_RECIPIENT_SUPPRESSEDGhi source='suppressed'.
Lọc ở vòng gửisrc/shared/email/daily.ts; Nhịp thư audience.ts + dispatch.tsdaily.ts loại người có unsubscribed_at ở truy vấn chọn. Nhịp thư lọc ở lúc lập kế hoạch và đọc lại lúc gửi.

Chưa xây ​

  • Trang tuỳ chọn trong tokyo (com.conan.school không gọi tới hai bảng này).
  • Chọn theo kênh (chỉ có một kênh).
  • Tuỳ chọn cho khách không có user_id (cần cho newsletter): link có chữ ký hiện ký theo user_id.

Tài liệu nội bộ nền tảng Conan School.