在一年多之前,我写了一篇博客(博客成长日志 | 准实时访问统计)介绍如何使用百度统计的API实现准实时的访问统计与展示。然而今年百度统计宣布个人版只允许保存一年的数据,而且很多功能会被关闭(例如OS统计等),再加上其API使用也不方便,因此我开始谋求其他的站点统计系统。

与百度统计同类型的竞品还有谷歌统计、51La、CNZZ等,但是这些网站与百度统计也或多或少存在类似类似的问题,同时作为个人小站,也不需要收集过于精细的用户信息(如年龄、详细地区等),所以我开始寻找自建的统计工具。

目前常用的一些开源统计工具可以查看:5 款免费开源的网站流量分析统计工具,在这其中UmamiPlausible是我认为不错的选择,再结合枋柚梓的自建个人网站数据统计分析系统,最终决定采用Umami

Umami也存在问题:

  1. 只记录了 country,无法精确到省份
  2. 地图存在问题,如果使用要避免直接展示地图

介绍

Umami is an open source, privacy-focused alternative to Google Analytics. Umami provides you with a powerful web analytics solution that does not violate the privacy of your users. Additionally, when you self-host Umami you are in complete control of your data.

Umami是一个开源的,关注隐私的谷歌统计替代品,由Nodejs编写,我们可以使用多种方式部署,包括Server自部署、Docker部署以及SeverLess服务部署。

在本文中,我们会在服务器上直接部署服务,使用MySQL作为数据库。

安装

环境要求:

  • Nodejs
  • yarn
  1. 源代码
git clone https://github.com/umami-software/umami.git
cd umami
yarn install
  1. 环境变量
    在目录下创建.env文件,写入内容:
PORT=8000
DATABASE_URL=mysql://username:mypassword@localhost:3306/mydb
  1. 构建与运行
yarn build
yarn start-dev

随后即可在localhost:8000上打开网页,初始默认用户名为admin,密码为umami

统计展示

API

Umami将通过https://<your-umami>/api调用API,并返回json格式的数据。在构建过程中,我们需要使用到以下三个API:

  1. POST /api/auth/login
    通过这个API我们可以获取TOKEN,作为后续API的认证,使用Python获取:
import json
import request
url = "https://<your-umami>/api/auth/login"
data = {
"username": "your-username",
"password": "your-password"
}
res = requests.post(url, data=data)
res = json.loads(res.content)
print(res)
  1. GET /api/website/{id}/pageviews
    通过这个API,我们可以获取选定时段的访问量和访问人数等信息,时间颗粒度可以选择为月、天、小时等。

  2. GET /api/website/{id}/metrics
    通过这个API,我们可以进一步获取国家/地区、来源等信息。

但是由于以下个原因,我们尽量避免直接使用API:

  1. 保密token:使用API需要将token作为请求header,容易暴露
  2. 地图需要转换:Umami中国家/地区使用二字代码存储,而Echarts需要用到其他格式,需要一步转换
  3. 地图问题:涉及香港、澳门、台湾等地

因此最终我们可以自建一个API,使用python的FastAPI库,代码如下:

main.py
import json
import requests
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()
origins = [
"http://localhost",
"example.com"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET"],
allow_headers=["*"],
)

root_path = "https://<your-umami>/api/website/1"
header = {
"accept": "application/json",
"authorization": "Bearer YOUR TOKEN",
"content-type": "application/json"
}

namemap = json.load(open("world.json", "r"))

@app.get("/day_view")
async def root(start_at, end_at):
url = root_path + "/pageviews?start_at={:d}&end_at={:d}&unit=day&tz=Asia/Shanghai".format(
int(start_at),
int(end_at)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
return res

@app.get("/month_view")
async def root(start_at, end_at):
url = root_path + "/pageviews?start_at={:d}&end_at={:d}&unit=month&tz=Asia/Shanghai".format(
int(start_at),
int(end_at)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
return res

@app.get("/referrer")
async def root(start_at, end_at):
url = root_path + "/metrics?start_at={:d}&end_at={:d}&type=referrer".format(
int(start_at),
int(end_at)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
return res

@app.get("/country")
async def root(start_at, end_at):
url = root_path + "/metrics?start_at={:d}&end_at={:d}&type=country".format(
int(start_at),
int(end_at)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
ans = []
for item in res:
name = namemap[item["x"]]
ans.append({
"x": name,
"y": item["y"]
})
china = 0
for k in range(len(ans)):
item = ans[k]
if item["x"] in ["Hong Kong", "Taiwan", "Macau"]:
china += item["y"]
del ans[k]
for item in ans:
if item["x"] == "China":
item["y"] += china
return ans

其中world.json是一个用于国家/地区转换的文件,内容如下:

world.json
{
"ES": "Spain",
"DE": "Germany",
"IE": "Ireland",
"IT": "Italy",
"AT": "Austria",
"FR": "France",
"BE": "Belgium",
"FI": "Finland",
"DK": "Denmark",
"CZ": "Czech Republic",
"EE": "Estonia",
"HU": "Hungaryngary",
"JE": "Jersey",
"LU": "Luxembourg",
"LV": "Latvia",
"MT": "Malta",
"NL": "Netherlands",
"PT": "Portugal",
"RO": "Romania",
"SI": "Slovenia",
"SK": "Slovakia",
"AE": "United Arab Emirates",
"AF": "Afghanistan",
"AL": "Albania",
"AM": "Armenia",
"AO": "Angola",
"AR": "Argentina",
"AU": "Australia",
"AZ": "Azerbaijan",
"BD": "Bangladesh",
"BF": "Burkina Faso",
"BG": "Bulgaria",
"BH": "Bahrein",
"BI": "Burundi",
"BJ": "Benin",
"BN": "Brunei Darussalam",
"BO": "Bolivia",
"BR": "Brazil",
"BW": "Botswana",
"BY": "Byelorussia",
"CA": "Canada",
"CF": "Central Africa",
"CG": "Congo",
"CH": "Switzerland",
"CL": "Chile",
"CM": "Cameroon",
"CN": "China",
"CO": "Colombia",
"CR": "Costa Rica",
"CS": "Czech Repubic",
"CU": "Cuba",
"CY": "Cyprus",
"DO": "Dominican Republic",
"DZ": "Algeria",
"EC": "Ecuador",
"EG": "Egypt",
"ET": "Ethiopia",
"FJ": "Fiji",
"GA": "Gabon",
"GB": "United Kingdom",
"GD": "Grenada",
"GE": "Georgia",
"GH": "Ghana",
"GN": "Guinea",
"GR": "Greece",
"GT": "Guatemala",
"HK": "Hong Kong",
"HN": "Honduras",
"ID": "Indonesia",
"IL": "Israel",
"IN": "India",
"IQ": "Iraq",
"IR": "Iran",
"IS": "Iceland",
"JM": "Jamaica",
"JO": "Jordan",
"JP": "Japan",
"KG": "Kyrgyzstan",
"KH": "Cambodia",
"KP": "Dem. Rep. Korea",
"KR": "Korea",
"KT": "Ivory Coast",
"KW": "Kuwati",
"KZ": "Kazakhstan",
"LA": "Laos",
"LB": "Lebanon",
"LC": "Saint Lueia",
"LI": "Liechtenstein",
"LK": "Sri Lanka",
"LR": "Liberia",
"LT": "Lithuania",
"LY": "Libyan",
"MA": "Morocco",
"MC": "Monaco",
"MD": "Moldova",
"MG": "Madagascar",
"ML": "Mali",
"MM": "Myanmar",
"MN": "Mongolia",
"MO": "Macau",
"MU": "Mauritius",
"MW": "Malawi",
"MX": "Mexico",
"MY": "Malaysia",
"MZ": "Mozambique",
"NA": "Namibia",
"NE": "Niger",
"NG": "Nigeria",
"NI": "Nicaragua",
"NO": "Norway",
"NP": "Nepal",
"NZ": "New Zealand",
"OM": "Oman",
"PA": "Panama",
"PE": "Peru",
"PG": "Papua New Guinea",
"PH": "Philippines",
"PK": "Pakistan",
"PL": "Poland",
"PY": "Paraguay",
"QA": "Qatar",
"RU": "Russian Federation",
"SA": "Saudi Arabia",
"SC": "Republic of Seychelles",
"SD": "Sudan",
"SE": "Sweden",
"SG": "Singapore",
"SM": "San Marino",
"SN": "Senegal",
"SO": "Somalia",
"SY": "Syria",
"SZ": "Swaziland",
"TD": "Chad",
"TG": "Togo",
"TH": "Thailand",
"TJ": "Tajikistan",
"TM": "Turkmenistan",
"TN": "Tunisia",
"TR": "Turkey",
"TW": "Taiwan",
"TZ": "Tanzania",
"UA": "Ukraine",
"UG": "Uganda",
"US": "United States",
"UY": "Uruguay",
"UZ": "Uzbekistan",
"VC": "Saint Vincent",
"VE": "Venezuela",
"VN": "Viet Nam",
"YE": "Yemen",
"YU": "Yugoslavia",
"ZA": "South Africa",
"ZM": "Zambia",
"ZR": "Zaire",
"ZW": "Zimbabwe"
}

数据显示

构建完API后,我们开始改写之前的census.js,内容如下:

census.js
// 访问日历
function generatePieces(maxValue, colorBox) {
var pieces = [];
var quotient = 1;
var temp = {'lt': 1, 'label': '0', 'color': colorBox[0]};
pieces.push(temp);

if (maxValue && maxValue >= 10) {
quotient = Math.floor(maxValue / 10)+1;
for (var i = 1; i <= 10; i++) {
var temp = {};
if (i == 1) temp.gte = 1;
else temp.gte = quotient * (i - 1);
temp.lte = quotient * i;
temp.color = colorBox[i];
pieces.push(temp);
}
}
return JSON.stringify(pieces);
}

function append_div_visitcalendar(parent, text) {
if (parent !== null) {
if (typeof text === 'string') {
var temp = document.createElement('div');
temp.innerHTML = text;
var frag = document.createDocumentFragment();
while (temp.firstChild) {
frag.appendChild(temp.firstChild);
}
parent.appendChild(frag);
} else {
parent.appendChild(text);
}
}
};

function compareFunction(propertyName){
return function (o1, o2) {
//获取比较的值
var v1 = o1[propertyName];
var v2 = o2[propertyName];
return v1 > v2 ? 1 : (v1 == v2 ? 0 : -1);
};
}

function filterTime(time) {
const date = new Date(time)
const Y = date.getFullYear()
const M = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
const D = date.getDate() < 10 ? '0' + (date.getDate()) : date.getDate()
return `${Y}-${M}-${D}`
}

function calChart() {
let script = document.createElement("script");

let now = new Date();
let date = new Date();
date.setFullYear(now.getFullYear() - 1);
let start_at = date.getTime() - 3600 * 24 * ((date.getDay() + 1) % 7);
let end_at = now.getTime();

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/day_view?start_at=' + start_at + '&end_at=' + end_at).then(data => data.json()).then(data => {
data = data.pageviews;
data.sort(compareFunction("t"));

let calArr = [];
let maxValue = 0, total = 0, weekdatacore = 0, thisweekdatacore = 0;
let colorBox = ['#EBEDF0', '#FFE9BB', '#FFD1A7', '#FFBB95', '#FFA383', '#FF8D70', '#FF745C', '#FF5C4A', '#FF4638', '#FF2E26', '#FF1812'];
for (let i = 0; i < data.length; i++) {
if (i > 0) {
let pre = new Date(data[i - 1].t.replace(/-/g, "/"));
let tmp = new Date(data[i].t.replace(/-/g, "/"));
if (tmp.getTime() - pre.getTime() != 86400 * 1000)
for (let k = 1; k < (tmp.getTime() - pre.getTime()) / (86400 * 1000); k++) {
tmp = new Date(pre.getTime() + 86400 * 1000 * k);
calArr.push([filterTime(tmp), 0]);
}
}

calArr.push([data[i].t, data[i].y]);
maxValue = data[i].y > maxValue ? data[i].y : maxValue;
total += data[i].y;
}
if (calArr[calArr.length - 1][0] != filterTime(now)) calArr.push([filterTime(now), 0]);

for (let i = calArr.length - 1; i >= calArr.length - 7; i--) weekdatacore += calArr[i][1];
for (let i = calArr.length - 1; i >= calArr.length - 30; i--) thisweekdatacore += calArr[i][1];
let calArrJson = JSON.stringify(calArr);
script.innerHTML = `
var calChart = echarts.init(document.getElementById("calendar_container"));
var option = {
title: { text: '访问日历', x: 'center' },
tooltip: {
padding: 10,
backgroundColor: '#555',
borderColor: '#777',
borderWidth: 1,
textStyle: { color: '#fff' },
formatter: function (obj) {
var value = obj.value;
return '<div style="font-size: 14px;">' + value[0] + ': ' + value[1] + '</div>';
}
},
visualMap: {
show: false,
showLabel: true,
min: 0,
max: ${maxValue},
type: 'piecewise',
orient: 'horizontal',
left: 'center',
bottom: 0,
pieces: ${generatePieces(maxValue, colorBox)}
},
calendar: [{
left: 'center',
range: ['${calArr[0][0]}', '${calArr[calArr.length - 1][0]}'],
cellSize: [14, 14],
splitLine: {
show: false
},
itemStyle: {
color: '#ebedf0',
borderColor: '#fff',
borderWidth: 2
},
yearLabel: {
show: false
},
monthLabel: {
nameMap: 'cn',
fontSize: 11
},
dayLabel: {
formatter: '{start} 1st',
nameMap: 'cn',
fontSize: 11
}
}],
series: [{
type: 'heatmap',
coordinateSystem: 'calendar',
calendarIndex: 0,
data: ${calArrJson},
}]
};
calChart.setOption(option);`;
let style = '<style>.number{margin-top: 10px;text-align:center;width:100%;padding:10px;margin:0 auto;}.contrib-column{text-align:center;border-left:1px solid #ddd;border-top:1px solid #ddd;}.contrib-column-first{border-left:0;}.table-column{padding:10px;display:table-cell;flex:1;vertical-align:top;}.contrib-number{font-weight:400;line-height:1.3em;font-size:24px;display:block;}.left.text-muted{float:left;margin-left:9px;color:#767676;}.left.text-muted a{color:#4078c0;text-decoration:none;}.left.text-muted a:hover{text-decoration:underline;}h2.f4.text-normal.mb-3{display:none;}.float-left.text-gray{float:left;}.position-relative{width:100%;}@media screen and (max-width:650px){.contrib-column{display:none}}</style>';
style = '<div style="display:flex;width:100%" class="number"><div class="contrib-column contrib-column-first table-column"><span class="text-muted">过去一年访问</span><span class="contrib-number">' + total + '</span><span class="text-muted">' + calArr[0][0] + '&nbsp;-&nbsp;' + calArr[calArr.length - 1][0] + '</span></div><div class="contrib-column table-column"><span class="text-muted">最近30天访问</span><span class="contrib-number">' + thisweekdatacore + '</span><span class="text-muted">' + calArr[calArr.length - 30][0] + '&nbsp;-&nbsp;' + calArr[calArr.length - 1][0] + '</span></div><div class="contrib-column table-column"><span class="text-muted">最近7天访问</span><span class="contrib-number">' + weekdatacore + '</span><span class="text-muted">' + calArr[calArr.length - 7][0] + '&nbsp;-&nbsp;' + calArr[calArr.length - 1][0] + '</span></div></div>' + style;

document.getElementById("calendar_container").after(script);
append_div_visitcalendar(calendar_container, style);
}).catch(function (error) {
console.log(error);
});
}

// 访问地图
function mapChart () {
let script = document.createElement("script");

let now = new Date();
let date = new Date();
// 这个根据自己开始的时间修改
date.setFullYear(2021, 01, 01);
let start_at = date.getTime();
let end_at = now.getTime();

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/country?start_at=' + start_at + '&end_at=' + end_at).then(data => data.json()).then(data => {
let mapArr = [];
let maxValue = 0;
for (let i = 0; i < data.length; i++) {
maxValue = data[i].y > maxValue ? data[i].y : maxValue ;
mapArr.push({ name: data[i].x, value: data[i].y });
}
let mapArrJson = JSON.stringify(mapArr);
script.innerHTML = `
var mapChart = echarts.init(document.getElementById('map_container'), 'light');
var mapOption = {
title: { text: '访问地点(按人数记)', x: 'center' },
tooltip: { trigger: 'item' },
visualMap: {
min: 0,
max: ${maxValue},
left: 'left',
top: 'bottom',
text: ['高','低'],
color: ['#1E90FF', '#AAFAFA'],
calculable: true
},
series: [{
name: '访问人数',
type: 'map',
mapType: 'world',
showLegendSymbol: false,
label: {
emphasis: { show: false }
},
itemStyle: {
normal: {
areaColor: 'rgba(255, 255, 255, 0.1)',
borderColor: '#121212'
},
emphasis: { areaColor: 'gold' }
},
data: ${mapArrJson}
}]
};
mapChart.setOption(mapOption);`;
document.getElementById('map_container').after(script);
}).catch(function (error) {
console.log(error);
});
}

function get_year(s) {
return parseInt(s.substr(0, 4));
}
function get_month(s) {
return parseInt(s.substr(5, 2));
}

// 访问趋势
function trendsChart () {
let script = document.createElement("script");

let now = new Date();
let date = new Date();
// 这个根据自己开始的时间修改
date.setFullYear(2021, 01, 01);
let start_at = date.getTime();
let end_at = now.getTime();

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/month_view?start_at=' + start_at + '&end_at=' + end_at).then(data => data.json()).then(data => {
data = data.pageviews;

let date = new Date();
let monthValueArr = {};
for (let i =2020; i <= date.getFullYear(); i++) monthValueArr[String(i)] = [ , , , , , , , , , , , ];
for (let i = 0; i < data.length; i++) {
let year = get_year(data[i].t);
let month = get_month(data[i].t);
monthValueArr[String(year)][String(month-1)] = data[i].y;
}
script.innerHTML = `
var trendsChart = echarts.init(document.getElementById('trends_container'), 'light');
var trendsOption = {
title: { text: '访问趋势', x: 'center' },
tooltip: { trigger: 'axis' },
legend: { data: ['2021', '2022'], x: 'right' },
xAxis: {
name: '日期', type: 'category', boundaryGap: false,
data: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
},
yAxis: { name: '访问次数', type: 'value' },
series: [
{
name: '2021', type: 'line', smooth: true,
data: [${monthValueArr["2021"]}],
markLine: { data: [{type: 'average', name: '平均值'}] }
},
{
name: '2022', type: 'line', smooth: true,
data: [${monthValueArr["2022"]}],
markLine: { data: [{type: 'average', name: '平均值'}] }
}
]
};
trendsChart.setOption(trendsOption);`;
document.getElementById('trends_container').after(script);
}).catch(function (error) {
console.log(error);
});
}

// 访问来源
function sourcesChart () {
let script = document.createElement("script");
var link = 0, direct = 0, search = 0;
var google = 0, baidu = 0, bing = 0;
var github = 0, travel = 0;

let now = new Date();
let date = new Date();
// 这个根据自己开始的时间修改
date.setFullYear(2021, 01, 01);
let start_at = date.getTime();
let end_at = now.getTime();

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/referrer?start_at=' + start_at + '&end_at=' + end_at).then(data => data.json()).then(data => {
for (let i = 0; i < data.length; i++) {
var ref = data[i].x;
if(ref == "" || ref.includes("foolishfox.cn")) direct += data[i].y;
else if(ref.includes("bing.com")) bing += data[i].y;
else if(ref.includes("baidu.com")) baidu += data[i].y;
else if(ref.includes("google.com")) google += data[i].y;
else if(ref.includes("sogou.com") || ref.includes("sm.cn") || ref.includes("toutiao.com") || ref.includes("so.com"))
search += data[i].y
else if(ref.includes("github.com")) github += data[i].y;
else if(ref.includes("travellings") || ref.includes("foreverblog")) travel += data[i].y;
else link += data[i].y
}

link += github + travel;
search += baidu + google + bing;
script.innerHTML += `
var sourcesChart = echarts.init(document.getElementById('sources_container'), 'light');
var sourcesOption = {
title: { text: '访问来源', x: 'center', },
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
legend: {
data: ['直达', '外链', '搜索', '百度', '谷歌', '必应', 'Github', '开往/十年之约'],
y: 'bottom'
},
series: [
{
name: '来源明细', type: 'pie', radius: ['45%', '60%'],
labelLine: { length: 30 },
label: {
formatter: '{a|{a}}{abg|}\\n{hr|}\\n {b|{b}: }{c} {per|{d}%} ',
backgroundColor: '#F6F8FC', borderColor: '#8C8D8E',
borderWidth: 1, borderRadius: 4,
rich: {
a: { color: '#6E7079', lineHeight: 22, align: 'center' },
hr: { borderColor: '#8C8D8E', width: '100%', borderWidth: 1, height: 0 },
b: { color: '#4C5058', fontSize: 14, fontWeight: 'bold', lineHeight: 33 },
per: { color: '#fff', backgroundColor: '#4C5058', padding: [3, 4], borderRadius: 4 }
}
},
data: [
{value: ${search - google - baidu - bing}, name: '其他', itemStyle: { color : '#008000' }},
{value: ${google}, name: '谷歌', itemStyle: { color : '#009000' }},
{value: ${baidu}, name: '百度', itemStyle: { color : '#00A000' }},
{value: ${bing}, name: '必应', itemStyle: { color : '#00B000' }},
{value: ${direct}, name: '直达', itemStyle: { color : '#FFDB5C' }},
{value: ${github}, name: 'Github', itemStyle: { color : '#10A3C7' }},
{value: ${travel}, name: '开往/十年之约', itemStyle: { color : '#21B4D8' }},
{value: ${link - github - travel}, name: '其他', itemStyle: { color : '#32C5E9' }}
]
},
{
name: '访问来源', type: 'pie', selectedMode: 'single', radius: [0, '30%'],
label: { position: 'inner', fontSize: 14},
labelLine: { show: false },
data: [
{value: ${search}, name: '搜索', itemStyle: { color : '#008000' }},
{value: ${direct}, name: '直达', itemStyle: { color : '#FFDB5C' }},
{value: ${link}, name: '外链', itemStyle: { color : '#32C5E9' }}
]
},
]
};
sourcesChart.setOption(sourcesOption);
window.addEventListener("resize", () => {
calChart.resize();
mapChart.resize();
trendsChart.resize();
sourcesChart.resize();
});`;
}).catch(function (error) {
console.log(error);
});
document.getElementById('sources_container').after(script);
}

if (document.getElementById("calendar_container")) calChart();
if (document.getElementById('map_container')) mapChart();
if (document.getElementById('trends_container')) trendsChart();
if (document.getElementById('sources_container')) sourcesChart();

效果展示

动态展示页面:戳这里

日历图与地图
日历图与地图
访问趋势与来源
访问趋势与来源

V2 版本更新

Umami 更新

本文安装时Umami的版本是1.38.0,目前已经更新到了2.6.2,跨大版本升级需要更新数据库,大致流程如下:

  1. 拉取最新仓库:
git pull
  1. 更新到V1的最新版本1.40.0
git checkout cc2be9f4
yarn install
yarn build
  1. 进行数据迁移
npx @umami/migrate-v1-v2@latest
  1. 升级到V2
git checkout master
yarn install
yarn build

注意在安装sharp包时可能会由于网络下载相关内容超时导致失败,可以在.npmrc中添加:

sharp_binary_host=https://npm.taobao.org/mirrors/sharp
sharp_libvips_binary_host=https://npm.taobao.org/mirrors/sharp-libvips

API程序更新

在升级之后,Umami的API也发生了改变,主要包括:

  1. GET /api/website/{id}/pageviews中的id进行了修改,变成了由数字和字母组成的混合字符串,例如74204a9f-8aca-4adf-9f03-d509f0a07116,此外website变为了websites
  2. API中的时间起止从start_atend_at改为了startAtendAt
  3. API中的时区标识从tz改为timezone
  4. 最长只能获取90天内的每日浏览数据,超过90天将会自动归并为每月的访问数据
  5. 返回数据中时间标识从t改为了x

根据上述更新,我们需要对main.pycensus.js进行修改,示例如下:

  • main.py
...
root_path = "https://<your-umami>/api/websites/{}"
...
@app.get("/day_view")
async def root(startAt, endAt):
url = root_path + "/pageviews?startAt={:d}&endAt={:d}&unit=day&timezone=Asia/Shanghai".format(
int(startAt),
int(endAt)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
return res
...
  • census.js
...
function calChart() {
...
let now = new Date();
let endAt = now.getTime();
let startAt = endAt - 90 * 86400 * 1000;

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/day_view?startAt=' + startAt + '&endAt=' + endAt).then(data => data.json()).then(data => {
data = data.pageviews;
data.sort(compareFunction("x"));
...
let pre = new Date(data[i - 1].x.replace(/-/g, "/"));
let tmp = new Date(data[i].x.replace(/-/g, "/"));
...
calArr.push([data[i].x, data[i].y]);
}
...
}
...

参考资料

  1. Migrating v1 to v2
  2. 一站式解决Node项目中遇到的 诸如sharp: Command failed.或Building fresh packages…始终执行问题