前言

51La挂了好久好久了,之前备考,一直没修,最近看百度统计 PV 数据达到了开通 API 的标准,正好得空给它换成了百度统计。因为有跨域问题,所以还是做了个 API 从后端获取。

本来想直接用下面洪哥这篇文章的代码,不过是用 PHP 写的,针对 docker 环境部署,采用 json 文件存储 tokens 数据,而 vercel 的文件都是只读的,所以还是得改用数据库存储 tokens 数据。

大体思路和之前三篇文章类似,采用 supbase 数据库存储 access_token、refresh_token 和 tokens 时效信息,当 tokens 时间超过20天(百度统计官方文档规定 access_token 有效期30天),服务端自动通过百度统计 API 刷新 tokens 数据。

前端采用 localStorage 缓存访问数据,有效期3小时,初次获取数据后将存入缓存,有效期内直接从缓存读取,超时则重新拉取,同时刷新缓存。

首先阅读百度账号接口说明,手动依次获取 siteId、code、access_token、refresh_token。可通过Tongji API调试工具调试。

注意 Vercel 部署时添加 API_KEY、SECRET_KEY、SITE_ID 到环境变量。

代码

  • 后端 index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// 导入所需包和模块
const axios = require('axios');
const express = require('express');
const {
startOfWeek,
startOfMonth,
startOfYear,
differenceInDays,
format,
} = require('date-fns');// 时间处理依赖包
const path = require('path');
const app = express();
app.use(function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
// 创建 Express 应用程序
app.use(express.json());
// 连接数据库
const { createClient } = require('@supabase/supabase-js');
const supabaseUrl = '你的数据库地址';
const supabaseKey = process.env.SUPABASE_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);

// 百度统计变量
const apiKey = process.env.API_KEY || '';
const secretKey = process.env.SECRET_KEY || '';
const siteId = process.env.SITE_ID || '';
let access_token = null;
let refresh_token = null;

// 百度统计接口
app.get('/baidupv', async (req, res) => {
const { data: tokenData, error } = await supabase
.from('PVTokens')
.select()
.eq('name', 'baidu')
if (error) {
console.error('Database Error:', error);
res.json({ error: 'database error' })
} else {
const today = new Date();
const todayFormat = format(today, 'yyyyMMdd');
let latestTime = tokenData[0].time; // Tokens 生成时间
let passTime = differenceInDays(today, latestTime); // Tokens 时效
let data = {
today_uv: null,
today_pv: null,
yesterday_uv: null,
yesterday_pv: null,
last_week_uv: null,
last_week_pv: null,
last_month_uv: null,
last_month_pv: null,
last_year_uv: null,
last_year_pv: null,
total_uv: null,
total_pv: null
};
if (passTime < 20) { // 小于 20 天直接返回
access_token = tokenData[0].tokens.access_token;
} else {
refresh_token = tokenData[0].tokens.refresh_token;
const newTokens = await refreshAccessToken(apiKey, secretKey, refresh_token); // 大于 20 天重新获取 tokens
access_token = newTokens.access_token;
refresh_token = newTokens.refresh_token;
const { data, error } = await supabase // 刷新数据库 tokens 信息
.from('PVTokens')
.update({ tokens: {"access_token": access_token, "refresh_token": refresh_token}, time: today })
.eq('name', 'baidu')
.select()
}
const totalData = await getData('20250403', todayFormat, 'pv_count,visitor_count', access_token, siteId);
if (totalData && totalData.result && totalData.result.items && totalData.result.items[1]) {
const dataPoints = totalData.result.items[1];
data.today_pv = dataPoints.slice(-2)[1][0]; // 今日访问量
data.today_uv = dataPoints.slice(-2)[1][1]; // 今日访客量
data.yesterday_pv = dataPoints.slice(-2)[0][0]; // 昨日访问量
data.yesterday_uv = dataPoints.slice(-2)[0][1]; // 昨日访客量
const daysPassedInWeek = differenceInDays(today, startOfWeek(today, { weekStartsOn: 1 })); // 计算本周已过天数(周一作为周起始日)
const daysPassedInMonth = differenceInDays(today, startOfMonth(today)); // 计算本月已过天数
const daysPassedInYear = differenceInDays(today, startOfYear(today)); // 计算本年已过天数
dataPoints.slice().reverse().forEach((point, index) => { // 倒序遍历,计算时间
var pv = point[0] == "--" ? 0 : point[0];
var uv = point[1] == "--" ? 0 : point[1]; // 访问量为0时API返回的是"--",需要替换成0
data.total_pv += pv; // 累加页面访问量
data.total_uv += uv; // 累加页面访客量
if(index < daysPassedInWeek) {
data.last_week_pv += pv; // 累加本周访问量
data.last_week_uv += uv; // 累加本周访客量
}
if(index < daysPassedInMonth) {
data.last_month_pv += pv; // 累加本月访问量
data.last_month_uv += uv; // 累加本月访客量
}
if(index < daysPassedInYear) {
data.last_year_pv += pv; // 累加本年访问量
data.last_year_uv += uv; // 累加本年访客量
}
});
}
res.json(data)
}
})

// 百度统计获取 PV 数据
async function getData(startDate, endDate, metrics, accessToken, siteId) {
const url = "https://openapi.baidu.com/rest/2.0/tongji/report/getData";
const params = new URLSearchParams({
access_token: accessToken,
site_id: siteId,
method: 'overview/getTimeTrendRpt',
start_date: startDate,
end_date: endDate,
metrics: metrics
});
try {
const response = await axios.get(url, { params });
return response.data;
} catch (error) {
console.error('Error fetching PV data:', error);
throw new Error('Error fetching PV data.');
}
}

// 百度统计刷新 Access Token
async function refreshAccessToken(apiKey, secretKey, refreshToken) {
const url = "https://openapi.baidu.com/oauth/2.0/token";
const params = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: apiKey,
client_secret: secretKey
});
try {
const response = await axios.get(url, { params });
return response.data;
} catch (error) {
console.error('Error refreshing access token:', error);
throw new Error('Error refreshing access token.');
}
}

// 启动服务器
const server = app.listen(process.env.PORT || 3000, () => {
const port = server.address().port;
console.log(`Server is running on port ${port}`);
});
  • 前端代码

可用 script 嵌入 about.pug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
let latestTime = (new Date).getTime();
if (localStorage.getItem('BlogPV') == null || localStorage.getItem('BlogPVTime') == null || (latestTime - localStorage.getItem('BlogPVTime')) > 10800000) { // 3小时时效
fetch('https://apis.cancin.cn/baidupv')
.then(response => response.json())
.then(data => {
localStorage.setItem('BlogPV', JSON.stringify(data));
localStorage.setItem('BlogPVTime', latestTime);
statisticShow(data);
console.log('BlogPV数据已更新');
})
.catch(error => {
console.error('获取 PV 失败:', error);
});
} else {
BlogPV = JSON.parse(localStorage.getItem('BlogPV'));
statisticShow(BlogPV);
console.log('BlogPV数据已过时'+(latestTime - localStorage.getItem('BlogPVTime'))+'ms');
}
function statisticShow(BlogPV) {
document.getElementById('statistic').innerHTML = `
<div>
<span>今日人数</span>
<span>${BlogPV.today_uv}</span>
</div>
<div>
<span>今日访问</span>
<span>${BlogPV.today_pv}</span>
</div>
<div>
<span>本周访问</span>
<span>${BlogPV.last_week_pv}</span>
</div>
<div>
<span>本月访问</span>
<span>${BlogPV.last_month_pv}</span>
</div>
<div>
<span>总访问量</span>
<span>${BlogPV.total_pv}</span>
</div>
`
}