支付回调必须用beego.Router注册专用Controller,禁用自动渲染、手动读原始body验签;订单更新需FOR UPDATE行锁+旧状态条件;配置变更须重启bee run;必须结合异步回调与分段轮询查单。

支付回调接口必须用 beego.Router 而不是 beego.Post
因为支付网关(如微信、支付宝)发起的回调请求是固定路径 + 固定 HTTP 方法(通常是 POST),但框架内部对 beego.Post 这类函数式路由的处理不支持自动绑定控制器生命周期方法(如 Prepare、Finish),导致无法统一做签名验签、日志记录、超时控制等关键动作。
正确做法是定义一个专用 Controller,显式注册到固定路径:
// controllers/payback.go
type PaybackController struct {
beego.Controller
}
func (c *PaybackController) Post() {
// 1. 先禁用自动渲染,避免返回 HTML 模板
c.ServeJSON()
// 2. 手动读 body(微信回调不带 Content-Type,不能依赖 c.Input.RequestBody)
var rawBody []byte
rawBody, _ = ioutil.ReadAll(c.Ctx.Request.Body)
// 3. 验签、解析、更新订单状态...
}
-
beego.Router("/pay/callback", &PaybackController{}, "post:Post")必须写在routers/router.go中,且不能漏掉"post:Post"映射 - 微信回调默认不带
Content-Type头,c.Input.RequestBody会为空,必须用ioutil.ReadAll直接读原始流 - 支付宝回调可能带
application/x-www-form-urlencoded,需用c.Input.ParseForm()配合c.Input().Get("sign")
订单状态更新必须加数据库行锁,避免重复回调导致资损
支付平台可能因网络问题多次推送同一笔回调,如果只靠业务层判断 status == "paid" 就跳过,高并发下仍可能双写成功。
Beego ORM 支持原生 SQL 行锁,推荐在更新前加 FOR UPDATE:
// models/order.go
func (o *Order) ConfirmPayment(tradeNo string) error {
o.Id = 0
err := orm.NewOrm().Raw(
"UPDATE order SET status = ?, updated = ? WHERE trade_no = ? AND status = ? FOR UPDATE",
"paid", time.Now(), tradeNo, "unpaid",
).Exec()
return err
}
- WHERE 条件中必须包含旧状态(如
status = "unpaid"),否则锁住整张表或大量无关行 - MySQL 默认隔离级别是
REPEATABLE READ,FOR UPDATE在该级别下会锁住匹配的索引行 + 间隙,防止幻读 - 不要用
o.Read(&order, "trade_no")+o.Update(),这会先查后更,中间存在时间窗口
bee run 热重载不触发支付回调逻辑的重新加载
bee 工具监听的是 .go 文件变更并重启进程,但支付回调常依赖外部配置(如密钥、网关地址)写在 conf/app.conf 或环境变量里。修改这些内容后,bee run 不会感知,导致调试时一直用旧配置。
- 改完
app.conf后必须手动Ctrl+C停掉bee run,再重新执行 - 密钥类敏感配置建议从环境变量读取:
beego.AppConfig.String("wx:mch_key")→ 改成os.Getenv("WX_MCH_KEY") - 微信沙箱回调地址必须是公网可访问域名,本地
localhost无法接收,调试要用ngrok http 8080透出
异步通知与主动查询必须共存,不能只信回调
所有支付文档都强调:回调不可靠。实际项目中,微信回调失败率约 0.3%,支付宝约 0.1%,尤其在高峰期。只依赖回调会导致订单长期卡在“支付中”。
必须实现定时任务主动查单:
// utils/poll_order.go
func CheckUnconfirmedOrders() {
orm.NewOrm().QueryTable("order").Filter("status", "unpaid").All(&orders)
for _, o := range orders {
resp := wxapi.QueryOrder(o.TradeNo) // 调用微信查单 API
if resp.Status == "SUCCESS" {
o.Status = "paid"
orm.NewOrm().Update(&o)
}
}
}
- 查单频率建议:前 2 分钟每 15 秒一次,之后降为每分钟一次,避免被限流
- 查单 API 必须带时间范围参数(
time_start/time_end),否则全表扫描性能爆炸 - 不要在回调里直接调查单 API,这是冗余操作;回调负责终态通知,查单负责兜底


















