From befb099b912a02bddd92f8bbf99c173fa6b04444 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 31 Jul 2026 00:54:03 +0800 Subject: [PATCH] feat: add comprehensive README documentation for AIAA Notification Service, detailing core concepts, setup instructions, API endpoints, and configuration options --- README.md | 571 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..16ffeed --- /dev/null +++ b/README.md @@ -0,0 +1,571 @@ +# AIAA Notification Service + +纯 Go 实现的多渠道通知服务。上游系统通过 Webhook 推送消息,服务按来源解析、匹配规则、条件过滤、渲染模板后,异步分发到钉钉 / 企业微信 / 邮件 / Bark。 + +## 核心概念 + +``` +source 1──N rule +template 1──N rule +rule N──M channel (via rule_channel,可独立开关) +``` + +| 实体 | 说明 | +|------|------| +| **Source** | 来源系统,持有独立 `api_key`,定义 body 解析方式(json / regex / text) | +| **Template** | Go `text/template` 模板 | +| **Channel** | 发送渠道配置(钉钉 / 企微 / 邮件 / Bark) | +| **Rule** | 绑定 `source + event → template + channels`,可选条件过滤 | +| **MessageLog** | 发送记录,便于排查 | + +**处理流程:** + +``` +POST /api/v1/notify + → 鉴权(source api_key)+ 限流 + → 按 parse_mode 解析 body + → 匹配 enabled 规则 + → 条件过滤(全部 AND) + → 渲染模板 + → 异步发送到启用渠道(失败重试 3 次:1s / 5s / 30s) + → 立即返回 accepted +``` + +## 技术栈 + +- Go 1.22+、Gin、sqlx、go-redis、Viper、slog +- MySQL 8.0、Redis 7 +- Docker 多阶段构建 + +## 快速开始 + +### 依赖 + +- Go 1.22+ +- MySQL 8.0 +- Redis 7 +- [golang-migrate](https://github.com/golang-migrate/migrate)(本地迁移时) + +### Docker Compose(推荐) + +```bash +docker-compose up -d +``` + +会启动 MySQL、Redis 与 API(默认 `http://localhost:8080`)。 + +### 本地开发 + +```bash +# 1. 启动依赖 +docker-compose up -d mysql redis + +# 2. 迁移数据库 +make migrate-up + +# 3. 修改配置(务必更换 admin_key) +# config/config.yaml + +# 4. 运行 +make run +# 或 +make build && ./bin/server +``` + +### 常用命令 + +| 命令 | 说明 | +|------|------| +| `make run` | 本地启动 | +| `make build` | 编译到 `bin/server` | +| `make test` | 运行单元测试 | +| `make migrate-up` / `migrate-down` | 数据库迁移 | +| `make docker-up` / `docker-down` | Compose 启停 | + +### 配置 + +主配置文件:`config/config.yaml`。支持 `${ENV_VAR}` / `${ENV_VAR:-default}` 插值,也可用 `NOTIFY_` 前缀环境变量覆盖(`.` → `_`,例如 `NOTIFY_DATABASE_HOST`)。 + +| 配置项 | 说明 | 默认 | +|--------|------|------| +| `server.port` | HTTP 端口 | `8080` | +| `server.admin_key` | 管理 API Bearer Token | 须修改 | +| `database.*` | MySQL 连接 | `notify` / `notification` | +| `redis.*` | Redis(缓存 + 限流) | `127.0.0.1:6379` | +| `smtp.*` | 邮件发送(email 渠道) | — | +| `rate_limit.default` | 每 source 每秒请求上限 | `100` | + +健康检查:`GET /health` → `{"status":"ok"}` + +--- + +## 认证 + +| 接口类型 | Header | 说明 | +|----------|--------|------| +| 通知接口 | `Authorization: Bearer ` | 创建 Source 时自动生成 | +| 管理接口 | `Authorization: Bearer ` | 来自 `config.yaml` 的 `server.admin_key` | + +未授权返回 `401`:`{"error":"unauthorized"}` 或 `{"error":"missing api key"}` / `{"error":"invalid api key"}`。 + +通知接口超限返回 `429`: + +```json +{ + "error": "rate_limit_exceeded", + "message": "...", + "retry_after": 1 +} +``` + +--- + +## 接口文档 + +Base URL:`http://localhost:8080` + +通用错误体:`{"error": ""}` + +--- + +### 1. 通知接口 + +#### `POST /api/v1/notify` + +上游业务推送入口。Body 解析方式由 Source 的 `parse_mode` 决定。 + +**鉴权:** Source API Key +**限流:** 按 source,默认 100 req/s(需 Redis) + +##### JSON 模式(`parse_mode=json`) + +```http +POST /api/v1/notify +Authorization: Bearer +Content-Type: application/json + +{ + "event": "trade.open", + "data": { + "symbol": "BTC", + "price": 65000 + } +} +``` + +- 必须包含 `event`(string) +- `data` 为模板变量;若省略 `data`,则除 `event` 外的顶层字段作为 data + +##### Regex 模式(`parse_mode=regex`) + +```http +POST /api/v1/notify +Authorization: Bearer +Content-Type: text/plain + +trade.open BTC 开仓 价格:65000 +``` + +- 使用 Source 的 `parse_pattern`(需含命名分组) +- 命名分组 `event` 作为事件名,其余命名分组进入模板 data + +示例 pattern:`(?P\w+)\s+(?P\w+)\s+(?P.+)` + +##### Text 模式(`parse_mode=text`) + +```http +POST /api/v1/notify +Authorization: Bearer +Content-Type: text/plain + +BTC 突破 68000,请注意风险 +``` + +- body 原样透传,模板变量为 `.Body` +- **固定 event 为 `default`**,规则需按 `event=default` 配置 + +##### 成功响应 + +匹配并接受发送: + +```json +{ + "matched": true, + "channels": ["dingtalk:1", "email:2"], + "accepted": true +} +``` + +匹配但条件未通过: + +```json +{ + "matched": true, + "filtered": true, + "reason": "condition not met" +} +``` + +无匹配规则: + +```json +{ + "matched": false +} +``` + +##### 错误码 + +| HTTP | 场景 | +|------|------| +| 400 | 读取 body 失败 | +| 401 | 鉴权失败 | +| 422 | 解析失败 / 模板渲染失败 / 规则 conditions 非法 | +| 429 | 限流 | +| 500 | 模板缺失等内部错误 | + +发送为异步:接口返回 `accepted` 不代表渠道侧已投递成功,请查 Message Log。 + +--- + +### 2. Source(来源) + +前缀:`/api/v1/sources` +**鉴权:** Admin Key + +#### `POST /api/v1/sources` — 创建 + +```json +{ + "name": "trading-system", + "parse_mode": "json", + "parse_pattern": "", + "status": 1 +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `name` | string | 是 | 唯一名称 | +| `parse_mode` | string | 否 | `json`(默认)/ `regex` / `text` | +| `parse_pattern` | string | regex 时建议填 | 正则(命名分组) | +| `status` | int | 否 | `1` 启用(默认)/ `0` 禁用 | + +**201** 返回完整 Source(含自动生成的 `api_key`)。名称冲突 → **409**。 + +Regex 示例: + +```json +{ + "name": "legacy-monitor", + "parse_mode": "regex", + "parse_pattern": "(?P\\w+)\\s+(?P.+)" +} +``` + +#### `GET /api/v1/sources` — 列表 + +**200** → `Source[]` + +#### `GET /api/v1/sources/:id` — 详情 + +**200** → Source;不存在 → **404** + +#### `PUT /api/v1/sources/:id` — 更新 + +Body 同创建。成功:`{"ok": true}` + +#### `DELETE /api/v1/sources/:id` — 删除 + +成功:`{"ok": true}` + +--- + +### 3. Template(模板) + +前缀:`/api/v1/templates` +**鉴权:** Admin Key + +模板语法为 Go `text/template`,变量来自解析后的 data。 + +示例: + +``` +### {{.symbol}} 开仓 +价格: {{.price}} +``` + +Text 模式示例:`{{.Body}}` + +#### `POST /api/v1/templates` — 创建 + +```json +{ + "name": "trade_open", + "content": "### {{.symbol}} 开仓\n价格: {{.price}}" +} +``` + +| 字段 | 类型 | 必填 | +|------|------|------| +| `name` | string | 是 | +| `content` | string | 是 | + +**201** → Template;冲突 → **409** + +#### `GET /api/v1/templates` / `GET /api/v1/templates/:id` + +#### `PUT /api/v1/templates/:id` / `DELETE /api/v1/templates/:id` + +更新/删除成功返回 `{"ok": true}` + +--- + +### 4. Channel(渠道) + +前缀:`/api/v1/channels` +**鉴权:** Admin Key + +#### `POST /api/v1/channels` — 创建 + +```json +{ + "name": "dingtalk-prod", + "type": "dingtalk", + "config": { ... }, + "status": 1 +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `name` | string | 是 | 唯一名称(规则里用此名引用) | +| `type` | string | 是 | `dingtalk` / `wecom` / `email` / `bark` | +| `config` | object | 是 | 见下方各渠道配置 | +| `status` | int | 否 | 默认 `1` | + +**201** → Channel;冲突 → **409** + +##### 渠道 config + +**钉钉 `dingtalk`** + +```json +{ + "webhook_url": "https://oapi.dingtalk.com/robot/send?access_token=xxx", + "secret": "SEC..." +} +``` + +`secret` 可选(加签机器人时填写)。消息类型:markdown。 + +**企业微信 `wecom`** + +```json +{ + "webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" +} +``` + +消息类型:markdown。 + +**邮件 `email`** + +```json +{ + "to": ["ops@example.com", "dev@example.com"] +} +``` + +SMTP 使用全局 `config.yaml` 的 `smtp` 段;支持 587 STARTTLS / 465 TLS。 + +**Bark `bark`** + +```json +{ + "url": "https://api.day.app/" +} +``` + +#### `GET /api/v1/channels` / `GET /api/v1/channels/:id` + +#### `PUT /api/v1/channels/:id` / `DELETE /api/v1/channels/:id` + +--- + +### 5. Rule(规则) + +前缀:`/api/v1/rules` +**鉴权:** Admin Key + +同一 Source 下 `event` 唯一。 + +#### `POST /api/v1/rules` — 创建 + +```json +{ + "source_name": "trading-system", + "event": "trade.open", + "template_name": "trade_open", + "channels": ["dingtalk-prod", "email-ops"], + "conditions": [ + {"field": "symbol", "op": "exists"}, + {"field": "price", "op": "gt", "value": "0"} + ], + "enabled": 1 +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `source_name` | string | 是 | Source 名称 | +| `event` | string | 是 | 事件名;text 模式请用 `default` | +| `template_name` | string | 是 | Template 名称 | +| `channels` | string[] | 否 | Channel 名称列表 | +| `conditions` | object[] | 否 | 过滤条件,全部 AND;省略则不过滤 | +| `enabled` | int | 否 | 默认 `1` | + +**条件操作符:** + +| op | 说明 | +|----|------| +| `eq` / `ne` | 等于 / 不等于(字符串比较) | +| `gt` / `gte` / `lt` / `lte` | 数值比较 | +| `exists` / `not_exists` | 字段是否存在 | +| `contains` | 字符串包含 | + +条件结构:`{"field":"<字段>","op":"<操作符>","value":"<可选>"}` + +**201** → Rule;冲突 → **409** + +#### `GET /api/v1/rules` / `GET /api/v1/rules/:id` + +#### `PUT /api/v1/rules/:id` — 更新 + +Body 同创建。成功:`{"ok": true}` + +#### `DELETE /api/v1/rules/:id` + +成功:`{"ok": true}`(关联 `rule_channel` 级联删除) + +#### 开关 + +| 方法 | 路径 | 说明 | +|------|------|------| +| `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` | 禁用该规则下某渠道 | + +均返回 `{"ok": true}`。`:channel_id` 为渠道数字 ID。 + +--- + +### 6. Message Log(消息记录) + +#### `GET /api/v1/message-logs` + +**鉴权:** Admin Key + +Query 参数: + +| 参数 | 说明 | +|------|------| +| `source` | 按来源名过滤 | +| `event` | 按事件过滤 | +| `status` | `pending` / `success` / `failed` / `retrying` | +| `page` | 页码(从 1) | +| `page_size` | 每页条数 | + +**200:** + +```json +{ + "data": [ /* MessageLog[] */ ], + "total": 100, + "page": 1 +} +``` + +MessageLog 主要字段:`rule_id`、`channel_id`、`source`、`event`、`payload`、`content`、`status`、`retry_count`、`response`、`error_msg`、`created_at`。 + +--- + +## 端到端示例 + +```bash +ADMIN="Authorization: Bearer admin-sk-change-me" + +# 1. 创建来源 +curl -s -X POST http://localhost:8080/api/v1/sources \ + -H "$ADMIN" -H "Content-Type: application/json" \ + -d '{"name":"trading-system","parse_mode":"json"}' +# 记下返回的 api_key + +# 2. 创建模板 +curl -s -X POST http://localhost:8080/api/v1/templates \ + -H "$ADMIN" -H "Content-Type: application/json" \ + -d '{"name":"trade_open","content":"### {{.symbol}} 开仓\n价格: {{.price}}"}' + +# 3. 创建渠道 +curl -s -X POST http://localhost:8080/api/v1/channels \ + -H "$ADMIN" -H "Content-Type: application/json" \ + -d '{ + "name":"dingtalk-prod", + "type":"dingtalk", + "config":{"webhook_url":"https://oapi.dingtalk.com/robot/send?access_token=xxx","secret":"SEC..."} + }' + +# 4. 创建规则 +curl -s -X POST http://localhost:8080/api/v1/rules \ + -H "$ADMIN" -H "Content-Type: application/json" \ + -d '{ + "source_name":"trading-system", + "event":"trade.open", + "template_name":"trade_open", + "channels":["dingtalk-prod"], + "conditions":[{"field":"symbol","op":"exists"}] + }' + +# 5. 发送通知 +curl -s -X POST http://localhost:8080/api/v1/notify \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"event":"trade.open","data":{"symbol":"BTC","price":65000}}' +``` + +--- + +## 项目结构 + +``` +cmd/server/ 入口 +config/ 配置文件 +migrations/ MySQL 迁移 +internal/ + handler/ HTTP(鉴权、限流、CRUD、notify) + engine/ 匹配 / 渲染 / 路由 + adapter/ 渠道适配器(ChannelSender) + parser/ json / regex / text + condition/ 条件求值 + store/ MySQL + cache/ Redis + model/ 数据模型 + retry/ 重试 + config/ 配置加载 +``` + +## 扩展渠道 + +实现 `adapter.ChannelSender` 接口并在工厂中注册即可: + +```go +type ChannelSender interface { + Type() string + Send(title, content string, config json.RawMessage) error +} +``` + +## License + +内部项目,按团队约定使用。