251 lines
6.7 KiB
Markdown
251 lines
6.7 KiB
Markdown
# notice
|
||
|
||
`notice` 是一个 Go 多渠道消息通知 package,统一接入飞书、钉钉、企业微信和通用 Webhook。调用方只需要构造统一的 `Message`,渠道差异、签名和响应业务码均由 package 内部处理。
|
||
|
||
HTTP 请求统一使用 [`git.zhangdeman.cn/zhangdeman/network`](https://git.zhangdeman.cn/zhangdeman/network),每次发送只调用一次目标 Webhook,不在 package 内自动重试。
|
||
|
||
## 安装
|
||
|
||
```bash
|
||
go get git.zhangdeman.cn/zhangdeman/notice
|
||
```
|
||
|
||
当前版本依赖 Go 1.25 及以上版本。
|
||
|
||
## 支持能力
|
||
|
||
| 消息类型 | 飞书 | 钉钉 | 企业微信 | 通用 Webhook |
|
||
|---|---|---|---|---|
|
||
| 普通文本 | Text | Text | Text | 统一 JSON |
|
||
| 带颜色文本 | 彩色卡片文本 | Markdown 语义前缀 | Markdown 内置颜色 | 统一 JSON |
|
||
| Markdown | 交互卡片 | Markdown | Markdown | 统一 JSON |
|
||
| 卡片 | Card JSON 2.0 | ActionCard | Template Card | 统一 JSON |
|
||
| 卡片主题 | 支持 | 保留内容 | 保留内容 | 原样传递 |
|
||
|
||
## 快速开始
|
||
|
||
### 创建 Factory
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"time"
|
||
|
||
"git.zhangdeman.cn/zhangdeman/notice"
|
||
)
|
||
|
||
func main() {
|
||
factory := notice.NewFactory(map[notice.Channel]notice.Config{
|
||
notice.ChannelFeishu: {
|
||
Webhook: os.Getenv("FEISHU_WEBHOOK"),
|
||
Security: ¬ice.SecurityConfig{
|
||
SignEnabled: true,
|
||
SignSecret: os.Getenv("FEISHU_SIGN_SECRET"),
|
||
},
|
||
Timeout: 5 * time.Second,
|
||
},
|
||
notice.ChannelDingTalk: {
|
||
Webhook: os.Getenv("DINGTALK_WEBHOOK"),
|
||
Security: ¬ice.SecurityConfig{
|
||
SignEnabled: true,
|
||
SignSecret: os.Getenv("DINGTALK_SIGN_SECRET"),
|
||
},
|
||
Timeout: 5 * time.Second,
|
||
},
|
||
notice.ChannelWeCom: {
|
||
Webhook: os.Getenv("WECOM_WEBHOOK"),
|
||
Timeout: 5 * time.Second,
|
||
},
|
||
})
|
||
|
||
sender, err := factory.Get(notice.ChannelFeishu)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
if err = sender.Send(context.Background(), notice.Text("服务启动完成")); err != nil {
|
||
panic(err)
|
||
}
|
||
}
|
||
```
|
||
|
||
`Factory.Get(channel)` 会缓存初始化成功的 `Sender`。同一 Factory 内再次获取相同渠道时直接返回已有实例,并发调用也只会初始化一个实例。
|
||
|
||
也可以绕过 Factory,直接创建单个渠道实例:
|
||
|
||
```go
|
||
sender, err := notice.New(notice.ChannelWebhook, notice.Config{
|
||
Webhook: "https://example.com/webhook",
|
||
})
|
||
```
|
||
|
||
Webhook 地址必须为 HTTPS;`Timeout` 为空时默认 5 秒。
|
||
|
||
### 独立签名校验
|
||
|
||
每个机器人配置拥有独立的安全校验开关和密钥:
|
||
|
||
```go
|
||
Security: ¬ice.SecurityConfig{
|
||
SignEnabled: true,
|
||
SignSecret: os.Getenv("ROBOT_SIGN_SECRET"),
|
||
},
|
||
```
|
||
|
||
- 飞书:在请求体中加入秒级 `timestamp` 和 `sign`。
|
||
- 钉钉:在 Webhook 查询参数中加入毫秒级 `timestamp` 和 `sign`。
|
||
- `SignEnabled` 为 `false` 或未设置 `Security` 时不生成签名。
|
||
- 启用签名后 `SignSecret` 不能为空。
|
||
- 企业微信群机器人和通用 Webhook 不启用该签名配置。
|
||
|
||
`SecurityConfig` 属于单个渠道的 `Config`,因此不同机器人可以分别启用签名并使用不同密钥。
|
||
|
||
## 消息格式
|
||
|
||
### 普通文本
|
||
|
||
```go
|
||
err := sender.Send(ctx, notice.Text("数据同步任务已完成"))
|
||
```
|
||
|
||
### 带颜色文本
|
||
|
||
```go
|
||
message := notice.ColorText(
|
||
notice.TextSegment{Text: "发布状态:"},
|
||
notice.TextSegment{
|
||
Text: "失败",
|
||
Color: notice.ColorDanger,
|
||
Bold: true,
|
||
},
|
||
)
|
||
|
||
err := sender.Send(ctx, message)
|
||
```
|
||
|
||
支持 `default`、`info`、`success`、`warning`、`danger` 和 `muted` 六种语义颜色。各平台会转换为最接近的原生显示效果。
|
||
|
||
### Markdown
|
||
|
||
```go
|
||
err := sender.Send(ctx, notice.Markdown(
|
||
"订单服务告警",
|
||
"**错误率超过阈值**\n\n当前值:3.2%",
|
||
))
|
||
```
|
||
|
||
### 卡片消息
|
||
|
||
```go
|
||
message := notice.Card(notice.CardContent{
|
||
Title: "发布结果",
|
||
Theme: notice.CardThemeGreen,
|
||
Markdown: "服务已成功发布到生产环境。",
|
||
Fields: []notice.CardField{
|
||
{Name: "服务", Value: "order-service"},
|
||
{Name: "版本", Value: "v1.8.0"},
|
||
},
|
||
Actions: []notice.CardAction{
|
||
{Text: "查看发布详情", URL: "https://example.com/releases/1001"},
|
||
},
|
||
})
|
||
|
||
err := sender.Send(ctx, message)
|
||
```
|
||
|
||
飞书支持 `default`、`blue`、`wathet`、`turquoise`、`green`、`yellow`、`orange`、`red`、`carmine`、`violet`、`purple`、`indigo` 和 `grey` 卡片主题。卡片操作地址必须使用 HTTPS。
|
||
|
||
## 同步与异步发送
|
||
|
||
同步发送直接返回结果:
|
||
|
||
```go
|
||
if err := sender.Send(ctx, message); err != nil {
|
||
return err
|
||
}
|
||
```
|
||
|
||
异步发送返回容量为 1 的只读错误通道:
|
||
|
||
```go
|
||
result := sender.SendAsync(ctx, message)
|
||
|
||
go func() {
|
||
if err := <-result; err != nil {
|
||
log.Printf("send notice failed: %v", err)
|
||
}
|
||
}()
|
||
```
|
||
|
||
异步任务运行在当前进程中。调用方需要保证进程和 `Context` 在发送完成前仍然有效。
|
||
|
||
## 包装 Zap Logger
|
||
|
||
`WrapZap` 在原 Logger Core 之外附加通知 Core,不改变原有控制台或文件日志输出。
|
||
|
||
```go
|
||
base, err := zap.NewProduction()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
logger, err := factory.WrapZap(base, notice.ZapConfig{
|
||
Channels: []notice.Channel{
|
||
notice.ChannelFeishu,
|
||
notice.ChannelDingTalk,
|
||
},
|
||
OnError: func(err error) {
|
||
// 使用独立的处理方式,避免写回 logger 形成递归通知。
|
||
fmt.Printf("send log notice failed: %v\n", err)
|
||
},
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer logger.Sync()
|
||
|
||
logger.Info("服务启动完成")
|
||
logger.Warn("订单延迟升高", zap.Int("delay_ms", 2500))
|
||
logger.Error("订单创建失败", zap.String("order_id", "O1001"))
|
||
```
|
||
|
||
`Levels` 为空时,仅精确匹配 `WarnLevel` 和 `ErrorLevel`。自定义等级示例:
|
||
|
||
```go
|
||
logger, err := factory.WrapZap(base, notice.ZapConfig{
|
||
Channels: []notice.Channel{notice.ChannelWeCom},
|
||
Levels: []zapcore.Level{
|
||
zapcore.ErrorLevel,
|
||
zapcore.DPanicLevel,
|
||
},
|
||
})
|
||
```
|
||
|
||
通知异步发送到指定渠道。`logger.Sync()` 会等待已触发的通知结束,适合在程序退出前调用。日志消息会转换为卡片,包含等级、Logger 名称、时间、调用位置以及 Zap 字段。
|
||
|
||
## 错误处理
|
||
|
||
- 参数错误可通过 `errors.Is` 判断 `ErrUnsupportedChannel`、`ErrChannelNotConfigured`、`ErrInvalidConfig`、`ErrInvalidMessage` 和 `ErrInvalidZapConfig`。
|
||
- 非 2xx 响应返回 `*notice.HTTPError`。
|
||
- 平台业务码失败返回 `*notice.PlatformError`。
|
||
- `SendAsync` 的错误通过返回通道读取;Zap 通知错误通过 `ZapConfig.OnError` 接收。
|
||
|
||
```go
|
||
var platformErr *notice.PlatformError
|
||
if errors.As(err, &platformErr) {
|
||
log.Printf("channel=%s code=%s message=%s", platformErr.Channel, platformErr.Code, platformErr.Message)
|
||
}
|
||
```
|
||
|
||
配置中包含 Webhook 和签名密钥,业务代码不要将完整配置写入日志。
|
||
|
||
## 开发验证
|
||
|
||
```bash
|
||
go test -race ./...
|
||
```
|
||
|
||
详细的协议映射与实现说明见[多渠道消息通知平台设计文档](./多渠道消息通知平台设计文档.md)。
|