package main import ( "crypto/tls" "embed" "encoding/json" "flag" "fmt" "io" "net/http" "net/url" "strings" "time" _ "time/tzdata" ) // 请求参数结构体 type RequestParams struct { Title string `json:"title" form:"title"` Content string `json:"content" form:"content"` AppID string `json:"appid" form:"appid"` Secret string `json:"secret" form:"secret"` UserID string `json:"userid" form:"userid"` TemplateID string `json:"template_id" form:"template_id"` BaseURL string `json:"base_url" form:"base_url"` Timezone string `json:"tz" form:"tz"` } // 全局变量用于存储命令行参数 var ( cliTitle string cliContent string cliAppID string cliSecret string cliUserID string cliTemplateID string cliBaseURL string startPort string cliTimezone string ) // 微信AccessToken响应 type AccessTokenResponse struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } // 微信模板消息请求 type TemplateMessageRequest struct { ToUser string `json:"touser"` TemplateID string `json:"template_id"` URL string `json:"url"` Data map[string]interface{} `json:"data"` } // 微信API响应 type WechatAPIResponse struct { Errcode int `json:"errcode"` Errmsg string `json:"errmsg"` } func main() { // 定义命令行参数 flag.StringVar(&cliTitle, "title", "", "消息标题") flag.StringVar(&cliContent, "content", "", "消息内容") flag.StringVar(&cliAppID, "appid", "", "AppID") flag.StringVar(&cliSecret, "secret", "", "AppSecret") flag.StringVar(&cliUserID, "userid", "", "openid") flag.StringVar(&cliTemplateID, "template_id", "", "模板ID") flag.StringVar(&cliBaseURL, "base_url", "", "跳转url") flag.StringVar(&cliTimezone, "tz", "Asia/Shanghai", "时区,默认东八区") flag.StringVar(&startPort, "port", "", "端口") // 解析命令行参数 flag.Parse() // 设置路由 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `go-wxpush is running...✅`) }) http.HandleFunc("/wxsend", handleWxSend) http.HandleFunc("/detail", handleDetail) // 启动服务器 //fmt.Println("Server is running on port 5566...") port := "5566" if startPort != "" { port = startPort } fmt.Println("Server is running on: " + "http://127.0.0.1:" + port) err := http.ListenAndServe(":"+port, nil) if err != nil { fmt.Printf("Error starting server: %v\n", err) } } // 嵌入静态HTML文件 // //go:embed msg_detail.html var htmlContent embed.FS // 处理详情页面请求 func handleDetail(w http.ResponseWriter, r *http.Request) { // 从嵌入的资源中读取HTML内容 htmlData, err := htmlContent.ReadFile("msg_detail.html") if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, `{"error": "Failed to read embedded HTML file: %v"}`, err) return } // 设置响应头 w.Header().Set("Content-Type", "text/html; charset=utf-8") // 返回HTML内容 w.Write(htmlData) } func handleWxSend(w http.ResponseWriter, r *http.Request) { // 解析参数 var params RequestParams // 根据请求方法解析参数 if r.Method == "POST" { // 解析JSON请求体 decoder := json.NewDecoder(r.Body) err := decoder.Decode(¶ms) if err != nil { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, `{"error": "Invalid JSON format: %v"}`, err) return } } else if r.Method == "GET" { // 解析GET查询参数 params.Title = r.URL.Query().Get("title") params.Content = r.URL.Query().Get("content") params.AppID = r.URL.Query().Get("appid") params.Secret = r.URL.Query().Get("secret") params.UserID = r.URL.Query().Get("userid") params.TemplateID = r.URL.Query().Get("template_id") params.BaseURL = r.URL.Query().Get("base_url") params.Timezone = r.URL.Query().Get("tz") } else { w.WriteHeader(http.StatusMethodNotAllowed) fmt.Fprintf(w, `{"error": "Method not allowed"}`) return } // 只有当GET/POST参数为空时,才使用命令行参数 if params.Title == "" && cliTitle != "" { params.Title = cliTitle } if params.Content == "" && cliContent != "" { params.Content = cliContent } if params.AppID == "" && cliAppID != "" { params.AppID = cliAppID } if params.Secret == "" && cliSecret != "" { params.Secret = cliSecret } if params.UserID == "" && cliUserID != "" { params.UserID = cliUserID } if params.TemplateID == "" && cliTemplateID != "" { params.TemplateID = cliTemplateID } if params.BaseURL == "" && cliBaseURL != "" { params.BaseURL = cliBaseURL } if params.Timezone == "" && cliTimezone != "" { params.Timezone = cliTimezone } // 验证必要参数 if params.AppID == "" || params.Secret == "" || params.UserID == "" || params.TemplateID == "" { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, `{"error": "Missing required parameters"}`) return } if params.BaseURL == "" { params.BaseURL = "https://push.hzz.cool" } if params.Content == "" { params.Content = "测试内容" } if params.Title == "" { params.Title = "测试标题" } // 获取AccessToken token, err := getAccessToken(params.AppID, params.Secret) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, `{"error": "Failed to get access token: %v"}`, err) return } //log.Println(token) // 发送模板消息 resp, err := sendTemplateMessage(token, params) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, `{"error": "Failed to send template message: %v"}`, err) return } // 返回结果 json.NewEncoder(w).Encode(resp) } // Token请求参数结构体 type TokenRequestParams struct { GrantType string `json:"grant_type"` AppID string `json:"appid"` Secret string `json:"secret"` ForceRefresh bool `json:"force_refresh"` } func getAccessToken(appid, secret string) (string, error) { // 构建请求参数 requestParams := TokenRequestParams{ GrantType: "client_credential", AppID: appid, Secret: secret, ForceRefresh: false, } // 转换为JSON jsonData, err := json.Marshal(requestParams) if err != nil { return "", err } // 忽略证书验证 client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } // 发送POST请求 resp, err := client.Post("https://api.weixin.qq.com/cgi-bin/stable_token", "application/json", strings.NewReader(string(jsonData))) if err != nil { return "", err } defer resp.Body.Close() // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { return "", err } //log.Println(string(body)) // 解析响应 var tokenResp AccessTokenResponse err = json.Unmarshal(body, &tokenResp) //log.Println(tokenResp) if err != nil { return "", err } return tokenResp.AccessToken, nil } func sendTemplateMessage(accessToken string, params RequestParams) (WechatAPIResponse, error) { // 构建请求URL apiUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", accessToken) // 处理时区,默认东八区 location, err := time.LoadLocation("Asia/Shanghai") if err != nil { location, _ = time.LoadLocation("Asia/Shanghai") // 确保默认使用东八区 } // 如果参数中有时区,则尝试使用该时区 if params.Timezone != "" { loc, err := time.LoadLocation(params.Timezone) if err == nil { location = loc } } // 获取当前时间 currentTime := time.Now().In(location) timeStr := currentTime.Format("2006-01-02 15:04:05") // 构建请求数据 requestData := TemplateMessageRequest{ ToUser: params.UserID, TemplateID: params.TemplateID, URL: params.BaseURL + `/detail?title=` + url.QueryEscape(params.Title) + `&message=` + url.QueryEscape(params.Content) + `&date=` + url.QueryEscape(timeStr), Data: map[string]interface{}{ "title": map[string]string{ "value": params.Title, }, "content": map[string]string{ "value": params.Content, }, }, } // 转换为JSON jsonData, err := json.Marshal(requestData) if err != nil { return WechatAPIResponse{}, err } // 忽略证书验证 client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } // 发送POST请求 resp, err := client.Post(apiUrl, "application/json", strings.NewReader(string(jsonData))) if err != nil { return WechatAPIResponse{}, err } defer resp.Body.Close() // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { return WechatAPIResponse{}, err } // 解析响应 var apiResp WechatAPIResponse err = json.Unmarshal(body, &apiResp) if err != nil { return WechatAPIResponse{}, err } return apiResp, nil }