package controllers import ( "io" "strconv" "strings" "time" "server/services/wechatmp" beego "github.com/beego/beego/v2/server/web" ) // WechatMpCallbackController 微信公众号服务器回调(微信服务器调用,无需平台鉴权) // // 公众号后台「设置与开发 → 服务器配置」填写: // URL(服务器地址):https://<你的域名>/api/wechat/mp/callback // Token:与平台「通知设置 → 微信配置」中的 Token 保持一致 // 消息加解密方式:明文 / 兼容 / 安全(三种均支持) type WechatMpCallbackController struct { beego.Controller } // Callback 服务器地址验证(GET,回显 echostr)与消息/事件推送(POST,被动回复) func (c *WechatMpCallbackController) Callback() { cfg, err := wechatmp.LoadEnabledConfig() if err != nil { c.Ctx.Output.SetStatus(403) c.Ctx.Output.Body([]byte("wechat mp not configured")) return } q := c.Ctx.Request.URL.Query() timestamp := q.Get("timestamp") nonce := q.Get("nonce") // ---------------- GET:服务器地址校验 ---------------- if strings.EqualFold(c.Ctx.Input.Method(), "GET") { echostr := q.Get("echostr") if cfg.EncryptMode == wechatmp.EncryptModeSafe { if !wechatmp.CheckMsgSignature(cfg.Token, timestamp, nonce, echostr, q.Get("msg_signature")) { c.Ctx.Output.SetStatus(403) c.Ctx.Output.Body([]byte("invalid signature")) return } plain, derr := wechatmp.DecryptMessage(cfg.AESKey, cfg.AppID, echostr) if derr != nil { c.Ctx.Output.SetStatus(403) c.Ctx.Output.Body([]byte("decrypt failed")) return } c.Ctx.Output.Body([]byte(plain)) return } if !wechatmp.CheckSignature(cfg.Token, timestamp, nonce, q.Get("signature")) { c.Ctx.Output.SetStatus(403) c.Ctx.Output.Body([]byte("invalid signature")) return } c.Ctx.Output.Body([]byte(echostr)) return } // ---------------- POST:消息 / 事件 ---------------- body, _ := io.ReadAll(c.Ctx.Request.Body) rawBody := string(body) if strings.TrimSpace(rawBody) == "" { c.Ctx.Output.Body([]byte("")) return } needEncryptReply := false if strings.Contains(rawBody, "") { // 安全/兼容模式:外层为加密 XML,先验签再解密 outer, perr := wechatmp.ParseInboundXML(rawBody) if perr != nil { c.Ctx.Output.Body([]byte("")) return } if !wechatmp.CheckMsgSignature(cfg.Token, timestamp, nonce, outer.Encrypt, q.Get("msg_signature")) { c.Ctx.Output.Body([]byte("")) return } plain, derr := wechatmp.DecryptMessage(cfg.AESKey, cfg.AppID, outer.Encrypt) if derr != nil { c.Ctx.Output.Body([]byte("")) return } rawBody = plain needEncryptReply = true } else if !wechatmp.CheckSignature(cfg.Token, timestamp, nonce, q.Get("signature")) { // 明文模式:签名校验不通过直接拒绝(微信会重试) c.Ctx.Output.Body([]byte("")) return } msg, merr := wechatmp.ParseInboundXML(rawBody) if merr != nil { c.Ctx.Output.Body([]byte("")) return } reply := wechatmp.HandleInbound(cfg, msg) if strings.TrimSpace(reply) == "" { // 无需回复:返回空串,微信视为处理成功 c.Ctx.Output.Body([]byte("")) return } now := time.Now().Unix() plainReply := wechatmp.BuildTextReply(msg.FromUserName, msg.ToUserName, reply, now) c.Ctx.Output.Header("Content-Type", "application/xml; charset=utf-8") if needEncryptReply { ts := strconv.FormatInt(now, 10) enc, eerr := wechatmp.BuildEncryptedReply(cfg, plainReply, ts, nonce) if eerr != nil { c.Ctx.Output.Body([]byte("")) return } c.Ctx.Output.Body([]byte(enc)) return } c.Ctx.Output.Body([]byte(plainReply)) }