2026-05-20 09:32:22 +00:00
|
|
|
|
# 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` 字串。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-05-20 07:01:08 +00:00
|
|
|
|
## 擴充指南
|
|
|
|
|
|
|
|
|
|
|
|
### 新增 Email Provider
|
|
|
|
|
|
|
2026-05-20 09:32:22 +00:00
|
|
|
|
已內建:`smtp_sender.go`、`ses_sender.go`。
|
2026-05-20 07:01:08 +00:00
|
|
|
|
|
|
|
|
|
|
1. 在 `provider/email/` 實作 `Sender`(`Name`、`Sort`、`Send`)。
|
2026-05-20 09:32:22 +00:00
|
|
|
|
2. 在 `config/config.go` 加設定,並在 `usecase/factory.go` 的 `collectEmailSenders` 註冊。
|
2026-05-20 07:01:08 +00:00
|
|
|
|
3. 補 `provider/email/*_test.go`。
|
|
|
|
|
|
|
|
|
|
|
|
### 新增 NotifyKind
|
|
|
|
|
|
|
|
|
|
|
|
1. `domain/enum/kind.go` 常數。
|
|
|
|
|
|
2. embed 模板 + `template/registry.go`。
|
2026-05-20 09:32:22 +00:00
|
|
|
|
3. OTP 類業務呼叫時設 `DoNotPersistBody: true`。
|
2026-05-20 07:01:08 +00:00
|
|
|
|
|
|
|
|
|
|
### 新增 HTTP API
|
|
|
|
|
|
|
|
|
|
|
|
1. 在 `generate/api/` 定義路由。
|
|
|
|
|
|
2. `make gen-api`。
|
2026-05-20 09:32:22 +00:00
|
|
|
|
3. `internal/logic` 只呼叫 `domain/usecase`,做 types ↔ DTO 映射。
|
2026-05-20 07:01:08 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 相關文件
|
|
|
|
|
|
|
2026-05-20 09:32:22 +00:00
|
|
|
|
- [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)
|