feat: 完成一般消息通知能力的开发

This commit is contained in:
2026-08-12 11:42:08 +08:00
commit 778c46a691
17 changed files with 2470 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
# 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: &notice.SecurityConfig{
SignEnabled: true,
SignSecret: os.Getenv("FEISHU_SIGN_SECRET"),
},
Timeout: 5 * time.Second,
},
notice.ChannelDingTalk: {
Webhook: os.Getenv("DINGTALK_WEBHOOK"),
Security: &notice.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: &notice.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)。