Files
notice/多渠道消息通知平台设计文档.md

26 KiB
Raw Permalink Blame History

多渠道消息通知 Package 设计

1. 目标

将消息通知能力实现为可被 Go 项目直接引入的第三方 package,不提供 HTTP 服务。

支持渠道:

  • 飞书自定义机器人
  • 钉钉自定义机器人
  • 企业微信群机器人
  • 通用 Webhook

支持统一消息格式:

  • 普通文本
  • 带颜色文本
  • Markdown
  • 卡片消息
  • 包装现有 Zap Logger,并按日志等级触发消息通知

支持同步和异步发送。所有渠道的 HTTP/HTTPS 请求统一使用:

git.zhangdeman.cn/zhangdeman/network

2. 官方能力与统一策略

2.1 能力矩阵

统一格式 飞书 钉钉 企业微信
普通文本 text text text
带颜色文本 卡片 plain_text.text_colorlark_md 彩色文本 官方 Webhook Markdown 未提供稳定的行内颜色契约,自动降级 markdowninfocommentwarning 三种内置颜色
Markdown 卡片 Markdown / lark_md markdown markdown
卡片 interactive Card JSON 2.0 actionCard;多图文可使用 feedCard template_card
卡片主题 Header template 原生支持 无等价主题,忽略 无等价主题,忽略

统一 package 只保证内容和语义一致,不保证三个客户端的颜色、间距、字体及卡片布局完全一致。

2.2 颜色使用语义

调用方使用语义颜色,不直接传平台颜色名称或十六进制值:

type Color string

const (
    ColorDefault Color = "default"
    ColorInfo    Color = "info"
    ColorSuccess Color = "success"
    ColorWarning Color = "warning"
    ColorDanger  Color = "danger"
    ColorMuted   Color = "muted"
)

Renderer 按渠道映射:

语义颜色 飞书 企业微信 钉钉降级
default default 普通文本 普通文本
info blue info(绿色) 【提示】 前缀
success green info(绿色) 【成功】 前缀
warning orange warning(橙红色) **【警告】**
danger red warning(橙红色) **【异常】**
muted grey comment(灰色) 普通文本

钉钉降级时必须保留全部文字内容,只丢失颜色表现。

2.3 飞书卡片主题

卡片主题独立于正文语义颜色。统一模型提供 CardTheme,飞书 Renderer 将其直接写入 Card Header 的 template

type CardTheme string

const (
    CardThemeDefault   CardTheme = "default"
    CardThemeBlue      CardTheme = "blue"
    CardThemeWathet    CardTheme = "wathet"
    CardThemeTurquoise CardTheme = "turquoise"
    CardThemeGreen     CardTheme = "green"
    CardThemeYellow    CardTheme = "yellow"
    CardThemeOrange    CardTheme = "orange"
    CardThemeRed       CardTheme = "red"
    CardThemeCarmine   CardTheme = "carmine"
    CardThemeViolet    CardTheme = "violet"
    CardThemePurple    CardTheme = "purple"
    CardThemeIndigo    CardTheme = "indigo"
    CardThemeGrey      CardTheme = "grey"
)

CardTheme 对飞书完整生效。钉钉 ActionCard 和企业微信 Template Card 没有等价的整卡主题字段,因此对应 Renderer 忽略该字段,但不会影响卡片正文和按钮。

3. 使用方式

3.1 创建 Factory

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,
    },
})

3.2 按 Channel 获取 Sender

sender, err := factory.Get(notice.ChannelFeishu)
if err != nil {
    return err
}

Factory.Get(channel) 的行为:

  • 实例已存在:直接返回缓存实例。
  • 实例不存在:读取该渠道配置,初始化、缓存并返回。
  • 渠道未配置:返回 ErrChannelNotConfigured
  • 并发获取同一 Channel:只初始化一个实例。

3.3 同步发送

err := sender.Send(ctx, notice.Markdown(
    "订单服务告警",
    "**错误率超过阈值**\n\n当前值:3.2%",
))

3.4 异步发送

result := sender.SendAsync(ctx, notice.Text("数据同步任务已完成"))

go func() {
    if err := <-result; err != nil {
        log.Printf("send notice failed: %v", err)
    }
}()

SendAsync 使用当前进程内的 goroutine,返回容量为 1 的只读错误通道。异步任务不持久化,进程退出时未完成的消息可能丢失。

3.5 包装 Zap Logger

baseLogger, err := zap.NewProduction()
if err != nil {
    return err
}

logger, err := factory.WrapZap(baseLogger, notice.ZapConfig{
    Channels: []notice.Channel{
        notice.ChannelFeishu,
        notice.ChannelDingTalk,
    },
    Levels: []zapcore.Level{
        zapcore.WarnLevel,
        zapcore.ErrorLevel,
    },
})
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 未设置或为空时,默认仅由 zapcore.WarnLevelzapcore.ErrorLevel 触发消息发送。等级采用精确匹配,不是最低等级阈值;如需由 DPanic、Panic 或 Fatal 触发,必须显式加入对应等级。

4. 对外数据模型

所有结构体字段统一包含以下 tag

  • jsonJSON 字段名称;可选字段增加 omitempty
  • dc:字段中文含义,用于文档生成、配置提示或反射读取。
  • 运行时字段使用 json:"-",明确禁止序列化。

4.1 基础类型

type Channel string

const (
    ChannelFeishu   Channel = "feishu"
    ChannelDingTalk Channel = "dingtalk"
    ChannelWeCom    Channel = "wecom"
    ChannelWebhook  Channel = "webhook"
)

type MessageType string

const (
    MessageText      MessageType = "text"
    MessageColorText MessageType = "color_text"
    MessageMarkdown  MessageType = "markdown"
    MessageCard      MessageType = "card"
)

4.2 消息结构

type Message struct {
    Type     MessageType   `json:"type" dc:"消息类型"`
    Title    string        `json:"title,omitempty" dc:"消息标题"`
    Content  string        `json:"content,omitempty" dc:"文本或 Markdown 消息正文"`
    Segments []TextSegment `json:"segments,omitempty" dc:"带颜色文本片段列表"`
    Card     *CardContent  `json:"card,omitempty" dc:"卡片消息内容"`
}

type TextSegment struct {
    Text  string `json:"text" dc:"文本内容"`
    Color Color  `json:"color,omitempty" dc:"文本语义颜色"`
    Bold  bool   `json:"bold,omitempty" dc:"是否加粗显示"`
}

type CardContent struct {
    Title    string       `json:"title,omitempty" dc:"卡片标题"`
    Theme    CardTheme    `json:"theme,omitempty" dc:"卡片主题,飞书映射为 Header template"`
    Markdown string       `json:"markdown,omitempty" dc:"卡片 Markdown 正文"`
    Fields   []CardField  `json:"fields,omitempty" dc:"卡片字段列表"`
    Actions  []CardAction `json:"actions,omitempty" dc:"卡片跳转操作列表"`
}

type CardField struct {
    Name  string `json:"name" dc:"字段名称"`
    Value string `json:"value" dc:"字段值"`
}

type CardAction struct {
    Text string `json:"text" dc:"操作按钮文案"`
    URL  string `json:"url" dc:"操作跳转地址"`
}

卡片 Action 仅支持打开 URL,不提供按钮回调。package 本身没有 HTTP 服务,无法接收平台交互事件。

4.3 消息构造方法

使用构造方法保证同一时间只有对应类型的字段生效:

func Text(content string) Message

func ColorText(segments ...TextSegment) Message

func Markdown(title, content string) Message

func Card(content CardContent) Message

调用示例:

message := notice.ColorText(
    notice.TextSegment{Text: "状态:"},
    notice.TextSegment{
        Text:  "失败",
        Color: notice.ColorDanger,
        Bold:  true,
    },
)
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"},
    },
})

5. Sender 与 Factory

5.1 对外接口

type Config struct {
    Webhook  string          `json:"webhook" dc:"机器人 Webhook 地址"`
    Security *SecurityConfig `json:"security,omitempty" dc:"当前机器人独立安全校验配置"`
    Timeout  time.Duration   `json:"timeout,omitempty" dc:"HTTP 请求超时时间"`
}

type SecurityConfig struct {
    SignEnabled bool   `json:"sign_enabled" dc:"是否启用机器人签名校验"`
    SignSecret  string `json:"sign_secret,omitempty" dc:"机器人签名校验密钥"`
}

type ZapConfig struct {
    Channels []Channel       `json:"channels" dc:"接收日志通知的消息渠道列表"`
    Levels   []zapcore.Level `json:"levels,omitempty" dc:"触发消息发送的 Zap 日志等级列表,空值默认 warn 和 error"`
    OnError  func(error)     `json:"-" dc:"异步消息发送失败时的处理函数"`
}

type Sender interface {
    Send(ctx context.Context, message Message) error
    SendAsync(ctx context.Context, message Message) <-chan error
}

func New(channel Channel, config Config) (Sender, error)
func NewFactory(configs map[Channel]Config) *Factory
func (f *Factory) Get(channel Channel) (Sender, error)
func (f *Factory) WrapZap(base *zap.Logger, config ZapConfig) (*zap.Logger, error)

Config 的 JSON tag 用于配置加载,不代表可以将配置直接写入日志或接口响应;Webhook 和签名密钥必须保持脱敏。SecurityConfig 隶属于单个机器人配置,不同渠道实例可独立决定是否启用签名并使用不同密钥。

ZapConfig.OnError 是运行时函数,因此使用 json:"-"。未配置时,异步发送错误不再写回被包装的 Logger,避免日志通知递归触发。

5.2 Factory 缓存

type Factory struct {
    mu        sync.RWMutex       `json:"-" dc:"实例缓存读写锁"`
    configs   map[Channel]Config `json:"-" dc:"按渠道保存的初始化配置"`
    instances map[Channel]Sender `json:"-" dc:"按渠道缓存的 Sender 实例"`
}

func (f *Factory) Get(channel Channel) (Sender, error) {
    f.mu.RLock()
    instance, ok := f.instances[channel]
    f.mu.RUnlock()
    if ok {
        return instance, nil
    }

    f.mu.Lock()
    defer f.mu.Unlock()

    if instance, ok = f.instances[channel]; ok {
        return instance, nil
    }

    config, ok := f.configs[channel]
    if !ok {
        return nil, ErrChannelNotConfigured
    }

    instance, err := New(channel, config)
    if err != nil {
        return nil, err
    }

    f.instances[channel] = instance
    return instance, nil
}

只有初始化成功的实例才写入缓存。

6. Renderer 设计

渠道差异由 Renderer 处理,Sender 不直接拼装平台 JSON。

type Capabilities struct {
    Text      bool `json:"text" dc:"是否支持普通文本"`
    ColorText bool `json:"color_text" dc:"是否支持带颜色文本"`
    Markdown  bool `json:"markdown" dc:"是否支持 Markdown"`
    Card      bool `json:"card" dc:"是否支持卡片消息"`
}

type renderedRequest struct {
    URL  string `json:"url" dc:"最终请求地址"`
    Body []byte `json:"body" dc:"平台请求体"`
}

type networkRequest struct {
    Context context.Context `json:"-" dc:"请求上下文"`
    URL     string          `json:"url" dc:"请求地址"`
    Body    []byte          `json:"body" dc:"JSON 请求体"`
    Timeout time.Duration   `json:"timeout" dc:"请求超时时间"`
}

type networkResponse struct {
    StatusCode int    `json:"status_code" dc:"HTTP 响应状态码"`
    Body       []byte `json:"body" dc:"HTTP 响应正文"`
}

type renderer interface {
    Capabilities() Capabilities
    Render(config Config, message Message) (renderedRequest, error)
    ValidateResponse(response networkResponse) error
}

type channelSender struct {
    channel  Channel       `json:"-" dc:"当前消息渠道"`
    renderer renderer      `json:"-" dc:"渠道消息渲染器"`
    client   networkClient `json:"-" dc:"HTTP 网络请求客户端"`
    config   Config        `json:"-" dc:"当前渠道配置"`
}

发送流程:

Sender.Send
  → 校验 Message
  → Renderer 转换平台请求体
  → 生成渠道签名
  → 使用 network package 发送
  → 检查 HTTP 状态码和平台业务错误码

禁止调用方传入钉钉、企微或飞书原生 JSON。这样可以避免业务代码绑定平台协议,也能统一处理转义、颜色映射和字段限制。

7. 各渠道 Renderer

7.1 飞书

  • 普通文本:发送 msg_type=text
  • 带颜色文本:渲染为 Card JSON 2.0 的 plain_text.text_colorlark_md 彩色文本。
  • Markdown:渲染为 interactive 卡片中的 Markdown 组件。
  • 卡片:渲染为 interactive Card JSON 2.0。
  • CardContent.Theme 原样映射为卡片 Header 的 template;未设置时使用 default
  • Fields 渲染为 Markdown 字段列表,Actions 渲染为直接位于 body.elements 的 Card JSON 2.0 Button。
  • 当前机器人启用 Security.SignEnabled 时,使用其独立 SignSecret 生成飞书要求的秒级时间戳和签名。

飞书 Card JSON 2.0 依赖较新的客户端版本;低版本客户端可能显示升级提示。若需要兼容旧客户端,可将 Renderer 切换为 Card JSON 1.0 实现,但统一接口不变。

7.2 钉钉

  • 普通文本:发送 msgtype=text
  • 带颜色文本:转换为 Markdown;保留粗体,并将颜色转换为语义前缀。
  • Markdown:发送 msgtype=markdown
  • 卡片:有按钮时发送 actionCard;只有多条图文链接时可扩展为 feedCard
  • Fields 拼接到 ActionCard 的 Markdown 正文。
  • Actions 映射为 ActionCard 的独立跳转按钮。
  • 当前机器人启用 Security.SignEnabled 时,使用其独立 SignSecret 生成钉钉要求的毫秒级时间戳和签名。

不使用未经官方承诺的 HTML 字体颜色写法,避免不同钉钉客户端显示不一致。

7.3 企业微信

  • 普通文本:发送 msgtype=text
  • 带颜色文本:发送 msgtype=markdown,映射为 <font color="info|comment|warning">
  • Markdown:发送 msgtype=markdown
  • 卡片:发送 msgtype=template_card,默认使用 text_notice
  • Fields 映射为 horizontal_content_list
  • Actions 优先映射为 jump_list;主操作可映射为 card_action
  • HTTP 成功后继续检查企业微信响应中的业务错误码。

企业微信颜色只有三种内置值,因此 dangerwarning 都映射为 warninginfosuccess 都映射为 info

7.4 通用 Webhook

通用 Webhook 直接发送统一结构:

{
  "type": "card",
  "card": {
    "title": "发布结果",
    "theme": "success",
    "markdown": "服务已成功发布到生产环境。",
    "fields": [
      {"name": "服务", "value": "order-service"}
    ],
    "actions": [
      {"text": "查看详情", "url": "https://example.com/releases/1001"}
    ]
  }
}

HTTP 状态码为 200299 时视为成功。

8. 参数校验

8.1 Config

  • Channel 必须受支持。
  • Factory 获取的 Channel 必须存在配置。
  • Webhook 不能为空且必须使用 HTTPS。
  • Timeout 未设置时默认为 5 秒。
  • 飞书和钉钉支持独立签名配置;启用签名时 SignSecret 不能为空。
  • Security 未设置或 SignEnabled=false 时不生成签名。
  • 企业微信和通用 Webhook 不接受该签名配置。

8.2 Message

  • Message.Type 必须为已支持类型。
  • 普通文本和 Markdown 的 Content 不能为空。
  • 带颜色文本至少包含一个非空 Segment。
  • 卡片的 TitleMarkdown 至少一个非空。
  • Segment 的 Color 必须为预定义语义颜色。
  • Card Theme 必须为预定义的 CardTheme;空值按 CardThemeDefault 处理。
  • Card Action 的 Text 和 HTTPS URL 不能为空。

校验失败不发起网络请求。

8.3 ZapConfig

  • Channels 至少包含一个渠道。
  • Channel 必须受支持并已在 Factory 中配置。
  • 重复 Channel 自动去重,保持首次出现的顺序。
  • Levels 为空时设置为 WarnLevelErrorLevel
  • Levels 非空时必须是合法的 Zap Level,并按精确等级匹配。
  • OnError 不能使用被包装后的 Logger 记录错误,否则可能形成通知递归。

9. 同步与异步

func (s *channelSender) Send(ctx context.Context, message Message) error {
    if err := validateMessage(message); err != nil {
        return err
    }
    request, err := s.renderer.Render(s.config, message)
    if err != nil {
        return err
    }
    response, err := s.client.Post(networkRequest{
        Context: ctx,
        URL:     request.URL,
        Body:    request.Body,
        Timeout: s.config.Timeout,
    })
    if err != nil {
        return err
    }
    return s.renderer.ValidateResponse(response)
}

func (s *channelSender) SendAsync(ctx context.Context, message Message) <-chan error {
    result := make(chan error, 1)

    go func() {
        defer close(result)
        result <- s.Send(ctx, message)
    }()

    return result
}

package 内不持久化异步任务、不自动重试。调用方必须保证 Context 在异步发送完成前有效。

10. Zap Logger 包装

10.1 包装方式

package 实现一个附加的 zapcore.Core,通过 zap.WrapCorezapcore.NewTee 与原 Logger Core 组合。原有控制台、文件或其他日志输出保持不变;只有匹配的日志额外发送消息通知。

func (f *Factory) WrapZap(
    base *zap.Logger,
    config ZapConfig,
) (*zap.Logger, error) {
    if base == nil {
        return nil, ErrInvalidZapConfig
    }

    config = normalizeZapConfig(config)
    senders, err := f.getSenders(config.Channels)
    if err != nil {
        return nil, err
    }

    noticeCore := newZapNoticeCore(senders, config)
    logger := base.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
        return zapcore.NewTee(core, noticeCore)
    }))

    return logger, nil
}

10.2 Core 数据结构

type zapNoticeCore struct {
    senders []Sender                     `json:"-" dc:"日志通知使用的消息发送实例列表"`
    levels  map[zapcore.Level]struct{}    `json:"-" dc:"触发消息通知的精确日志等级集合"`
    fields  []zapcore.Field               `json:"-" dc:"通过 Logger.With 附加的上下文字段"`
    pending *sync.WaitGroup               `json:"-" dc:"等待尚未完成的异步消息发送"`
    onError func(error)                    `json:"-" dc:"异步消息发送失败处理函数"`
}

Core 实现 EnabledWithCheckWriteSync

  • Enabled:判断日志等级是否在 levels 集合中。
  • With:复制 Core 并保存 Zap 上下文字段。
  • Check:等级匹配时将当前 Core 加入 CheckedEntry
  • Write:将 Entry 和 Fields 转为卡片消息,并在 goroutine 中依次发送到渠道列表。
  • Sync:等待已触发的异步消息发送完成。

10.3 等级匹配

func normalizeZapConfig(config ZapConfig) ZapConfig {
    if len(config.Levels) == 0 {
        config.Levels = []zapcore.Level{
            zapcore.WarnLevel,
            zapcore.ErrorLevel,
        }
    }
    return config
}

func (c *zapNoticeCore) Enabled(level zapcore.Level) bool {
    _, ok := c.levels[level]
    return ok
}

默认配置只精确匹配 Warn 和 Error:

Zap 方法 默认发送消息
Debug
Info
Warn
Error
DPanic 否,需显式配置
Panic 否,需显式配置
Fatal 否,需显式配置

10.4 日志消息转换

日志统一转换为 CardContent

  • 标题:[LEVEL] LoggerNameLoggerName 为空时使用 [LEVEL] 日志告警
  • 正文:Zap Entry 的 Message。
  • 字段:Logger.With 字段和当前调用字段,转换为 Card Fields。
  • 时间:增加 timestamp 字段。
  • 调用位置:Entry 中存在 Caller 时增加 caller 字段。
  • 主题:Debug/Info 使用蓝色,Warn 使用橙色,Error 及更严重等级使用红色。

日志字段由 Zap Encoder 转为 JSON 后再生成卡片字段,避免自行判断 Zap Field 的内部类型。Webhook、Token、签名密钥等敏感字段应由业务方在写日志前脱敏。

10.5 异步发送约束

Zap 的 Core.Write 没有 context.Context 参数,因此日志通知使用 context.Background(),实际超时由各 Sender 的 Config.Timeout 控制。

Write 不等待第三方平台响应,避免网络请求阻塞正常日志写入。应用退出前调用 logger.Sync(),通知 Core 的 Sync 会等待已触发的发送任务完成。发送失败时调用 ZapConfig.OnError;未配置 OnError 时忽略回调,但不得把错误重新写入被包装 Logger。

11. Network 包约束

所有外部请求必须通过 git.zhangdeman.cn/zhangdeman/network 发起,不直接创建 net/http.Client,也不引入各平台 SDK。

type networkClient interface {
    Post(request networkRequest) (networkResponse, error)
}
  • 每次发送只执行一次 HTTP 请求。
  • 使用 Config.Timeout 控制超时。
  • 不关闭 TLS 证书校验。
  • 错误和日志不输出完整 Webhook、Token 或签名密钥。
  • 具体构造函数以锁定版本的 network package API 为准。
  • 单元测试通过 Fake Network Client 验证请求体。

12. 错误定义

var (
	ErrUnsupportedChannel   = errors.New("notice: unsupported channel")
	ErrChannelNotConfigured = errors.New("notice: channel not configured")
	ErrInvalidConfig        = errors.New("notice: invalid config")
	ErrInvalidMessage       = errors.New("notice: invalid message")
	ErrInvalidZapConfig     = errors.New("notice: invalid zap config")
)

参数错误使用 %w 包装,调用方通过 errors.Is 判断类型。HTTP 非 2xx 返回带渠道、状态码与响应正文的 HTTPError;平台业务码失败返回带渠道、业务码与错误信息的 PlatformError

13. 建议代码结构

notice/
  README.md
  errors.go
  factory.go
  go.mod
  go.sum
  message.go
  notice_test.go
  renderer.go
  renderer_dingtalk.go
  renderer_feishu.go
  renderer_webhook.go
  renderer_wecom.go
  sender.go
  transport.go
  types.go
  zap.go

14. 测试重点

  • Factory 并发获取同一 Channel 只初始化一次。
  • 四种消息构造方法生成正确的 Message。
  • 所有结构体字段均包含 jsondc tag,运行时字段均为 json:"-"
  • 各 Renderer 对四种消息类型生成正确的官方请求结构。
  • 企微六种语义颜色正确收敛到三种官方颜色。
  • 钉钉带颜色文本降级后不丢失文字。
  • 飞书 Card JSON 2.0 的主题、字段和按钮映射正确。
  • 所有 CardTheme 枚举都能正确写入飞书 Header template
  • JSON、Markdown、URL 和特殊字符正确转义。
  • HTTP 2xx 但平台业务码失败时返回 PlatformError
  • 日志和错误中不包含 Webhook、Token 或签名密钥。
  • Fake Network Client 可以覆盖同步和异步发送。
  • ZapConfig 未设置 Levels 时只触发 Warn 和 Error。
  • 自定义 Levels 采用精确匹配,不错误扩展为等级阈值。
  • ZapConfig 的重复 Channels 被去重,未配置 Channel 返回错误。
  • 被包装 Logger 保留原 Core 输出,同时增加消息通知输出。
  • Zap 的 Logger.With 字段和当前日志字段都能进入卡片消息。
  • Zap 通知异步发送,不阻塞 Core.Write;Sync 等待在途任务完成。
  • OnError 不会通过被包装 Logger 形成递归通知。

15. 验收标准

  • 业务项目通过引入 package 即可发送消息,无需部署 HTTP 服务。
  • 支持普通文本、带颜色文本、Markdown 和卡片消息。
  • 支持飞书、钉钉、企业微信和通用 Webhook。
  • 通过 Factory.Get(channel) 获取并复用 Sender 实例。
  • 支持 SendSendAsync
  • 渠道差异只存在于 Renderer,调用方不拼装平台 JSON。
  • 颜色不受支持时按语义降级且不丢失内容。
  • 所有网络请求使用指定 network package。
  • 可以将现有 *zap.Logger 包装为带消息通知能力的新 Logger。
  • 可以指定一个或多个消息渠道,日志通知发送到全部指定渠道。
  • 可以指定触发日志等级,未指定时默认 Warn 和 Error。
  • Zap 原有日志输出不受影响,消息通知默认异步执行。

16. 官方参考