feat: project scaffold, config loading, skeleton main

This commit is contained in:
2026-06-27 12:57:11 +08:00
commit 976ec3ca70
10 changed files with 4430 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,452 @@
# 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 / texttext=直接透传 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 <source.api_key>` — 通过 api_key 识别来源系统
- 管理接口:`Authorization: Bearer <admin_key>` — 全局管理 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 <source_api_key>
Content-Type: application/json
{
"event": "trade.open",
"data": {
"symbol": "BTC",
"price": 65000
}
}
```
**Regex 模式(parse_mode=regexparse_pattern 定义提取规则):**
```
POST /api/v1/notify
Authorization: Bearer <source_api_key>
Content-Type: text/plain
BTC 开仓 价格:65000 方向:多
```
**Text 模式(parse_mode=textbody 直接给模板):**
```
POST /api/v1/notify
Authorization: Bearer <source_api_key>
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
GET /api/v1/sources/:id
PUT /api/v1/sources/:id
DELETE /api/v1/sources/:id
模板:
POST /api/v1/templates
GET /api/v1/templates
GET /api/v1/templates/:id
PUT /api/v1/templates/:id
DELETE /api/v1/templates/:id
渠道:
POST /api/v1/channels
GET /api/v1/channels
GET /api/v1/channels/:id
PUT /api/v1/channels/:id
DELETE /api/v1/channels/:id
规则:
POST /api/v1/rules
GET /api/v1/rules
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"}
]
}
```
**创建 sourceRegex 解析模式):**
```json
POST /api/v1/sources
{
"name": "legacy-monitor",
"parse_mode": "regex",
"parse_pattern": "(?P<event>\\w+)\\s+(?P<message>.+)"
}
```
---
## 核心执行流程
```
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 部署