每日发送umami统计信息到钉钉
每日发送umami统计信息到钉钉

目录
[toc]
背景
优秀的时光大佬已经实现了这个umami统计信息到钉钉的功能,那么我也要拥有哦哈哈。😜
次方法已亲测有效。(2025年12月27日)
- 时光文档
2025年12月26日记录。
https://notes.ksah.cn/skills/python


环境
1云服务器:CentOS 7.6.1810
2python3(系统默认自带)(Python 3.6.8)
3pip3(系统默认自带)(pip 9.0.3)
0、准备环境
需先安装下requests库:
1pip3 install requests
1、创建脚本
(1)创建一个python脚本,名字叫umami.py,内容如下:
vim /root/umami.py
1
2#!/usr/bin/env python3
3# -*- coding: utf-8 -*-
4import requests
5import time
6import hmac
7import hashlib
8import base64
9import urllib.parse
10from datetime import datetime, timedelta
11import sys
12
13# ========== 替换为你的信息 ==========
14# 请替换为你的实际信息
15UMAMI_DOMAIN="" # Umami部署域名
16UMAMI_USERNAME="" # Umami登录账号
17UMAMI_PASSWORD="" # Umami登录密码
18WEBSITE_ID="" # Umami站点ID
19DINGTALK_WEBHOOK="" # 钉钉机器人access_token
20DINGTALK_SECRET="" # 钉钉机器人密钥(SECRET)
21
22# ==================================
23
24class UmamiDingtalkReporter:
25 def __init__(self):
26 self.session = requests.Session()
27 self.session.headers.update({
28 "Content-Type": "application/json",
29 "User-Agent": "Mozilla/5.0"
30 })
31
32 def get_current_time(self):
33 return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
34
35 def print_with_time(self, message):
36 print(f"{self.get_current_time()} {message}")
37
38 def login_umami(self):
39 """登录Umami获取token"""
40 self.print_with_time("正在登录Umami获取token...")
41 login_url = f"{UMAMI_DOMAIN}/api/auth/login"
42 login_data = {"username": UMAMI_USERNAME, "password": UMAMI_PASSWORD}
43
44 try:
45 response = self.session.post(login_url, json=login_data, timeout=10)
46 response.raise_for_status()
47 token = response.json().get('token')
48 if not token:
49 self.print_with_time("Umami登录失败:未获取到token")
50 return None
51 self.print_with_time("成功获取token")
52 return token
53 except Exception as e:
54 self.print_with_time(f"Umami登录失败: {str(e)}")
55 return None
56
57 def calculate_timestamp(self, time_str):
58 """计算时间戳(毫秒)"""
59 try:
60 if time_str == "today_start":
61 dt = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
62 elif time_str == "today_end":
63 dt = datetime.now()
64 elif time_str == "yesterday_start":
65 dt = (datetime.now() - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
66 elif time_str == "yesterday_end":
67 dt = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
68 return int(dt.timestamp() * 1000)
69 except Exception:
70 return int(time.time() * 1000)
71
72 def get_stats_data(self, token, start_at, end_at):
73 """获取指定时间段的统计数据"""
74 url = f"{UMAMI_DOMAIN}/api/websites/{WEBSITE_ID}/stats"
75 headers = {"Authorization": f"Bearer {token}"}
76 params = {"startAt": start_at, "endAt": end_at}
77
78 try:
79 response = self.session.get(url, headers=headers, params=params, timeout=10)
80 response.raise_for_status()
81 return response.json()
82 except Exception as e:
83 self.print_with_time(f"获取统计数据失败: {str(e)}")
84 return {}
85
86 def extract_numeric_value(self, data, key, default=0):
87 """安全提取数值型数据"""
88 value = data.get(key, default)
89 if isinstance(value, dict):
90 value = value.get('value', default)
91 try:
92 return int(value) # 访问人数/访问量都是整数,直接转int
93 except (ValueError, TypeError):
94 return default
95
96 def generate_ding_signature(self):
97 """生成钉钉签名"""
98 timestamp = str(round(time.time() * 1000))
99 secret_enc = DINGTALK_SECRET.encode('utf-8')
100 string_to_sign = f"{timestamp}\n{DINGTALK_SECRET}"
101 hmac_code = hmac.new(secret_enc, string_to_sign.encode('utf-8'), hashlib.sha256).digest()
102 sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
103 return timestamp, sign
104
105 def send_dingtalk_message(self, content):
106 """发送钉钉消息"""
107 if not DINGTALK_WEBHOOK or not DINGTALK_SECRET:
108 return "未配置钉钉WEBHOOK或secret"
109
110 self.print_with_time("正在推送数据到钉钉...")
111 try:
112 timestamp, signature = self.generate_ding_signature()
113 webhook_url = f"{DINGTALK_WEBHOOK}×tamp={timestamp}&sign={signature}"
114
115 message = {
116 "msgtype": "markdown",
117 "markdown": {"title": "网站访问数据统计", "text": content},
118 "at": {"isAtAll": False}
119 }
120
121 response = self.session.post(webhook_url, json=message, timeout=10)
122 result = response.json()
123 if result.get('errcode') == 0 and result.get('errmsg') == 'ok':
124 return "成功"
125 else:
126 self.print_with_time(f"钉钉推送失败: 错误码{result.get('errcode')},信息{result.get('errmsg')}")
127 return "失败"
128 except Exception as e:
129 self.print_with_time(f"钉钉推送异常: {str(e)}")
130 return "失败"
131
132 def run(self):
133 """主函数"""
134 self.print_with_time("开始执行Umami数据推送")
135
136 # 1. 登录获取token
137 token = self.login_umami()
138 if not token:
139 return False
140
141 # 2. 计算时间戳(今日/昨日)
142 today_start = self.calculate_timestamp("today_start")
143 today_end = self.calculate_timestamp("today_end")
144 yesterday_start = self.calculate_timestamp("yesterday_start")
145 yesterday_end = self.calculate_timestamp("yesterday_end")
146
147 # 3. 获取统计数据
148 self.print_with_time("正在抓取Umami统计数据...")
149 today_data = self.get_stats_data(token, today_start, today_end)
150 yesterday_data = self.get_stats_data(token, yesterday_start, yesterday_end)
151
152 # 4. 提取核心数据(访问人数=UV,访问量=PV)
153 today_visitor = self.extract_numeric_value(today_data, 'visitors', 0) # 今日访问人数
154 today_pv = self.extract_numeric_value(today_data, 'pageviews', 0) # 今日访问量
155 yesterday_visitor = self.extract_numeric_value(yesterday_data, 'visitors', 0) # 昨日访问人数
156 yesterday_pv = self.extract_numeric_value(yesterday_data, 'pageviews', 0) # 昨日访问量
157
158 # 5. 终端输出(仅保留需求的4个字段)
159 data_log = f"""
160{self.get_current_time()} 核心访问数据:
161 今日访问人数:{today_visitor} 人
162 今日访问量:{today_pv} 次
163 昨日访问人数:{yesterday_visitor} 人
164 昨日访问量:{yesterday_pv} 次
165"""
166 print(data_log)
167
168 # 6. 构造钉钉消息(仅展示需求的4个字段)
169 markdown_content = f"""# 📊 你的网站名称 网站访问数据统计
170**统计日期:** {datetime.now().strftime('%Y-%m-%d')}
171
172- 今日访问人数:{today_visitor} 人
173- 今日访问量:{today_pv} 次
174- 昨日访问人数:{yesterday_visitor} 人
175- 昨日访问量:{yesterday_pv} 次
176
177**数据更新时间:** {self.get_current_time()}
178"""
179
180 # 7. 发送钉钉消息
181 ding_status = self.send_dingtalk_message(markdown_content)
182 self.print_with_time(f"数据推送{ding_status}")
183
184 return ding_status == "成功"
185
186
187def main():
188 reporter = UmamiDingtalkReporter()
189 success = reporter.run()
190 sys.exit(0 if success else 1)
191
192
193if __name__ == "__main__":
194 main()
以上脚本主要修改2处地方:
1.修改开头的配置信息地方,将相关信息替换为自己的信息
2.修改 你的网站名称 为你自己的网站名称
具体截图如下:
修改位置1:

修改位置2:(可选)

(2)先测试下效果
执行脚本:
1python3 umami.py
(3)观察现象
观察钉钉是否收到了推送的消息:
云服务器输出:

钉钉已经收到了消息:

2、设置定时任务
(1)设置定时任务
执行crontab -e:(将如下内容填到定时任务文件中)
10 8,10,12,22,23 * * * /usr/bin/python3 /root/umami.py >> /root/umami_cron.log 2>&1
(2)到指定时间查看是否有执行记录
执行cat /root/umami_cron.log。
本次执行的定时任务时间为:2025-12-27 8点、10点、12点、22点、23点。
FAQ
时光文档
警告
自己在用时光文档里提供的2个shell和python脚本,是有问题的,可以正常推送内容到钉钉,但我移动端钉钉内容的表格是乱序的。但是时光试了多次是OK的,我这里仅仅记录下他的脚本,和我的报错截图。
另外:时光这个脚本内容太过丰富,而我只需要每次推送一些关键的数据就好。当然,我这一版是豆包帮我优化了的。
- 时光源文档
https://notes.ksah.cn/skills/python


- 自己使用时光脚本的报错截图

- 时光2个脚本如下:
python脚本:
1
2#!/usr/bin/env python3
3# -*- coding: utf-8 -*-
4import json
5import requests
6import time
7import hmac
8import hashlib
9import base64
10import urllib.parse
11from datetime import datetime, timedelta
12import sys
13import os
14
15# ========== 替换为你的信息 ==========
16UMAMI_DOMAIN = "" # Umami部署域名
17UMAMI_USERNAME = "" # Umami登录账号
18UMAMI_PASSWORD = "" # Umami登录密码
19WEBSITE_ID = "" # Umami站点ID
20DINGTALK_WEBHOOK = "" # 钉钉机器人access_token
21DINGTALK_SECRET = "" # 钉钉机器人密钥(SECRET)
22
23
24# ==================================
25
26class UmamiDingtalkReporter:
27 def __init__(self):
28 self.session = requests.Session()
29 self.session.headers.update({
30 "Content-Type": "application/json",
31 "User-Agent": "Mozilla/5.0"
32 })
33
34 def get_current_time(self):
35 return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
36
37 def print_with_time(self, message):
38 print(f"{self.get_current_time()} {message}")
39
40 def login_umami(self):
41 """登录Umami获取token"""
42 self.print_with_time("正在登录Umami获取token...")
43
44 login_url = f"{UMAMI_DOMAIN}/api/auth/login"
45 login_data = {
46 "username": UMAMI_USERNAME,
47 "password": UMAMI_PASSWORD
48 }
49
50 try:
51 response = self.session.post(login_url, json=login_data, timeout=10)
52 response.raise_for_status()
53 result = response.json()
54 token = result.get('token')
55
56 if not token:
57 self.print_with_time(f"Umami登录失败: {result}")
58 return None
59
60 self.print_with_time("成功获取token")
61 return token
62
63 except Exception as e:
64 self.print_with_time(f"Umami登录失败: {str(e)}")
65 return None
66
67 def get_website_info(self, token):
68 """获取网站基本信息"""
69 self.print_with_time("正在获取网站信息...")
70
71 url = f"{UMAMI_DOMAIN}/api/websites/{WEBSITE_ID}"
72 headers = {"Authorization": f"Bearer {token}"}
73
74 try:
75 response = self.session.get(url, headers=headers, timeout=10)
76 response.raise_for_status()
77 result = response.json()
78
79 website_name = result.get('name', '未命名网站')
80 website_domain = result.get('domain', '')
81
82 return website_name, website_domain
83
84 except Exception as e:
85 self.print_with_time(f"获取网站信息失败: {str(e)}")
86 return "未命名网站", ""
87
88 def get_stats_data(self, token, start_at, end_at):
89 """获取统计数据"""
90 url = f"{UMAMI_DOMAIN}/api/websites/{WEBSITE_ID}/stats"
91 headers = {"Authorization": f"Bearer {token}"}
92 params = {
93 "startAt": start_at,
94 "endAt": end_at
95 }
96
97 try:
98 response = self.session.get(url, headers=headers, params=params, timeout=10)
99 response.raise_for_status()
100 return response.json()
101 except Exception as e:
102 self.print_with_time(f"获取统计数据失败: {str(e)}")
103 return {}
104
105 def calculate_timestamp(self, time_str):
106 """计算时间戳(毫秒)"""
107 try:
108 if time_str == "now":
109 dt = datetime.now()
110 elif "ago" in time_str:
111 days = int(time_str.split()[0])
112 dt = datetime.now() - timedelta(days=days)
113 elif time_str.startswith("today"):
114 dt = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
115 if ":" in time_str:
116 time_part = time_str.split()[1]
117 hour, minute, second = map(int, time_part.split(":"))
118 dt = dt.replace(hour=hour, minute=minute, second=second)
119 elif time_str.startswith("yesterday"):
120 dt = datetime.now() - timedelta(days=1)
121 dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
122 if ":" in time_str:
123 time_part = time_str.split()[1]
124 hour, minute, second = map(int, time_part.split(":"))
125 dt = dt.replace(hour=hour, minute=minute, second=second)
126 else:
127 dt = datetime.now()
128
129 return int(dt.timestamp() * 1000)
130 except Exception:
131 return int(time.time() * 1000)
132
133 def calculate_growth(self, current, previous):
134 """计算增长率"""
135 if previous == 0:
136 return "N/A"
137 try:
138 growth = ((current - previous) / previous) * 100
139 return round(growth, 2)
140 except:
141 return 0
142
143 def generate_ding_signature(self):
144 """生成钉钉签名"""
145 # 修正:使用毫秒级时间戳字符串
146 timestamp = str(round(time.time() * 1000))
147 secret_enc = DINGTALK_SECRET.encode('utf-8')
148 string_to_sign = f"{timestamp}\n{DINGTALK_SECRET}"
149 string_to_sign_enc = string_to_sign.encode('utf-8')
150
151 hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
152 sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
153
154 return timestamp, sign
155
156 def send_dingtalk_message(self, markdown_content, title="Umami网站统计报告"):
157 """发送钉钉消息"""
158 if not DINGTALK_WEBHOOK or not DINGTALK_SECRET:
159 return "未配置钉钉WEBHOOK或secret"
160
161 self.print_with_time("正在推送数据到钉钉...")
162
163 try:
164 # 生成签名
165 timestamp, signature = self.generate_ding_signature()
166 self.print_with_time(f"钉钉签名参数 - timestamp: {timestamp}, signature: {signature}")
167
168 # 构建钉钉Webhook URL
169 webhook_url = f"{DINGTALK_WEBHOOK}×tamp={timestamp}&sign={signature}"
170 self.print_with_time(f"钉钉Webhook URL: {webhook_url}")
171
172 # 构建消息体
173 message = {
174 "msgtype": "markdown",
175 "markdown": {
176 "title": title,
177 "text": markdown_content
178 },
179 "at": {
180 "isAtAll": False
181 }
182 }
183
184 # 发送请求
185 response = self.session.post(webhook_url, json=message, timeout=10)
186 result = response.json()
187
188 if result.get('errcode') == 0 and result.get('errmsg') == 'ok':
189 return "成功"
190 else:
191 errcode = result.get('errcode', 'unknown')
192 errmsg = result.get('errmsg', 'unknown')
193 self.print_with_time(f"钉钉推送失败详情 - 错误码: {errcode}, 错误信息: {errmsg}")
194 return "失败"
195
196 except Exception as e:
197 self.print_with_time(f"钉钉推送异常: {str(e)}")
198 return "失败"
199
200 def run(self):
201 """主函数"""
202 self.print_with_time("开始执行Umami数据推送")
203
204 # 1. 登录Umami获取token
205 token = self.login_umami()
206 if not token:
207 return False
208
209 # 2. 获取网站信息
210 website_name, website_domain = self.get_website_info(token)
211
212 # 3. 计算时间范围
213 today_start = self.calculate_timestamp("today 00:00:00")
214 today_end = self.calculate_timestamp("now")
215 yesterday_start = self.calculate_timestamp("yesterday 00:00:00")
216 yesterday_end = today_start
217 last_month_start = self.calculate_timestamp("30 days ago")
218 last_year_start = self.calculate_timestamp("365 days ago")
219 today_date = datetime.now().strftime('%Y-%m-%d')
220
221 # 4. 获取统计数据
222 self.print_with_time("正在抓取Umami统计数据...")
223
224 today_data = self.get_stats_data(token, today_start, today_end)
225 yesterday_data = self.get_stats_data(token, yesterday_start, yesterday_end)
226 last_month_data = self.get_stats_data(token, last_month_start, today_end)
227 last_year_data = self.get_stats_data(token, last_year_start, today_end)
228
229 # 5. 解析数据
230 today_uv = today_data.get('visitors', 0) or 0
231 today_pv = today_data.get('pageviews', 0) or 0
232 today_bounce = today_data.get('bounces', 0) or 0
233 today_visits = today_data.get('visits', 0) or 0
234 today_totaltime = today_data.get('totaltime', 0) or 0
235
236 yesterday_uv = yesterday_data.get('visitors', 0) or 0
237 yesterday_pv = yesterday_data.get('pageviews', 0) or 0
238
239 last_month_pv = last_month_data.get('pageviews', 0) or 0
240 last_year_pv = last_year_data.get('pageviews', 0) or 0
241
242 # 6. 计算平均访问时长和跳出率
243 if today_visits > 0:
244 avg_duration = round(today_totaltime / today_visits, 2)
245 bounce_rate = round((today_bounce / today_visits) * 100, 2)
246 else:
247 avg_duration = 0
248 bounce_rate = 0
249
250 # 7. 计算环比增长率
251 uv_growth = self.calculate_growth(today_uv, yesterday_uv)
252 pv_growth = self.calculate_growth(today_pv, yesterday_pv)
253
254 # 趋势符号
255 def get_trend_symbol(growth):
256 if growth == "N/A":
257 return "➖"
258 elif growth >= 0:
259 return "📈"
260 else:
261 return "📉"
262
263 uv_trend = get_trend_symbol(uv_growth)
264 pv_trend = get_trend_symbol(pv_growth)
265
266 # 格式化增长率
267 def format_growth(growth):
268 if growth == "N/A":
269 return "N/A"
270 else:
271 return f"{growth:.2f}%"
272
273 uv_growth_formatted = format_growth(uv_growth)
274 pv_growth_formatted = format_growth(pv_growth)
275
276 # 8. 输出数据到终端
277 data_log = f"""
278{self.get_current_time()} 数据统计:
279 网站名称: {website_name}
280 网站域名: {website_domain}
281 今日访客数(UV): {today_uv} 人
282 今日访问量(PV): {today_pv} 次
283 今日访问次数: {today_visits} 次
284 平均访问时长: {avg_duration} 秒
285 跳出率: {bounce_rate}%
286 昨日访客数: {yesterday_uv} 人
287 昨日访问量: {yesterday_pv} 次
288 最近30天访问量: {last_month_pv} 次
289 最近365天访问量: {last_year_pv} 次
290 访客环比: {uv_growth_formatted} {uv_trend}
291 访问量环比: {pv_growth_formatted} {pv_trend}
292"""
293 print(data_log)
294
295 # 9. 构造钉钉机器人消息
296 markdown_content = f"""# 📊 Umami网站统计报告
297**网站名称:** {website_name}
298**统计时间:** {today_date} {datetime.now().strftime('%H:%M:%S')}
299**网站域名:** {website_domain}
300
301---
302
303## 📈 今日核心数据
304
305| 指标 | 数值 | 环比昨日 |
306|------|------|----------|
307| 👥 独立访客(UV) | {today_uv} 人 | {uv_growth_formatted} {uv_trend} |
308| 🔄 页面浏览量(PV) | {today_pv} 次 | {pv_growth_formatted} {pv_trend} |
309| 🚶♂️ 访问次数 | {today_visits} 次 | - |
310| ⏱️ 平均访问时长 | {avg_duration} 秒 | - |
311| 🚪 跳出率 | {bounce_rate}% | - |
312
313---
314
315## 📊 历史数据对比
316
317| 时间段 | 独立访客(UV) | 页面浏览量(PV) |
318|--------|--------------|----------------|
319| 昨日({(datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')}) | {yesterday_uv} 人 | {yesterday_pv} 次 |
320| 最近30天 | - | {last_month_pv} 次 |
321| 最近365天 | - | {last_year_pv} 次 |
322
323---
324
325## 📋 数据说明
326- **UV (Unique Visitors)**: 独立访客数,统计去重的访问用户
327- **PV (Page Views)**: 页面浏览量,统计所有页面访问次数
328- **跳出率**: 只访问一个页面就离开的会话占比
329- **环比**: 与昨日同时段数据对比
330
331**报告生成时间:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
332"""
333
334 # 10. 发送钉钉消息
335 ding_status = self.send_dingtalk_message(markdown_content)
336
337 # 11. 输出最终结果
338 self.print_with_time(f"数据推送{ding_status}")
339
340 return ding_status == "成功"
341
342
343def main():
344 reporter = UmamiDingtalkReporter()
345 success = reporter.run()
346 sys.exit(0 if success else 1)
347
348
349if __name__ == "__main__":
350 main()
shell脚本:
1
2#!/bin/bash
3
4# ========== 配置信息 ==========
5# 请替换为你的实际信息
6UMAMI_DOMAIN="" # Umami部署域名
7UMAMI_USERNAME="" # Umami登录账号
8UMAMI_PASSWORD="" # Umami登录密码
9WEBSITE_ID="" # Umami站点ID
10DINGTALK_WEBHOOK="" # 钉钉机器人access_token
11DINGTALK_SECRET="" # 钉钉机器人密钥(SECRET)
12
13# ========== 全局变量 ==========
14UMAMI_TOKEN=""
15WEBSITE_NAME="未命名网站"
16WEBSITE_DOMAIN=""
17CURRENT_TIME=$(date '+%Y-%m-%d %H:%M:%S')
18TODAY_DATE=$(date '+%Y-%m-%d')
19YESTERDAY_DATE=$(date -d "1 day ago" '+%Y-%m-%d')
20
21# ========== 颜色定义 ==========
22RED='\033[0;31m'
23GREEN='\033[0;32m'
24YELLOW='\033[1;33m'
25BLUE='\033[0;34m'
26NC='\033[0m' # No Color
27
28# ========== 工具函数 ==========
29
30# 带时间戳的输出
31print_with_time() {
32 echo -e "$(date '+%Y-%m-%d %H:%M:%S') $1"
33}
34
35# 检查命令是否存在
36check_command() {
37 if ! command -v "$1" &> /dev/null; then
38 echo -e "${RED}错误: 缺少命令 '$1'${NC}"
39 echo "请安装:"
40 if [[ "$1" == "jq" ]]; then
41 echo " Ubuntu/Debian: sudo apt-get install jq"
42 echo " CentOS/RHEL: sudo yum install jq"
43 elif [[ "$1" == "bc" ]]; then
44 echo " Ubuntu/Debian: sudo apt-get install bc"
45 echo " CentOS/RHEL: sudo yum install bc"
46 else
47 echo " sudo apt-get install $1 # Ubuntu/Debian"
48 echo " sudo yum install $1 # CentOS/RHEL"
49 fi
50 exit 1
51 fi
52}
53
54# 计算时间戳(毫秒)
55calculate_timestamp() {
56 local time_str="$1"
57
58 case "$time_str" in
59 "now")
60 date +%s%3N
61 ;;
62 "today 00:00:00")
63 date -d "today 00:00:00" +%s%3N 2>/dev/null || date +%s000
64 ;;
65 "yesterday 00:00:00")
66 date -d "yesterday 00:00:00" +%s%3N 2>/dev/null || echo $(($(date +%s) - 86400))000
67 ;;
68 "30 days ago")
69 date -d "30 days ago" +%s%3N 2>/dev/null || echo $(($(date +%s) - 2592000))000
70 ;;
71 "365 days ago")
72 date -d "365 days ago" +%s%3N 2>/dev/null || echo $(($(date +%s) - 31536000))000
73 ;;
74 *)
75 date +%s%3N
76 ;;
77 esac
78}
79
80# 登录Umami获取token
81login_umami() {
82 print_with_time "正在登录Umami获取token..."
83
84 local login_url="${UMAMI_DOMAIN}/api/auth/login"
85 local login_data="{\"username\":\"${UMAMI_USERNAME}\",\"password\":\"${UMAMI_PASSWORD}\"}"
86
87 local response
88 response=$(curl -s -X POST "$login_url" \
89 -H "Content-Type: application/json" \
90 -d "$login_data" \
91 --max-time 10 2>/dev/null)
92
93 if [ $? -ne 0 ]; then
94 print_with_time "Umami登录失败: 网络请求错误"
95 return 1
96 fi
97
98 local token
99 token=$(echo "$response" | jq -r '.token // empty')
100
101 if [ -z "$token" ] || [ "$token" == "null" ]; then
102 print_with_time "Umami登录失败: 无法获取token"
103 echo "响应: $response"
104 return 1
105 fi
106
107 UMAMI_TOKEN="$token"
108 print_with_time "成功获取token"
109 return 0
110}
111
112# 获取网站信息
113get_website_info() {
114 print_with_time "正在获取网站信息..."
115
116 local url="${UMAMI_DOMAIN}/api/websites/${WEBSITE_ID}"
117
118 local response
119 response=$(curl -s -X GET "$url" \
120 -H "Authorization: Bearer ${UMAMI_TOKEN}" \
121 --max-time 10 2>/dev/null)
122
123 if [ $? -ne 0 ]; then
124 print_with_time "获取网站信息失败: 网络请求错误"
125 return 1
126 fi
127
128 WEBSITE_NAME=$(echo "$response" | jq -r '.name // "未命名网站"')
129 WEBSITE_DOMAIN=$(echo "$response" | jq -r '.domain // ""')
130
131 print_with_time "网站名称: ${WEBSITE_NAME}"
132 print_with_time "网站域名: ${WEBSITE_DOMAIN}"
133 return 0
134}
135
136# 获取统计数据
137get_stats_data() {
138 local start_at="$1"
139 local end_at="$2"
140
141 local url="${UMAMI_DOMAIN}/api/websites/${WEBSITE_ID}/stats"
142 local params="startAt=${start_at}&endAt=${end_at}"
143
144 local response
145 response=$(curl -s -X GET "${url}?${params}" \
146 -H "Authorization: Bearer ${UMAMI_TOKEN}" \
147 --max-time 10 2>/dev/null)
148
149 if [ $? -ne 0 ]; then
150 echo "{}"
151 return 1
152 fi
153
154 echo "$response"
155}
156
157# 计算增长率
158calculate_growth() {
159 local current="$1"
160 local previous="$2"
161
162 if [ "$previous" -eq 0 ]; then
163 echo "N/A"
164 return
165 fi
166
167 local growth
168 growth=$(echo "scale=2; ($current - $previous) / $previous * 100" | bc 2>/dev/null)
169
170 if [ $? -eq 0 ]; then
171 echo "$growth"
172 else
173 echo "0"
174 fi
175}
176
177# 生成钉钉签名
178generate_ding_signature() {
179 local timestamp
180 timestamp=$(date +%s%3N)
181
182 local string_to_sign="${timestamp}\n${DINGTALK_SECRET}"
183 local sign
184
185 # 使用openssl生成HMAC-SHA256签名
186 sign=$(echo -en "$string_to_sign" | openssl dgst -hmac "$DINGTALK_SECRET" -sha256 -binary | base64)
187
188 # URL编码
189 sign=$(echo -n "$sign" | sed 's/+/%2B/g;s/\//%2F/g;s/=/%3D/g')
190
191 echo "$timestamp $sign"
192}
193
194# 发送钉钉消息
195send_dingtalk_message() {
196 local markdown_content="$1"
197 local title="${2:-Umami网站统计报告}"
198
199 if [ -z "$DINGTALK_WEBHOOK" ] || [ -z "$DINGTALK_SECRET" ]; then
200 print_with_time "未配置钉钉WEBHOOK或secret"
201 return 1
202 fi
203
204 print_with_time "正在推送数据到钉钉..."
205
206 # 生成签名
207 local signature_info
208 signature_info=$(generate_ding_signature)
209 local timestamp=$(echo "$signature_info" | cut -d' ' -f1)
210 local signature=$(echo "$signature_info" | cut -d' ' -f2)
211
212 print_with_time "钉钉签名参数 - timestamp: ${timestamp}, signature: ${signature}"
213
214 # 构建钉钉Webhook URL
215 local webhook_url="${DINGTALK_WEBHOOK}×tamp=${timestamp}&sign=${signature}"
216
217 # 构建消息体
218 local message="{
219 \"msgtype\": \"markdown\",
220 \"markdown\": {
221 \"title\": \"${title}\",
222 \"text\": \"${markdown_content//\"/\\\"}\"
223 },
224 \"at\": {
225 \"isAtAll\": false
226 }
227 }"
228
229 # 发送请求
230 local response
231 response=$(curl -s -X POST "$webhook_url" \
232 -H "Content-Type: application/json" \
233 -d "$message" \
234 --max-time 10 2>/dev/null)
235
236 if [ $? -ne 0 ]; then
237 print_with_time "钉钉推送失败: 网络请求错误"
238 return 1
239 fi
240
241 local errcode
242 local errmsg
243 errcode=$(echo "$response" | jq -r '.errcode // "unknown"')
244 errmsg=$(echo "$response" | jq -r '.errmsg // "unknown"')
245
246 if [ "$errcode" = "0" ] && [ "$errmsg" = "ok" ]; then
247 print_with_time "钉钉推送成功"
248 return 0
249 else
250 print_with_time "钉钉推送失败 - 错误码: ${errcode}, 错误信息: ${errmsg}"
251 return 1
252 fi
253}
254
255# 转义Markdown内容中的特殊字符
256escape_markdown() {
257 echo "$1" | sed 's/_/\\_/g;s/*/\\*/g;s/\[/\\[/g;s/\]/\\]/g;s/(/\\(/g;s/)/\\)/g;s/`/\\`/g'
258}
259
260# ========== 主函数 ==========
261main() {
262 print_with_time "开始执行Umami数据推送"
263
264 # 检查必需的命令
265 check_command curl
266 check_command jq
267 check_command bc
268 check_command openssl
269 check_command base64
270
271 # 1. 登录Umami获取token
272 if ! login_umami; then
273 echo -e "${RED}Umami登录失败,请检查配置${NC}"
274 exit 1
275 fi
276
277 # 2. 获取网站信息
278 if ! get_website_info; then
279 echo -e "${YELLOW}警告: 获取网站信息失败,使用默认值${NC}"
280 fi
281
282 # 3. 计算时间范围
283 print_with_time "正在计算时间范围..."
284 TODAY_START=$(calculate_timestamp "today 00:00:00")
285 TODAY_END=$(calculate_timestamp "now")
286 YESTERDAY_START=$(calculate_timestamp "yesterday 00:00:00")
287 YESTERDAY_END=$TODAY_START
288 LAST_MONTH_START=$(calculate_timestamp "30 days ago")
289 LAST_YEAR_START=$(calculate_timestamp "365 days ago")
290
291 # 4. 获取统计数据
292 print_with_time "正在抓取Umami统计数据..."
293
294 TODAY_DATA=$(get_stats_data "$TODAY_START" "$TODAY_END")
295 YESTERDAY_DATA=$(get_stats_data "$YESTERDAY_START" "$YESTERDAY_END")
296 LAST_MONTH_DATA=$(get_stats_data "$LAST_MONTH_START" "$TODAY_END")
297 LAST_YEAR_DATA=$(get_stats_data "$LAST_YEAR_START" "$TODAY_END")
298
299 # 5. 解析数据
300 TODAY_UV=$(echo "$TODAY_DATA" | jq -r '.visitors // 0')
301 TODAY_PV=$(echo "$TODAY_DATA" | jq -r '.pageviews // 0')
302 TODAY_BOUNCE=$(echo "$TODAY_DATA" | jq -r '.bounces // 0')
303 TODAY_VISITS=$(echo "$TODAY_DATA" | jq -r '.visits // 0')
304 TODAY_TOTALTIME=$(echo "$TODAY_DATA" | jq -r '.totaltime // 0')
305
306 YESTERDAY_UV=$(echo "$YESTERDAY_DATA" | jq -r '.visitors // 0')
307 YESTERDAY_PV=$(echo "$YESTERDAY_DATA" | jq -r '.pageviews // 0')
308
309 LAST_MONTH_PV=$(echo "$LAST_MONTH_DATA" | jq -r '.pageviews // 0')
310 LAST_YEAR_PV=$(echo "$LAST_YEAR_DATA" | jq -r '.pageviews // 0')
311
312 # 设置默认值
313 TODAY_UV=${TODAY_UV:-0}
314 TODAY_PV=${TODAY_PV:-0}
315 TODAY_BOUNCE=${TODAY_BOUNCE:-0}
316 TODAY_VISITS=${TODAY_VISITS:-0}
317 TODAY_TOTALTIME=${TODAY_TOTALTIME:-0}
318 YESTERDAY_UV=${YESTERDAY_UV:-0}
319 YESTERDAY_PV=${YESTERDAY_PV:-0}
320 LAST_MONTH_PV=${LAST_MONTH_PV:-0}
321 LAST_YEAR_PV=${LAST_YEAR_PV:-0}
322
323 # 6. 计算平均访问时长和跳出率
324 local AVG_DURATION=0
325 local BOUNCE_RATE=0
326
327 if [ "$TODAY_VISITS" -gt 0 ]; then
328 AVG_DURATION=$(echo "scale=2; $TODAY_TOTALTIME / $TODAY_VISITS" | bc 2>/dev/null || echo "0")
329 BOUNCE_RATE=$(echo "scale=2; $TODAY_BOUNCE / $TODAY_VISITS * 100" | bc 2>/dev/null || echo "0")
330 fi
331
332 # 7. 计算环比增长率
333 UV_GROWTH=$(calculate_growth "$TODAY_UV" "$YESTERDAY_UV")
334 PV_GROWTH=$(calculate_growth "$TODAY_PV" "$YESTERDAY_PV")
335
336 # 趋势符号
337 local UV_TREND="➖"
338 local PV_TREND="➖"
339
340 if [ "$UV_GROWTH" != "N/A" ]; then
341 if (( $(echo "$UV_GROWTH >= 0" | bc -l 2>/dev/null || echo "1") )); then
342 UV_TREND="📈"
343 else
344 UV_TREND="📉"
345 fi
346 fi
347
348 if [ "$PV_GROWTH" != "N/A" ]; then
349 if (( $(echo "$PV_GROWTH >= 0" | bc -l 2>/dev/null || echo "1") )); then
350 PV_TREND="📈"
351 else
352 PV_TREND="📉"
353 fi
354 fi
355
356 # 格式化增长率
357 local UV_GROWTH_FORMATTED="N/A"
358 local PV_GROWTH_FORMATTED="N/A"
359
360 if [ "$UV_GROWTH" != "N/A" ]; then
361 UV_GROWTH_FORMATTED=$(printf "%.2f%%" "$UV_GROWTH")
362 fi
363
364 if [ "$PV_GROWTH" != "N/A" ]; then
365 PV_GROWTH_FORMATTED=$(printf "%.2f%%" "$PV_GROWTH")
366 fi
367
368 # 8. 输出数据到终端
369 echo ""
370 print_with_time "数据统计:"
371 echo " 网站名称: ${WEBSITE_NAME}"
372 echo " 网站域名: ${WEBSITE_DOMAIN}"
373 echo " 今日访客数(UV): ${TODAY_UV} 人"
374 echo " 今日访问量(PV): ${TODAY_PV} 次"
375 echo " 今日访问次数: ${TODAY_VISITS} 次"
376 echo " 平均访问时长: ${AVG_DURATION} 秒"
377 echo " 跳出率: ${BOUNCE_RATE}%"
378 echo " 昨日访客数: ${YESTERDAY_UV} 人"
379 echo " 昨日访问量: ${YESTERDAY_PV} 次"
380 echo " 最近30天访问量: ${LAST_MONTH_PV} 次"
381 echo " 最近365天访问量: ${LAST_YEAR_PV} 次"
382 echo " 访客环比: ${UV_GROWTH_FORMATTED} ${UV_TREND}"
383 echo " 访问量环比: ${PV_GROWTH_FORMATTED} ${PV_TREND}"
384 echo ""
385
386 # 9. 构造钉钉机器人消息
387 # 转义特殊字符
388 local ESCAPED_WEBSITE_NAME=$(escape_markdown "$WEBSITE_NAME")
389 local ESCAPED_WEBSITE_DOMAIN=$(escape_markdown "$WEBSITE_DOMAIN")
390
391 # 获取当前时间
392 local CURRENT_DATETIME=$(date '+%Y-%m-%d %H:%M:%S')
393
394 # 构建Markdown内容
395 local MARKDOWN_CONTENT="# 📊 Umami网站统计报告
396**网站名称:** ${ESCAPED_WEBSITE_NAME}
397**统计时间:** ${TODAY_DATE} $(date '+%H:%M:%S')
398**网站域名:** ${ESCAPED_WEBSITE_DOMAIN}
399
400---
401
402## 📈 今日核心数据
403
404| 指标 | 数值 | 环比昨日 |
405|------|------|----------|
406| 👥 独立访客(UV) | ${TODAY_UV} 人 | ${UV_GROWTH_FORMATTED} ${UV_TREND} |
407| 🔄 页面浏览量(PV) | ${TODAY_PV} 次 | ${PV_GROWTH_FORMATTED} ${PV_TREND} |
408| 🚶♂️ 访问次数 | ${TODAY_VISITS} 次 | - |
409| ⏱️ 平均访问时长 | ${AVG_DURATION} 秒 | - |
410| 🚪 跳出率 | ${BOUNCE_RATE}% | - |
411
412---
413
414## 📊 历史数据对比
415
416| 时间段 | 独立访客(UV) | 页面浏览量(PV) |
417|--------|--------------|----------------|
418| 昨日(${YESTERDAY_DATE}) | ${YESTERDAY_UV} 人 | ${YESTERDAY_PV} 次 |
419| 最近30天 | - | ${LAST_MONTH_PV} 次 |
420| 最近365天 | - | ${LAST_YEAR_PV} 次 |
421
422---
423
424## 📋 数据说明
425- **UV (Unique Visitors)**: 独立访客数,统计去重的访问用户
426- **PV (Page Views)**: 页面浏览量,统计所有页面访问次数
427- **跳出率**: 只访问一个页面就离开的会话占比
428- **环比**: 与昨日同时段数据对比
429
430**报告生成时间:** ${CURRENT_DATETIME}"
431
432 # 10. 发送钉钉消息
433 if send_dingtalk_message "$MARKDOWN_CONTENT"; then
434 echo -e "${GREEN}✓ 数据推送成功${NC}"
435 exit 0
436 else
437 echo -e "${RED}✗ 数据推送失败${NC}"
438 exit 1
439 fi
440}
441
442# ========== 脚本入口 ==========
443# 处理命令行参数
444case "${1:-}" in
445 -h|--help)
446 echo "Umami网站统计推送脚本"
447 echo "用法: $0 [选项]"
448 echo ""
449 echo "选项:"
450 echo " -h, --help 显示此帮助信息"
451 echo " -t, --test 测试模式,只输出数据不发送到钉钉"
452 echo ""
453 echo "环境变量配置:"
454 echo " 编辑脚本开头的配置部分,设置您的Umami和钉钉信息"
455 exit 0
456 ;;
457 -t|--test)
458 echo -e "${YELLOW}测试模式: 将只输出数据,不发送到钉钉${NC}"
459 # 这里可以添加测试逻辑
460 ;;
461esac
462
463# 运行主函数
464main "$@"
关于我
我的博客主旨:
- 排版美观,语言精炼;
- 文档即手册,步骤明细,拒绝埋坑,提供源码;
- 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!
🍀 个人网站

🍀 微信二维码
x2675263825 (舍得), qq:2675263825。

🍀 微信公众号
《云原生架构师实战》

🍀 csdn
https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421

🍀 知乎
https://www.zhihu.com/people/foryouone

最后
如果你还有疑惑,可以去我的网站查看更多内容或者联系我帮忙查看。
如果你有更好的方式,评论区留言告诉我。谢谢!
好了,本次就到这里了,感谢大家阅读,最后祝大家生活快乐,每天都过的有意义哦,我们下期见!



- 01pip 2025-12-27
- 02pip安装python库报ssl错误 2025-12-27
- 03snowshot 2025-12-27