先说结论
浏览器弹出“允许通知”只是 Web Push 的开头,不是结尾。
在 Odoo 里,真正要跑通 Web Push,还要完成设备登记、VAPID 公钥校验、payload 入队、批量发送,以及对不可达 endpoint 的清理。
所以“明明浏览器允许了,为什么还是收不到”通常不是一句权限问题能解释的。
一、为什么要有 mail.push.device
mail.push.device 存的不是抽象用户偏好,而是具体浏览器设备订阅:
endpointkeysexpiration_timepartner_id
这说明 Odoo 不是按“用户开了通知”做粗粒度管理,而是按一个个浏览器订阅终端来管理。
这非常合理,因为同一个人可能:
- 办公电脑开了通知
- 手机浏览器开了通知
- 旧笔记本上的订阅已经失效
这些都不该混成一个状态。
二、为什么注册设备时要校验 VAPID 公钥
register_devices() 会校验前端传来的 vapid_public_key 是否与数据库一致。
这一步很关键,因为 Web Push 不是随便给个 endpoint 就能发。
VAPID 这一层是在确保:
- 当前浏览器订阅确实属于这套推送身份
- 后端和前端对使用的推送密钥是一致的
如果密钥不一致,设备就算登记了,也难以稳定接收。
三、为什么缺公钥时会重建密钥并清空设备
get_web_push_vapid_public_key() 里,如果系统发现公钥不存在,会:
- 清空已有设备
- 重新生成 VAPID 公私钥
这看起来激进,但逻辑是成立的。
因为一旦密钥体系丢失,旧设备订阅其实已经不再可信;继续留着只会制造“看起来已注册,实际上永远发不到”的假象。
四、消息为什么不是立刻直接推,而是先进队列
mail.push 模型本质上是待发送通知队列。
发送逻辑 _push_notification_to_endpoint() 会:
- 取一批待发通知
- 用私钥、公钥去推给 endpoint
- 失败时记录不可达设备
- 成功或失败后清掉本批通知
- 如果还有剩余,再触发 cron 继续跑
这说明 Odoo 把推送当成异步批处理任务,而不是阻塞主业务动作的同步调用。
五、为什么要删不可达设备
如果某个设备 endpoint 已失效,源码会把它加入待删除集合,最终清掉。
这是很重要的健康机制。
否则系统会长期保留:
- 早已卸载或过期的浏览器订阅
- 无法送达的旧 endpoint
- 重复报错的垃圾设备
长久下来,通知队列会越来越脏。
六、最容易误解的地方
误区 1:允许浏览器通知 = Odoo 一定能推送
不是,中间还有注册、密钥、队列、投递等多步。
误区 2:一个用户只对应一个推送设备
不是,设备是按 endpoint 细分的。
误区 3:设备注册成功后就永远有效
不是,endpoint 可能失效,密钥体系也可能变化。
七、排错顺序
当用户说“开了通知但没收到”时,建议按这个顺序查:
- 设备是否真的完成
register_devices() - 前端传的 VAPID 公钥是否与后台一致
- 通知是否已进入
mail.push队列 - 发送后该 endpoint 是否被判定为 unreachable
- 系统是否因为密钥缺失重建过 VAPID,导致旧设备全部失效
最后一句
Web Push 在 Odoo 里不是浏览器权限小开关,而是一条完整的设备生命周期管理链。
只有按这条链去理解,通知问题才排得清楚。
DISCUSSION
评论区