mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-03 00:33:13 +00:00
perf(usage): 优化 /admin/usage 打开速度与刷新响应
根因:页面 mount 并发 6 个请求,其中 5 个在原始 usage_logs 上 live 聚合, 且有 1 个重复 getModelStats。优化(不引入预聚合): 前端 - UsageView 经 :model-options 下传 model 列表,移除 UsageFilters 重复的 getModelStats(mount 请求 6→5,少一次 usage_logs 全表 GROUP BY model) - 刷新/换筛选保留旧模型数据(invalidateModelStatsCache 只失效标记不清空数据), 图表不再闪空,刷新期间页面保持可交互 后端 - GetStatsWithFilters 4 条聚合 errgroup 并行(仅 *sql.DB 连接池路径,ent.Tx 顺序回退以保事务内不并发),endpoint 明细 best-effort;抑制取消级联噪声日志 - /admin/usage/stats 复用 dashboard 的 newSnapshotCache 30s 处理器层缓存,按 filters+窗口为 key;前端手动刷新带 nocache=1 强制回源(刷新=最新) 注:自合并提交 4c8396c 迁移而来,仅取"列表打开速度与刷新响应"部分;原提交的 "审计查看弹窗大 body 渲染"改动(AuditLogModal / audit-log-format / 审计 i18n) 依赖尚未迁移的审计功能(d8389ade),本次已排除。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b60d8bb4cc
commit
bf24b61139
10 files changed
+325
-74
No files matched your search
@@ -50,6 +50,9 @@ vi.mock('vue-i18n', async () => {
|
||||
// Mock the admin API module — we control searchUsers return value per test
|
||||
const mockSearchUsers = vi.fn()
|
||||
const mockSearchApiKeys = vi.fn().mockResolvedValue([])
|
||||
const mockGroupsList = vi.fn().mockResolvedValue({ items: [] })
|
||||
const mockGetModelStats = vi.fn().mockResolvedValue({ models: [] })
|
||||
const mockAccountsList = vi.fn().mockResolvedValue({ items: [] })
|
||||
|
||||
vi.mock('@/api/admin', () => ({
|
||||
adminAPI: {
|
||||
@@ -57,15 +60,9 @@ vi.mock('@/api/admin', () => ({
|
||||
searchUsers: (...args: any[]) => mockSearchUsers(...args),
|
||||
searchApiKeys: (...args: any[]) => mockSearchApiKeys(...args),
|
||||
},
|
||||
groups: {
|
||||
list: vi.fn().mockResolvedValue({ items: [] }),
|
||||
},
|
||||
dashboard: {
|
||||
getModelStats: vi.fn().mockResolvedValue({ models: [] }),
|
||||
},
|
||||
accounts: {
|
||||
list: vi.fn().mockResolvedValue({ items: [] }),
|
||||
},
|
||||
groups: { list: (...args: any[]) => mockGroupsList(...args) },
|
||||
dashboard: { getModelStats: (...args: any[]) => mockGetModelStats(...args) },
|
||||
accounts: { list: (...args: any[]) => mockAccountsList(...args) },
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -91,6 +88,7 @@ function mountFilters(filters = defaultFilters()) {
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-05-28',
|
||||
showActions: false,
|
||||
modelOptions: [],
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
@@ -166,3 +164,32 @@ describe('UsageFilters — user search dropdown', () => {
|
||||
expect(wrapper.props('modelValue').user_id).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UsageFilters — model options come from prop (no dup request)', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
mockGetModelStats.mockClear()
|
||||
mockGroupsList.mockClear()
|
||||
})
|
||||
afterEach(() => { vi.useRealTimers() })
|
||||
|
||||
it('does not call dashboard.getModelStats on mount and renders model options from prop', async () => {
|
||||
const wrapper = mount(UsageFilters, {
|
||||
props: {
|
||||
modelValue: defaultFilters(),
|
||||
exporting: false,
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-05-28',
|
||||
showActions: false,
|
||||
modelOptions: ['claude-3', 'gpt-4o'],
|
||||
},
|
||||
global: { stubs: { Select: true, Teleport: true } },
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(mockGetModelStats).not.toHaveBeenCalled()
|
||||
|
||||
const opts = (wrapper.vm as any).modelOptions as Array<{ value: string | null; label: string }>
|
||||
expect(opts.map((o) => o.value)).toEqual([null, 'claude-3', 'gpt-4o'])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user