package handler import ( "crypto/sha1" "encoding/hex" "encoding/json" "encoding/xml" "io" "log" "net/http" "sort" "strings" "github.com/gin-gonic/gin" ) // secCallbackMsg 兼容消息推送的两种数据格式:JSON 和 XML(字段名一致,标签两套都标) type secCallbackMsg struct { Event string `json:"Event" xml:"Event"` MsgType string `json:"MsgType" xml:"MsgType"` TraceID string `json:"trace_id" xml:"trace_id"` Result struct { Suggest string `json:"suggest" xml:"suggest"` } `json:"result" xml:"result"` } // 图片异步安全检测的结果回调(微信「消息推送」服务器配置指向这里)。 // GET 校验服务器:把 echostr 原样返回 // POST 接收 wxa_media_check 结果:用 trace_id 找回帖子/评论并放出或打回 // // 明文模式(消息加解密方式选「明文」)下 body 就是 JSON,直接解析; // 若配了加密模式还需要 AES 解密,这里暂不处理(上线时消息推送选明文即可)。 // verifyWxSignature 校验 signature = sha1(sort(token, timestamp, nonce)) func (h *Handler) verifyWxSignature(c *gin.Context) bool { token := h.cfg.WeChat.MsgToken if token == "" { return true // 没配 token(本地调试):不校验 } sig := c.Query("signature") parts := []string{token, c.Query("timestamp"), c.Query("nonce")} sort.Strings(parts) sum := sha1.Sum([]byte(strings.Join(parts, ""))) return hex.EncodeToString(sum[:]) == sig } // WxSecCallback GET+POST /api/wx/sec-callback func (h *Handler) WxSecCallback(c *gin.Context) { if !h.verifyWxSignature(c) { c.String(http.StatusForbidden, "invalid signature") return } if c.Request.Method == http.MethodGet { c.String(http.StatusOK, c.Query("echostr")) // 服务器配置校验 return } raw, _ := io.ReadAll(c.Request.Body) // 数据格式 JSON / XML 都兼容:以 '<' 开头当 XML,否则当 JSON var msg secCallbackMsg var err error if trimmed := strings.TrimSpace(string(raw)); strings.HasPrefix(trimmed, "<") { err = xml.Unmarshal(raw, &msg) } else { err = json.Unmarshal(raw, &msg) } if err != nil { log.Printf("[warn] sec-callback 解析失败: %v, body=%s", err, string(raw)) c.String(http.StatusOK, "success") // 回 success 让微信别重推 return } if msg.Event == "wxa_media_check" && msg.TraceID != "" { h.svc.ResolveMediaCheck(msg.TraceID, msg.Result.Suggest) } c.String(http.StatusOK, "success") }