fix(n+1): 批量查询替代循环单查

- IsAdminBootstrapRequired: userRepo.GetByID 循环 → GetByIDs 批量
- AssignRoles: roleRepo.GetByID 循环 → GetByIDs 批量
- 在 userRepositoryInterface 补充 GetByIDs 方法签名
This commit is contained in:
2026-05-08 08:05:26 +08:00
parent 9b1cea246e
commit 2a18a6fb47
39 changed files with 3169 additions and 393 deletions

View File

@@ -1,5 +1,5 @@
import process from 'node:process'
import { access, mkdir, mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
import { access, mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
import { constants as fsConstants } from 'node:fs'
import { spawn } from 'node:child_process'
import { createHmac } from 'node:crypto'
@@ -8,6 +8,8 @@ import { tmpdir } from 'node:os'
import path from 'node:path'
import { chromium, expect } from '@playwright/test'
import { parseSelectedScenarioNames, selectScenarioNames } from './playwright-e2e-scenarios.mjs'
const TEXT = {
accessControl: '\u8bbf\u95ee\u63a7\u5236',
active: '\u542f\u7528',
@@ -84,6 +86,10 @@ const TEXT = {
permissionsAction: '\u6743\u9650',
permissionsHint: '\u9009\u62e9\u8981\u5206\u914d\u7ed9\u8be5\u89d2\u8272\u7684\u6743\u9650',
profile: '\u4e2a\u4eba\u8d44\u6599',
profileBioPlaceholder: '\u4ecb\u7ecd\u4e00\u4e0b\u81ea\u5df1...',
profileNicknamePlaceholder: '\u8bf7\u8f93\u5165\u6635\u79f0',
profileRegionPlaceholder: '\u8bf7\u8f93\u5165\u5730\u533a',
profileSaveChanges: '\u4fdd\u5b58\u4fee\u6539',
profileConfirmPasswordPlaceholder: '\u8bf7\u518d\u6b21\u8f93\u5165\u65b0\u5bc6\u7801',
registerEmailPlaceholder: '\u90ae\u7bb1\u5730\u5740\uff08\u9009\u586b\uff09',
registerSuccess: '\u6ce8\u518c\u6210\u529f',
@@ -426,25 +432,23 @@ async function resolveManagedBrowserPath() {
throw new Error('No compatible browser found for Playwright CDP E2E.')
}
async function createManagedBrowserProfileDir(browserPath, port) {
if (!isHeadlessShellBrowser(browserPath)) {
return await mkdtemp(path.join(tmpdir(), 'pw-playwright-cdp-'))
}
const profileRoot = path.join(process.cwd(), '.cache', 'cdp-profiles')
await mkdir(profileRoot, { recursive: true })
return path.join(profileRoot, `pw-profile-playwright-cdp-${port}`)
async function createManagedBrowserProfileDir() {
return await mkdtemp(path.join(tmpdir(), 'pw-playwright-cdp-'))
}
function startManagedBrowser(browserPath, port, profileDir) {
const args = [
`--remote-debugging-port=${port}`,
`--user-data-dir=${profileDir}`,
'--noerrdialogs',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--disable-breakpad',
'--disable-crash-reporter',
'--disable-crashpad-for-testing',
'--disable-sync',
'--disable-gpu',
]
@@ -951,8 +955,8 @@ async function forceFillInput(locator, value) {
}
await locator.evaluate((element, nextValue) => {
if (!(element instanceof HTMLInputElement)) {
throw new Error('Target element is not an input.')
if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement)) {
throw new Error('Target element is not a text input.')
}
element.focus()
@@ -2317,6 +2321,62 @@ async function verifySettings(page) {
await expect(page).toHaveURL(/\/login$/)
}
async function verifyProfileManagement(page) {
logDebug('verifyProfileManagement: admin login /login')
await loginFromLoginPage(page)
const profileUsername = `e2e_profile_${Date.now()}`
const profilePassword = 'Profile123!@#'
const profileLandingPattern = /\/profile$/
const suffix = Date.now()
const updatedNickname = `Profile User ${suffix}`
const updatedRegion = `Hangzhou-${suffix}`
logDebug('verifyProfileManagement: goto /users as admin')
await page.goto(appUrl('/users'))
await expect(page).toHaveURL(/\/users$/)
const createdUser = await createUserFromUsersPage(page, profileUsername, profilePassword)
logDebug(`verifyProfileManagement: created user ${createdUser.username}`)
logDebug('verifyProfileManagement: reset session before profile user login')
await resetSessionToLogin(page)
logDebug(`verifyProfileManagement: profile user login ${createdUser.username}`)
await loginWithPassword(page, createdUser.username, profilePassword, profileLandingPattern)
await installFetchDiagnostics(page)
await expect(page).toHaveURL(/\/profile$/)
await expect(page.getByRole('heading', { name: TEXT.profile })).toBeVisible({ timeout: 10 * 1000 })
await expect(page.locator('body')).toContainText(createdUser.username, { timeout: 10 * 1000 })
await expect(page.locator('body')).toContainText(createdUser.email)
await forceFillInput(page.getByPlaceholder(TEXT.profileNicknamePlaceholder).first(), updatedNickname)
await forceFillInput(page.getByPlaceholder(TEXT.profileRegionPlaceholder).first(), updatedRegion)
await forceFillInput(page.getByPlaceholder(TEXT.profileBioPlaceholder).first(), `Profile bio ${suffix}`)
const updateProfileFetchCount = await getFetchDiagnosticsCount(page)
const updateProfileFetch = await performActionAndWaitForFetchLogEntry(page, (entry) => {
return fetchLogPathMatches(entry, /\/api\/v1\/users\/\d+$/) && entry.method === 'PUT'
}, async () => {
await forceClick(page.getByRole('button', { name: TEXT.profileSaveChanges }).first())
}, {
afterCount: updateProfileFetchCount,
label: 'update profile fetch',
})
assertFetchLogSuccess(updateProfileFetch, 'update profile')
await expect(page.getByPlaceholder(TEXT.profileNicknamePlaceholder).first()).toHaveValue(updatedNickname, { timeout: 20 * 1000 })
await expect(page.getByPlaceholder(TEXT.profileRegionPlaceholder).first()).toHaveValue(updatedRegion, { timeout: 20 * 1000 })
await expect(page.getByPlaceholder(TEXT.profileBioPlaceholder).first()).toHaveValue(`Profile bio ${suffix}`)
await forceClick(page.locator('a[href="/profile/security"]').first())
await expect(page).toHaveURL(/\/profile\/security$/)
await expect(page.getByRole('button', { name: TEXT.changePassword })).toBeVisible({ timeout: 10 * 1000 })
await resetSessionToLogin(page)
logDebug('verifyProfileManagement: completed')
}
async function verifyProfileAndSecurity(page) {
logDebug('verifyProfileAndSecurity: admin login /login')
await loginFromLoginPage(page)
@@ -2436,17 +2496,44 @@ async function main() {
let runtime = null
let managedBrowser = null
let managedProfileDir = null
const selectedScenarioNames = new Set(
(process.env.E2E_SCENARIOS ?? '')
.split(',')
.map((name) => name.trim())
.filter(Boolean),
)
const selectedScenarioNames = parseSelectedScenarioNames(process.env.E2E_SCENARIOS ?? '')
const scenarioEntries = new Map([
['admin-bootstrap', verifyAdminBootstrapWorkflow],
['public-registration', verifyPublicRegistration],
['email-activation', verifyEmailActivationWorkflow],
['password-reset', verifyPasswordResetWorkflow],
['login-surface', verifyLoginSurface],
['auth-workflow', verifyAuthWorkflow],
['responsive-login', verifyResponsiveLogin],
['desktop-mobile-navigation', verifyDesktopAndMobileNavigation],
['user-management-crud', verifyUserManagementCRUD],
['user-management-batch', verifyUserManagementBatch],
['role-management-crud', verifyRoleManagementCRUD],
['permissions-management-crud', verifyPermissionsManagementCRUD],
['device-management', verifyDeviceManagement],
['login-logs', verifyLoginLogs],
['operation-logs', verifyOperationLogs],
['webhook-management', verifyWebhookManagement],
['import-export', verifyImportExport],
['profile-management', verifyProfileManagement],
['profile-and-security', verifyProfileAndSecurity],
['settings', verifySettings],
['dashboard-stats', verifyDashboardStats],
])
const scenarioNamesToRun = selectScenarioNames({
requestedScenarioNames: selectedScenarioNames,
expectAdminBootstrap: process.env.E2E_EXPECT_ADMIN_BOOTSTRAP === '1',
})
if (process.env.E2E_LIST_SCENARIOS === '1') {
console.log(scenarioNamesToRun.join('\n'))
return
}
if (process.env.E2E_MANAGED_BROWSER === '1') {
const browserPath = await resolveManagedBrowserPath()
const port = await getFreePort()
managedProfileDir = await createManagedBrowserProfileDir(browserPath, port)
managedProfileDir = await createManagedBrowserProfileDir()
managedBrowser = startManagedBrowser(browserPath, port, managedProfileDir)
managedCdpUrl = `http://127.0.0.1:${port}`
console.log(`LAUNCH playwright-cdp ${browserPath}`)
@@ -2466,35 +2553,13 @@ async function main() {
throw new Error('No persistent Chromium context is available through CDP.')
}
const scenarios = []
if (process.env.E2E_EXPECT_ADMIN_BOOTSTRAP === '1') {
scenarios.push(['admin-bootstrap', verifyAdminBootstrapWorkflow])
}
scenarios.push(
['public-registration', verifyPublicRegistration],
['email-activation', verifyEmailActivationWorkflow],
['password-reset', verifyPasswordResetWorkflow],
['login-surface', verifyLoginSurface],
['auth-workflow', verifyAuthWorkflow],
['responsive-login', verifyResponsiveLogin],
['desktop-mobile-navigation', verifyDesktopAndMobileNavigation],
['user-management-crud', verifyUserManagementCRUD],
['user-management-batch', verifyUserManagementBatch],
['role-management-crud', verifyRoleManagementCRUD],
['permissions-management-crud', verifyPermissionsManagementCRUD],
['device-management', verifyDeviceManagement],
['login-logs', verifyLoginLogs],
['operation-logs', verifyOperationLogs],
['webhook-management', verifyWebhookManagement],
['import-export', verifyImportExport],
['profile-and-security', verifyProfileAndSecurity],
['settings', verifySettings],
['dashboard-stats', verifyDashboardStats],
)
const scenariosToRun = selectedScenarioNames.size === 0
? scenarios
: scenarios.filter(([name]) => name === 'admin-bootstrap' || selectedScenarioNames.has(name))
const scenariosToRun = scenarioNamesToRun.map((name) => {
const handler = scenarioEntries.get(name)
if (!handler) {
throw new Error(`No Playwright CDP scenario handler is registered for ${name}.`)
}
return [name, handler]
})
if (scenariosToRun.length === 0) {
throw new Error(`No E2E scenarios matched E2E_SCENARIOS=${process.env.E2E_SCENARIOS ?? ''}`)