
GoWind Admin风行 — 开箱即用的企业级全栈中后台框架通知域实战大多数管理系统的通知长成这样站内信是一张表、邮件发送散落在某个 service 里、Webhook 是临时加的一段 curl——三个互不相认的半成品。想回答这条通知发了没有、为什么失败时只能翻日志。风行GoWind Admin把通知做成了一个完整的域业务代码一个入口发通知路由规则决定走哪个渠道投递台账记录每一次的成败与原因。本文拆解这套统一投递体系。一、痛点对话两个互不相认的半个域风行在设计通知域之前面对的正是多数项目的现状站内信成熟但只能站内投递通知渠道SMTP 邮箱配置能发站外但没有调度层——谁在什么时候该发什么没人管。收敛后的通知域回答三个问题业务代码想通知某个人时唯一的入口是什么一条通知走了哪些渠道、成功没有、失败为什么——台账在哪站内信与站外发送如何收敛成同一套渠道抽象已有的收件箱不必重写。二、统一入口业务只依赖一个接口业务 service找回密码验证码、邮箱换绑验证码、未来的告警与审批……只依赖一个Notifier接口由SendDirect实现业务 service │ 只依赖 Notifier 接口 ▼ NotificationService.SendDirect ├─ 查路由事件类型 → 渠道 同步/异步sys_notification_rules ├─ 落台账先记 SENDING带 request_id 幂等锚再发送 └─ 按规则的派发方式分流 ├─ 异步预检通过 → 任务队列入队 → 立刻返回调用方不等待 SMTP └─ 同步当场发送 回写结论测试邮件、站内信走这条路一个容易被忽略、但做对了很重要的细节发送是系统上下文不是操作人上下文。租户到期扫描、定时任务、审计日报发起的通知没有操作人可言找回密码更是免鉴权入口——把通知发送绑在登录态上这些场景全会哑火。三、路由规则事件到渠道的映射是数据不是代码“密码重置码走邮件异步发”“站内信事件走站内信同步发”——这些映射存在sys_notification_rules表里管理页「通知管理 → 通知路由规则」可视化维护每条规则三个要素业务事件 → 投递渠道 → 派发方式同步/异步 启用开关。空表启动时自动播种全集找回密码码、联系人绑定码、渠道测试邮件、站内信四条业务代码零配置即可跑通改路由是管理页改一行的操作不发版。内置渠道三类邮件SMTP渠道管理页维护多个 SMTP 配置服务器/端口/加密方式/发件人支持测试发送——测试邮件的产物就是 SMTP 报错原文配错当场看见Webhook出站回调支持载荷模板拨号期按解析后的真实 IP 拦截内网地址SSRF 防护做在最后一跳站内信复用既有收件箱不重写。四、投递台账每一次发送都有账可查sys_notification_deliveries是整个域的账本一条通知 × 一个渠道 × 一个收件人 一行。设计里有几个值得抄走的决策状态机四态SENDING / SENT / FAILED / SKIPPED。SENDING不是永久态——常驻清扫任务把超期未结算的行定案为FAILED并写明原因台账里不存在永远在发送中的僵尸行request_id 幂等锚一次业务调用产生的多条投递比如同时发邮件 站内信用同一个请求号串起来复合唯一索引(request_id, channel)保证重试不重复记账失败也落渠道配置 IDFAILED的行照样记录当时选中的是哪个渠道配置排障时不用猜它到底用的哪个 SMTP投递目标脱敏邮箱存o***example.com形态——台账是永久数据敏感目标不留全量不存正文快照验证码类通知的正文就是 OTP 本身快照进永久台账等于建了一张明文验证码表。台账只存related_id按事件类型解释的业务对象 ID要追溯走关联回跳台账只读没有 Update/Delete 业务方法——改一条已发生的投递等于伪造事实只有结果回写这一个入口。五、异步投递预检 队列 清扫验证码邮件要立刻知道渠道没配没发出去日报类通知要发了就行不用等——两类诉求靠规则行上的派发方式分流异步路径发送前先过渠道预检配置是否可用不拨号→ 预检通过入任务队列接口立刻返回 → 消费者真实投递、每次尝试记attempts预检不过当场结台账调用方拿到的结论与同步路径一字不差同步路径当场发送当场回写attempts1与结论写在同一条更新里。六、试投递发出去才算配好了渠道配置页的「测试发送」与规则页的「测试投递」产物就是投递台账里的一行——用系统自己渲染的样例文案只含规则 ID 与事件名不带任何用户数据验证这条规则选的渠道现在真的能通。站内信渠道不走这个口要有真实正文去站内信页发邮件渠道收件地址留空直接拒绝——测试入口收窄成验证配置不是给任意邮箱发信的口子。结语通知域的设计哲学与风行其他横切系统一脉相承入口收敛成接口、决策外置成数据、过程留痕成台账、边界收口在 fail-closed。业务代码从怎么发里解放出来只说发什么运维在管理页看到每一次投递的成败与原因——这就是把通知当域做和当功能做的差别。项目地址https://github.com/tx7do/go-wind-admin / https://gitee.com/tx7do/go-wind-admin在线演示https://demo.admin.gowind.cloud前端/ https://api.demo.admin.gowind.cloud/docs/后端 Swagger