Files
2026-10-31 19:17:37 +08:00

186 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}