Skip to content

Commit 8959d91

Browse files
author
ocean
committed
初始化 项目代码
1 parent 15f5e5a commit 8959d91

29 files changed

+3279
-2
lines changed

README.md

Lines changed: 360 additions & 2 deletions
Large diffs are not rendered by default.

TECHNICAL_DOCUMENT.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# Notify 项目技术文档
2+
3+
## 项目概述
4+
5+
Notify 是一个统一消息通知管理系统,旨在提供一套支持多种消息渠道(如 Email、SMS、企业微信、飞书、钉钉、Webhook 等)并可灵活扩展的消息通知解决方案。系统提供标准化 API 接口,支持多渠道同时发送,实现消息模板管理、发送状态追踪及渠道配置管理。
6+
7+
## 核心功能
8+
9+
### 1. 渠道管理
10+
- 支持动态添加/删除消息渠道(Email、SMS、企业微信、飞书、钉钉、Webhook 等)
11+
- 每种渠道配置专属参数(如 Email 的 SMTP 服务器/端口/账号,SMS 的 API 密钥等)
12+
- 支持渠道启用/禁用状态管理
13+
- 通过接口标准化实现新渠道的无缝接入
14+
15+
### 2. 消息发送与调度
16+
- 支持单次请求指定多个渠道同时发送
17+
- 各渠道发送过程并行处理,提升发送效率
18+
- 支持直接传入内容或指定模板 ID + 变量参数
19+
- 支持设置消息优先级和定时发送
20+
21+
### 3. 发送状态跟踪
22+
- 记录消息整体状态和各渠道单独状态
23+
- 支持失败重试机制
24+
- 详细记录每条消息的发送时间、耗时、响应数据、错误信息等
25+
26+
## 技术架构
27+
28+
### 1. 架构设计
29+
采用分层架构设计:
30+
- **接口层**:提供 RESTful API 供外部调用
31+
- **业务层**:处理消息组装、渠道选择、发送逻辑
32+
- **渠道层**:各消息渠道的具体实现(基于统一接口)
33+
- **存储层**:消息、模板、渠道配置的数据持久化
34+
35+
### 2. 核心组件
36+
37+
#### 2.1 Sender 接口
38+
所有消息渠道都必须实现 [Sender](file:///Users/zhangdonghai/work/dev/go/notify/notify.go#L12-L15) 接口:
39+
40+
```go
41+
type Sender interface {
42+
Send(to []string, title string, content string) (*result.SendResult, error)
43+
ChannelType() string // 返回渠道类型(如"email"、"sms")
44+
}
45+
```
46+
47+
#### 2.2 Manager 管理器
48+
[Manager](file:///Users/zhangdonghai/work/dev/go/notify/sender/sender.go#L34-L39) 是通知管理器,负责管理通知配置和发送消息:
49+
50+
```go
51+
type Manager struct {
52+
Conf *types.NotifyConfig
53+
MsgType string `json:"msg_type" yaml:"msg_type"`
54+
ToParty, ToTag []string
55+
MaxConcurrency int // 最大并发数,默认为0表示无限制
56+
}
57+
```
58+
59+
#### 2.3 消息发送结果
60+
[SendResult](file:///Users/zhangdonghai/work/dev/go/notify/result/result.go#L12-L21) 表示单个渠道的消息发送结果:
61+
62+
```go
63+
type SendResult struct {
64+
ChannelType string `json:"channel_type"` // 渠道类型
65+
Success bool `json:"success"` // 发送是否成功
66+
MessageID string `json:"message_id"` // 消息在系统中的唯一ID
67+
ChannelMsgID *string `json:"channel_msg_id"` // 渠道返回的消息ID
68+
Error *string `json:"error"` // 失败原因
69+
SendTime time.Time `json:"send_time"` // 实际发送时间
70+
CostMs int64 `json:"cost_ms"` // 发送耗时(毫秒)
71+
}
72+
```
73+
74+
## 支持的消息渠道
75+
76+
### 1. Email
77+
通过 SMTP 协议发送邮件,支持 TLS 加密。
78+
79+
关键配置:
80+
- SMTP 服务器地址
81+
- SMTP 端口
82+
- 发件人账号和密码(或授权码)
83+
- TLS 加密选项
84+
85+
### 2. 钉钉 (DingDing)
86+
通过钉钉机器人 Webhook 发送消息,支持签名安全模式。
87+
88+
关键配置:
89+
- Webhook URL
90+
- 安全签名密钥
91+
- 消息类型
92+
93+
### 3. 飞书 (Lark)
94+
通过飞书机器人 Webhook 发送消息,支持签名安全模式。
95+
96+
关键配置:
97+
- Webhook URL
98+
- 安全签名密钥
99+
- 消息类型
100+
101+
### 4. 企业微信 (WeCom)
102+
通过企业微信应用发送消息。
103+
104+
关键配置:
105+
- 企业 ID (CorpID)
106+
- 应用 AgentID
107+
- 应用 Secret
108+
109+
### 5. 短信 (SMS)
110+
通过短信服务提供商发送短信。
111+
112+
关键配置:
113+
- 服务提供商 Host
114+
- AccessKey ID 和 Secret
115+
- 短信签名和模板
116+
117+
### 6. Webhook
118+
通过 HTTP 请求发送消息到指定 URL。
119+
120+
关键配置:
121+
- URL 地址
122+
- 超时时间
123+
- 自定义请求头
124+
125+
## 使用示例
126+
127+
### 1. 初始化 Manager
128+
```go
129+
config := &types.NotifyConfig{
130+
Channels: []string{"email", "dingding"},
131+
Email: &types.EmailConfig{
132+
SMTPServer: "smtp.example.com",
133+
SMTPPort: 587,
134+
Username: "user@example.com",
135+
Password: "password",
136+
},
137+
Ding: &types.DingDing{
138+
WebhookUrl: "https://oapi.dingtalk.com/robot/send?access_token=xxx",
139+
Secret: "your-secret",
140+
},
141+
}
142+
143+
manager := sender.NewNotifySender(config, 10)
144+
```
145+
146+
### 2. 发送消息
147+
```go
148+
to := types.NotifyToIds{
149+
{Email: "user@example.com", Ding: "123456789"},
150+
}
151+
152+
msg := sender.Msg{
153+
Title: "通知标题",
154+
EmailBody: "邮件内容",
155+
ImBody: "IM消息内容",
156+
}
157+
158+
results, err := manager.Send(to, msg, sender.SendOptions{})
159+
if err != nil {
160+
// 处理错误
161+
}
162+
163+
// 处理发送结果
164+
for _, result := range results {
165+
if result.Success {
166+
fmt.Printf("渠道 %s 发送成功\n", result.ChannelType)
167+
} else {
168+
fmt.Printf("渠道 %s 发送失败: %s\n", result.ChannelType, *result.Error)
169+
}
170+
}
171+
```
172+
173+
## 扩展新渠道
174+
175+
要添加新的消息渠道,需要:
176+
177+
1. 实现 [Sender](file:///Users/zhangdonghai/work/dev/go/notify/notify.go#L12-L15) 接口
178+
2.[SendToChannel](file:///Users/zhangdonghai/work/dev/go/notify/sender/sender.go#L109-L257) 方法中添加渠道类型判断和初始化逻辑
179+
3.[NotifyConfig](file:///Users/zhangdonghai/work/dev/go/notify/types/types.go#L12-L21) 中添加相应的配置结构体
180+
181+
示例:
182+
```go
183+
type NewChannel struct {
184+
// 渠道特定配置
185+
}
186+
187+
func (n *NewChannel) Send(to []string, title, content string) (*result.SendResult, error) {
188+
// 实现发送逻辑
189+
}
190+
191+
func (n *NewChannel) ChannelType() string {
192+
return "newchannel"
193+
}
194+
```
195+
196+
## 性能与可靠性
197+
198+
### 性能要求
199+
- 单节点支持每秒处理≥500 条消息请求
200+
- 多渠道并发发送时,单条消息整体处理延迟≤1 秒
201+
202+
### 可靠性要求
203+
- 消息不丢失:通过持久化确保消息至少被处理一次
204+
- 失败重试:临时错误自动重试,重试策略可配置
205+
- 系统可用性:≥99.9%
206+
207+
## 安全性
208+
209+
### 安全措施
210+
- 敏感配置加密:渠道的密钥、账号等信息加密存储
211+
- 消息内容加密:支持对敏感消息内容加密传输
212+
- 接口鉴权:所有 API 接口需通过 Token 或签名验证

dingding/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
### 如何使用钉钉群的机器人来接收通知
2+
- 添加群机器人
3+
点击 **钉钉群设置**->**智能群助手**->**添加机器人**,选择**自定义**,然后点击**添加**
4+
填写**机器人名称****安全设置**,然后选择安全设置,有三类自定义关键词、加签、IP地址,任选一项然后点击完成
5+
得到一个Webhook,
6+
- 初始化
7+
使用`NewDing`来进行初始化,第一个参数就是这个Webhook url,第二个参数是安全设置,`CustomKey`是对应的的自定义关键字,`Sign`是加签,`IPCidr`是IP地址,注意如果选择了安全设置,这时会得到一个签名,然后把这个签名作为第三个参数传入,如果安全设置不是选择的加签,则第三个参数为空字符串即可。注意如果想在消息通知中@某个人则需要把这个人加入群众并且tos参数要穿入此人注册钉钉的手机号
8+
9+
### Example
10+
```go
11+
var (
12+
secret := "SEC..."
13+
webhook := "https://oapi.dingtalk.com/robot/send?access_token=..."
14+
)
15+
func SendDing() {
16+
ding := NewDing(webhook, Sign, secret)
17+
err := ding.Send([]string{"..."}, "测试标题", "测试内容")
18+
19+
if err != nil {
20+
t.Error(err)
21+
}
22+
}
23+
```

dingding/dingding.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package dingding
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/base64"
7+
"encoding/json"
8+
"fmt"
9+
"github.com/v-mars/notify"
10+
"github.com/v-mars/notify/result"
11+
"github.com/v-mars/notify/types"
12+
"net/http"
13+
"net/url"
14+
"strconv"
15+
"time"
16+
)
17+
18+
// getsign generate a sign when secure level is needsign
19+
func getsign(secret string, now string) string {
20+
signstr := now + "\n" + secret
21+
// HmacSHA256
22+
h := hmac.New(sha256.New, []byte(secret))
23+
_, _ = h.Write([]byte(signstr))
24+
hm := h.Sum(nil)
25+
// Base64 encode
26+
b := base64.StdEncoding.EncodeToString(hm)
27+
// urlEncode
28+
sign := url.QueryEscape(b)
29+
return sign
30+
}
31+
32+
// Secrue dingding secrue setting
33+
// pls reading https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq
34+
type Secrue int
35+
36+
const (
37+
// CustomKey Custom keywords
38+
CustomKey Secrue = iota + 1
39+
// Sign need sign up
40+
Sign
41+
// IPCdir IP addres
42+
IPCdir
43+
)
44+
45+
// Ding dingding alarm conf
46+
type Ding struct {
47+
types.DingDing
48+
sl Secrue
49+
Result *result.SendResult
50+
}
51+
52+
// Result post resp
53+
type Result struct {
54+
ErrCode int `json:"errcode"`
55+
ErrMsg string `json:"errmsg"`
56+
}
57+
58+
type text struct {
59+
Content string `json:"content"`
60+
}
61+
62+
type at struct {
63+
AtMobiles []string `json:"atMobiles"`
64+
IsAtAll bool `json:"isAtAll"`
65+
}
66+
67+
// SendMsg post json data
68+
type SendMsg struct {
69+
MsgType string `json:"msgtype"`
70+
Text text `json:"text"`
71+
At at `json:"at"`
72+
}
73+
74+
// NewDing init a Dingding send conf
75+
func NewDing(webhookurl string, sl Secrue, secret string) *Ding {
76+
d := Ding{
77+
DingDing: types.DingDing{
78+
MsgType: "text",
79+
WebhookUrl: webhookurl,
80+
Secret: secret,
81+
},
82+
sl: sl,
83+
Result: &result.SendResult{
84+
ChannelType: NotifyTypeDingDing,
85+
ChannelMsgID: nil,
86+
Success: false,
87+
MessageID: "",
88+
SendTime: time.Now(),
89+
Error: nil,
90+
CostMs: 0,
91+
},
92+
}
93+
94+
return &d
95+
}
96+
97+
// Send to notify tos is phone number
98+
func (d *Ding) Send(tos []string, title string, content string) (sendResult *result.SendResult, err error) {
99+
sendResult = d.Result
100+
defer func() {
101+
sendResult.CostMs = time.Now().Sub(sendResult.SendTime).Milliseconds()
102+
sendResult.ChannelMsgID = result.PtrOf(fmt.Sprintf("%d", time.Now().UnixNano()))
103+
sendResult.Success = err == nil
104+
sendResult.MessageID = *sendResult.ChannelMsgID
105+
if err != nil {
106+
sendResult.Error = result.PtrOf(err.Error())
107+
}
108+
}()
109+
var reqUrl = d.WebhookUrl
110+
if d.sl == Sign && len(d.Secret) > 0 {
111+
now := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
112+
sign := getsign(d.Secret, now)
113+
reqUrl += fmt.Sprintf("&timestamp=%s&sign=%s", now, sign)
114+
}
115+
sendMsg := SendMsg{
116+
MsgType: "text",
117+
Text: text{
118+
Content: title + "\n" + content + "\n",
119+
},
120+
At: at{
121+
AtMobiles: tos,
122+
IsAtAll: false,
123+
},
124+
}
125+
126+
resp, err := notify.JSONPost(http.MethodPost, reqUrl, sendMsg, http.DefaultClient, nil)
127+
if err != nil {
128+
return sendResult, err
129+
}
130+
res := Result{}
131+
err = json.Unmarshal(resp, &res)
132+
if err != nil {
133+
return sendResult, err
134+
}
135+
if res.ErrCode != 0 {
136+
return sendResult, fmt.Errorf("errmsg: %s errcode: %d", res.ErrMsg, res.ErrCode)
137+
}
138+
return sendResult, nil
139+
}
140+
141+
const NotifyTypeDingDing = "dingding"
142+
143+
func (d *Ding) ChannelType() string {
144+
return NotifyTypeDingDing
145+
}

0 commit comments

Comments
 (0)