Ethscriptions commited on
Commit
6b5ca20
·
verified ·
1 Parent(s): 9caa877

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +23 -0
  2. README.md +191 -10
  3. go.mod +1 -0
  4. main.go +345 -0
  5. msg_detail.html +388 -0
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 第一阶段:构建环境
2
+ FROM golang:1.21-alpine AS builder
3
+ WORKDIR /app
4
+ # 复制所有文件
5
+ COPY . .
6
+ # 编译可执行文件
7
+ RUN go build -o go-wxpush main.go
8
+
9
+ # 第二阶段:运行环境
10
+ FROM alpine:latest
11
+ WORKDIR /app
12
+ # 安装时区数据和基础证书(微信 API 需要 HTTPS)
13
+ RUN apk --no-cache add ca-certificates tzdata
14
+ # 从构建阶段复制二进制文件
15
+ COPY --from=builder /app/go-wxpush .
16
+
17
+ # Hugging Face Spaces 默认使用 7860 端口
18
+ EXPOSE 7860
19
+
20
+ # 启动命令:将端口固定为 7860
21
+ # 注意:命令行参数可以通过 HF 的环境变量或在此处指定
22
+ ENTRYPOINT ["./go-wxpush"]
23
+ CMD ["-port", "7860"]
README.md CHANGED
@@ -1,10 +1,191 @@
1
- ---
2
- title: Tz
3
- emoji: 💻
4
- colorFrom: red
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Go-WXPush - 微信消息推送服务 (基于golang)
2
+
3
+ 这是一个基于 golang 开发的微信测试公众号模板消息推送服务。它提供了一个简单的 API 接口,让您可以轻松地通过 HTTP 请求将消息推送到指定的微信用户。感谢[frankiejun/wxpush](https://github.com/frankiejun/wxpush)
4
+
5
+ <p align="center">
6
+ <img src="./img/logo.png">
7
+ </p>
8
+
9
+ ## ✨ 特性
10
+
11
+ ✅ 完全免费,下载即使用
12
+ ✅ 支持 Docker 一键部署(镜像容器大小仅2MB)
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
+ gox -osarch="linux/arm64" -ldflags "-s -w" -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}"
49
+ ```
50
+
51
+ ### 🐳 Docker 启动
52
+ - 将编译好的文件放在与 Dockerfile 同目录
53
+ - 构建镜像
54
+ ```
55
+ docker build -t go-wxpush:v2 .
56
+ ```
57
+ - 启动镜像,参数与命令行保持一致
58
+ ```
59
+ docker run -d -p 5566:5566 --name go-wxpush0 go-wxpush:v2 \
60
+ -port "5566" \
61
+ -title "测试标题" \
62
+ -content "测试内容" \
63
+ -appid "xxx" \
64
+ -secret "xxx" \
65
+ -userid "xxx-k08" \
66
+ -template_id "xxx-Ks_PwGm--GSzllU"
67
+ ```
68
+
69
+ ### 🐳 Docker 一键部署
70
+ ```
71
+ # 重新部署请先拉一遍最新的镜像
72
+ docker pull hezhizheng/go-wxpush:v4
73
+ # 参数格式与终端启动保持一致, 替换成实际值即可
74
+ docker run -it -d -p 5566:5566 --init --name go-wxpush4 hezhizheng/go-wxpush:v4 \
75
+ -port "5566" \
76
+ -title "测试标题5566" \
77
+ -content "测试内容5566" \
78
+ -appid "xxx" \
79
+ -secret "xxx" \
80
+ -userid "xxx-k08" \
81
+ -template_id "xxx-Ks_PwGm--GSzllU" \
82
+ -tz "Asia/Shanghai"
83
+ ```
84
+
85
+ ## 🗭 默认消息详情页
86
+
87
+ 服务启动成功后会自带消息详情页界面(即消息模板跳转的页面),访问地址 `http://127.0.0.1:5566/detail` ,如有公网地址,可设置base_url参数为对应的host即可(无需加/detail)。
88
+ ![wx3.png](img/msg.png)
89
+
90
+ ## ⚙️ API 使用方法
91
+
92
+ 服务部署成功后,您可以通过构造 URL 发起 `GET` 请求来推送消息。
93
+
94
+ ### 请求地址
95
+
96
+ ```
97
+ http://127.0.0.1:5566/wxsend
98
+ ```
99
+
100
+ ### 请求参数
101
+
102
+ | 参数名 | 类型 | 是否必填 | 描述 |
103
+ |---------------|--------|------|----------------------|
104
+ | `port` | String | 否 | 指定启动端口(仅针对命令行) |
105
+ | `title` | String | 是 | 消息的标题。 |
106
+ | `content` | String | 是 | 消息的具体内容。 |
107
+ | `appid` | String | 是 | 临时覆盖默认的微信 AppID。 |
108
+ | `secret` | String | 是 | 临时覆盖默认的微信 AppSecret。 |
109
+ | `userid` | String | 是 | 临时覆盖默认的接收用户 OpenID。 |
110
+ | `template_id` | String | 是 | 临时覆盖默认的模板消息 ID。 |
111
+ | `base_url` | String | 否 | 临时覆盖默认的跳转 URL。 |
112
+ | `tz` | String | 否 | 时区(默认东八区) |
113
+
114
+ ### ���用示例
115
+
116
+ **基础推送**
117
+
118
+ 向默认配置的所有用户推送一条消息:
119
+
120
+ ```
121
+ http://127.0.0.1:5566/wxsend?title=服务器通知&content=服务已于北京时间%2022:00%20重启
122
+ ```
123
+
124
+ **临时覆盖用户**
125
+
126
+ 向一个临时指定的用户推送消息:
127
+
128
+ ```
129
+ http://127.0.0.1:5566/wxsend?title=私人提醒&content=记得带钥匙&userid=temporary_openid_here
130
+ ```
131
+
132
+ ### Webhook / POST 请求
133
+
134
+ 除了 `GET` 请求,服务也支持 `POST` 方法,更适合用于自动化的 Webhook 集成。
135
+
136
+ **请求地址**
137
+
138
+ ```
139
+ http://127.0.0.1:5566/wxsend
140
+ ```
141
+
142
+ **请求方法**
143
+
144
+ ```
145
+ POST
146
+ ```
147
+
148
+ **请求头 (Headers)**
149
+
150
+ ```json
151
+ {
152
+ "Content-Type": "application/json"
153
+ }
154
+ ```
155
+
156
+ **请求体 (Body)**
157
+
158
+ 请求体需要是一个 JSON 对象,包含与 `GET` 请求相同的参数。
159
+
160
+ ```json
161
+ {
162
+ "title": "Webhook 通知",
163
+ "content": "这是一个通过 POST 请求发送的 Webhook 消息。"
164
+ }
165
+ ```
166
+
167
+ **使用示例 (cURL)**
168
+
169
+ ```bash
170
+ curl --location --request POST 'http://127.0.0.1:5566/wxsend' \
171
+ --data-raw '{
172
+ "title": "来自 cURL 的消息",
173
+ "content": "自动化任务已完成。"
174
+ }'
175
+ ```
176
+
177
+ ### 成功响应
178
+
179
+ 如果消息成功发送给至少一个用户,服务会返回 `"errcode": 0` 状态码。
180
+
181
+ ### 失败响应
182
+
183
+ 如果发生错误(如 token 错误、缺少参数、微信接口调用失败等),服务会返回相应的状态码和错误信息。
184
+
185
+ ## 🤝 贡献
186
+
187
+ 欢迎任何形式的贡献!如果您有好的想法或发现了 Bug,请随时提交 Pull Request 或创建 Issue。
188
+
189
+ ## 📜 许可证
190
+
191
+ 本项目采用 [MIT License](./LICENSE.txt) 开源许可证。
go.mod ADDED
@@ -0,0 +1 @@
 
 
1
+ module go-wxpush
main.go ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ _ "time/tzdata"
15
+ )
16
+
17
+ // 请求参数结构体
18
+ type RequestParams struct {
19
+ Title string `json:"title" form:"title"`
20
+ Content string `json:"content" form:"content"`
21
+ AppID string `json:"appid" form:"appid"`
22
+ Secret string `json:"secret" form:"secret"`
23
+ UserID string `json:"userid" form:"userid"`
24
+ TemplateID string `json:"template_id" form:"template_id"`
25
+ BaseURL string `json:"base_url" form:"base_url"`
26
+ Timezone string `json:"tz" form:"tz"`
27
+ }
28
+
29
+ // 全局变量用于存储命令行参数
30
+ var (
31
+ cliTitle string
32
+ cliContent string
33
+ cliAppID string
34
+ cliSecret string
35
+ cliUserID string
36
+ cliTemplateID string
37
+ cliBaseURL string
38
+ startPort string
39
+ cliTimezone string
40
+ )
41
+
42
+ // 微信AccessToken响应
43
+ type AccessTokenResponse struct {
44
+ AccessToken string `json:"access_token"`
45
+ ExpiresIn int `json:"expires_in"`
46
+ }
47
+
48
+ // 微信模板消息请求
49
+ type TemplateMessageRequest struct {
50
+ ToUser string `json:"touser"`
51
+ TemplateID string `json:"template_id"`
52
+ URL string `json:"url"`
53
+ Data map[string]interface{} `json:"data"`
54
+ }
55
+
56
+ // 微信API响应
57
+ type WechatAPIResponse struct {
58
+ Errcode int `json:"errcode"`
59
+ Errmsg string `json:"errmsg"`
60
+ }
61
+
62
+ func main() {
63
+ // 定义命令行参数
64
+ flag.StringVar(&cliTitle, "title", "", "消息标题")
65
+ flag.StringVar(&cliContent, "content", "", "消息内容")
66
+ flag.StringVar(&cliAppID, "appid", "", "AppID")
67
+ flag.StringVar(&cliSecret, "secret", "", "AppSecret")
68
+ flag.StringVar(&cliUserID, "userid", "", "openid")
69
+ flag.StringVar(&cliTemplateID, "template_id", "", "模板ID")
70
+ flag.StringVar(&cliBaseURL, "base_url", "", "跳转url")
71
+ flag.StringVar(&cliTimezone, "tz", "Asia/Shanghai", "时区,默认东八区")
72
+ flag.StringVar(&startPort, "port", "", "端口")
73
+
74
+ // 解析命令行参数
75
+ flag.Parse()
76
+
77
+ // 设置路由
78
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
79
+ w.WriteHeader(http.StatusOK)
80
+ fmt.Fprintf(w, `go-wxpush is running...✅`)
81
+ })
82
+ http.HandleFunc("/wxsend", handleWxSend)
83
+ http.HandleFunc("/detail", handleDetail)
84
+
85
+ // 启动服务器
86
+ //fmt.Println("Server is running on port 5566...")
87
+ port := "5566"
88
+ if startPort != "" {
89
+ port = startPort
90
+ }
91
+ fmt.Println("Server is running on: " + "http://127.0.0.1:" + port)
92
+
93
+ err := http.ListenAndServe(":"+port, nil)
94
+
95
+ if err != nil {
96
+ fmt.Printf("Error starting server: %v\n", err)
97
+ }
98
+
99
+ }
100
+
101
+ // 嵌入静态HTML文件
102
+ //
103
+ //go:embed msg_detail.html
104
+ var htmlContent embed.FS
105
+
106
+ // 处理详情页面请求
107
+ func handleDetail(w http.ResponseWriter, r *http.Request) {
108
+ // 从嵌入的资源中读取HTML内容
109
+ htmlData, err := htmlContent.ReadFile("msg_detail.html")
110
+ if err != nil {
111
+ w.WriteHeader(http.StatusInternalServerError)
112
+ fmt.Fprintf(w, `{"error": "Failed to read embedded HTML file: %v"}`, err)
113
+ return
114
+ }
115
+
116
+ // 设置响应头
117
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
118
+
119
+ // 返回HTML内容
120
+ w.Write(htmlData)
121
+ }
122
+
123
+ func handleWxSend(w http.ResponseWriter, r *http.Request) {
124
+
125
+ // 解析参数
126
+ var params RequestParams
127
+
128
+ // 根据请求方法解析参数
129
+ if r.Method == "POST" {
130
+ // 解析JSON请求体
131
+ decoder := json.NewDecoder(r.Body)
132
+ err := decoder.Decode(&params)
133
+ if err != nil {
134
+ w.WriteHeader(http.StatusBadRequest)
135
+ fmt.Fprintf(w, `{"error": "Invalid JSON format: %v"}`, err)
136
+ return
137
+ }
138
+ } else if r.Method == "GET" {
139
+ // 解析GET查询参数
140
+ params.Title = r.URL.Query().Get("title")
141
+ params.Content = r.URL.Query().Get("content")
142
+ params.AppID = r.URL.Query().Get("appid")
143
+ params.Secret = r.URL.Query().Get("secret")
144
+ params.UserID = r.URL.Query().Get("userid")
145
+ params.TemplateID = r.URL.Query().Get("template_id")
146
+ params.BaseURL = r.URL.Query().Get("base_url")
147
+ params.Timezone = r.URL.Query().Get("tz")
148
+ } else {
149
+ w.WriteHeader(http.StatusMethodNotAllowed)
150
+ fmt.Fprintf(w, `{"error": "Method not allowed"}`)
151
+ return
152
+ }
153
+
154
+ // 只有当GET/POST参数为空时,才使用命令行参数
155
+ if params.Title == "" && cliTitle != "" {
156
+ params.Title = cliTitle
157
+ }
158
+ if params.Content == "" && cliContent != "" {
159
+ params.Content = cliContent
160
+ }
161
+ if params.AppID == "" && cliAppID != "" {
162
+ params.AppID = cliAppID
163
+ }
164
+ if params.Secret == "" && cliSecret != "" {
165
+ params.Secret = cliSecret
166
+ }
167
+ if params.UserID == "" && cliUserID != "" {
168
+ params.UserID = cliUserID
169
+ }
170
+ if params.TemplateID == "" && cliTemplateID != "" {
171
+ params.TemplateID = cliTemplateID
172
+ }
173
+ if params.BaseURL == "" && cliBaseURL != "" {
174
+ params.BaseURL = cliBaseURL
175
+ }
176
+ if params.Timezone == "" && cliTimezone != "" {
177
+ params.Timezone = cliTimezone
178
+ }
179
+
180
+ // 验证必要参数
181
+ if params.AppID == "" || params.Secret == "" || params.UserID == "" || params.TemplateID == "" {
182
+ w.WriteHeader(http.StatusBadRequest)
183
+ fmt.Fprintf(w, `{"error": "Missing required parameters"}`)
184
+ return
185
+ }
186
+ if params.BaseURL == "" {
187
+ params.BaseURL = "https://push.hzz.cool"
188
+ }
189
+ if params.Content == "" {
190
+ params.Content = "测试内容"
191
+ }
192
+ if params.Title == "" {
193
+ params.Title = "测试标题"
194
+ }
195
+
196
+ // 获取AccessToken
197
+ token, err := getAccessToken(params.AppID, params.Secret)
198
+ if err != nil {
199
+ w.WriteHeader(http.StatusInternalServerError)
200
+ fmt.Fprintf(w, `{"error": "Failed to get access token: %v"}`, err)
201
+ return
202
+ }
203
+
204
+ //log.Println(token)
205
+ // 发送模板消息
206
+ resp, err := sendTemplateMessage(token, params)
207
+ if err != nil {
208
+ w.WriteHeader(http.StatusInternalServerError)
209
+ fmt.Fprintf(w, `{"error": "Failed to send template message: %v"}`, err)
210
+ return
211
+ }
212
+
213
+ // 返回结果
214
+ json.NewEncoder(w).Encode(resp)
215
+ }
216
+
217
+ // Token请求参数结构体
218
+ type TokenRequestParams struct {
219
+ GrantType string `json:"grant_type"`
220
+ AppID string `json:"appid"`
221
+ Secret string `json:"secret"`
222
+ ForceRefresh bool `json:"force_refresh"`
223
+ }
224
+
225
+ func getAccessToken(appid, secret string) (string, error) {
226
+ // 构建请求参数
227
+ requestParams := TokenRequestParams{
228
+ GrantType: "client_credential",
229
+ AppID: appid,
230
+ Secret: secret,
231
+ ForceRefresh: false,
232
+ }
233
+
234
+ // 转换为JSON
235
+ jsonData, err := json.Marshal(requestParams)
236
+ if err != nil {
237
+ return "", err
238
+ }
239
+
240
+ // 忽略证书验证
241
+ client := &http.Client{
242
+ Transport: &http.Transport{
243
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
244
+ },
245
+ }
246
+
247
+ // 发送POST请求
248
+ resp, err := client.Post("https://api.weixin.qq.com/cgi-bin/stable_token", "application/json", strings.NewReader(string(jsonData)))
249
+ if err != nil {
250
+ return "", err
251
+ }
252
+ defer resp.Body.Close()
253
+
254
+ // 读取响应
255
+ body, err := io.ReadAll(resp.Body)
256
+ if err != nil {
257
+ return "", err
258
+ }
259
+
260
+ //log.Println(string(body))
261
+
262
+ // 解析响应
263
+ var tokenResp AccessTokenResponse
264
+ err = json.Unmarshal(body, &tokenResp)
265
+ //log.Println(tokenResp)
266
+
267
+ if err != nil {
268
+ return "", err
269
+ }
270
+
271
+ return tokenResp.AccessToken, nil
272
+ }
273
+
274
+ func sendTemplateMessage(accessToken string, params RequestParams) (WechatAPIResponse, error) {
275
+ // 构建请求URL
276
+ apiUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", accessToken)
277
+
278
+ // 处理时区,默认东八区
279
+ location, err := time.LoadLocation("Asia/Shanghai")
280
+ if err != nil {
281
+ location, _ = time.LoadLocation("Asia/Shanghai") // 确保默认使用东八区
282
+ }
283
+
284
+ // 如果参数中有时区,则尝试使用该时区
285
+ if params.Timezone != "" {
286
+ loc, err := time.LoadLocation(params.Timezone)
287
+ if err == nil {
288
+ location = loc
289
+ }
290
+ }
291
+
292
+ // 获取当前时间
293
+ currentTime := time.Now().In(location)
294
+ timeStr := currentTime.Format("2006-01-02 15:04:05")
295
+
296
+ // 构建请求数据
297
+ requestData := TemplateMessageRequest{
298
+ ToUser: params.UserID,
299
+ TemplateID: params.TemplateID,
300
+ URL: params.BaseURL + `/detail?title=` + url.QueryEscape(params.Title) + `&message=` + url.QueryEscape(params.Content) + `&date=` + url.QueryEscape(timeStr),
301
+ Data: map[string]interface{}{
302
+ "title": map[string]string{
303
+ "value": params.Title,
304
+ },
305
+ "content": map[string]string{
306
+ "value": params.Content,
307
+ },
308
+ },
309
+ }
310
+
311
+ // 转换为JSON
312
+ jsonData, err := json.Marshal(requestData)
313
+ if err != nil {
314
+ return WechatAPIResponse{}, err
315
+ }
316
+
317
+ // 忽略证书验证
318
+ client := &http.Client{
319
+ Transport: &http.Transport{
320
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
321
+ },
322
+ }
323
+
324
+ // 发送POST请求
325
+ resp, err := client.Post(apiUrl, "application/json", strings.NewReader(string(jsonData)))
326
+ if err != nil {
327
+ return WechatAPIResponse{}, err
328
+ }
329
+ defer resp.Body.Close()
330
+
331
+ // 读取响应
332
+ body, err := io.ReadAll(resp.Body)
333
+ if err != nil {
334
+ return WechatAPIResponse{}, err
335
+ }
336
+
337
+ // 解析响应
338
+ var apiResp WechatAPIResponse
339
+ err = json.Unmarshal(body, &apiResp)
340
+ if err != nil {
341
+ return WechatAPIResponse{}, err
342
+ }
343
+
344
+ return apiResp, nil
345
+ }
msg_detail.html ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>消息推送</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
13
+ }
14
+
15
+ body {
16
+ background: linear-gradient(135deg, #0c0c2e 0%, #1a1a3e 100%);
17
+ color: #e0f7fa;
18
+ min-height: 100vh;
19
+ display: flex;
20
+ justify-content: center;
21
+ align-items: center;
22
+ padding: 20px;
23
+ overflow-x: hidden;
24
+ position: relative;
25
+ }
26
+
27
+ /* 动态背景效果 */
28
+ body::before {
29
+ content: '';
30
+ position: absolute;
31
+ top: 0;
32
+ left: 0;
33
+ width: 100%;
34
+ height: 100%;
35
+ background:
36
+ radial-gradient(circle at 20% 30%, rgba(0, 150, 136, 0.15) 0%, transparent 50%),
37
+ radial-gradient(circle at 80% 70%, rgba(0, 188, 212, 0.15) 0%, transparent 50%);
38
+ z-index: -1;
39
+ }
40
+
41
+ .container {
42
+ max-width: 800px;
43
+ width: 100%;
44
+ background: rgba(18, 18, 40, 0.85);
45
+ backdrop-filter: blur(10px);
46
+ border-radius: 16px;
47
+ padding: 40px;
48
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5),
49
+ 0 0 0 1px rgba(0, 150, 136, 0.2),
50
+ 0 0 20px rgba(0, 188, 212, 0.3);
51
+ position: relative;
52
+ overflow: hidden;
53
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
54
+ }
55
+
56
+ .container:hover {
57
+ transform: translateY(-5px);
58
+ box-shadow: 0 15px 35px rgba(0, 0, 0, 0.6),
59
+ 0 0 0 1px rgba(0, 150, 136, 0.4),
60
+ 0 0 30px rgba(0, 188, 212, 0.5);
61
+ }
62
+
63
+ .container::before {
64
+ content: '';
65
+ position: absolute;
66
+ top: 0;
67
+ left: 0;
68
+ width: 100%;
69
+ height: 4px;
70
+ background: linear-gradient(90deg, #00bcd4, #009688);
71
+ }
72
+
73
+ .title {
74
+ text-align: center;
75
+ margin-bottom: 40px;
76
+ font-size: 2.2rem;
77
+ font-weight: 300;
78
+ letter-spacing: 2px;
79
+ color: #00bcd4;
80
+ position: relative;
81
+ padding-bottom: 15px;
82
+ }
83
+
84
+ .title::after {
85
+ content: '';
86
+ position: absolute;
87
+ bottom: 0;
88
+ left: 50%;
89
+ transform: translateX(-50%);
90
+ width: 100px;
91
+ height: 2px;
92
+ background: linear-gradient(90deg, transparent, #00bcd4, transparent);
93
+ }
94
+
95
+ .info-card {
96
+ background: rgba(30, 30, 60, 0.7);
97
+ border-radius: 12px;
98
+ padding: 25px;
99
+ margin-bottom: 25px;
100
+ border-left: 4px solid #00bcd4;
101
+ transition: all 0.3s ease;
102
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
103
+ }
104
+
105
+ .info-card:hover {
106
+ transform: translateX(5px);
107
+ background: rgba(40, 40, 70, 0.8);
108
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
109
+ }
110
+
111
+ .info-label {
112
+ font-size: 1.3rem;
113
+ color: #80deea;
114
+ margin-bottom: 10px;
115
+ display: flex;
116
+ align-items: center;
117
+ }
118
+
119
+ .info-label::before {
120
+ content: '';
121
+ display: inline-block;
122
+ width: 8px;
123
+ height: 8px;
124
+ border-radius: 50%;
125
+ background: #00bcd4;
126
+ margin-right: 10px;
127
+ }
128
+
129
+ .info-content {
130
+ font-size: 1.2rem;
131
+ color: #e0f7fa;
132
+ font-weight: 500;
133
+ word-break: break-word;
134
+ line-height: 1.6;
135
+ white-space: pre-line;
136
+ }
137
+
138
+ .pulse {
139
+ animation: pulse 2s infinite;
140
+ }
141
+
142
+ @keyframes pulse {
143
+ 0% {
144
+ box-shadow: 0 0 0 0 rgba(0, 188, 212, 0.4);
145
+ }
146
+ 70% {
147
+ box-shadow: 0 0 0 10px rgba(0, 188, 212, 0);
148
+ }
149
+ 100% {
150
+ box-shadow: 0 0 0 0 rgba(0, 188, 212, 0);
151
+ }
152
+ }
153
+
154
+ /* Markdown 样式覆盖 */
155
+ .info-content h1, .info-content h2, .info-content h3, .info-content h4, .info-content h5, .info-content h6 {
156
+ color: #00bcd4;
157
+ margin-top: 1em;
158
+ margin-bottom: 0.5em;
159
+ font-weight: 400;
160
+ }
161
+ .info-content p {
162
+ margin-bottom: 1em;
163
+ line-height: 1.6;
164
+ }
165
+ .info-content strong {
166
+ color: #e0f7fa;
167
+ font-weight: 600;
168
+ }
169
+ .info-content em {
170
+ color: #80deea;
171
+ font-style: italic;
172
+ }
173
+ .info-content code {
174
+ background: rgba(0, 0, 0, 0.3);
175
+ color: #00bcd4;
176
+ padding: 2px 4px;
177
+ border-radius: 4px;
178
+ font-family: 'Courier New', monospace;
179
+ }
180
+ .info-content pre {
181
+ background: rgba(0, 0, 0, 0.4);
182
+ color: #e0f7fa;
183
+ padding: 1em;
184
+ border-radius: 8px;
185
+ overflow-x: auto;
186
+ margin-bottom: 1em;
187
+ }
188
+ .info-content blockquote {
189
+ border-left: 4px solid #009688;
190
+ margin: 1em 0;
191
+ padding-left: 1em;
192
+ color: #80deea;
193
+ font-style: italic;
194
+ }
195
+ .info-content ul, .info-content ol {
196
+ margin-bottom: 1em;
197
+ padding-left: 2em;
198
+ }
199
+ .info-content li {
200
+ margin-bottom: 0.5em;
201
+ }
202
+ .info-content a {
203
+ color: #00bcd4;
204
+ text-decoration: none;
205
+ }
206
+ .info-content a:hover {
207
+ text-decoration: underline;
208
+ }
209
+ .info-content table {
210
+ width: 100%;
211
+ border-collapse: collapse;
212
+ margin-bottom: 1em;
213
+ background: rgba(0, 0, 0, 0.2);
214
+ border-radius: 8px;
215
+ overflow: hidden;
216
+ }
217
+ .info-content th, .info-content td {
218
+ padding: 0.75em;
219
+ text-align: left;
220
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
221
+ }
222
+ .info-content th {
223
+ background: rgba(0, 188, 212, 0.2);
224
+ color: #00bcd4;
225
+ }
226
+
227
+ /* 响应式设计 */
228
+ @media (max-width: 768px) {
229
+ .container {
230
+ padding: 25px;
231
+ }
232
+
233
+ .title {
234
+ font-size: 1.9rem;
235
+ }
236
+
237
+ .info-content {
238
+ font-size: 1.1rem;
239
+ }
240
+
241
+ .info-label {
242
+ font-size: 1.2rem;
243
+ }
244
+ }
245
+
246
+ @media (max-width: 480px) {
247
+ .container {
248
+ padding: 20px;
249
+ }
250
+
251
+ .title {
252
+ font-size: 1.6rem;
253
+ }
254
+
255
+ .info-content {
256
+ font-size: 1rem;
257
+ }
258
+
259
+ .info-card {
260
+ padding: 20px;
261
+ }
262
+
263
+ .info-label {
264
+ font-size: 1.1rem;
265
+ }
266
+ }
267
+
268
+ /* 动态粒子背景 */
269
+ .particles {
270
+ position: absolute;
271
+ top: 0;
272
+ left: 0;
273
+ width: 100%;
274
+ height: 100%;
275
+ z-index: -1;
276
+ overflow: hidden;
277
+ }
278
+
279
+ .particle {
280
+ position: absolute;
281
+ background: rgba(0, 188, 212, 0.3);
282
+ border-radius: 50%;
283
+ animation: float 15s infinite linear;
284
+ }
285
+
286
+ @keyframes float {
287
+ 0% {
288
+ transform: translateY(0) translateX(0);
289
+ opacity: 0;
290
+ }
291
+ 10% {
292
+ opacity: 1;
293
+ }
294
+ 90% {
295
+ opacity: 1;
296
+ }
297
+ 100% {
298
+ transform: translateY(-100vh) translateX(100px);
299
+ opacity: 0;
300
+ }
301
+ }
302
+ </style>
303
+ </head>
304
+ <body>
305
+ <div class="particles" id="particles"></div>
306
+
307
+ <div class="container pulse">
308
+ <div class="title" id="title">消息推送</div>
309
+
310
+ <div class="info-card">
311
+ <div class="info-label">通知内容</div>
312
+ <div class="info-content" id="message">无告警信息</div>
313
+ </div>
314
+
315
+ <div class="info-card">
316
+ <div class="info-label">时间</div>
317
+ <div class="info-content" id="date">无时间信息</div>
318
+ </div>
319
+ </div>
320
+ <script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
321
+ <script>
322
+ // 从 URL 参数读取数据
323
+ function getUrlParams() {
324
+ const urlParams = new URLSearchParams(window.location.search);
325
+ return {
326
+ title: urlParams.get('title') || '消息推送',
327
+ message: urlParams.get('message') || '无告警信息',
328
+ date: urlParams.get('date') || '无时间信息'
329
+ };
330
+ }
331
+
332
+ // 创建动态粒子背景
333
+ function createParticles() {
334
+ const particlesContainer = document.getElementById('particles');
335
+ const particleCount = 25;
336
+ const colors = [
337
+ 'rgba(0, 188, 212, 0.2)',
338
+ 'rgba(0, 150, 136, 0.2)',
339
+ 'rgba(77, 182, 172, 0.15)'
340
+ ];
341
+
342
+ for (let i = 0; i < particleCount; i++) {
343
+ const particle = document.createElement('div');
344
+ particle.classList.add('particle');
345
+
346
+ const size = Math.random() * 3 + 1;
347
+ particle.style.width = `${size}px`;
348
+ particle.style.height = `${size}px`;
349
+
350
+ const randomColor = colors[Math.floor(Math.random() * colors.length)];
351
+ particle.style.background = randomColor;
352
+
353
+ particle.style.left = `${Math.random() * 100}%`;
354
+ particle.style.top = `${Math.random() * 100}%`;
355
+
356
+ particle.style.animationDelay = `${Math.random() * 20}s`;
357
+ particle.style.animationDuration = `${20 + Math.random() * 15}s`;
358
+
359
+ particlesContainer.appendChild(particle);
360
+ }
361
+ }
362
+
363
+ // 处理 Markdown 渲染
364
+ function renderMarkdown() {
365
+ const messageEl = document.getElementById('message');
366
+ if (messageEl && typeof marked !== 'undefined') {
367
+ const markdownText = messageEl.textContent || messageEl.innerText;
368
+ messageEl.innerHTML = marked.parse(markdownText);
369
+ }
370
+ }
371
+
372
+ // 填充页面内容
373
+ function fillContent() {
374
+ const params = getUrlParams();
375
+ document.getElementById('title').textContent = params.title;
376
+ document.getElementById('message').textContent = params.message;
377
+ document.getElementById('date').textContent = params.date;
378
+ renderMarkdown(); // 渲染 Markdown
379
+ }
380
+
381
+ // 页面加载时调用
382
+ window.onload = function() {
383
+ createParticles();
384
+ fillContent();
385
+ };
386
+ </script>
387
+ </body>
388
+ </html>