Spaces:
Running
Running
Upload 5 files
Browse files- Dockerfile +23 -0
- README.md +191 -10
- go.mod +1 -0
- main.go +345 -0
- 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+

|
| 24 |
+
- 获取appid 、appsecret
|
| 25 |
+

|
| 26 |
+
- 关注测试公众号,获取userid(微信号),新增测试模板(注意模版内容填写格式 `内容: {{content.DATA}}`) 获取template_id(模板ID)
|
| 27 |
+

|
| 28 |
+
- 将以上获取到的参数代入下面使用即可
|
| 29 |
+

|
| 30 |
+

|
| 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 |
+

|
| 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(¶ms)
|
| 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>
|