176 lines
5.3 KiB
Markdown
176 lines
5.3 KiB
Markdown
# Notification 模組
|
||
|
||
統一對外通知入口(Email / SMS),支援同步 `Send`、異步 `Enqueue` + Redis 重試 Worker、冪等、配額、DLQ。
|
||
|
||
---
|
||
|
||
## 測試(本機)
|
||
|
||
單一 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 §11](../../../docs/identity-member-design.md#11-notification-module) — 產品設計
|
||
- [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)
|