perf(frontend): add api cache layer and reduce report polling pressure

This commit is contained in:
2026-02-07 18:36:55 +08:00
parent c285d1e348
commit 4874aa37f6
38 changed files with 205 additions and 131 deletions

View File

@@ -1,7 +1,11 @@
import { api } from './client'
import { createCachedGetter } from './cache'
export async function fetchBrowserPoolStats() {
const browserPoolStatsGetter = createCachedGetter(async () => {
const { data } = await api.get('/browser_pool/stats')
return data
}
}, 4_000)
export async function fetchBrowserPoolStats(options = {}) {
return browserPoolStatsGetter.run(options)
}

View File

@@ -0,0 +1,46 @@
export function createCachedGetter(fetcher, ttlMs = 0) {
let hasValue = false
let cachedValue = null
let expiresAt = 0
let inflight = null
async function run(options = {}) {
const force = Boolean(options?.force)
const now = Date.now()
if (!force && hasValue && now < expiresAt) {
return cachedValue
}
if (!force && inflight) {
return inflight
}
inflight = Promise.resolve()
.then(() => fetcher())
.then((data) => {
cachedValue = data
hasValue = true
const ttl = Math.max(0, Number(ttlMs) || 0)
expiresAt = Date.now() + ttl
return data
})
.finally(() => {
inflight = null
})
return inflight
}
function clear() {
hasValue = false
cachedValue = null
expiresAt = 0
inflight = null
}
return {
run,
clear,
}
}

View File

@@ -1,4 +1,10 @@
import { api } from './client'
import { createCachedGetter } from './cache'
const emailStatsGetter = createCachedGetter(async () => {
const { data } = await api.get('/email/stats')
return data
}, 10_000)
export async function fetchEmailSettings() {
const { data } = await api.get('/email/settings')
@@ -7,12 +13,12 @@ export async function fetchEmailSettings() {
export async function updateEmailSettings(payload) {
const { data } = await api.post('/email/settings', payload)
emailStatsGetter.clear()
return data
}
export async function fetchEmailStats() {
const { data } = await api.get('/email/stats')
return data
export async function fetchEmailStats(options = {}) {
return emailStatsGetter.run(options)
}
export async function fetchEmailLogs(params) {
@@ -22,6 +28,6 @@ export async function fetchEmailLogs(params) {
export async function cleanupEmailLogs(days) {
const { data } = await api.post('/email/logs/cleanup', { days })
emailStatsGetter.clear()
return data
}

View File

@@ -1,36 +1,40 @@
import { api } from './client'
import { createCachedGetter } from './cache'
let _feedbackStatsInflight = null
const FEEDBACK_STATS_TTL_MS = 10_000
const feedbackStatsGetter = createCachedGetter(async () => {
const { data } = await api.get('/feedbacks', { params: { limit: 1, offset: 0 } })
return data?.stats
}, FEEDBACK_STATS_TTL_MS)
export async function fetchFeedbacks(status = '') {
const { data } = await api.get('/feedbacks', { params: status ? { status } : {} })
return data
}
export async function fetchFeedbackStats() {
if (_feedbackStatsInflight) return _feedbackStatsInflight
export async function fetchFeedbackStats(options = {}) {
return feedbackStatsGetter.run(options)
}
_feedbackStatsInflight = api
.get('/feedbacks', { params: { limit: 1, offset: 0 } })
.then(({ data }) => data?.stats)
.finally(() => {
_feedbackStatsInflight = null
})
return _feedbackStatsInflight
export function clearFeedbackStatsCache() {
feedbackStatsGetter.clear()
}
export async function replyFeedback(feedbackId, reply) {
const { data } = await api.post(`/feedbacks/${feedbackId}/reply`, { reply })
clearFeedbackStatsCache()
return data
}
export async function closeFeedback(feedbackId) {
const { data } = await api.post(`/feedbacks/${feedbackId}/close`)
clearFeedbackStatsCache()
return data
}
export async function deleteFeedback(feedbackId) {
const { data } = await api.delete(`/feedbacks/${feedbackId}`)
clearFeedbackStatsCache()
return data
}

View File

@@ -1,42 +1,17 @@
import { api } from './client'
import { createCachedGetter } from './cache'
const _statsCache = {
value: null,
expiresAt: 0,
inflight: null,
}
const SYSTEM_STATS_TTL_MS = 15_000
const SYSTEM_STATS_TTL_MS = 2000
const systemStatsGetter = createCachedGetter(async () => {
const { data } = await api.get('/stats')
return data
}, SYSTEM_STATS_TTL_MS)
export async function fetchSystemStats(options = {}) {
const force = Boolean(options?.force)
const now = Date.now()
if (!force && _statsCache.value && now < _statsCache.expiresAt) {
return _statsCache.value
}
if (!force && _statsCache.inflight) {
return _statsCache.inflight
}
const request = api
.get('/stats')
.then(({ data }) => {
_statsCache.value = data
_statsCache.expiresAt = Date.now() + SYSTEM_STATS_TTL_MS
return data
})
.finally(() => {
_statsCache.inflight = null
})
_statsCache.inflight = request
return request
return systemStatsGetter.run(options)
}
export function clearSystemStatsCache() {
_statsCache.value = null
_statsCache.expiresAt = 0
_statsCache.inflight = null
systemStatsGetter.clear()
}

View File

@@ -1,12 +1,18 @@
import { api } from './client'
import { createCachedGetter } from './cache'
export async function fetchSystemConfig() {
const systemConfigGetter = createCachedGetter(async () => {
const { data } = await api.get('/system/config')
return data
}, 15_000)
export async function fetchSystemConfig(options = {}) {
return systemConfigGetter.run(options)
}
export async function updateSystemConfig(payload) {
const { data } = await api.post('/system/config', payload)
systemConfigGetter.clear()
return data
}
@@ -14,4 +20,3 @@ export async function executeScheduleNow() {
const { data } = await api.post('/schedule/execute', {})
return data
}

View File

@@ -1,33 +1,58 @@
import { api } from './client'
import { createCachedGetter } from './cache'
export async function fetchServerInfo() {
const serverInfoGetter = createCachedGetter(async () => {
const { data } = await api.get('/server/info')
return data
}
}, 30_000)
export async function fetchDockerStats() {
const dockerStatsGetter = createCachedGetter(async () => {
const { data } = await api.get('/docker_stats')
return data
}
}, 8_000)
export async function fetchRequestMetrics() {
const requestMetricsGetter = createCachedGetter(async () => {
const { data } = await api.get('/request_metrics')
return data
}
}, 10_000)
export async function fetchSlowSqlMetrics() {
const slowSqlMetricsGetter = createCachedGetter(async () => {
const { data } = await api.get('/slow_sql_metrics')
return data
}
}, 10_000)
export async function fetchTaskStats() {
const taskStatsGetter = createCachedGetter(async () => {
const { data } = await api.get('/task/stats')
return data
}
}, 4_000)
export async function fetchRunningTasks() {
const runningTasksGetter = createCachedGetter(async () => {
const { data } = await api.get('/task/running')
return data
}, 2_000)
export async function fetchServerInfo(options = {}) {
return serverInfoGetter.run(options)
}
export async function fetchDockerStats(options = {}) {
return dockerStatsGetter.run(options)
}
export async function fetchRequestMetrics(options = {}) {
return requestMetricsGetter.run(options)
}
export async function fetchSlowSqlMetrics(options = {}) {
return slowSqlMetricsGetter.run(options)
}
export async function fetchTaskStats(options = {}) {
return taskStatsGetter.run(options)
}
export async function fetchRunningTasks(options = {}) {
return runningTasksGetter.run(options)
}
export async function fetchTaskLogs(params) {
@@ -37,5 +62,7 @@ export async function fetchTaskLogs(params) {
export async function clearOldTaskLogs(days) {
const { data } = await api.post('/task/logs/clear', { days })
taskStatsGetter.clear()
runningTasksGetter.clear()
return data
}

View File

@@ -25,11 +25,8 @@ const stats = ref({})
const adminUsername = computed(() => stats.value?.admin_username || '')
async function refreshStats() {
try {
stats.value = await fetchSystemStats()
} finally {
}
async function refreshStats(options = {}) {
stats.value = await fetchSystemStats(options)
}
const loadingBadges = ref(false)

View File

@@ -550,9 +550,17 @@ const mobileModules = computed(() => [
},
])
const REPORT_SYNC_STATS_EVERY = 3
let reportRefreshTick = 0
async function refreshAll(options = {}) {
const showLoading = options.showLoading ?? true
if (refreshing.value) return
const forceStatsSync = Boolean(options.forceStatsSync)
const shouldSyncStats = forceStatsSync || reportRefreshTick % REPORT_SYNC_STATS_EVERY === 0
reportRefreshTick += 1
refreshing.value = true
if (showLoading) {
loading.value = true
@@ -593,7 +601,9 @@ async function refreshAll(options = {}) {
if (slowSqlResult.status === 'fulfilled') slowSqlMetrics.value = slowSqlResult.value
if (configResult.status === 'fulfilled') systemConfig.value = configResult.value
await refreshStats?.()
if (shouldSyncStats) {
await refreshStats?.({ force: forceStatsSync })
}
recordUpdatedAt()
} finally {
refreshing.value = false
@@ -637,7 +647,7 @@ function onVisibilityChange() {
}
onMounted(() => {
refreshAll({ showLoading: false })
refreshAll({ showLoading: false, forceStatsSync: true })
.catch(() => {})
.finally(() => {
scheduleRefreshLoop()

View File

@@ -72,7 +72,7 @@ async function onEnableUser(row) {
await approveUser(row.id)
ElMessage.success('用户已启用')
await loadUsers()
await refreshStats?.()
await refreshStats?.({ force: true })
} catch {
// handled by interceptor
}
@@ -93,7 +93,7 @@ async function onDisableUser(row) {
await rejectUser(row.id)
ElMessage.success('用户已禁用')
await loadUsers()
await refreshStats?.()
await refreshStats?.({ force: true })
} catch {
// handled by interceptor
}
@@ -114,7 +114,7 @@ async function onDelete(row) {
await deleteUser(row.id)
ElMessage.success('用户已删除')
await loadUsers()
await refreshStats?.()
await refreshStats?.({ force: true })
} catch {
// handled by interceptor
}
@@ -136,7 +136,7 @@ async function onSetVip(row, days) {
const res = await setUserVip(row.id, days)
ElMessage.success(res?.message || 'VIP设置成功')
await loadUsers()
await refreshStats?.()
await refreshStats?.({ force: true })
} catch {
// handled by interceptor
}
@@ -157,7 +157,7 @@ async function onRemoveVip(row) {
const res = await removeUserVip(row.id)
ElMessage.success(res?.message || 'VIP已移除')
await loadUsers()
await refreshStats?.()
await refreshStats?.({ force: true })
} catch {
// handled by interceptor
}