mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
perf: 全栈查询优化、前端缓存去重与页面可见性优化
后端: - SQL count 查询统一改用 func.count() 子查询替代 query.count() - Dashboard/Audit 等页面多次独立查询合并为单次聚合查询 - Provider summary 列表改为批量查询消除 N+1 问题 - DailyStats 逐天循环查询改为 CASE 分桶单次查询 - 使用 load_only() 减少不必要的列加载 - cache_decorator 支持嵌套属性路径解析(dotted vary_by) - 多个管理/公共端点新增 @cache_result 缓存装饰 前端: - cache.ts 新增 in-flight 请求复用、dedupedRequest、buildCacheKey - 大量 API 调用添加前端缓存或去重 - 多个页面定时器在标签页隐藏时暂停、可见时恢复 - Auth 检查从 setInterval 改为 storage + visibilitychange 事件驱动 - 请求竞态防护(requestId 模式) 数据库: - Usage 表新增 idx_usage_status_user_created 复合索引
This commit is contained in:
@@ -144,7 +144,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { Card, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
|
||||
import LineChart from '@/components/charts/LineChart.vue'
|
||||
import { LoadingState, TimeRangePicker } from '@/components/common'
|
||||
@@ -184,6 +184,15 @@ const summaryLoading = ref(false)
|
||||
const series = ref<TimeSeriesItem[]>([])
|
||||
const comparisonSeries = ref<TimeSeriesItem[]>([])
|
||||
const seriesLoading = ref(false)
|
||||
let leaderboardRequestId = 0
|
||||
let summaryRequestId = 0
|
||||
let seriesRequestId = 0
|
||||
let leaderboardLoadPromise: Promise<void> | null = null
|
||||
let hasPendingLeaderboardLoad = false
|
||||
let leaderboardDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let userPanelsLoadPromise: Promise<void> | null = null
|
||||
let hasPendingUserPanelsLoad = false
|
||||
let userPanelsDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function buildTimeRangeParams() {
|
||||
return {
|
||||
@@ -204,6 +213,12 @@ async function loadUsers() {
|
||||
}
|
||||
|
||||
async function loadLeaderboard() {
|
||||
if (leaderboardLoadPromise) {
|
||||
hasPendingLeaderboardLoad = true
|
||||
return leaderboardLoadPromise
|
||||
}
|
||||
leaderboardLoadPromise = (async () => {
|
||||
const requestId = ++leaderboardRequestId
|
||||
leaderboardLoading.value = true
|
||||
try {
|
||||
const response = await adminApi.getLeaderboardUsers({
|
||||
@@ -211,46 +226,90 @@ async function loadLeaderboard() {
|
||||
metric: metric.value,
|
||||
limit: 10
|
||||
})
|
||||
if (requestId !== leaderboardRequestId) return
|
||||
leaderboard.value = response.items
|
||||
} finally {
|
||||
leaderboardLoading.value = false
|
||||
if (requestId === leaderboardRequestId) {
|
||||
leaderboardLoading.value = false
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
leaderboardLoadPromise = null
|
||||
if (hasPendingLeaderboardLoad) {
|
||||
hasPendingLeaderboardLoad = false
|
||||
void loadLeaderboard()
|
||||
}
|
||||
})
|
||||
return leaderboardLoadPromise
|
||||
}
|
||||
|
||||
async function loadSummary() {
|
||||
if (!selectedUserId.value) return
|
||||
const requestId = ++summaryRequestId
|
||||
summaryLoading.value = true
|
||||
try {
|
||||
userSummary.value = await usageApi.getUsageStats({
|
||||
const summary = await usageApi.getUsageStats({
|
||||
...buildTimeRangeParams(),
|
||||
user_id: selectedUserId.value
|
||||
})
|
||||
if (requestId !== summaryRequestId) return
|
||||
userSummary.value = summary
|
||||
} finally {
|
||||
summaryLoading.value = false
|
||||
if (requestId === summaryRequestId) {
|
||||
summaryLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSeries() {
|
||||
if (!selectedUserId.value) return
|
||||
const requestId = ++seriesRequestId
|
||||
seriesLoading.value = true
|
||||
try {
|
||||
series.value = await adminApi.getTimeSeries({
|
||||
const baseParams = {
|
||||
...buildTimeRangeParams(),
|
||||
user_id: selectedUserId.value
|
||||
})
|
||||
|
||||
comparisonSeries.value = []
|
||||
if (compareUserId.value && compareUserId.value !== '__none__') {
|
||||
comparisonSeries.value = await adminApi.getTimeSeries({
|
||||
}
|
||||
const shouldCompare = Boolean(compareUserId.value && compareUserId.value !== '__none__')
|
||||
const comparePromise: Promise<TimeSeriesItem[]> = shouldCompare
|
||||
? adminApi.getTimeSeries({
|
||||
...buildTimeRangeParams(),
|
||||
user_id: compareUserId.value
|
||||
})
|
||||
}
|
||||
: Promise.resolve([])
|
||||
|
||||
const [primarySeries, compareSeries] = await Promise.all([
|
||||
adminApi.getTimeSeries(baseParams),
|
||||
comparePromise
|
||||
])
|
||||
|
||||
if (requestId !== seriesRequestId) return
|
||||
series.value = primarySeries
|
||||
comparisonSeries.value = compareSeries
|
||||
} finally {
|
||||
seriesLoading.value = false
|
||||
if (requestId === seriesRequestId) {
|
||||
seriesLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserPanels() {
|
||||
if (userPanelsLoadPromise) {
|
||||
hasPendingUserPanelsLoad = true
|
||||
return userPanelsLoadPromise
|
||||
}
|
||||
userPanelsLoadPromise = Promise.all([loadSummary(), loadSeries()])
|
||||
.then(() => undefined)
|
||||
.finally(() => {
|
||||
userPanelsLoadPromise = null
|
||||
if (hasPendingUserPanelsLoad) {
|
||||
hasPendingUserPanelsLoad = false
|
||||
void loadUserPanels()
|
||||
}
|
||||
})
|
||||
return userPanelsLoadPromise
|
||||
}
|
||||
|
||||
const seriesChartData = computed(() => ({
|
||||
labels: series.value.map(item => item.date),
|
||||
datasets: [
|
||||
@@ -284,16 +343,52 @@ const comparisonChartData = computed(() => ({
|
||||
]
|
||||
}))
|
||||
|
||||
watch([timeRange, metric], loadLeaderboard, { deep: true })
|
||||
watch([timeRange, selectedUserId, compareUserId], () => {
|
||||
loadSummary()
|
||||
loadSeries()
|
||||
}, { deep: true })
|
||||
function scheduleLeaderboardLoad() {
|
||||
if (leaderboardDebounceTimer) {
|
||||
clearTimeout(leaderboardDebounceTimer)
|
||||
}
|
||||
leaderboardDebounceTimer = setTimeout(() => {
|
||||
leaderboardDebounceTimer = null
|
||||
void loadLeaderboard()
|
||||
}, 120)
|
||||
}
|
||||
|
||||
function scheduleUserPanelsLoad() {
|
||||
if (userPanelsDebounceTimer) {
|
||||
clearTimeout(userPanelsDebounceTimer)
|
||||
}
|
||||
userPanelsDebounceTimer = setTimeout(() => {
|
||||
userPanelsDebounceTimer = null
|
||||
void loadUserPanels()
|
||||
}, 120)
|
||||
}
|
||||
|
||||
watch([timeRange, metric], scheduleLeaderboardLoad, { deep: true })
|
||||
watch([timeRange, selectedUserId, compareUserId], scheduleUserPanelsLoad, { deep: true })
|
||||
|
||||
onMounted(async () => {
|
||||
await loadUsers()
|
||||
await loadLeaderboard()
|
||||
await loadSummary()
|
||||
await loadSeries()
|
||||
await Promise.all([
|
||||
loadLeaderboard(),
|
||||
loadUserPanels()
|
||||
])
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (leaderboardDebounceTimer) {
|
||||
clearTimeout(leaderboardDebounceTimer)
|
||||
leaderboardDebounceTimer = null
|
||||
}
|
||||
if (userPanelsDebounceTimer) {
|
||||
clearTimeout(userPanelsDebounceTimer)
|
||||
userPanelsDebounceTimer = null
|
||||
}
|
||||
hasPendingLeaderboardLoad = false
|
||||
hasPendingUserPanelsLoad = false
|
||||
leaderboardLoadPromise = null
|
||||
userPanelsLoadPromise = null
|
||||
leaderboardRequestId += 1
|
||||
summaryRequestId += 1
|
||||
seriesRequestId += 1
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user