Gin处理支付回调后必须解耦通知逻辑,立即返回200并投递消息队列,通过幂等控制、状态机、用户级WebSocket聚合及失败隔离重试机制保障高并发下通知的时效性与准确性。

订单提醒不能只靠前端轮询或简单发个 HTTP 请求就完事——Gin 处理完支付回调后,必须立刻解耦通知逻辑,否则高并发下单时会卡住接口、丢消息、重复推。
webhook 回调里别直接发通知
微信/支付宝的 /notify 接口收到回调后,常见错误是直接调用短信 SDK 或 WebSocket 广播。这会导致:HTTP 响应超时(微信 5 秒内不回 200 就重试)、DB 写入失败导致整个回调失败、同一笔订单被重放多次造成重复提醒。
- 快速校验签名和业务 ID 后,立即返回
http.StatusOK - 只把
biz_id和必要上下文(如event_type)塞进消息队列,不要传原始 body 或密钥 - 用
asynq.TaskID(fmt.Sprintf("notify_%s", biz_id))强制去重,避免多次推送生成多个任务
通知投递必须带状态机和幂等控制
“已发送”不等于“用户看到”。消息记录必须有明确状态字段:pending → sending → sent / failed / read,不能用布尔值 IsNotified。
- Worker 拿到任务后,先查 DB:若该
biz_id对应订单的notify_status == "sent",直接跳过 - 发送成功后,用事务更新 DB 状态 + 插入通知记录(PostgreSQL 的
jsonb存扩展字段,比如跳转链接、按钮文案) - Redis 只缓存“已读”状态,不做主存储;别用 MongoDB 存核心通知记录——分页错、审计难、改 schema 成本高
WebSocket 推送要绑定 UserID,不是 session ID
用户在 App 点了“全部已读”,网页端红点还在?大概率是用 session.ID 当作连接标识,而不同端登录的是同一个 user_id,但 session 不互通。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 建立 WebSocket 连接时,必须携带有效 JWT 或登录态 token,并解析出真实
user_id - 服务端用
map[uint64][]*websocket.Conn按user_id聚合连接,发通知时遍历这个 slice - 连接断开时,只从 map 中删掉对应 conn,不删 user_id 键——新连接上来能自动续上
- 别在 goroutine 里无限制起协程广播;用带缓冲 channel 控制并发,例如
pushCh := make(chan *NotifyMsg, 100),worker 数固定为 5~10
失败消息不能原地重试
短信网关超时、微信模板消息审核未过、WebSocket 连接已断——这些失败必须隔离处理,否则一个坏消息会堵死整个 worker。
-
sendSMS()必须带context.WithTimeout(ctx, 5*time.Second),不能依赖 SDK 默认超时 - 失败后写入独立的
retry_queue表(含重试次数、下次执行时间),由定时任务按指数退避拉取 - 超过 3 次重试仍失败,转入人工告警队列,而不是无限循环
- 别把失败日志打在 gin 日志里就完事;要单独落库字段
fail_reason,方便后续归因(比如“模板 ID 无效”“手机号格式错误”)
最易被忽略的一点:通知的“时效性”和“准确性”永远在博弈。订单创建 5 秒内必须触达?那就要接受少量重复;要求绝对不重?就得容忍延迟几秒再聚合去重。选哪个,得看业务场景,而不是技术方案本身。

















