优化小程序部分

This commit is contained in:
hero920103 committed 2026-10-31 19:17:37 +08:00
1 parent 727319a6eb
commit d4e89788c5
46 files changed
+4906 -63

No files matched your search

+185
View File
@@ -0,0 +1,185 @@
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
}