feat(analytics): 在访问统计中增加同比趋势显示

- 在访问概览 API 中新增昨日、上周、上月 PV 字段,用于计算趋势
- 在前端统计卡片中显示今日、本周、本月访问量的同比变化百分比
- 支持 163 邮箱作为邮件通知服务提供商,更新相关文档和设置界面
This commit is contained in:
anghunk
2026-02-05 16:13:05 +08:00
parent 65c0c20040
commit 5a0ecdaa91
6 changed files with 176 additions and 14 deletions

View File

@@ -5,8 +5,11 @@ type VisitOverview = {
totalPv: number;
totalPages: number;
todayPv: number;
yesterdayPv: number;
weekPv: number;
lastWeekPv: number;
monthPv: number;
lastMonthPv: number;
last30Days: {
date: string;
total: number;
@@ -101,9 +104,31 @@ export const getVisitOverview = async (
const monthStartDate = new Date(Date.UTC(year, month, 1));
const monthStartKey = toKey(monthStartDate);
// Calculate date ranges for last month and last week queries
const lastMonthStartDate = new Date(Date.UTC(year, month - 1, 1));
const lastMonthEndDate = new Date(monthStartDate.getTime() - 24 * 60 * 60 * 1000);
// For last week, we need the start date of last week.
// weekStartDate is the start of current week.
// So lastWeekStartDate is weekStartDate - 7 days.
const weekStartDate = (() => {
const d = new Date(Date.UTC(year, month, day));
const weekday = d.getUTCDay();
const offset = (weekday + 6) % 7;
return new Date(d.getTime() - offset * 24 * 60 * 60 * 1000);
})();
const lastWeekStartDate = new Date(weekStartDate.getTime() - 7 * 24 * 60 * 60 * 1000);
const lastWeekEndDate = new Date(weekStartDate.getTime() - 24 * 60 * 60 * 1000);
// We need to fetch enough history for last month.
// The earliest date could be either startDate30 or lastMonthStartDate.
let earliestDate = startDate30;
if (monthStartKey < earliestDate) {
earliestDate = monthStartKey;
if (toKey(lastMonthStartDate) < earliestDate) {
earliestDate = toKey(lastMonthStartDate);
}
if (toKey(lastWeekStartDate) < earliestDate) {
earliestDate = toKey(lastWeekStartDate);
}
let dailySql =
@@ -140,28 +165,39 @@ export const getVisitOverview = async (
}
const todayKey = toKey(now);
const yesterdayKey = toKey(new Date(now.getTime() - 24 * 60 * 60 * 1000));
const weekStartDate = (() => {
const d = new Date(Date.UTC(year, month, day));
const weekday = d.getUTCDay();
const offset = (weekday + 6) % 7;
return new Date(d.getTime() - offset * 24 * 60 * 60 * 1000);
})();
// weekStartDate is already calculated above
const weekStartKey = toKey(weekStartDate);
let todayPv = dailyMap.get(todayKey) || 0;
let yesterdayPv = dailyMap.get(yesterdayKey) || 0;
let weekPv = 0;
let lastWeekPv = 0;
let monthPv = 0;
let lastMonthPv = 0;
// Calculate Week PV
{
let cursor = new Date(weekStartDate.getTime());
while (cursor.getTime() <= now.getTime()) {
const key = toKey(cursor);
weekPv += dailyMap.get(key) || 0;
cursor = new Date(cursor.getTime() - 0 + 24 * 60 * 60 * 1000);
cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000);
}
}
// Calculate Last Week PV
{
let cursor = new Date(lastWeekStartDate.getTime());
while (cursor.getTime() <= lastWeekEndDate.getTime()) {
const key = toKey(cursor);
lastWeekPv += dailyMap.get(key) || 0;
cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000);
}
}
// Calculate Month PV
{
let cursor = new Date(monthStartDate.getTime());
while (cursor.getTime() <= now.getTime()) {
@@ -171,6 +207,16 @@ export const getVisitOverview = async (
}
}
// Calculate Last Month PV
{
let cursor = new Date(lastMonthStartDate.getTime());
while (cursor.getTime() <= lastMonthEndDate.getTime()) {
const key = toKey(cursor);
lastMonthPv += dailyMap.get(key) || 0;
cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000);
}
}
if (todayPv > totalPv) {
todayPv = totalPv;
}
@@ -195,8 +241,11 @@ export const getVisitOverview = async (
totalPv,
totalPages,
todayPv,
yesterdayPv,
weekPv,
lastWeekPv,
monthPv,
lastMonthPv,
last30Days
};