# 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)。