fix(usage): session touch 独立提交避免行锁阻塞 & 管理员页面顺序加载降低并发压力

后端: 将 session touch 的 commit 从请求事务中分离,防止管理员 usage
页面的长查询持有 user_sessions 行锁阻塞后续请求。touch_session 改为
返回 bool 以支持按需提交。

前端: 管理员 Usage 页面将并行 API 调用改为顺序加载,优先显示记录表格,
统计面板在后台异步刷新,避免瞬时并发打满后端 worker。loadRecords 支持
传入 dateRange 参数确保时间范围一致性。
This commit is contained in:
fawney19
2026-03-18 00:12:00 +08:00
parent eeb5f41bad
commit 684689a82b
6 changed files with 373 additions and 57 deletions

View File

@@ -77,13 +77,8 @@ export function useUsageData(options: UseUsageDataOptions) {
try {
if (isAdminPage.value) {
// 管理员页面,并行加载统计数据
const [statsData, modelData, providerData, apiFormatData] = await Promise.all([
usageApi.getUsageStats(dateRange),
usageApi.getUsageByModel(dateRange),
usageApi.getUsageByProvider(dateRange),
usageApi.getUsageByApiFormat(dateRange)
])
// 管理员页面顺序加载统计数据,避免刷新使用记录时瞬时打满后端 worker。
const statsData = await usageApi.getUsageStats(dateRange)
if (requestId !== loadStatsRequestId) {
return
@@ -104,6 +99,11 @@ export function useUsageData(options: UseUsageDataOptions) {
period_end: '',
}
const modelData = await usageApi.getUsageByModel(dateRange)
if (requestId !== loadStatsRequestId) {
return
}
modelStats.value = modelData.map(item => {
const raw = item as Record<string, unknown>
return {
@@ -120,6 +120,11 @@ export function useUsageData(options: UseUsageDataOptions) {
}
})
const providerData = await usageApi.getUsageByProvider(dateRange)
if (requestId !== loadStatsRequestId) {
return
}
providerStats.value = providerData.map(item => ({
provider: item.provider,
requests: item.request_count,
@@ -137,6 +142,11 @@ export function useUsageData(options: UseUsageDataOptions) {
: '-'
}))
const apiFormatData = await usageApi.getUsageByApiFormat(dateRange)
if (requestId !== loadStatsRequestId) {
return
}
apiFormatStats.value = apiFormatData.map(item => ({
api_format: item.api_format,
request_count: item.request_count || 0,
@@ -255,19 +265,24 @@ export function useUsageData(options: UseUsageDataOptions) {
// 加载记录(真正的后端分页)
async function loadRecords(
pagination: PaginationParams,
filters?: FilterParams
filters?: FilterParams,
dateRange?: DateRangeParams
): Promise<void> {
const requestId = ++loadRecordsRequestId
isLoadingRecords.value = true
try {
const offset = (pagination.page - 1) * pagination.pageSize
const effectiveDateRange = dateRange ?? currentDateRange.value
if (dateRange) {
currentDateRange.value = dateRange
}
// 构建请求参数
const params: Record<string, unknown> = {
limit: pagination.pageSize,
offset,
...currentDateRange.value
...effectiveDateRange
}
// 添加筛选条件

View File

@@ -196,6 +196,9 @@ const {
const activityHeatmapData = ref<ActivityHeatmap | null>(null)
const isLoadingHeatmap = ref(false)
const heatmapError = ref(false)
const ADMIN_ANALYTICS_REFRESH_INTERVAL = 60000
let adminAnalyticsRefreshInFlight: Promise<void> | null = null
let lastAdminAnalyticsRefreshAt = 0
// 加载热力图数据
async function loadHeatmapData() {
@@ -215,6 +218,57 @@ async function loadHeatmapData() {
}
}
async function loadAdminUsers() {
try {
const users = await usersApi.getAllUsers()
availableUsers.value = users.map(u => ({ id: u.id, username: u.username, email: u.email }))
} catch (error) {
log.error('加载用户列表失败:', error)
}
}
async function refreshAdminAnalytics(options: { force?: boolean } = {}) {
if (!isAdminPage.value) return
if (!options.force && !isPageVisible.value) return
const now = Date.now()
if (!options.force && now - lastAdminAnalyticsRefreshAt < ADMIN_ANALYTICS_REFRESH_INTERVAL) {
return
}
if (adminAnalyticsRefreshInFlight) {
return adminAnalyticsRefreshInFlight
}
adminAnalyticsRefreshInFlight = (async () => {
let hasSuccessfulRefresh = false
try {
await loadStats(timeRange.value)
hasSuccessfulRefresh = true
} catch (error) {
log.error('加载统计数据失败:', error)
warning('统计数据加载失败,请刷新重试')
}
try {
await loadHeatmapData()
hasSuccessfulRefresh = true
} catch (error) {
log.error('加载热力图数据失败:', error)
}
if (hasSuccessfulRefresh) {
lastAdminAnalyticsRefreshAt = Date.now()
}
})()
try {
await adminAnalyticsRefreshInFlight
} finally {
adminAnalyticsRefreshInFlight = null
}
}
// 用户页面需要前端筛选
const filteredRecords = computed(() => {
if (!isAdminPage.value) {
@@ -488,32 +542,29 @@ const selectedRequestId = ref<string | null>(null)
onMounted(async () => {
document.addEventListener('visibilitychange', handleVisibilityChange)
// 所有数据源并行加载stats/heatmap/records/users 之间没有数据依赖)
const statsTask = loadStats(timeRange.value).catch(err => {
log.error('加载统计数据失败:', err)
warning('统计数据加载失败,请刷新重试')
})
const heatmapTask = loadHeatmapData().catch(err => {
log.error('加载热力图数据失败:', err)
})
const tasks: Promise<unknown>[] = [statsTask, heatmapTask]
if (isAdminPage.value) {
// 管理员页面stats 和 records 分开加载(后端分页)
tasks.push(loadRecords(
// 管理员页面优先加载记录,统计面板在后台顺序刷新,避免瞬时并发打满后端。
await loadRecords(
{ page: currentPage.value, pageSize: pageSize.value },
getCurrentFilters()
))
tasks.push(
usersApi.getAllUsers().then(users => {
availableUsers.value = users.map(u => ({ id: u.id, username: u.username, email: u.email }))
})
getCurrentFilters(),
timeRange.value
)
void (async () => {
await refreshAdminAnalytics({ force: true })
await loadAdminUsers()
})()
} else {
// 用户页面loadStats 已包含记录加载,不需要单独调用 loadRecords
await Promise.allSettled([
loadStats(timeRange.value).catch(err => {
log.error('加载统计数据失败:', err)
warning('统计数据加载失败,请刷新重试')
}),
loadHeatmapData().catch(err => {
log.error('加载热力图数据失败:', err)
})
])
}
// 用户页面loadStats 已包含记录加载,不需要单独调用 loadRecords
await Promise.allSettled(tasks)
if (globalAutoRefresh.value && isPageVisible.value) {
startGlobalAutoRefresh()
@@ -524,10 +575,12 @@ onMounted(async () => {
async function handleTimeRangeChange(value: DateRangeParams) {
timeRange.value = value
currentPage.value = 1 // 重置到第一页
await loadStats(timeRange.value)
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
await refreshAdminAnalytics({ force: true })
return
}
await loadStats(timeRange.value)
// 用户页面loadStats 已包含记录加载
}
@@ -535,7 +588,7 @@ async function handleTimeRangeChange(value: DateRangeParams) {
async function handlePageChange(page: number) {
currentPage.value = page
if (isAdminPage.value) {
await loadRecords({ page, pageSize: pageSize.value }, getCurrentFilters())
await loadRecords({ page, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
// 用户页面使用前端分页,无需重新请求
}
@@ -545,7 +598,7 @@ async function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1 // 重置到第一页
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: size }, getCurrentFilters())
await loadRecords({ page: 1, pageSize: size }, getCurrentFilters(), timeRange.value)
}
// 用户页面使用前端分页,无需重新请求
}
@@ -568,7 +621,7 @@ async function handleFilterSearchChange(value: string) {
currentPage.value = 1
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
// 用户页面search 需要重新从后端拉取数据(后端支持 search 参数)
// 但通过 filteredRecords 做前端过滤已覆盖,无需额外请求
@@ -579,7 +632,7 @@ async function handleFilterUserChange(value: string) {
currentPage.value = 1 // 重置到第一页
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
}
@@ -588,7 +641,7 @@ async function handleFilterModelChange(value: string) {
currentPage.value = 1 // 重置到第一页
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
}
@@ -597,7 +650,7 @@ async function handleFilterProviderChange(value: string) {
currentPage.value = 1
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
}
@@ -606,7 +659,7 @@ async function handleFilterApiFormatChange(value: string) {
currentPage.value = 1
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
}
@@ -615,7 +668,7 @@ async function handleFilterStatusChange(value: string) {
currentPage.value = 1
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
}
@@ -626,11 +679,12 @@ async function refreshData() {
refreshInFlight = (async () => {
if (isAdminPage.value) {
// loadStats 会同步更新 currentDateRange随后 loadRecords 复用同一时间范围
await Promise.all([
loadStats(timeRange.value),
loadRecords({ page: currentPage.value, pageSize: pageSize.value }, getCurrentFilters())
])
await loadRecords(
{ page: currentPage.value, pageSize: pageSize.value },
getCurrentFilters(),
timeRange.value
)
void refreshAdminAnalytics()
return
}