# Notification 模組 統一對外通知入口(Email / SMS),支援同步 `Send`、異步 `Enqueue` + Redis 重試 Worker、冪等、配額、DLQ。 > 規格書(Data Dictionary `notifications` / `notification_dlq`、NotifyKind 一覽、API)→ [`SDD.md`](./SDD.md) --- ## 測試(本機) 單一 CLI:`cmd/notify-test`,Makefile 包一層: ```bash make deps-up # Mongo + Redis(異步必備) make mongo-index # 建索引 make notify-test METHOD=<名稱> [TO=] [PHONE=] [MOCK=1] ``` 完整 METHOD 表見 [`docs/notification-testing.md`](../../../docs/notification-testing.md)。 ### 同步 vs 異步 | 方式 | UseCase | 何時完成 | 需要 Redis | |------|---------|----------|:----------:| | **同步** | `Notifier.Send` | 呼叫當下送完(或失敗) | 建議(冪等/配額) | | **異步** | `Notifier.Enqueue` | 先寫 Mongo `pending`,由 **RetryWorker** 從 Redis 佇列取出再送 | **必須** | 異步流程: ```mermaid sequenceDiagram participant App participant Mongo participant Redis participant Worker participant Provider App->>Mongo: Insert status=pending App->>Redis: ZADD 排程 job(立即或退避) App-->>App: 回傳 pending DTO Worker->>Redis: ClaimDue 到期 job Worker->>Provider: Send 渲染後內容 Worker->>Mongo: Update status=sent / failed / retrying Note over Worker,Mongo: 超過 MaxRetry → notification_dlq ``` ### 測異步(Enqueue) **前置:** `etc/gateway.dev.yaml` 要有 `Mongo`、`Redis`,且 `Notification.Async` 可維持預設(`Worker` ≥ 1)。 ```bash # Email 異步(歡迎信模板 tenant_welcome) make notify-test METHOD=email-enqueue TO=you@example.com # SMS 異步 make notify-test METHOD=sms-enqueue PHONE=0912345678 # mock:log 會印 [notification mock email/sms] 寄了什麼 make notify-test METHOD=email-enqueue TO=test@example.com MOCK=1 ``` `notify-test` 在 `email-enqueue` / `sms-enqueue` 時會**暫時啟動** process 內 RetryWorker(與 `make run-dev` 相同邏輯),輪詢 Redis 佇列直到 Mongo 紀錄變 `sent` 或逾時(預設 45 秒,`-poll` 可改)。 **成功時輸出範例:** ```text method=email-enqueue email=mock sms=mock notification_id=... provider=mock status=sent OK ``` **失敗常見原因:** - 沒開 Redis → `async notification requires redis retry queue` - Worker 沒跑、佇列卡住 → `timeout after 45s` - `Email.From` 空白 → 啟動即失敗 **關於 log:** 若看到 `FindOne - fail(mongo: no documents in result)` 且 filter 含 `idempotency_key`,代表「第一次用這個 key 發送」,屬冪等查詢的正常結果,不是寄信失敗。新版 `FindByIdempotency` 已改用 `Find` 避免此誤導性 error log。 ### 用 Gateway 長期跑 Worker(接近正式環境) 異步在 production 由 **同一個 Gateway process** 背景跑 Worker,不是只靠 CLI: ```bash make run-dev # ServiceContext.StartWorkers → NotificationRetry ``` 此時由 API/其他 use case 呼叫 `Enqueue` 即可;本機若尚無 Notification HTTP,仍用 `notify-test METHOD=email-enqueue` 觸發。 ### 查 Mongo 確認異步結果 ```bash mongosh gateway --eval ' db.notifications.find({ tenant_id: "notify-test" }) .sort({ occurred_at: -1 }) .limit(3) .forEach(d => print(d.kind, d.status, d.provider, d.attempts)) ' ``` | `status` | 意義 | |----------|------| | `pending` | 已 Enqueue,Worker 尚未送完 | | `sent` | 已送達 | | `retrying` | 失敗、等待退避重試 | | `failed` / `dropped` | 放棄或進 DLQ | DLQ: ```bash make notify-test METHOD=admin-dlq ``` ### Redis 佇列 key 預設 zset:`notif:retry:zset`(可由 `Notification.Async.QueueRedisKey` 覆寫,example 為 `notification:queue`)。 ### 同步對照(順便測) ```bash make notify-test METHOD=email-send TO=you@example.com MOCK=1 make notify-test METHOD=sms-send PHONE=0912345678 MOCK=1 ``` --- ## 設定摘要(`gateway.dev.yaml`) ```yaml Notification: Async: QueueRedisKey: notification:queue # 可省略,預設 notif:retry:zset Worker: 2 MaxRetry: 5 BackoffSeconds: [1, 5, 30, 300, 1800] Email: From: noreply@example.com # 必填 SMTP: Enable: true # 真實 SMTP;MOCK=1 時 CLI 會關閉 ``` Email provider 看 `SMTP.Enable` / `SES.Enable`,不是 `Provider: smtp` 字串。 --- ## 擴充指南 ### 新增 Email Provider 已內建:`smtp_sender.go`、`ses_sender.go`。 1. 在 `provider/email/` 實作 `Sender`(`Name`、`Sort`、`Send`)。 2. 在 `config/config.go` 加設定,並在 `usecase/factory.go` 的 `collectEmailSenders` 註冊。 3. 補 `provider/email/*_test.go`。 ### 新增 NotifyKind 1. `domain/enum/kind.go` 常數。 2. embed 模板 + `template/registry.go`。 3. OTP 類業務呼叫時設 `DoNotPersistBody: true`。 ### 新增 HTTP API 1. 在 `generate/api/` 定義路由。 2. `make gen-api`。 3. `internal/logic` 只呼叫 `domain/usecase`,做 types ↔ DTO 映射。 --- ## 相關文件 - [`docs/notification-testing.md`](../../../docs/notification-testing.md) — `notify-test` METHOD 速查 - [`docs/model.md`](../../../docs/model.md) — domain 分包、Redis/Mongo 生命週期 - [`docs/identity-member-design.md`](../../../docs/identity-member-design.md) §8 — 在跨模組流程中的角色 - [`etc/README.md`](../../../etc/README.md) — Gateway 設定 - [`internal/library/redis/README.md`](../../library/redis/README.md) - [`internal/library/mongo/README.md`](../../library/mongo/README.md)