通知与站内信
本文面向带客服团队的站长与运营主管,讲清站内信、邮件、短信三条通知通道各自怎么开关、模板怎么配、哪些业务动作会真的发信,以及发不出去时从哪个后台入口查。变量语法、模板编号与触发点都取自当前代码;某项能力「后端有定义但代码里没有调用」的地方会直接写明。
1. 三条通道的分工与总开关
| 通道 | 存储与投递 | 总开关位置 | 默认值 |
|---|---|---|---|
| 站内信 | 写 user_notifications 表,用户在控制台查看 | 无全局开关;受用户自己的通知偏好约束 | 开启 |
| 邮件 | 走邮件驱动(集成插件)同步或入队发送,落 message_logs | 「站点内容 → 站点设置」里的「启用邮件通知」 | 关闭 |
| 短信 | 走短信驱动(集成插件),落 message_logs | 「站点内容 → 站点设置」里的「启用短信通知」 | 关闭 |
两个总开关的存储名分别是 notification.email_enabled 与 notification.sms_enabled,取值为 1、true、on 时才算开启。它们只拦「系统主动发信」,不拦两条旁路:
- 「集成插件」页里的测试发信走驱动直发,不受总开关约束——测试成功不代表对外能发。
- 关闭总开关后,系统仍会先写一条
pending的message_logs记录,再以「邮件通知未启用」或「短信通知未启用」把这条记录置为failed。所以日志里查得到失败记录、错误信息就是这句话,这是判断「是开关没开而不是通道坏了」的最快依据。
通道的「发不发得出去」还取决于驱动能不能解析出来:邮件与短信都先从当前启用绑定里挑一个可用驱动,挑不到就报「邮件驱动 [xxx] 未注册」或「短信驱动 [xxx] 未注册」。也就是说开关开了、模板也配了,但插件没装或没绑定,一样发不出去。配置步骤见邮件与短信。
2. 站内信
站内信是唯一不依赖外部通道的通知,插件没配好也能送达。用户在控制台的通知中心看到的是两类内容的合并流:一类是公告(内容文章 + 已读记录),一类是个人通知(user_notifications)。未读数也按这两类相加。
每条个人通知包含类型、标题、正文、跳转链接和一段结构化数据。与工单相关的类型有 4 个:
| 类型常量 | 触发时机 | 跳转 |
|---|---|---|
ticket_staff_reply | 员工回复工单,或上游管理员回复经回调写入 | /client/tickets/{id} |
ticket_pending_reminder | 员工回复后长时间无客户跟进,关闭前催办 | /client/tickets/{id} |
ticket_auto_closed | 工单被超时自动关闭 | /client/tickets/{id} |
ticket_reopened | 工单被重开(管理端或客户自己触发) | /client/tickets/{id} |
运营要点:
- 写入是防御性的。 表不存在或写库抛错时只记一条 warning 日志,不会阻断建单、回复等主流程。所以「站内信少了」可能是写入被吞,去
storage/logs/laravel.log搜[站内信] 写入失败。 - 营销类受用户偏好控制。 写入时带营销标记的通知,只在用户开启了
marketing_alert时才落库;工单、账单这类业务必要提醒不受影响。 - 标题会被截断到 191 字符;正文按写入内容原样保存,不截断。
- 用户在控制台可以逐条已读或全部已读;已读状态存
read_at,不存在「已读但未读数不减」的情况——未读数就是read_at为空的行数。
3. 邮件模板与变量
模板入口在「站点内容 → 通知与接口 → 邮件模板」,权限码 settings.view;点进单个模板是「邮件模板详情」页,同一个页面也服务短信模板。模板不是纯设置项,而是 notification_templates 表里的行,按 channel(email / sms)+ code(编号)唯一。
系统内置 31 个邮件模板(编号 100001 至 100031),全部由 EmailNotificationTemplateDefaults 提供默认主题与 HTML 正文,可用命令 php artisan app:seed-email-templates 幂等恢复默认。字段与影响范围:
| 字段 | 说明 |
|---|---|
subject | 邮件主题模板;渲染后会去掉换行 |
content | 正文模板,可填完整 HTML 文档或正文片段;片段会被套上统一外框 |
is_enabled | 关掉后该模板的邮件直接跳过发送,不写日志、不报错 |
variables | 该模板可用变量清单,页面上以「可用变量」展示 |
provider_template_id | 仅短信:运营商审核通过的模板 ID |
改主题或正文后 is_custom 会被标记为自定义;没有「一键回滚单个模板」的按钮,恢复默认要重跑上一条命令(会覆盖全部邮件模板的自定义内容,慎用)。
变量占位符有两种写法,邮件与短信都支持:
{{变量名}}直接替换。在邮件正文里,HTML 模板中的替换值会被转义,纯文本模板则自动转成段落。{{#变量名}}…{{/变量名}}条件块:变量有值时保留块内内容,空值(null、空字符串、false)时整块删掉。邮件{{login_tip}}、短信正文里的可选段落都靠它。
此外短信正文额外支持单花括号 {变量名},用于兼容运营商自有模板语法;邮件正文不支持单花括号写法。两个变量 site_name 与 site_logo 由系统自动注入,不需要调用方传入。写成 {{ }} 之外的其它形式(例如拼错的变量名)不会被替换,而是原样留在正文里,因此改完模板务必用测试发送看真实结果。
预览与测试:详情页右侧是实时预览,右上角「测试发送」会按模板里出现的全部变量自动填充示例值(联系方式类变量给的是示例值),真实走一次发送链路并回显成功/失败条数。测试发送需要 settings.manage 权限,且一次只允许一个收件人。
4. 工单通知的实际触发点
下表是当前代码里真实会发的工单通知。收件人范围由工单处理人决定,规则见工单与客服第 4 节。
| 业务动作 | 邮件模板 | 收件人 | 同时写站内信 |
|---|---|---|---|
| 客户新建工单 | 100026 管理员新工单提示 | 客服(见下方规则) | 否 |
| 客户回复工单 | 100027 管理员工单回复通知 | 客服(见下方规则) | 否 |
| 员工回复工单 | 100024 工单新回复通知 | 客户 | 是 |
| 工单超时自动关闭 | 100025 工单自动关闭通知 | 客户 | 是 |
| 工单重开 | 无邮件 | — | 是 |
| 关闭前催办 | 无邮件 | — | 是 |
| 上游管理员回复经回调写入 | 无邮件 | — | 是 |
三个容易踩的点:
- 客户侧只有「员工回复」和「自动关闭」发邮件,重开与催办只有站内信。想让客户一定看到,就要接受「他必须登录控制台」这个前提。
- 给客服的两封邮件受处理人影响极大:工单未指派时发给全部启用且持
ticket.reply的管理员,已指派时只发给处理人一个人。指派出去了,就等于替其他人静音了。 100023工单开通通知、以及短信目录里的工单类编号(100010、100012、100028)在当前代码里没有任何调用方。 它们定义了编号与文案模板,但改它们不会产生任何发送行为(未在代码中确认存在其它调用路径)。
模板变量与实际传参存在缺口,这是已知现象:100025 的默认正文含 {{ticket_closed_at}} 与 {{close_reason}},但自动关闭时只传了 display_name、ticket_id、ticket_subject、status、tickets_url、login_tip,这两个变量渲染为空。要客户看到关闭时间,得自己改模板,改用实际传入的变量(未在代码中确认这两个变量是否计划补齐)。
5. 短信的实际能力
短信这条通道要先明确一件事:当前代码里真正会发出去的只有验证码短信(编号 100001),它只有一个入口——客户端获取手机验证码接口 POST /api/v2/client/auth/phone-code(未在代码中确认存在其它发送入口),用 purpose 区分用途:注册、登录、重置密码、换绑/绑定手机号、校验已绑定手机号,以及未指定时的通用场景。该接口限流为每分钟 3 次,发送前另按「渠道 + 手机号 + IP」做一道发码频控,超限直接返回错误而不发短信;「校验已绑定手机号」这一类还要求目标号码与当前账号已绑定号码一致。邮箱验证码走另一条链路(POST /api/v2/client/auth/email-code,同样每分钟 3 次,邮件模板 100001)。短信侧非阿里云驱动使用系统内置正文,阿里云驱动走驱动自带的模板与服务商模板 ID。
其余短信模板的情况是:
notification_templates里存在channel=sms的编号目录(100001至100033),后台「短信模板」页可编辑正文、启用状态与「供应商模板 ID」,但除验证码外的正文没有内置默认值,需要按服务商要求自行填写。- 目录里的业务编号(登录提醒、续费提醒、账单提醒、工单类等)在当前代码中没有调用方(未在代码中确认存在其它调用路径)。配了也不会发,不要据此向客户承诺短信提醒。
- 阿里云驱动的验证码正文由插件按
purpose提供,系统层只保留等价的回退文案;purpose不影响系统其它逻辑,也不改变日志里的模板编号。
投放前的最小验证:在「集成插件 → 短信」页确认已启用并绑定驱动,再用「短信模板」页对 100001 做一次测试发送,然后去「日志中心 → 日志管理 → 短信日志」看 status 是否为 success。
6. 用户的接信偏好
用户在控制台「通知偏好」里能改的开关存在用户表上,接口是 GET/PUT /api/v2/client/auth/notification-preferences。它们对系统发信的影响并不相同,混为一谈会得出错误结论:
| 用户偏好 | 实际影响 |
|---|---|
marketing_alert | 控制带营销标记的站内信是否落库;默认关闭,关闭时营销通知直接丢弃 |
login_email_alert | 是否发送登录提醒邮件;邮箱为空或该值为 0 时登录提醒被静默跳过 |
login_notify | 登录通知的当前生效值,未设置时回退取 login_email_alert |
login_location_alert | 异地登录提醒 |
password_change_alert | 密码变更提醒 |
phone_change_alert | 手机号变更提醒 |
email_change_alert | 邮箱变更提醒 |
也就是说:用户关不掉工单、账单这类业务通知——站内信照写、邮件照发。能被用户关掉的是营销推送与安全提醒,而安全提醒关掉后账号被盗用也没有邮件可查,续费与工单通知不受其影响。
7. 发送日志与排查入口
三条通道的记录都落在 message_logs 表,后台按通道分两个页面展示,权限码都是 log.list:
| 页面 | 列表里能直接看到的列 | 常用过滤器 |
|---|---|---|
| 「日志中心 → 日志管理 → 邮件日志」 | 收件人、模板编号、主题、正文摘要、状态、驱动、Trace ID、错误信息、记录时间 | 邮箱、插件 ID、驱动、Trace ID、关键词(模板编号/主题/正文)、状态 |
| 「日志中心 → 日志管理 → 短信日志」 | 接收号码、模板编号、内容摘要、状态、驱动、Trace ID、服务商请求号、错误信息、记录时间 | 手机号、插件 ID、驱动、Trace ID、关键词(模板编号/请求号/内容)、状态 |
排查按固定顺序走,能省掉大部分来回:
- 先确认动作有没有触发:工单是否被指派决定了收件人范围;客户手机号/邮箱是否为空决定客户侧邮件是否被静默跳过(服务端在收件人为空时直接返回,不记日志)。
- 再看总开关:日志里出现「邮件通知未启用」「短信通知未启用」就是开关问题,不是通道问题。
- 再看模板开关:模板被停用时邮件是完全跳过的——不写日志、不报错。日志里查不到记录,又确认动作和开关都正常,就去模板页核对
is_enabled。 - 再看驱动:报「驱动未注册」说明插件没装、没启用或没绑定;报驱动自己的错误码说明是服务商侧问题,错误原文就在
error_msg字段里。 - 最后看 Trace ID:日志里带
trace_id,同步发给服务商排查时用它对齐同一次请求。
几个后台入口能直接串起上下游:邮件与短信日志每行都有「详情」,正文、请求号与错误信息在那里看全;详情里带 ID 类字段时可按 ID 类型(工单 ID、用户 ID、订单 ID 等)跳到对应页面。工单维度的发送情况可回到工单交流页对照;工单上游投递另有专门页面「日志中心 → 日志管理 → 上游工单投递」(过滤器含工单号、回复 ID、原因码、供应商),上传与清理记录在「上游附件上传」,自动任务(含工单自动关闭)在「自动任务日志」。
关于日志内容本身:邮件正文与短信正文在日志里是完整保存、不脱敏的,收件地址也按原值展示,这是为了让运营能真实复核发出去了什么。相应地,这条数据里会出现客户邮箱、手机号与通知正文,按贵站的权限管理决定谁能看「日志中心」——log.list 是一个单独的权限码,不要随手发给所有人。
8. 失败重试与限流
重试能力只覆盖三个入队任务,它们的参数完全一致:最多尝试 3 次,退避间隔 30 秒、120 秒、300 秒,单次超时 120 秒,重试耗尽后由 failed() 记一条 warning 日志并带上收件地址与上下文。
| 任务 | 覆盖的通知 |
|---|---|
SendTicketNotificationEmailJob | 工单相关的全部邮件 |
SendClientLoginEmailAlertJob | 用户登录提醒邮件 |
SendClientLoginFailureEmailAlertJob | 用户登录失败提醒邮件 |
其余邮件与全部短信都是同步发送、不重试:NotificationService::sendEmail 与 SmsService::sendTemplateSms 在请求内直接调用驱动,失败立刻把异常抛回调用方(同时把 message_logs 的状态更新为 failed 并写入 error_msg)。工单邮件也只在队列可用时才排队:队列驱动为 sync 时改为「响应后发送」,database 驱动下 jobs 表不存在、redis 驱动下没有可用客户端时同样退回不重试的路径。
发送侧没有额外的通知专用限流;真正拦你的是接口层的限流,例如客户建单、回复、关闭各为每分钟 10 次,图片上传每分钟 12 次。批量触达要自己控制节奏,撞到限流时才会在日志里看到服务商返回的 429 之类的错误。
9. 上线前的通知体检
按顺序做一遍,能在放量前发现绝大多数通知问题:
- 「站点设置」里两个总开关打开,站点名称与 Logo 已换成自家品牌(模板里的
{{site_name}}、{{site_logo}}取的就是这两个值)。 - 「集成插件」里邮件与短信各自启用并绑定了一个驱动,用到「测试」页确认连接正常。
- 用「测试发送」各发一次邮件与短信验证码(两边的编号都叫
100001,按通道区分),确认收到。 - 去「邮件日志」「短信日志」确认两条记录都是
success,并记下它们的 Trace ID 以熟悉排查入口。 - 用一个测试账号走一遍:提交工单、客服回复、超时催办(可临时把自动关闭小时数调小来验证)、自动关闭。核对四条站内信与两封客户邮件是否都到。
- 确认改过正文的模板仍能渲染:详情页预览没有残留的
{{ }}占位符,测试发送的正文与预期一致。 - 密码变更、登录提醒等安全邮件的开关由用户自己掌握,发布公告时说明可以在控制台通知偏好里调整,避免客服被当成开关责任方。