package controllers import ( "io" "strings" "time" "server/services/wechatmini" "server/services/wechatmp" "github.com/beego/beego/v2/core/logs" beego "github.com/beego/beego/v2/server/web" ) // WechatMiniCallbackController 微信小程序消息推送回调(微信服务器调用,无需平台鉴权) // // 小程序后台「开发管理 → 开发设置 → 消息推送」填写: // // URL(服务器地址):https://<你的域名>/api/wechat/mini/callback // Token:与平台端「小程序配置 - 消息推送」中的 Token 一致 // EncodingAESKey:与平台端配置一致(安全 / 兼容模式必填) // 消息加解密方式:明文 / 兼容 / 安全(三种均支持) // 数据格式:XML / JSON(均支持,按平台端配置输出) // // 平台端维护入口:系统设置 → 小程序配置 → 编辑小程序 → 消息推送 type WechatMiniCallbackController struct { beego.Controller } // Callback 服务器地址验证(GET,回显 echostr)与消息 / 事件推送(POST,被动回复) func (c *WechatMiniCallbackController) Callback() { cfgs, err := wechatmini.LoadPushConfigs() if err != nil || len(cfgs) == 0 { c.Ctx.Output.SetStatus(403) c.Ctx.Output.Body([]byte("wechat mini push 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 reply, ok := verifyPushEchostr(cfgs, timestamp, nonce, q.Get("signature"), q.Get("msg_signature"), echostr); ok { c.Ctx.Output.Body([]byte(reply)) return } c.Ctx.Output.SetStatus(403) c.Ctx.Output.Body([]byte("invalid signature")) return } // ---------------- POST:消息 / 事件 ---------------- body, _ := io.ReadAll(c.Ctx.Request.Body) raw := strings.TrimSpace(string(body)) if raw == "" { c.Ctx.Output.Body([]byte("")) return } format := wechatmini.DetectPushFormat(raw) msg, perr := parsePushBody(raw, format) if perr != nil { logs.Info("[wechat-mini] 推送消息解析失败: %v", perr) c.Ctx.Output.Body([]byte("")) return } var cfg *wechatmini.PushConfig encrypted := false if msg.Encrypt != "" { // 安全模式:整个请求体是密文信封,验签后解密 msgSig := q.Get("msg_signature") if msgSig == "" { msgSig = msg.MsgSignature } if c2, ok := wechatmini.MatchPushConfigByMsgSignature(cfgs, timestamp, nonce, msg.Encrypt, msgSig); ok && c2.EncryptMode == wechatmp.EncryptModeSafe { plain, derr := wechatmini.DecryptPushMessage(c2, msg.Encrypt) if derr != nil { logs.Info("[wechat-mini] 推送消息解密失败: %v", derr) c.Ctx.Output.Body([]byte("")) return } inner, ierr := parsePushBody(plain, format) if ierr != nil { logs.Info("[wechat-mini] 解密后消息解析失败: %v", ierr) c.Ctx.Output.Body([]byte("")) return } cfg = c2 msg = inner encrypted = true } } if cfg == nil { // 明文模式 / 兼容模式(兼容模式请求体本身即明文,仅额外携带 Encrypt 字段) if c3, ok := wechatmini.MatchPushConfigBySignature(cfgs, timestamp, nonce, q.Get("signature")); ok { cfg = c3 } } if cfg == nil { logs.Info("[wechat-mini] 推送消息签名校验失败(timestamp=%s nonce=%s)", timestamp, nonce) c.Ctx.Output.Body([]byte("")) return } // 排查用:记录微信推送的消息 / 事件类型 logs.Info("[wechat-mini] 收到推送: appid=%s type=%s event=%s from=%s key=%s", cfg.AppID, msg.MsgType, msg.Event, msg.FromUserName, msg.EventKey) reply := wechatmini.HandlePushMessage(cfg, msg) if reply == "" { // 无需回复:返回空串,微信视为处理成功(不再重试) c.Ctx.Output.Body([]byte("")) return } now := time.Now().Unix() replyBody, berr := wechatmini.BuildTextReply(cfg.DataFormat, msg.FromUserName, msg.ToUserName, reply, now) if berr != nil { c.Ctx.Output.Body([]byte("")) return } if encrypted { ts := q.Get("timestamp") if ts == "" { ts = msg.TimeStamp } replyNonce := nonce if replyNonce == "" { replyNonce = msg.Nonce } enc, eerr := wechatmini.BuildEncryptedReply(cfg, replyBody, ts, replyNonce, cfg.DataFormat) if eerr != nil { c.Ctx.Output.Body([]byte("")) return } replyBody = enc } if cfg.DataFormat == wechatmini.DataFormatJSON { c.Ctx.Output.Header("Content-Type", "application/json; charset=utf-8") } else { c.Ctx.Output.Header("Content-Type", "application/xml; charset=utf-8") } c.Ctx.Output.Body([]byte(replyBody)) } // parsePushBody 按数据格式解析推送体 func parsePushBody(raw, format string) (*wechatmini.PushMessage, error) { if format == wechatmini.DataFormatJSON { return wechatmini.ParsePushJSON(raw) } return wechatmini.ParsePushXML(raw) } // verifyPushEchostr 校验微信「服务器地址验证」请求(GET),返回应回显的内容。 // // - 明文 / 兼容模式:校验 sha1(sort(token,timestamp,nonce)) == signature,通过后原样回显 echostr // - 安全模式:echostr 为密文,校验 msg_signature 后解密,回显解密后的明文 // // 命中任意一个已配置的小程序即视为验证通过(微信回调不携带 appid)。 func verifyPushEchostr(cfgs []wechatmini.PushConfig, timestamp, nonce, signature, msgSignature, echostr string) (string, bool) { if echostr == "" { return "", false } // 安全模式优先 if cfg, ok := wechatmini.MatchPushConfigByMsgSignature(cfgs, timestamp, nonce, echostr, msgSignature); ok && cfg.EncryptMode == wechatmp.EncryptModeSafe { if plain, derr := wechatmini.DecryptPushMessage(cfg, echostr); derr == nil { return plain, true } } // 明文 / 兼容模式 if _, ok := wechatmini.MatchPushConfigBySignature(cfgs, timestamp, nonce, signature); ok { return echostr, true } return "", false }