# Notification Service — 设计规格 ## 概述 纯 Go 实现的通知服务。上游业务系统通过 webhook 推送消息,服务匹配规则、渲染模板、路由到多渠道发送。 **核心三层关系:** ``` source 1──N rule template 1──N rule rule N──M channel (via rule_channel) ``` ## 技术栈 | 层 | 选型 | |----|------| | 语言 | Go 1.22+ | | HTTP 框架 | gin-gonic/gin | | 数据库驱动 | go-sql-driver/mysql | | SQL 工具 | jmoiron/sqlx | | 模板引擎 | text/template (标准库) | | 缓存 | go-redis/redis | | 配置 | viper | | 日志 | slog (标准库) | | 迁移 | golang-migrate | **外部依赖:** MySQL 8.0、Redis 7 **部署形态:** Docker 容器化 (docker-compose),最终镜像 ~12MB --- ## 架构 ``` notification-service (Go) │ ├── cmd/server/main.go ← 入口 ├── internal/ │ ├── handler/ ← HTTP Handler (gin) │ │ ├── notify.go ← POST /api/v1/notify (上游调用) │ │ ├── source.go ← CRUD /api/v1/sources │ │ ├── template.go ← CRUD /api/v1/templates │ │ ├── channel.go ← CRUD /api/v1/channels │ │ └── rule.go ← CRUD /api/v1/rules + 开关控制 │ │ │ ├── engine/ ← 核心引擎 │ │ ├── matcher.go ← source + event → rule 匹配 │ │ ├── renderer.go ← Go template 渲染 │ │ └── router.go ← rule → channels 路由 │ │ │ ├── adapter/ ← 渠道适配器 │ │ ├── adapter.go ← ChannelSender 接口 │ │ ├── dingtalk.go ← 钉钉 webhook │ │ ├── wecom.go ← 企业微信机器人 │ │ ├── email.go ← SMTP 邮件 │ │ └── bark.go ← Bark HTTP │ │ │ ├── model/ ← 数据模型 │ ├── store/ ← MySQL 访问层 │ └── cache/ ← Redis 缓存层 │ ├── config/config.yaml ├── migrations/ ├── Dockerfile ├── docker-compose.yml └── go.mod ``` --- ## 数据模型 ### source(来源系统) | 列 | 类型 | 说明 | |----|------|------| | id | INT PK | | | name | VARCHAR(64) UNIQUE | trading-system | | api_key | VARCHAR(128) UNIQUE | 通知 API 鉴权 token,创建 source 时自动生成 | | parse_mode | VARCHAR(16) DEFAULT 'json' | json / regex / text(text=直接透传 body 到模板) | | parse_pattern | VARCHAR(512) | regex 模式(含命名分组),parse_mode=regex 时必填 | | status | TINYINT | 1=启用 0=禁用 | ### template(模板) | 列 | 类型 | 说明 | |----|------|------| | id | INT PK | | | name | VARCHAR(64) UNIQUE | trade_open | | content | TEXT | Go template 语法,markdown/纯文本 | ### channel(渠道配置) | 列 | 类型 | 说明 | |----|------|------| | id | INT PK | | | name | VARCHAR(32) UNIQUE | dingtalk-prod | | type | VARCHAR(32) | dingtalk / wecom / email / bark | | config | JSON | 渠道特定配置 (webhook URL 等) | | status | TINYINT | 1=启用 0=禁用 | ### rule(规则 — 核心绑定) | 列 | 类型 | 说明 | |----|------|------| | id | INT PK | | | source_id | INT FK→source | | | event | VARCHAR(64) | trade.open / error / order.created | | template_id | INT FK→template | | | conditions | JSON | 过滤条件数组,null=不过滤。例: `[{"field":"symbol","op":"exists"}]` | | enabled | TINYINT | 1=启用 0=禁用 | | UNIQUE | (source_id, event) | 每个 source 下 event 唯一 | **条件操作符:** `eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `exists` / `not_exists` / `contains` 多个条件 AND 关系,全部满足才转发。 ### rule_channel(规则-渠道关联 — 独立开关) | 列 | 类型 | 说明 | |----|------|------| | id | INT PK | | | rule_id | INT FK→rule | ON DELETE CASCADE | | channel_id | INT FK→channel | | | enabled | TINYINT | 每条规则下每个渠道可独立开关 | | UNIQUE | (rule_id, channel_id) | | ### message_log(消息记录 — 排查用) | 列 | 类型 | 说明 | |----|------|------| | id | BIGINT PK | | | rule_id | INT | | | channel_id | INT | | | source | VARCHAR(64) | 冗余,方便查询 | | event | VARCHAR(64) | 冗余,方便查询 | | payload | JSON | 原始请求 data | | content | TEXT | 渲染后内容 | | status | ENUM | pending/success/failed/retrying | | retry_count | INT | | | response | TEXT | 渠道返回 | | error_msg | TEXT | | | created_at | DATETIME | | --- ## API 设计 ### 认证 - **通知接口**:`Authorization: Bearer ` — 通过 api_key 识别来源系统 - 管理接口:`Authorization: Bearer ` — 全局管理 Key - **Content-Type 不限** — source.parse_mode 决定如何解析 body: - `json`: body 按 JSON 解析,`event` 取自 JSON 字段 - `regex`: body 按正则解析,命名分组提取字段,`event` 需在 body 中显式提供 - `text`: body 原样透传到模板(模板引用 `.Body`) ### 通知接口 **JSON 模式(parse_mode=json):** ``` POST /api/v1/notify Authorization: Bearer Content-Type: application/json { "event": "trade.open", "data": { "symbol": "BTC", "price": 65000 } } ``` **Regex 模式(parse_mode=regex,parse_pattern 定义提取规则):** ``` POST /api/v1/notify Authorization: Bearer Content-Type: text/plain BTC 开仓 价格:65000 方向:多 ``` **Text 模式(parse_mode=text,body 直接给模板):** ``` POST /api/v1/notify Authorization: Bearer Content-Type: text/plain 🚀 BTC 突破 68000,请注意风险 ``` **响应:** ``` Response 200 (匹配且通过条件): { "matched": true, "channels": ["dingtalk", "email"], "accepted": true } Response 200 (匹配但条件不满足): { "matched": true, "filtered": true, "reason": "condition not met" } Response 200 (无匹配规则): { "matched": false } Response 401: { "error": "unauthorized" } Response 429: { "error": "rate_limit_exceeded", "message": "...", "retry_after": 1 } ``` ### 管理 API ``` 来源: POST /api/v1/sources GET /api/v1/sources?page=1&page_size=20 GET /api/v1/sources/:id PUT /api/v1/sources/:id DELETE /api/v1/sources/:id 模板: POST /api/v1/templates GET /api/v1/templates?page=1&page_size=20 GET /api/v1/templates/:id PUT /api/v1/templates/:id DELETE /api/v1/templates/:id 渠道: POST /api/v1/channels GET /api/v1/channels?page=1&page_size=20 GET /api/v1/channels/:id PUT /api/v1/channels/:id DELETE /api/v1/channels/:id 规则: POST /api/v1/rules GET /api/v1/rules?page=1&page_size=20 GET /api/v1/rules/:id PUT /api/v1/rules/:id DELETE /api/v1/rules/:id PATCH /api/v1/rules/:id/enable PATCH /api/v1/rules/:id/disable PATCH /api/v1/rules/:id/channels/:channel_id/enable PATCH /api/v1/rules/:id/channels/:channel_id/disable 消息记录: GET /api/v1/message-logs?source=&event=&status=&page=1&page_size=20 ``` ### 关键请求示例 **创建渠道:** ```json POST /api/v1/channels { "name": "dingtalk-prod", "type": "dingtalk", "config": { "webhook_url": "https://oapi.dingtalk.com/robot/send?access_token=xxx", "secret": "SEC..." } } ``` **创建规则(带条件过滤):** ```json POST /api/v1/rules { "source_name": "trading-system", "event": "trade.open", "template_name": "trade_open", "channels": ["dingtalk", "email"], "conditions": [ {"field": "symbol", "op": "exists"}, {"field": "price", "op": "gt", "value": "0"} ] } ``` **创建 source(Regex 解析模式):** ```json POST /api/v1/sources { "name": "legacy-monitor", "parse_mode": "regex", "parse_pattern": "(?P\\w+)\\s+(?P.+)" } ``` --- ## 核心执行流程 ``` POST /api/v1/notify │ ▼ 1. 鉴权 ── api_key 反查 source → source_id + parse_mode + parse_pattern key 无效 → 401 │ ▼ 2. 消息解析 ── 根据 source.parse_mode: json: body → JSON decode → event + data regex: body → regex 匹配 → 命名分组 → data map text: body → .Body 字段 │ ▼ 3. 规则匹配 ── 缓存: notify:rule:{source_id}:{event} miss → SELECT * FROM rule WHERE source_id=? AND event=? AND enabled=1 无匹配 → 200 {matched: false} │ ▼ 4. 条件过滤 ── 检查 rule.conditions (如果非空) 对每条 condition 求值 (eq/ne/gt/lt/exists/contains...) 任一不满足 → 200 {matched: true, filtered: true} │ ▼ 5. 模板渲染 ── Go text/template 渲染 template.content + 解析后的 data 失败 → 422 {error: "template render failed"} │ ▼ 6. 查找渠道 ── 缓存: notify:channels:{rule_id} miss → SELECT * FROM rule_channel WHERE rule_id=? AND enabled=1 无可用渠道 → 200 {matched: true, sent: 0} │ ▼ 7. 并发发送 ── 每个渠道 goroutine 异步发送 记录 message_log 失败 → 重试队列 (3次, 指数退避 1s/5s/30s) │ ▼ 8. 立即返回 200 {matched: true, channels: [...], accepted: true} ``` --- ## 渠道适配器 ```go type ChannelSender interface { Type() string Send(title, content string, config json.RawMessage) error } ``` | 渠道 | 协议 | 备注 | |------|------|------| | 钉钉 | POST webhook | 签名计算 + markdown 消息体 | | 企业微信 | POST webhook | markdown/json 消息体 | | 邮件 | SMTP | net/smtp 标准库,支持 TLS | | Bark | POST HTTP | title + body POST 到设备 URL | 新增渠道:实现 ChannelSender 接口 + 工厂注册一行。 --- ## 缓存策略 | Key | 内容 | TTL | 失效 | |-----|------|-----|------| | `notify:rule:{source_id}:{event}` | rule_id + template_id + template_content | 5min | 规则/模板 CUD 时主动删除 | | `notify:channels:{rule_id}` | [channel_id, type, config] | 5min | 规则-渠道变更时主动删除 | 模式:Cache-Aside,管理 API 做 CUD 时主动失效,不依赖 TTL 被动过期。 --- ## 限流 & 重试 **限流:** - Per-source 限流,默认 100 req/s - Redis 滑动窗口: `ratelimit:{source_id}:{window}` 1秒窗口 - 超限返回 429 + Retry-After **重试:** - 异步发送失败 → 内存重试队列 - 最多 3 次,退避 1s → 5s → 30s - 3 次全失败 → message_log.status = 'failed' - 后续可扩展为 Redis 死信队列 + 手动重发 API --- ## 配置 ```yaml server: port: 8080 admin_key: "admin-sk-xxx" database: host: mysql port: 3306 user: notify password: ${DB_PASSWORD} database: notification redis: host: redis port: 6379 smtp: host: smtp.example.com port: 587 user: notify@example.com password: ${SMTP_PASSWORD} rate_limit: default: 100 ``` --- ## 部署 **docker-compose.yml:** ```yaml services: mysql: image: mysql:8.0 redis: image: redis:7-alpine api: build: . ports: ["8080:8080"] depends_on: [mysql, redis] ``` **Dockerfile:** 多阶段构建 (golang:1.22-alpine → alpine:3.20),最终镜像 ~12MB。 --- ## v1 范围 - [x] Source/Template/Channel/Rule CRUD 管理 API - [x] POST /api/v1/notify 核心通知接口 - [x] 消息解析:JSON / Regex / Text 三种模式 - [x] 规则条件过滤(eq/ne/gt/lt/exists/not_exists/contains) - [x] API Key 鉴权(source 用 source key,管理用 admin key) - [x] 4 个渠道:钉钉、企业微信、邮件、Bark - [x] Go template 渲染 - [x] 规则级 + 规则-渠道级开关控制 - [x] 异步发送 + 3 次指数退避重试 - [x] Redis 缓存规则和渠道配置 - [x] Per-source 限流 - [x] message_log 消息记录 - [x] Docker 部署