增加微信通知功能

This commit is contained in:
2026-09-16 18:06:18 +08:00
parent b48688cbab
commit f386899625
23 changed files with 3539 additions and 18 deletions
+124
View File
@@ -0,0 +1,124 @@
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, "<Encrypt>") {
// 安全/兼容模式:外层为加密 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))
}