feat: add user download traffic reports and restore OSS direct downloads

This commit is contained in:
2026-02-17 17:36:26 +08:00
parent 7687397954
commit 3a22b88f23
4 changed files with 600 additions and 8 deletions

View File

@@ -62,7 +62,7 @@ function clearOssUsageCache(userId) {
console.log(`[OSS缓存] 已清除: 用户 ${userId}`);
}
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB, TransactionDB, WalManager } = require('./database');
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, DownloadTrafficReportDB, SystemLogDB, TransactionDB, WalManager } = require('./database');
const StorageUsageCache = require('./utils/storage-cache');
const { JWT_SECRET, generateToken, generateRefreshToken, refreshAccessToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth');
const { StorageInterface, LocalStorageClient, OssStorageClient, formatFileSize, formatOssError } = require('./storage');
@@ -691,6 +691,31 @@ function formatDateTimeForSqlite(date = new Date()) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
function getDateKeyFromDate(date = new Date()) {
const target = date instanceof Date ? date : new Date(date);
if (Number.isNaN(target.getTime())) {
return null;
}
const year = target.getFullYear();
const month = String(target.getMonth() + 1).padStart(2, '0');
const day = String(target.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function getRecentDateKeys(days = 30, now = new Date()) {
const safeDays = Math.max(1, Math.floor(Number(days) || 30));
const keys = [];
for (let i = safeDays - 1; i >= 0; i -= 1) {
const date = new Date(now.getTime());
date.setDate(date.getDate() - i);
const key = getDateKeyFromDate(date);
if (key) {
keys.push(key);
}
}
return keys;
}
function getNextDownloadResetTime(lastResetAt, resetCycle) {
const baseDate = parseDateTimeValue(lastResetAt);
if (!baseDate) {
@@ -847,11 +872,16 @@ const applyDownloadTrafficUsageTransaction = db.transaction((userId, bytesToAdd)
UserDB.update(userId, { download_traffic_used: nextUsed });
const addedBytes = Math.max(0, nextUsed - trafficState.used);
if (addedBytes > 0) {
DownloadTrafficReportDB.addUsage(userId, addedBytes, 1, new Date());
}
return {
quota: trafficState.quota,
usedBefore: trafficState.used,
usedAfter: nextUsed,
added: Math.max(0, nextUsed - trafficState.used)
added: addedBytes
};
});
@@ -2526,6 +2556,112 @@ app.get('/api/user/profile', authMiddleware, (req, res) => {
});
});
// 获取用户下载流量额度与报表
app.get('/api/user/download-traffic-report', authMiddleware, (req, res) => {
try {
const days = DownloadTrafficReportDB.normalizeDays(req.query.days, 30, 365);
const policyState = enforceDownloadTrafficPolicy(req.user.id, 'traffic_report');
const latestUser = policyState?.user || UserDB.findById(req.user.id);
if (!latestUser) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
const trafficState = getDownloadTrafficState(latestUser);
const dailyRows = DownloadTrafficReportDB.getDailyUsage(latestUser.id, days);
const dailyRowMap = new Map(
dailyRows.map(row => [row.date_key, row])
);
const dateKeys = getRecentDateKeys(days, new Date());
const dailySeries = dateKeys.map(dateKey => {
const row = dailyRowMap.get(dateKey);
const bytesUsed = Number(row?.bytes_used || 0);
const downloadCount = Number(row?.download_count || 0);
return {
date: dateKey,
bytes_used: Number.isFinite(bytesUsed) && bytesUsed > 0 ? Math.floor(bytesUsed) : 0,
download_count: Number.isFinite(downloadCount) && downloadCount > 0 ? Math.floor(downloadCount) : 0
};
});
const selectedSummary = DownloadTrafficReportDB.getPeriodSummary(latestUser.id, days);
const todaySummary = DownloadTrafficReportDB.getPeriodSummary(latestUser.id, 1);
const sevenDaySummary = DownloadTrafficReportDB.getPeriodSummary(latestUser.id, 7);
const thirtyDaySummary = DownloadTrafficReportDB.getPeriodSummary(latestUser.id, 30);
const allSummary = DownloadTrafficReportDB.getPeriodSummary(latestUser.id, null);
const peakDay = dailySeries.reduce((peak, current) => {
if (!peak || current.bytes_used > peak.bytes_used) {
return current;
}
return peak;
}, null);
const safeQuota = trafficState.quota > 0 ? trafficState.quota : 0;
const remainingBytes = safeQuota > 0 ? Math.max(0, safeQuota - trafficState.used) : null;
const usagePercentage = safeQuota > 0
? Math.min(100, Math.round((trafficState.used / safeQuota) * 100))
: null;
res.json({
success: true,
quota: {
quota: safeQuota,
used: trafficState.used,
remaining: remainingBytes,
usage_percentage: usagePercentage,
is_unlimited: trafficState.isUnlimited,
reset_cycle: latestUser.download_traffic_reset_cycle || 'none',
expires_at: latestUser.download_traffic_quota_expires_at || null,
last_reset_at: latestUser.download_traffic_last_reset_at || null
},
report: {
days,
daily: dailySeries,
summary: {
today: {
bytes_used: Number(todaySummary?.bytes_used || 0),
download_count: Number(todaySummary?.download_count || 0)
},
last_7_days: {
bytes_used: Number(sevenDaySummary?.bytes_used || 0),
download_count: Number(sevenDaySummary?.download_count || 0)
},
last_30_days: {
bytes_used: Number(thirtyDaySummary?.bytes_used || 0),
download_count: Number(thirtyDaySummary?.download_count || 0)
},
selected_range: {
bytes_used: Number(selectedSummary?.bytes_used || 0),
download_count: Number(selectedSummary?.download_count || 0),
average_daily_bytes: Math.round(Number(selectedSummary?.bytes_used || 0) / Math.max(1, days))
},
all_time: {
bytes_used: Number(allSummary?.bytes_used || 0),
download_count: Number(allSummary?.download_count || 0)
},
peak_day: peakDay
? {
date: peakDay.date,
bytes_used: peakDay.bytes_used,
download_count: peakDay.download_count
}
: null
}
}
});
} catch (error) {
console.error('获取下载流量报表失败:', error);
res.status(500).json({
success: false,
message: '获取下载流量报表失败'
});
}
});
// 获取用户主题偏好(包含全局默认主题)
app.get('/api/user/theme', authMiddleware, (req, res) => {
try {