├── go.mod ├── .gitignore ├── .gitattributes ├── img ├── msg.png ├── w0.jpg ├── w1.jpg ├── wx1.png ├── wx2.png ├── wx3.png ├── logo.jpg └── logo.png ├── Dockerfile ├── LICENSE.txt ├── README.md ├── main.go └── msg_detail.html /go.mod: -------------------------------------------------------------------------------- 1 | module go-wxpush -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.exe 3 | *.exe~ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-language=Go -------------------------------------------------------------------------------- /img/msg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-wxpush/HEAD/img/msg.png -------------------------------------------------------------------------------- /img/w0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-wxpush/HEAD/img/w0.jpg -------------------------------------------------------------------------------- /img/w1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-wxpush/HEAD/img/w1.jpg -------------------------------------------------------------------------------- /img/wx1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-wxpush/HEAD/img/wx1.png -------------------------------------------------------------------------------- /img/wx2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-wxpush/HEAD/img/wx2.png -------------------------------------------------------------------------------- /img/wx3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-wxpush/HEAD/img/wx3.png -------------------------------------------------------------------------------- /img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-wxpush/HEAD/img/logo.jpg -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezhizheng/go-wxpush/HEAD/img/logo.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 第一阶段:使用 Alpine 获取时区数据库 2 | FROM alpine:latest AS builder 3 | RUN apk --no-cache add tzdata 4 | 5 | # 第二阶段:使用 scratch 作为最终镜像 6 | FROM scratch 7 | 8 | WORKDIR $GOPATH/src/github.com/hezhizheng/go-wxpush 9 | 10 | # 复制时区数据库文件 11 | COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 12 | 13 | COPY . $GOPATH/src/github.com/hezhizheng/go-wxpush 14 | 15 | ENTRYPOINT ["./go-wxpush_linux_amd64"] 16 | # 默认参数 17 | CMD ["-port", "5566", "-appid", "","-secret","","-userid","","-template_id","","-base_url","https://push.hzz.cool/detail","-title","","-content","","-tz",""] -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) DexterHo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go-WXPush - 微信消息推送服务 (基于golang) 2 | 3 | 这是一个基于 golang 开发的微信测试公众号模板消息推送服务。它提供了一个简单的 API 接口,让您可以轻松地通过 HTTP 请求将消息推送到指定的微信用户。感谢[frankiejun/wxpush](https://github.com/frankiejun/wxpush) 4 | 5 |

6 | 7 |

8 | 9 | ## ✨ 特性 10 | 11 | ✅ 完全免费,下载即使用 12 | ✅ 支持 Docker 一键部署 13 | ✅ 每天 10 万次额度,个人用不完 14 | ✅ 真正的微信原生弹窗 + 声音提醒 15 | ✅ 支持多用户 16 | ✅ 提供免费服务[https://push.hzz.cool](https://push.hzz.cool)(请勿滥用) 17 | ✅ 跳转稳定,自带消息详情页面 (默认使用[https://push.hzz.cool/detail](https://push.hzz.cool/detail), 可自己部署后使用参数替换) 18 | ✅ 可无限换皮肤 (使用项目[wxpushSkin](https://github.com/frankiejun/wxpushSkin)) 19 | 20 | ## ⚠️ 部署条件 21 | 22 | - [微信公众平台接口测试帐号申请](https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login) 23 | ![wx1.png](img/wx1.png) 24 | - 获取appid 、appsecret 25 | ![wx2.png](img/wx2.png) 26 | - 关注测试公众号,获取userid(微信号),新增测试模板(注意模版内容填写格式 `内容: {{content.DATA}}`) 获取template_id(模板ID) 27 | ![wx3.png](img/wx3.png) 28 | - 将以上获取到的参数代入下面使用即可 29 | ![wx3.png](img/w0.jpg) 30 | ![wx3.png](img/w1.jpg) 31 | 32 | ## 🚀 部署指南 33 | 34 | ### [下载编译好的文件启动](https://github.com/hezhizheng/go-wxpush/releases/) 35 | 36 | - 启动参数 37 | * 命令行启动参数(可不加,启动之后直接在url上拼接参数也可) `./go-wxpush_windows_amd64.exe -port "5566" -title "测试标题" -content "测试内容" -appid "xxx" -secret "xxx" -userid "xxx-k08" -template_id "xxx-Ks_PwGm--GSzllU" -base_url "https://push.hzz.cool"` 38 | * url请求参数(get) `与命令行参数名称一致` `/wxsend?appid=xxx&secret=xxx&userid=xxx-k08&template_id=xxx-Ks_PwGm--GSzllU&base_url=https://push.hzz.cool&content=保持微笑,代码无 bug!` 39 | 40 | ### 自行编译可执行文件(跨平台) 41 | 42 | ``` 43 | # 用法参考 https://github.com/mitchellh/gox 44 | # 生成文件可直接执行 45 | gox -osarch="windows/amd64" -ldflags "-s -w" -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" 46 | gox -osarch="darwin/amd64" -ldflags "-s -w" -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" 47 | gox -osarch="linux/amd64" -ldflags "-s -w" -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" 48 | ``` 49 | 50 | ### 🐳 Docker 启动 51 | - 将编译好的文件放在与 Dockerfile 同目录 52 | - 构建镜像 53 | ``` 54 | docker build -t go-wxpush:v2 . 55 | ``` 56 | - 启动镜像,参数与命令行保持一致 57 | ``` 58 | docker run -d -p 5566:5566 --name go-wxpush0 go-wxpush:v2 \ 59 | -port "5566" \ 60 | -title "测试标题" \ 61 | -content "测试内容" \ 62 | -appid "xxx" \ 63 | -secret "xxx" \ 64 | -userid "xxx-k08" \ 65 | -template_id "xxx-Ks_PwGm--GSzllU" 66 | ``` 67 | 68 | ### 🐳 Docker 一键部署 69 | ``` 70 | # 重新部署请先拉一遍最新的镜像 71 | docker pull hezhizheng/go-wxpush:v3 72 | # 参数格式与终端启动保持一致, 替换成实际值即可 73 | docker run -it -d -p 5566:5566 --init --name go-wxpush3 hezhizheng/go-wxpush:v3 \ 74 | -port "5566" \ 75 | -title "测试标题5566" \ 76 | -content "测试内容5566" \ 77 | -appid "xxx" \ 78 | -secret "xxx" \ 79 | -userid "xxx-k08" \ 80 | -template_id "xxx-Ks_PwGm--GSzllU" \ 81 | -tz "Asia/Shanghai" 82 | ``` 83 | 84 | ## 🗭 默认消息详情页 85 | 86 | 服务启动成功后会自带消息详情页界面(即消息模板跳转的页面),访问地址 `http://127.0.0.1:5566/detail` ,如有公网地址,可设置base_url参数为对应的host即可(无需加/detail)。 87 | ![wx3.png](img/msg.png) 88 | 89 | ## ⚙️ API 使用方法 90 | 91 | 服务部署成功后,您可以通过构造 URL 发起 `GET` 请求来推送消息。 92 | 93 | ### 请求地址 94 | 95 | ``` 96 | http://127.0.0.1:5566/wxsend 97 | ``` 98 | 99 | ### 请求参数 100 | 101 | | 参数名 | 类型 | 是否必填 | 描述 | 102 | |---------------|--------|------|----------------------| 103 | | `port` | String | 否 | 指定启动端口(仅针对命令行) | 104 | | `title` | String | 是 | 消息的标题。 | 105 | | `content` | String | 是 | 消息的具体内容。 | 106 | | `appid` | String | 是 | 临时覆盖默认的微信 AppID。 | 107 | | `secret` | String | 是 | 临时覆盖默认的微信 AppSecret。 | 108 | | `userid` | String | 是 | 临时覆盖默认的接收用户 OpenID。 | 109 | | `template_id` | String | 是 | 临时覆盖默认的模板消息 ID。 | 110 | | `base_url` | String | 否 | 临时覆盖默认的跳转 URL。 | 111 | | `tz` | String | 否 | 时区(默认东八区) | 112 | 113 | ### 使用示例 114 | 115 | **基础推送** 116 | 117 | 向默认配置的所有用户推送一条消息: 118 | 119 | ``` 120 | http://127.0.0.1:5566/wxsend?title=服务器通知&content=服务已于北京时间%2022:00%20重启 121 | ``` 122 | 123 | **临时覆盖用户** 124 | 125 | 向一个临时指定的用户推送消息: 126 | 127 | ``` 128 | http://127.0.0.1:5566/wxsend?title=私人提醒&content=记得带钥匙&userid=temporary_openid_here 129 | ``` 130 | 131 | ### Webhook / POST 请求 132 | 133 | 除了 `GET` 请求,服务也支持 `POST` 方法,更适合用于自动化的 Webhook 集成。 134 | 135 | **请求地址** 136 | 137 | ``` 138 | http://127.0.0.1:5566/wxsend 139 | ``` 140 | 141 | **请求方法** 142 | 143 | ``` 144 | POST 145 | ``` 146 | 147 | **请求头 (Headers)** 148 | 149 | ```json 150 | { 151 | "Content-Type": "application/json" 152 | } 153 | ``` 154 | 155 | **请求体 (Body)** 156 | 157 | 请求体需要是一个 JSON 对象,包含与 `GET` 请求相同的参数。 158 | 159 | ```json 160 | { 161 | "title": "Webhook 通知", 162 | "content": "这是一个通过 POST 请求发送的 Webhook 消息。" 163 | } 164 | ``` 165 | 166 | **使用示例 (cURL)** 167 | 168 | ```bash 169 | curl --location --request POST 'http://127.0.0.1:5566/wxsend' \ 170 | --data-raw '{ 171 | "title": "来自 cURL 的消息", 172 | "content": "自动化任务已完成。" 173 | }' 174 | ``` 175 | 176 | ### 成功响应 177 | 178 | 如果消息成功发送给至少一个用户,服务会返回 `"errcode": 0` 状态码。 179 | 180 | ### 失败响应 181 | 182 | 如果发生错误(如 token 错误、缺少参数、微信接口调用失败等),服务会返回相应的状态码和错误信息。 183 | 184 | ## 🤝 贡献 185 | 186 | 欢迎任何形式的贡献!如果您有好的想法或发现了 Bug,请随时提交 Pull Request 或创建 Issue。 187 | 188 | ## 📜 许可证 189 | 190 | 本项目采用 [MIT License](./LICENSE.txt) 开源许可证。 191 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "embed" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // 请求参数结构体 17 | type RequestParams struct { 18 | Title string `json:"title" form:"title"` 19 | Content string `json:"content" form:"content"` 20 | AppID string `json:"appid" form:"appid"` 21 | Secret string `json:"secret" form:"secret"` 22 | UserID string `json:"userid" form:"userid"` 23 | TemplateID string `json:"template_id" form:"template_id"` 24 | BaseURL string `json:"base_url" form:"base_url"` 25 | Timezone string `json:"tz" form:"tz"` 26 | } 27 | 28 | // 全局变量用于存储命令行参数 29 | var ( 30 | cliTitle string 31 | cliContent string 32 | cliAppID string 33 | cliSecret string 34 | cliUserID string 35 | cliTemplateID string 36 | cliBaseURL string 37 | startPort string 38 | cliTimezone string 39 | ) 40 | 41 | // 微信AccessToken响应 42 | type AccessTokenResponse struct { 43 | AccessToken string `json:"access_token"` 44 | ExpiresIn int `json:"expires_in"` 45 | } 46 | 47 | // 微信模板消息请求 48 | type TemplateMessageRequest struct { 49 | ToUser string `json:"touser"` 50 | TemplateID string `json:"template_id"` 51 | URL string `json:"url"` 52 | Data map[string]interface{} `json:"data"` 53 | } 54 | 55 | // 微信API响应 56 | type WechatAPIResponse struct { 57 | Errcode int `json:"errcode"` 58 | Errmsg string `json:"errmsg"` 59 | } 60 | 61 | func main() { 62 | // 定义命令行参数 63 | flag.StringVar(&cliTitle, "title", "", "消息标题") 64 | flag.StringVar(&cliContent, "content", "", "消息内容") 65 | flag.StringVar(&cliAppID, "appid", "", "AppID") 66 | flag.StringVar(&cliSecret, "secret", "", "AppSecret") 67 | flag.StringVar(&cliUserID, "userid", "", "openid") 68 | flag.StringVar(&cliTemplateID, "template_id", "", "模板ID") 69 | flag.StringVar(&cliBaseURL, "base_url", "", "跳转url") 70 | flag.StringVar(&cliTimezone, "tz", "Asia/Shanghai", "时区,默认东八区") 71 | flag.StringVar(&startPort, "port", "", "端口") 72 | 73 | // 解析命令行参数 74 | flag.Parse() 75 | 76 | // 设置路由 77 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 78 | w.WriteHeader(http.StatusOK) 79 | fmt.Fprintf(w, `go-wxpush is running...✅`) 80 | }) 81 | http.HandleFunc("/wxsend", handleWxSend) 82 | http.HandleFunc("/detail", handleDetail) 83 | 84 | // 启动服务器 85 | //fmt.Println("Server is running on port 5566...") 86 | port := "5566" 87 | if startPort != "" { 88 | port = startPort 89 | } 90 | fmt.Println("Server is running on: " + "http://127.0.0.1:" + port) 91 | 92 | err := http.ListenAndServe(":"+port, nil) 93 | 94 | if err != nil { 95 | fmt.Printf("Error starting server: %v\n", err) 96 | } 97 | 98 | } 99 | 100 | // 嵌入静态HTML文件 101 | // 102 | //go:embed msg_detail.html 103 | var htmlContent embed.FS 104 | 105 | // 处理详情页面请求 106 | func handleDetail(w http.ResponseWriter, r *http.Request) { 107 | // 从嵌入的资源中读取HTML内容 108 | htmlData, err := htmlContent.ReadFile("msg_detail.html") 109 | if err != nil { 110 | w.WriteHeader(http.StatusInternalServerError) 111 | fmt.Fprintf(w, `{"error": "Failed to read embedded HTML file: %v"}`, err) 112 | return 113 | } 114 | 115 | // 设置响应头 116 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 117 | 118 | // 返回HTML内容 119 | w.Write(htmlData) 120 | } 121 | 122 | func handleWxSend(w http.ResponseWriter, r *http.Request) { 123 | 124 | // 解析参数 125 | var params RequestParams 126 | 127 | // 根据请求方法解析参数 128 | if r.Method == "POST" { 129 | // 解析JSON请求体 130 | decoder := json.NewDecoder(r.Body) 131 | err := decoder.Decode(¶ms) 132 | if err != nil { 133 | w.WriteHeader(http.StatusBadRequest) 134 | fmt.Fprintf(w, `{"error": "Invalid JSON format: %v"}`, err) 135 | return 136 | } 137 | } else if r.Method == "GET" { 138 | // 解析GET查询参数 139 | params.Title = r.URL.Query().Get("title") 140 | params.Content = r.URL.Query().Get("content") 141 | params.AppID = r.URL.Query().Get("appid") 142 | params.Secret = r.URL.Query().Get("secret") 143 | params.UserID = r.URL.Query().Get("userid") 144 | params.TemplateID = r.URL.Query().Get("template_id") 145 | params.BaseURL = r.URL.Query().Get("base_url") 146 | params.Timezone = r.URL.Query().Get("tz") 147 | } else { 148 | w.WriteHeader(http.StatusMethodNotAllowed) 149 | fmt.Fprintf(w, `{"error": "Method not allowed"}`) 150 | return 151 | } 152 | 153 | // 只有当GET/POST参数为空时,才使用命令行参数 154 | if params.Title == "" && cliTitle != "" { 155 | params.Title = cliTitle 156 | } 157 | if params.Content == "" && cliContent != "" { 158 | params.Content = cliContent 159 | } 160 | if params.AppID == "" && cliAppID != "" { 161 | params.AppID = cliAppID 162 | } 163 | if params.Secret == "" && cliSecret != "" { 164 | params.Secret = cliSecret 165 | } 166 | if params.UserID == "" && cliUserID != "" { 167 | params.UserID = cliUserID 168 | } 169 | if params.TemplateID == "" && cliTemplateID != "" { 170 | params.TemplateID = cliTemplateID 171 | } 172 | if params.BaseURL == "" && cliBaseURL != "" { 173 | params.BaseURL = cliBaseURL 174 | } 175 | if params.Timezone == "" && cliTimezone != "" { 176 | params.Timezone = cliTimezone 177 | } 178 | 179 | // 验证必要参数 180 | if params.AppID == "" || params.Secret == "" || params.UserID == "" || params.TemplateID == "" { 181 | w.WriteHeader(http.StatusBadRequest) 182 | fmt.Fprintf(w, `{"error": "Missing required parameters"}`) 183 | return 184 | } 185 | if params.BaseURL == "" { 186 | params.BaseURL = "https://push.hzz.cool" 187 | } 188 | if params.Content == "" { 189 | params.Content = "测试内容" 190 | } 191 | if params.Title == "" { 192 | params.Title = "测试标题" 193 | } 194 | 195 | // 获取AccessToken 196 | token, err := getAccessToken(params.AppID, params.Secret) 197 | if err != nil { 198 | w.WriteHeader(http.StatusInternalServerError) 199 | fmt.Fprintf(w, `{"error": "Failed to get access token: %v"}`, err) 200 | return 201 | } 202 | 203 | //log.Println(token) 204 | // 发送模板消息 205 | resp, err := sendTemplateMessage(token, params) 206 | if err != nil { 207 | w.WriteHeader(http.StatusInternalServerError) 208 | fmt.Fprintf(w, `{"error": "Failed to send template message: %v"}`, err) 209 | return 210 | } 211 | 212 | // 返回结果 213 | json.NewEncoder(w).Encode(resp) 214 | } 215 | 216 | // Token请求参数结构体 217 | type TokenRequestParams struct { 218 | GrantType string `json:"grant_type"` 219 | AppID string `json:"appid"` 220 | Secret string `json:"secret"` 221 | ForceRefresh bool `json:"force_refresh"` 222 | } 223 | 224 | func getAccessToken(appid, secret string) (string, error) { 225 | // 构建请求参数 226 | requestParams := TokenRequestParams{ 227 | GrantType: "client_credential", 228 | AppID: appid, 229 | Secret: secret, 230 | ForceRefresh: false, 231 | } 232 | 233 | // 转换为JSON 234 | jsonData, err := json.Marshal(requestParams) 235 | if err != nil { 236 | return "", err 237 | } 238 | 239 | // 忽略证书验证 240 | client := &http.Client{ 241 | Transport: &http.Transport{ 242 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 243 | }, 244 | } 245 | 246 | // 发送POST请求 247 | resp, err := client.Post("https://api.weixin.qq.com/cgi-bin/stable_token", "application/json", strings.NewReader(string(jsonData))) 248 | if err != nil { 249 | return "", err 250 | } 251 | defer resp.Body.Close() 252 | 253 | // 读取响应 254 | body, err := io.ReadAll(resp.Body) 255 | if err != nil { 256 | return "", err 257 | } 258 | 259 | //log.Println(string(body)) 260 | 261 | // 解析响应 262 | var tokenResp AccessTokenResponse 263 | err = json.Unmarshal(body, &tokenResp) 264 | //log.Println(tokenResp) 265 | 266 | if err != nil { 267 | return "", err 268 | } 269 | 270 | return tokenResp.AccessToken, nil 271 | } 272 | 273 | func sendTemplateMessage(accessToken string, params RequestParams) (WechatAPIResponse, error) { 274 | // 构建请求URL 275 | apiUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", accessToken) 276 | 277 | // 处理时区,默认东八区 278 | location, err := time.LoadLocation("Asia/Shanghai") 279 | if err != nil { 280 | location, _ = time.LoadLocation("Asia/Shanghai") // 确保默认使用东八区 281 | } 282 | 283 | // 如果参数中有时区,则尝试使用该时区 284 | if params.Timezone != "" { 285 | loc, err := time.LoadLocation(params.Timezone) 286 | if err == nil { 287 | location = loc 288 | } 289 | } 290 | 291 | // 获取当前时间 292 | currentTime := time.Now().In(location) 293 | timeStr := currentTime.Format("2006-01-02 15:04:05") 294 | 295 | // 构建请求数据 296 | requestData := TemplateMessageRequest{ 297 | ToUser: params.UserID, 298 | TemplateID: params.TemplateID, 299 | URL: params.BaseURL + `/detail?title=` + url.QueryEscape(params.Title) + `&message=` + url.QueryEscape(params.Content) + `&date=` + url.QueryEscape(timeStr), 300 | Data: map[string]interface{}{ 301 | "title": map[string]string{ 302 | "value": params.Title, 303 | }, 304 | "content": map[string]string{ 305 | "value": params.Content, 306 | }, 307 | }, 308 | } 309 | 310 | // 转换为JSON 311 | jsonData, err := json.Marshal(requestData) 312 | if err != nil { 313 | return WechatAPIResponse{}, err 314 | } 315 | 316 | // 忽略证书验证 317 | client := &http.Client{ 318 | Transport: &http.Transport{ 319 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 320 | }, 321 | } 322 | 323 | // 发送POST请求 324 | resp, err := client.Post(apiUrl, "application/json", strings.NewReader(string(jsonData))) 325 | if err != nil { 326 | return WechatAPIResponse{}, err 327 | } 328 | defer resp.Body.Close() 329 | 330 | // 读取响应 331 | body, err := io.ReadAll(resp.Body) 332 | if err != nil { 333 | return WechatAPIResponse{}, err 334 | } 335 | 336 | // 解析响应 337 | var apiResp WechatAPIResponse 338 | err = json.Unmarshal(body, &apiResp) 339 | if err != nil { 340 | return WechatAPIResponse{}, err 341 | } 342 | 343 | return apiResp, nil 344 | } 345 | -------------------------------------------------------------------------------- /msg_detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 消息推送 7 | 303 | 304 | 305 |
306 | 307 |
308 |
消息推送
309 | 310 |
311 |
通知内容
312 |
无告警信息
313 |
314 | 315 |
316 |
时间
317 |
无时间信息
318 |
319 |
320 | 321 | 387 | 388 | --------------------------------------------------------------------------------