每日发送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}&timestamp={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}&timestamp={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}&timestamp=${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 "$@"

关于我

我的博客主旨:

  • 排版美观,语言精炼;
  • 文档即手册,步骤明细,拒绝埋坑,提供源码;
  • 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!

🍀 个人网站

image-20250109220325748

🍀 微信二维码

x2675263825 (舍得), qq:2675263825。

image-20230107215114763

🍀 微信公众号

《云原生架构师实战》

image-20230107215126971

🍀 csdn

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

image-20230107215149885

🍀 知乎

https://www.zhihu.com/people/foryouone

image-20230107215203185

最后

如果你还有疑惑,可以去我的网站查看更多内容或者联系我帮忙查看。

如果你有更好的方式,评论区留言告诉我。谢谢!

好了,本次就到这里了,感谢大家阅读,最后祝大家生活快乐,每天都过的有意义哦,我们下期见!

推荐使用微信支付
微信支付二维码
推荐使用支付宝
支付宝二维码
最新文章

文档导航