2870 lines
101 KiB
TypeScript
2870 lines
101 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useRef, useEffect } from 'react'
|
||
import { useToast } from '@/hooks/useToast'
|
||
import ToastBubble from './ToastBubble'
|
||
import { formatHumanSpeed } from '@/lib/human-readable-transfer'
|
||
|
||
interface NetworkDiagnoseModalProps {
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
}
|
||
|
||
interface TestResult {
|
||
type:
|
||
| 'network-env' // 网络环境检测
|
||
| 'connection' // DNS和连接测试
|
||
| 'download' // 下载测速
|
||
| 'upload' // 上传测速
|
||
| 'storage' // 存储连通性
|
||
| 'preview-image' // 预览图片加载
|
||
| 'preview-video' // 预览视频加载
|
||
| 'preview-audio' // 音频文件加载
|
||
| 'proxy-performance' // 代理性能测试
|
||
| 'video-range' // 视频Range请求测试
|
||
| 'intranet-minio' // 内网MinIO直连测试
|
||
| 'public-s3' // 公网S3直连测试
|
||
| 'concurrent' // 并发测试
|
||
| 'cache' // 缓存测试
|
||
| 's3-performance' // S3存储性能分析
|
||
| 'api-materials-list' // API: 素材列表
|
||
| 'api-material-detail' // API: 素材详情
|
||
| 'api-material-stats' // API: 素材统计
|
||
| 'api-upload-init' // API: 上传初始化
|
||
| 'api-download' // API: 下载接口
|
||
| 'api-user-profile' // API: 用户信息
|
||
| 'api-system-settings' // API: 系统设置
|
||
| 'api-health' // API: 健康检查
|
||
| 'database-query' // 数据库查询性能
|
||
| 'redis-connection' // Redis连接测试
|
||
| 'page-home-load' // 页面: 首页加载
|
||
| 'page-detail-load' // 页面: 详情页加载
|
||
| 'upload-flow' // 上传流程测试
|
||
| 'download-flow' // 下载流程测试
|
||
| 'transcode-service' // 转码服务测试
|
||
| 'auth-session' // 认证Session测试
|
||
| 'intranet-url-rewrite' // 内网用户URL重写测试
|
||
| 'public-url-rewrite' // 外网用户URL重写测试
|
||
status: 'testing' | 'success' | 'error' | 'warning'
|
||
speed?: number
|
||
message?: string
|
||
details?: {
|
||
dnsTime?: number
|
||
connectTime?: number
|
||
ttfb?: number
|
||
downloadTime?: number
|
||
totalTime?: number
|
||
fileSize?: number
|
||
url?: string
|
||
[key: string]: any // 允许其他详细信息
|
||
}
|
||
}
|
||
|
||
const PUBLIC_S3_DOMAINS = ['s3.jiansun.vip', 's3.1080p.hk']
|
||
|
||
function isPublicS3Url(url: string): boolean {
|
||
return PUBLIC_S3_DOMAINS.some((domain) => url.includes(domain))
|
||
}
|
||
|
||
export default function NetworkDiagnoseModal({ isOpen, onClose }: NetworkDiagnoseModalProps) {
|
||
const [testing, setTesting] = useState(false)
|
||
const [results, setResults] = useState<TestResult[]>([])
|
||
const [copied, setCopied] = useState(false)
|
||
const [networkInfo, setNetworkInfo] = useState<any>(null)
|
||
const { toast, showToast } = useToast()
|
||
|
||
// 跟踪需要清理的资源
|
||
const createdBlobs = useRef<Blob[]>([])
|
||
const createdUrls = useRef<string[]>([])
|
||
const createdUploadIds = useRef<string[]>([])
|
||
|
||
// ✅ 组件卸载时自动清理资源
|
||
useEffect(() => {
|
||
return () => {
|
||
cleanupDiagnosticResources()
|
||
}
|
||
}, [])
|
||
|
||
// 格式化文件大小
|
||
const formatFileSize = (bytes: number) => {
|
||
if (bytes < 1024) return `${bytes} B`
|
||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
|
||
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
||
}
|
||
|
||
// 清理诊断过程中创建的资源
|
||
const cleanupDiagnosticResources = async () => {
|
||
try {
|
||
// 1. 清理Blob对象(释放内存)
|
||
createdBlobs.current.forEach(blob => {
|
||
if (blob && typeof blob === 'object' && 'close' in blob) {
|
||
// 如果是可关闭的Blob(如File),尝试关闭
|
||
try {
|
||
(blob as any).close?.()
|
||
} catch (e) {
|
||
// 忽略错误
|
||
}
|
||
}
|
||
})
|
||
createdBlobs.current = []
|
||
|
||
// 2. 清理URL对象(revokeObjectURL)
|
||
createdUrls.current.forEach(url => {
|
||
try {
|
||
URL.revokeObjectURL(url)
|
||
} catch (e) {
|
||
// 忽略错误
|
||
}
|
||
})
|
||
createdUrls.current = []
|
||
|
||
// 3. 清理服务器端的uploadId(删除临时文件)
|
||
if (createdUploadIds.current.length > 0) {
|
||
// 批量清理服务器端的临时文件
|
||
const cleanupPromises = createdUploadIds.current.map(uploadId =>
|
||
fetch('/api/materials/upload/cleanup', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ uploadId })
|
||
}).catch(err => {
|
||
console.warn(`清理uploadId ${uploadId}失败:`, err)
|
||
return null
|
||
})
|
||
)
|
||
await Promise.all(cleanupPromises)
|
||
console.log(`已清理 ${createdUploadIds.current.length} 个上传会话的临时文件`)
|
||
createdUploadIds.current = []
|
||
}
|
||
} catch (error) {
|
||
console.warn('清理诊断资源时出错:', error)
|
||
}
|
||
}
|
||
|
||
// 1. 网络环境检测
|
||
const testNetworkEnvironment = async (): Promise<TestResult> => {
|
||
try {
|
||
const response = await fetch('/api/network-status', { cache: 'no-store' })
|
||
if (!response.ok) throw new Error('网络环境检测失败')
|
||
|
||
const data = await response.json()
|
||
setNetworkInfo(data)
|
||
|
||
// 获取浏览器网络信息
|
||
const connection = (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection
|
||
const networkType = connection?.effectiveType || 'unknown'
|
||
const downlink = connection?.downlink || 'unknown'
|
||
const rtt = connection?.rtt || 'unknown'
|
||
const saveData = connection?.saveData || false
|
||
const browserDownlinkSpeed = typeof downlink === 'number'
|
||
? formatHumanSpeed((downlink * 1000 * 1000) / 8)
|
||
: '未知'
|
||
|
||
const message = [
|
||
`网络类型: ${data.isIntranet ? '内网' : '外网'}`,
|
||
`访问域名: ${data.hostname}`,
|
||
`客户端IP: ${data.clientIp}`,
|
||
`浏览器网络: ${networkType}`,
|
||
`浏览器估算下行: ${browserDownlinkSpeed}`,
|
||
`RTT延迟: ${rtt !== 'unknown' ? `${rtt} ms` : '未知'}`,
|
||
saveData ? '省流模式: 开启' : ''
|
||
].filter(Boolean).join(', ')
|
||
|
||
return {
|
||
type: 'network-env',
|
||
status: 'success',
|
||
message,
|
||
details: {
|
||
isIntranet: data.isIntranet,
|
||
isPublicDomain: data.isPublicDomain,
|
||
clientIp: data.clientIp,
|
||
hostname: data.hostname,
|
||
networkType,
|
||
downlink,
|
||
rtt,
|
||
saveData
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'network-env',
|
||
status: 'error',
|
||
message: `网络环境检测失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. DNS和连接测试(增强版)
|
||
const testDNSAndConnection = async (): Promise<TestResult> => {
|
||
try {
|
||
const testUrl = `${window.location.origin}/api/network-status`
|
||
|
||
const startTime = performance.now()
|
||
const response = await fetch(testUrl, { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!response.ok) throw new Error('连接测试失败')
|
||
|
||
// 使用Resource Timing API获取详细的网络时序
|
||
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||
const entry = entries.find(e => e.name.includes('/api/network-status'))
|
||
|
||
let dnsTime = 0
|
||
let connectTime = 0
|
||
let ttfb = 0
|
||
let sslTime = 0
|
||
let requestTime = 0
|
||
let responseTime = 0
|
||
|
||
if (entry) {
|
||
dnsTime = entry.domainLookupEnd - entry.domainLookupStart
|
||
connectTime = entry.connectEnd - entry.connectStart
|
||
sslTime = entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart : 0
|
||
ttfb = entry.responseStart - entry.requestStart
|
||
requestTime = entry.responseStart - entry.requestStart
|
||
responseTime = entry.responseEnd - entry.responseStart
|
||
}
|
||
|
||
const totalTime = endTime - startTime
|
||
const details: any = {
|
||
dnsTime: dnsTime > 0 ? dnsTime : undefined,
|
||
connectTime: connectTime > 0 ? connectTime : undefined,
|
||
sslTime: sslTime > 0 ? sslTime : undefined,
|
||
ttfb: ttfb > 0 ? ttfb : undefined,
|
||
requestTime: requestTime > 0 ? requestTime : undefined,
|
||
responseTime: responseTime > 0 ? responseTime : undefined,
|
||
totalTime
|
||
}
|
||
|
||
const issues: string[] = []
|
||
if (dnsTime > 100) issues.push(`DNS解析较慢: ${dnsTime.toFixed(0)}ms`)
|
||
if (connectTime > 200) issues.push(`TCP连接较慢: ${connectTime.toFixed(0)}ms`)
|
||
if (ttfb > 500) issues.push(`首字节时间较长: ${ttfb.toFixed(0)}ms`)
|
||
|
||
const message = issues.length > 0
|
||
? `总耗时: ${totalTime.toFixed(0)}ms (${issues.join(', ')})`
|
||
: `总耗时: ${totalTime.toFixed(0)}ms`
|
||
|
||
return {
|
||
type: 'connection',
|
||
status: totalTime > 1000 ? 'warning' : 'success',
|
||
message,
|
||
details
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'connection',
|
||
status: 'error',
|
||
message: `连接测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 下载测速(增强版:测试不同大小)
|
||
const testDownloadSpeed = async (): Promise<TestResult> => {
|
||
try {
|
||
const testFileSize = 1024 * 1024 // 1MB
|
||
const startTime = Date.now()
|
||
|
||
const response = await fetch('/api/network/test-download', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ size: testFileSize })
|
||
})
|
||
|
||
if (!response.ok) throw new Error('下载测试失败')
|
||
|
||
const blob = await response.blob()
|
||
createdBlobs.current.push(blob) // 跟踪Blob以便清理
|
||
const endTime = Date.now()
|
||
const duration = (endTime - startTime) / 1000
|
||
const speed = blob.size / duration
|
||
|
||
// 获取Resource Timing信息
|
||
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||
const entry = entries.find(e => e.name.includes('/api/network/test-download'))
|
||
|
||
let ttfb = 0
|
||
if (entry) {
|
||
ttfb = entry.responseStart - entry.requestStart
|
||
}
|
||
|
||
return {
|
||
type: 'download',
|
||
status: speed < 1024 * 1024 ? 'warning' : 'success', // < 1MB/s 警告
|
||
speed,
|
||
message: `下载速度: ${formatHumanSpeed(speed)}${ttfb > 0 ? `, TTFB: ${ttfb.toFixed(0)}ms` : ''}`,
|
||
details: {
|
||
ttfb: ttfb > 0 ? ttfb : undefined,
|
||
totalTime: duration * 1000,
|
||
fileSize: blob.size
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'download',
|
||
status: 'error',
|
||
message: '下载测试失败'
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. 上传测速
|
||
const testUploadSpeed = async (): Promise<TestResult> => {
|
||
try {
|
||
const testData = new Blob([new ArrayBuffer(1024 * 1024)]) // 1MB
|
||
createdBlobs.current.push(testData) // 跟踪Blob以便清理
|
||
const formData = new FormData()
|
||
formData.append('file', testData, 'test.bin')
|
||
|
||
const startTime = Date.now()
|
||
|
||
const response = await fetch('/api/network/test-upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
|
||
if (!response.ok) throw new Error('上传测试失败')
|
||
|
||
const endTime = Date.now()
|
||
const duration = (endTime - startTime) / 1000
|
||
const speed = testData.size / duration
|
||
|
||
return {
|
||
type: 'upload',
|
||
status: speed < 512 * 1024 ? 'warning' : 'success', // < 512KB/s 警告
|
||
speed,
|
||
message: `上传速度: ${formatHumanSpeed(speed)}`,
|
||
details: {
|
||
totalTime: duration * 1000,
|
||
fileSize: testData.size
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'upload',
|
||
status: 'error',
|
||
message: '上传测试失败'
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. 存储连通性测试
|
||
const testStorageConnectivity = async (): Promise<TestResult> => {
|
||
try {
|
||
const response = await fetch('/api/network/test-storage')
|
||
|
||
if (!response.ok) throw new Error('存储连通性测试失败')
|
||
|
||
const data = await response.json()
|
||
|
||
return {
|
||
type: 'storage',
|
||
status: 'success',
|
||
message: data.message || '存储服务连接正常'
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'storage',
|
||
status: 'error',
|
||
message: '存储服务连接失败'
|
||
}
|
||
}
|
||
}
|
||
|
||
// 6. 预览图片加载测试(增强版)
|
||
const testPreviewImageLoad = async (): Promise<TestResult> => {
|
||
try {
|
||
const response = await fetch('/api/materials?pageSize=1&materialType=3D场景')
|
||
if (!response.ok) throw new Error('获取素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'preview-image',
|
||
status: 'warning',
|
||
message: '没有可测试的预览图片'
|
||
}
|
||
}
|
||
|
||
const imageUrl = materials[0].previewUrl
|
||
if (!imageUrl) {
|
||
return {
|
||
type: 'preview-image',
|
||
status: 'warning',
|
||
message: '素材没有预览图'
|
||
}
|
||
}
|
||
|
||
// 使用Performance API测量加载时间
|
||
const startTime = performance.now()
|
||
const img = new Image()
|
||
|
||
await new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => reject(new Error('图片加载超时')), 10000)
|
||
img.onload = () => {
|
||
clearTimeout(timeout)
|
||
resolve(undefined)
|
||
}
|
||
img.onerror = () => {
|
||
clearTimeout(timeout)
|
||
reject(new Error('图片加载失败'))
|
||
}
|
||
img.src = imageUrl
|
||
})
|
||
|
||
const endTime = performance.now()
|
||
const loadTime = endTime - startTime
|
||
|
||
// 获取文件大小和详细时序
|
||
let fileSize = 0
|
||
let ttfb = 0
|
||
try {
|
||
const sizeResponse = await fetch(imageUrl, { method: 'HEAD' })
|
||
fileSize = parseInt(sizeResponse.headers.get('content-length') || '0')
|
||
|
||
// 获取Resource Timing
|
||
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||
const imageFileName = imageUrl.split('/').pop() || ''
|
||
const entry = entries.find((e: PerformanceResourceTiming) => e.name.includes(imageFileName))
|
||
if (entry) {
|
||
ttfb = entry.responseStart - entry.requestStart
|
||
}
|
||
} catch (e) {
|
||
// 忽略错误
|
||
}
|
||
|
||
const speed = fileSize > 0 ? (fileSize / (loadTime / 1000)) : 0
|
||
|
||
return {
|
||
type: 'preview-image',
|
||
status: loadTime > 3000 ? 'warning' : 'success',
|
||
speed,
|
||
message: `加载时间: ${(loadTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(${formatHumanSpeed(speed)})` : ''}`,
|
||
details: {
|
||
totalTime: loadTime,
|
||
ttfb: ttfb > 0 ? ttfb : undefined,
|
||
fileSize,
|
||
url: imageUrl
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'preview-image',
|
||
status: 'error',
|
||
message: `预览图片加载失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 7. 预览视频加载测试(增强版)
|
||
const testPreviewVideoLoad = async (): Promise<TestResult> => {
|
||
try {
|
||
const response = await fetch('/api/materials?pageSize=1&materialType=淘系-主图视频')
|
||
if (!response.ok) throw new Error('获取视频素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'preview-video',
|
||
status: 'warning',
|
||
message: '没有可测试的视频素材'
|
||
}
|
||
}
|
||
|
||
const videoUrl = materials[0].videoPreviewClipUrl || materials[0].videoPreview540Url || materials[0].videoSourceUrl
|
||
if (!videoUrl) {
|
||
return {
|
||
type: 'preview-video',
|
||
status: 'warning',
|
||
message: '视频素材没有预览URL'
|
||
}
|
||
}
|
||
|
||
const startTime = performance.now()
|
||
|
||
// 测试视频元数据加载(首字节时间)
|
||
const video = document.createElement('video')
|
||
video.preload = 'metadata'
|
||
|
||
await new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => reject(new Error('视频加载超时')), 15000)
|
||
video.onloadedmetadata = () => {
|
||
clearTimeout(timeout)
|
||
resolve(undefined)
|
||
}
|
||
video.onerror = () => {
|
||
clearTimeout(timeout)
|
||
reject(new Error('视频加载失败'))
|
||
}
|
||
video.src = videoUrl
|
||
})
|
||
|
||
const metadataTime = performance.now() - startTime
|
||
|
||
// 获取文件大小
|
||
let fileSize = 0
|
||
try {
|
||
const sizeResponse = await fetch(videoUrl, { method: 'HEAD' })
|
||
fileSize = parseInt(sizeResponse.headers.get('content-length') || '0')
|
||
} catch (e) {
|
||
// 忽略错误
|
||
}
|
||
|
||
return {
|
||
type: 'preview-video',
|
||
status: metadataTime > 5000 ? 'warning' : 'success',
|
||
message: `元数据加载: ${(metadataTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(文件: ${formatFileSize(fileSize)})` : ''}`,
|
||
details: {
|
||
ttfb: metadataTime,
|
||
totalTime: metadataTime,
|
||
fileSize,
|
||
url: videoUrl
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'preview-video',
|
||
status: 'error',
|
||
message: `视频加载失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 8. 音频文件加载测试
|
||
const testPreviewAudioLoad = async (): Promise<TestResult> => {
|
||
try {
|
||
const response = await fetch('/api/materials?pageSize=1&materialType=音乐')
|
||
if (!response.ok) throw new Error('获取音频素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'preview-audio',
|
||
status: 'warning',
|
||
message: '没有可测试的音频素材'
|
||
}
|
||
}
|
||
|
||
const material = materials[0]
|
||
let audioUrl = material.zipFileName || ''
|
||
|
||
if (!audioUrl && material.zipFilePath) {
|
||
const zipFilePath = material.zipFilePath
|
||
if (zipFilePath.startsWith('http://') || zipFilePath.startsWith('https://')) {
|
||
audioUrl = zipFilePath
|
||
} else if (zipFilePath.startsWith('/uploads/') || zipFilePath.startsWith('uploads/')) {
|
||
audioUrl = zipFilePath.startsWith('/') ? zipFilePath : `/${zipFilePath}`
|
||
} else {
|
||
audioUrl = `/uploads/${zipFilePath}`
|
||
}
|
||
}
|
||
|
||
if (!audioUrl && material.zipFileName && !material.zipFileName.startsWith('http')) {
|
||
audioUrl = `/uploads/${material.zipFileName}`
|
||
}
|
||
|
||
if (!audioUrl) {
|
||
return {
|
||
type: 'preview-audio',
|
||
status: 'warning',
|
||
message: '音频素材没有可用的URL'
|
||
}
|
||
}
|
||
|
||
const startTime = performance.now()
|
||
|
||
const audio = new Audio()
|
||
audio.preload = 'metadata'
|
||
|
||
await new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => reject(new Error('音频加载超时')), 10000)
|
||
audio.onloadedmetadata = () => {
|
||
clearTimeout(timeout)
|
||
resolve(undefined)
|
||
}
|
||
audio.onerror = () => {
|
||
clearTimeout(timeout)
|
||
reject(new Error('音频加载失败'))
|
||
}
|
||
audio.src = audioUrl
|
||
})
|
||
|
||
const metadataTime = performance.now() - startTime
|
||
|
||
// 获取文件大小
|
||
let fileSize = 0
|
||
try {
|
||
const sizeResponse = await fetch(audioUrl, { method: 'HEAD' })
|
||
fileSize = parseInt(sizeResponse.headers.get('content-length') || '0')
|
||
} catch (e) {
|
||
// 忽略错误
|
||
}
|
||
|
||
return {
|
||
type: 'preview-audio',
|
||
status: metadataTime > 3000 ? 'warning' : 'success',
|
||
message: `元数据加载: ${(metadataTime / 1000).toFixed(2)}秒 ${fileSize > 0 ? `(文件: ${formatFileSize(fileSize)})` : ''}`,
|
||
details: {
|
||
ttfb: metadataTime,
|
||
totalTime: metadataTime,
|
||
fileSize,
|
||
url: audioUrl
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'preview-audio',
|
||
status: 'error',
|
||
message: `音频加载失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 9. 代理性能测试
|
||
const testProxyPerformance = async (): Promise<TestResult> => {
|
||
try {
|
||
// 获取真实的预览图片URL(通常会走代理),增加pageSize提高找到有效素材的概率
|
||
const response = await fetch('/api/materials?pageSize=10')
|
||
if (!response.ok) throw new Error('获取测试素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'proxy-performance',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
// 查找第一个有previewUrl的素材
|
||
const materialWithPreview = materials.find((m: any) => m.previewUrl)
|
||
if (!materialWithPreview || !materialWithPreview.previewUrl) {
|
||
return {
|
||
type: 'proxy-performance',
|
||
status: 'warning',
|
||
message: '没有找到有预览URL的素材'
|
||
}
|
||
}
|
||
|
||
const testUrl = materialWithPreview.previewUrl
|
||
|
||
// 检查URL是否为内网MinIO地址(内网用户不应该通过代理访问内网地址)
|
||
const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1')
|
||
if (isIntranetMinIO) {
|
||
return {
|
||
type: 'proxy-performance',
|
||
status: 'success',
|
||
message: '内网用户直接访问内网MinIO,无需代理',
|
||
details: { url: testUrl }
|
||
}
|
||
}
|
||
|
||
// 如果URL已经是代理URL,直接测试;否则构造代理URL(仅对公网URL)
|
||
let proxyUrl = testUrl
|
||
if (!testUrl.includes('/api/proxy-media')) {
|
||
// 确保URL是公网S3地址,才构造代理URL
|
||
const isPublicS3 = isPublicS3Url(testUrl)
|
||
if (isPublicS3) {
|
||
proxyUrl = `/api/proxy-media?url=${encodeURIComponent(testUrl)}`
|
||
} else {
|
||
// 如果不是公网S3地址,直接测试原始URL
|
||
proxyUrl = testUrl
|
||
}
|
||
}
|
||
|
||
const startTime = performance.now()
|
||
const testResponse = await fetch(proxyUrl, { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!testResponse.ok) throw new Error('代理测试失败')
|
||
|
||
const blob = await testResponse.blob()
|
||
createdBlobs.current.push(blob) // 跟踪Blob以便清理
|
||
const totalTime = endTime - startTime
|
||
|
||
// 获取Resource Timing
|
||
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||
const entry = entries.find(e => e.name.includes('/api/proxy-media'))
|
||
|
||
let dnsTime = 0
|
||
let connectTime = 0
|
||
let ttfb = 0
|
||
if (entry) {
|
||
dnsTime = entry.domainLookupEnd - entry.domainLookupStart
|
||
connectTime = entry.connectEnd - entry.connectStart
|
||
ttfb = entry.responseStart - entry.requestStart
|
||
}
|
||
|
||
const speed = blob.size / (totalTime / 1000)
|
||
|
||
return {
|
||
type: 'proxy-performance',
|
||
status: ttfb > 1000 ? 'warning' : 'success',
|
||
speed,
|
||
message: `代理响应: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`,
|
||
details: {
|
||
dnsTime: dnsTime > 0 ? dnsTime : undefined,
|
||
connectTime: connectTime > 0 ? connectTime : undefined,
|
||
ttfb: ttfb > 0 ? ttfb : totalTime,
|
||
totalTime,
|
||
fileSize: blob.size,
|
||
url: proxyUrl
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'proxy-performance',
|
||
status: 'error',
|
||
message: `代理性能测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 10. 视频Range请求测试
|
||
const testVideoRangeRequest = async (): Promise<TestResult> => {
|
||
try {
|
||
const response = await fetch('/api/materials?pageSize=1&materialType=淘系-主图视频')
|
||
if (!response.ok) throw new Error('获取视频素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'video-range',
|
||
status: 'warning',
|
||
message: '没有可测试的视频素材'
|
||
}
|
||
}
|
||
|
||
const videoUrl = materials[0].videoPreviewClipUrl || materials[0].videoPreview540Url || materials[0].videoSourceUrl
|
||
if (!videoUrl) {
|
||
return {
|
||
type: 'video-range',
|
||
status: 'warning',
|
||
message: '视频素材没有预览URL'
|
||
}
|
||
}
|
||
|
||
// 测试Range请求(模拟视频拖拽)
|
||
const rangeSize = 1024 * 1024 // 1MB
|
||
const startTime = performance.now()
|
||
|
||
const rangeResponse = await fetch(videoUrl, {
|
||
headers: {
|
||
'Range': `bytes=0-${rangeSize - 1}`
|
||
}
|
||
})
|
||
|
||
const endTime = performance.now()
|
||
const rangeTime = endTime - startTime
|
||
|
||
if (!rangeResponse.ok && rangeResponse.status !== 206) {
|
||
throw new Error('Range请求不支持')
|
||
}
|
||
|
||
const blob = await rangeResponse.blob()
|
||
createdBlobs.current.push(blob) // 跟踪Blob以便清理
|
||
const speed = blob.size / (rangeTime / 1000)
|
||
|
||
return {
|
||
type: 'video-range',
|
||
status: rangeTime > 2000 ? 'warning' : 'success',
|
||
speed,
|
||
message: `Range请求: ${rangeTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`,
|
||
details: {
|
||
totalTime: rangeTime,
|
||
fileSize: blob.size,
|
||
rangeSize,
|
||
url: videoUrl,
|
||
supportsRange: rangeResponse.status === 206
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'video-range',
|
||
status: 'error',
|
||
message: `Range请求测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 11. 内网MinIO直连测试
|
||
const testIntranetMinIO = async (): Promise<TestResult> => {
|
||
try {
|
||
// 获取一个素材,检查URL是否会被重写为内网MinIO地址
|
||
const response = await fetch('/api/materials?pageSize=1')
|
||
if (!response.ok) throw new Error('获取测试素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'intranet-minio',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
const testUrl = materials[0].previewUrl
|
||
if (!testUrl) {
|
||
return {
|
||
type: 'intranet-minio',
|
||
status: 'warning',
|
||
message: '素材没有预览URL'
|
||
}
|
||
}
|
||
|
||
// 检查URL是否为内网MinIO地址
|
||
const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1')
|
||
|
||
if (!isIntranetMinIO) {
|
||
return {
|
||
type: 'intranet-minio',
|
||
status: 'warning',
|
||
message: '当前URL不是内网MinIO地址(可能被重写为公网地址)',
|
||
details: { url: testUrl }
|
||
}
|
||
}
|
||
|
||
const startTime = performance.now()
|
||
const testResponse = await fetch(testUrl, { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!testResponse.ok) throw new Error('内网MinIO连接失败')
|
||
|
||
const blob = await testResponse.blob()
|
||
createdBlobs.current.push(blob) // 跟踪Blob以便清理
|
||
const totalTime = endTime - startTime
|
||
const speed = blob.size / (totalTime / 1000)
|
||
|
||
// 获取Resource Timing
|
||
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||
const entry = entries.find(e => e.name.includes(testUrl.split('/').pop() || ''))
|
||
|
||
let ttfb = 0
|
||
if (entry) {
|
||
ttfb = entry.responseStart - entry.requestStart
|
||
}
|
||
|
||
return {
|
||
type: 'intranet-minio',
|
||
status: ttfb > 500 ? 'warning' : 'success',
|
||
speed,
|
||
message: `内网MinIO: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`,
|
||
details: {
|
||
ttfb: ttfb > 0 ? ttfb : totalTime,
|
||
totalTime,
|
||
fileSize: blob.size,
|
||
url: testUrl
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'intranet-minio',
|
||
status: 'error',
|
||
message: `内网MinIO测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 12. 公网S3直连测试
|
||
const testPublicS3 = async (): Promise<TestResult> => {
|
||
try {
|
||
// 增加pageSize提高找到有效素材的概率
|
||
const response = await fetch('/api/materials?pageSize=10')
|
||
if (!response.ok) throw new Error('获取测试素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'public-s3',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
// 查找第一个有previewUrl的素材
|
||
const materialWithPreview = materials.find((m: any) => m.previewUrl)
|
||
if (!materialWithPreview || !materialWithPreview.previewUrl) {
|
||
return {
|
||
type: 'public-s3',
|
||
status: 'warning',
|
||
message: '没有找到有预览URL的素材'
|
||
}
|
||
}
|
||
|
||
const testUrl = materialWithPreview.previewUrl
|
||
|
||
// 检查URL是否为公网S3地址
|
||
const isPublicS3 = isPublicS3Url(testUrl)
|
||
const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1')
|
||
|
||
// 如果是内网MinIO地址,说明是内网用户,这是正常的(内网用户应该使用内网地址)
|
||
if (isIntranetMinIO) {
|
||
return {
|
||
type: 'public-s3',
|
||
status: 'success',
|
||
message: '内网用户使用内网MinIO地址(正常,无需测试公网S3)',
|
||
details: { url: testUrl }
|
||
}
|
||
}
|
||
|
||
if (!isPublicS3 && !testUrl.includes('/api/proxy-media')) {
|
||
return {
|
||
type: 'public-s3',
|
||
status: 'warning',
|
||
message: '当前URL不是公网S3地址',
|
||
details: { url: testUrl }
|
||
}
|
||
}
|
||
|
||
// 如果是代理URL,提取原始URL
|
||
let directUrl = testUrl
|
||
if (testUrl.includes('/api/proxy-media')) {
|
||
const urlParams = new URLSearchParams(testUrl.split('?')[1])
|
||
directUrl = urlParams.get('url') || testUrl
|
||
}
|
||
|
||
const startTime = performance.now()
|
||
const testResponse = await fetch(directUrl, {
|
||
cache: 'no-store',
|
||
signal: AbortSignal.timeout(10000)
|
||
})
|
||
const endTime = performance.now()
|
||
|
||
if (!testResponse.ok) throw new Error('公网S3连接失败')
|
||
|
||
const blob = await testResponse.blob()
|
||
createdBlobs.current.push(blob) // 跟踪Blob以便清理
|
||
const totalTime = endTime - startTime
|
||
const speed = blob.size / (totalTime / 1000)
|
||
|
||
// 获取Resource Timing
|
||
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||
const entry = entries.find(e => e.name.includes(directUrl.split('/').pop() || ''))
|
||
|
||
let dnsTime = 0
|
||
let connectTime = 0
|
||
let ttfb = 0
|
||
if (entry) {
|
||
dnsTime = entry.domainLookupEnd - entry.domainLookupStart
|
||
connectTime = entry.connectEnd - entry.connectStart
|
||
ttfb = entry.responseStart - entry.requestStart
|
||
}
|
||
|
||
return {
|
||
type: 'public-s3',
|
||
status: ttfb > 2000 ? 'warning' : 'success',
|
||
speed,
|
||
message: `公网S3: TTFB ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms, 速度 ${formatHumanSpeed(speed)}`,
|
||
details: {
|
||
dnsTime: dnsTime > 0 ? dnsTime : undefined,
|
||
connectTime: connectTime > 0 ? connectTime : undefined,
|
||
ttfb: ttfb > 0 ? ttfb : totalTime,
|
||
totalTime,
|
||
fileSize: blob.size,
|
||
url: directUrl
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'public-s3',
|
||
status: 'error',
|
||
message: `公网S3测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 13. 并发测试
|
||
const testConcurrent = async (): Promise<TestResult> => {
|
||
try {
|
||
const response = await fetch('/api/materials?pageSize=5')
|
||
if (!response.ok) throw new Error('获取测试素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'concurrent',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
// 获取5个图片URL
|
||
const imageUrls: string[] = materials
|
||
.map((m: any) => m.previewUrl)
|
||
.filter((url: string | null | undefined): url is string => !!url)
|
||
.slice(0, 5)
|
||
|
||
if (imageUrls.length === 0) {
|
||
return {
|
||
type: 'concurrent',
|
||
status: 'warning',
|
||
message: '没有可测试的图片URL'
|
||
}
|
||
}
|
||
|
||
const startTime = performance.now()
|
||
|
||
// 并发加载5个图片
|
||
const promises = imageUrls.map((url: string) =>
|
||
fetch(url, { cache: 'no-store' }).then(r => r.blob())
|
||
)
|
||
|
||
const results = await Promise.all(promises)
|
||
// 跟踪所有Blob以便清理
|
||
results.forEach(blob => createdBlobs.current.push(blob))
|
||
const endTime = performance.now()
|
||
|
||
const totalTime = endTime - startTime
|
||
const totalSize = results.reduce((sum, blob) => sum + blob.size, 0)
|
||
const avgSpeed = totalSize / (totalTime / 1000)
|
||
const avgTimePerFile = totalTime / imageUrls.length
|
||
|
||
return {
|
||
type: 'concurrent',
|
||
status: avgTimePerFile > 2000 ? 'warning' : 'success',
|
||
speed: avgSpeed,
|
||
message: `并发${imageUrls.length}个文件: 平均${avgTimePerFile.toFixed(0)}ms/文件, 总速度 ${formatHumanSpeed(avgSpeed)}`,
|
||
details: {
|
||
totalTime,
|
||
totalSize,
|
||
fileCount: imageUrls.length,
|
||
avgTimePerFile,
|
||
urls: imageUrls
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'concurrent',
|
||
status: 'error',
|
||
message: `并发测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 14. 缓存测试
|
||
const testCache = async (): Promise<TestResult> => {
|
||
try {
|
||
// 增加pageSize提高找到有效素材的概率
|
||
const response = await fetch('/api/materials?pageSize=10')
|
||
if (!response.ok) throw new Error('获取测试素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'cache',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
// 查找第一个有previewUrl的素材
|
||
const materialWithPreview = materials.find((m: any) => m.previewUrl)
|
||
if (!materialWithPreview || !materialWithPreview.previewUrl) {
|
||
return {
|
||
type: 'cache',
|
||
status: 'warning',
|
||
message: '没有找到有预览URL的素材'
|
||
}
|
||
}
|
||
|
||
const testUrl = materialWithPreview.previewUrl
|
||
|
||
// 第一次加载(无缓存,跳过浏览器缓存)
|
||
const firstStart = performance.now()
|
||
const firstResponse = await fetch(testUrl, { cache: 'no-store' })
|
||
const firstBlob = await firstResponse.blob()
|
||
createdBlobs.current.push(firstBlob) // 跟踪Blob以便清理
|
||
const firstTime = performance.now() - firstStart
|
||
|
||
// 检查响应头的缓存控制
|
||
const cacheControl = firstResponse.headers.get('cache-control') || ''
|
||
const etag = firstResponse.headers.get('etag') || ''
|
||
const canCache = !cacheControl.includes('no-store') && !cacheControl.includes('no-cache')
|
||
const fileSize = firstBlob.size
|
||
|
||
// 等待更长时间,确保浏览器有足够时间缓存(特别是小文件)
|
||
await new Promise(resolve => setTimeout(resolve, 300))
|
||
|
||
// 第二次加载(尝试使用缓存)
|
||
const secondStart = performance.now()
|
||
const secondResponse = await fetch(testUrl, {
|
||
cache: 'default' // 使用默认缓存策略
|
||
})
|
||
const secondBlob = await secondResponse.blob()
|
||
createdBlobs.current.push(secondBlob) // 跟踪Blob以便清理
|
||
const secondTime = performance.now() - secondStart
|
||
|
||
const cacheSpeedup = firstTime > 0 ? ((firstTime - secondTime) / firstTime) * 100 : 0
|
||
|
||
// 改进的判断逻辑:
|
||
// 1. 对于小文件(<100KB),缓存效果可能不明显(网络延迟占比大),降低判断标准
|
||
// 2. 对于大文件,缓存应该明显更快(至少快30%)
|
||
// 3. 如果服务器不允许缓存(no-cache/no-store),则认为缓存无效是正常的
|
||
const isSmallFile = fileSize < 100 * 1024 // 100KB
|
||
const threshold = isSmallFile ? 0.7 : 0.5 // 小文件降低标准到70%
|
||
const isCached = canCache && secondTime < firstTime * threshold
|
||
|
||
// 生成更详细的提示信息
|
||
let message = `缓存效果: ${isCached ? '有效' : '无效'}`
|
||
if (!canCache) {
|
||
message += ' (服务器不允许缓存)'
|
||
} else if (isSmallFile) {
|
||
message += ` (小文件,缓存效果可能不明显)`
|
||
}
|
||
message += `, 加速 ${cacheSpeedup.toFixed(1)}% (首次${firstTime.toFixed(0)}ms, 二次${secondTime.toFixed(0)}ms)`
|
||
|
||
return {
|
||
type: 'cache',
|
||
status: isCached ? 'success' : (canCache ? 'warning' : 'success'), // 如果服务器不允许缓存,标记为success(这是正常的)
|
||
message,
|
||
details: {
|
||
firstTime,
|
||
secondTime,
|
||
cacheSpeedup,
|
||
isCached,
|
||
url: testUrl,
|
||
cacheControl,
|
||
canCache,
|
||
fileSize
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'cache',
|
||
status: 'error',
|
||
message: `缓存测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 15. S3存储性能分析(增强版)
|
||
const testS3Performance = async (): Promise<TestResult> => {
|
||
try {
|
||
// 增加pageSize提高找到有效素材的概率
|
||
const response = await fetch('/api/materials?pageSize=10')
|
||
if (!response.ok) throw new Error('获取测试素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 's3-performance',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
// 查找第一个有previewUrl的素材
|
||
const materialWithPreview = materials.find((m: any) => m.previewUrl)
|
||
if (!materialWithPreview || !materialWithPreview.previewUrl) {
|
||
return {
|
||
type: 's3-performance',
|
||
status: 'warning',
|
||
message: '没有找到有预览URL的素材'
|
||
}
|
||
}
|
||
|
||
const testUrl = materialWithPreview.previewUrl
|
||
|
||
let fullTestUrl = testUrl
|
||
if (!testUrl.startsWith('http://') && !testUrl.startsWith('https://')) {
|
||
fullTestUrl = new URL(testUrl, window.location.origin).toString()
|
||
}
|
||
|
||
let urlObj: URL
|
||
let s3Domain: string
|
||
try {
|
||
urlObj = new URL(fullTestUrl)
|
||
s3Domain = urlObj.hostname
|
||
} catch (e) {
|
||
return {
|
||
type: 's3-performance',
|
||
status: 'error',
|
||
message: `S3性能测试失败: 无效的URL格式 - ${testUrl}`
|
||
}
|
||
}
|
||
|
||
// 测试1: 检测是否使用frp(仅对公网域名,跳过内网IP以避免CORS错误)
|
||
let isFrp = false
|
||
const isPublicDomain = isPublicS3Url(s3Domain)
|
||
const isInternalIP = /^192\.168\.|^10\.|^172\.(1[6-9]|2[0-9]|3[01])\./.test(s3Domain)
|
||
// 仅对公网域名进行FRP检测,跳过内网IP和localhost(避免CORS错误)
|
||
if (isPublicDomain && !isInternalIP && !['localhost', '127.0.0.1'].includes(s3Domain)) {
|
||
try {
|
||
const frpTestUrl = `${urlObj.protocol}//${s3Domain}/`
|
||
const frpResponse = await fetch(frpTestUrl, {
|
||
method: 'GET',
|
||
signal: AbortSignal.timeout(5000)
|
||
})
|
||
const text = await frpResponse.text()
|
||
if (text.includes('frp') || text.includes('内网穿透')) {
|
||
isFrp = true
|
||
}
|
||
} catch (e) {
|
||
// 忽略错误(CORS等)
|
||
}
|
||
}
|
||
|
||
// 测试2: 下载速度和TTFB测试
|
||
const startTime = performance.now()
|
||
let ttfb = 0
|
||
|
||
const testResponse = await fetch(fullTestUrl, {
|
||
signal: AbortSignal.timeout(30000)
|
||
})
|
||
|
||
ttfb = performance.now() - startTime
|
||
const blob = await testResponse.blob()
|
||
createdBlobs.current.push(blob) // 跟踪Blob以便清理
|
||
const totalTime = performance.now() - startTime
|
||
|
||
const downloadTime = (totalTime - ttfb) / 1000
|
||
const downloadSpeed = downloadTime > 0 ? blob.size / downloadTime : 0
|
||
|
||
// 获取Resource Timing
|
||
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||
const entry = entries.find(e => e.name.includes(s3Domain))
|
||
|
||
let dnsTime = 0
|
||
let connectTime = 0
|
||
if (entry) {
|
||
dnsTime = entry.domainLookupEnd - entry.domainLookupStart
|
||
connectTime = entry.connectEnd - entry.connectStart
|
||
if (ttfb === 0) {
|
||
ttfb = entry.responseStart - entry.requestStart
|
||
}
|
||
}
|
||
|
||
// 分析结果
|
||
const issues: string[] = []
|
||
const suggestions: string[] = []
|
||
|
||
if (isFrp) {
|
||
issues.push('⚠ 当前资源仍需经入口层回源,峰值时吞吐会波动')
|
||
suggestions.push('• 优先确认展示图是否统一落到 s3.jiansun.vip 的 public-display 前缀')
|
||
suggestions.push('• 大源文件、压缩包、视频源文件不要纳入展示缓存')
|
||
suggestions.push('• 如问题集中在公网入口,优先排查 180.97.215.57 缓存层与 192.168.58.20:9000 回源链路')
|
||
}
|
||
|
||
if (ttfb > 1000) {
|
||
issues.push(`🔴 首字节时间过长: ${Math.round(ttfb)}ms(正常<200ms)`)
|
||
if (!isFrp) {
|
||
suggestions.push('• TTFB过高可能是服务器响应慢或网络延迟大')
|
||
}
|
||
}
|
||
|
||
if (downloadSpeed < 1024 * 1024 && downloadSpeed > 0) {
|
||
issues.push(`🔴 下载速度偏慢: ${formatHumanSpeed(downloadSpeed)}(低于 ${formatHumanSpeed(1024 * 1024)})`)
|
||
if (!isFrp) {
|
||
suggestions.push('• 带宽不足,建议升级服务器带宽或使用CDN')
|
||
}
|
||
}
|
||
|
||
if (dnsTime > 100) {
|
||
issues.push(`⚠ DNS解析较慢: ${dnsTime.toFixed(0)}ms`)
|
||
}
|
||
|
||
if (connectTime > 200) {
|
||
issues.push(`⚠ TCP连接较慢: ${connectTime.toFixed(0)}ms`)
|
||
}
|
||
|
||
const isSuccess = !isFrp && ttfb < 1000 && downloadSpeed > 1024 * 1024
|
||
const isWarning = !isSuccess && (ttfb < 3000 || downloadSpeed > 100 * 1024)
|
||
|
||
let message = ''
|
||
if (isSuccess) {
|
||
message = `性能良好\n下载速度: ${formatHumanSpeed(downloadSpeed)}, TTFB: ${Math.round(ttfb)}ms`
|
||
} else if (isWarning) {
|
||
message = `性能一般\n${issues.slice(0, 2).join('\n')}`
|
||
} else {
|
||
message = `性能较差\n${issues.slice(0, 2).join('\n')}`
|
||
}
|
||
|
||
return {
|
||
type: 's3-performance',
|
||
status: isSuccess ? 'success' : isWarning ? 'warning' : 'error',
|
||
message,
|
||
details: {
|
||
dnsTime: dnsTime > 0 ? dnsTime : undefined,
|
||
connectTime: connectTime > 0 ? connectTime : undefined,
|
||
ttfb,
|
||
totalTime,
|
||
fileSize: blob.size,
|
||
downloadSpeed,
|
||
url: fullTestUrl,
|
||
isFrp,
|
||
issues,
|
||
suggestions
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 's3-performance',
|
||
status: 'error',
|
||
message: `S3性能测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 16. API: 素材列表测试
|
||
const testApiMaterialsList = async (): Promise<TestResult> => {
|
||
try {
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/materials?pageSize=10', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!response.ok) throw new Error('素材列表API失败')
|
||
|
||
const data = await response.json()
|
||
const totalTime = endTime - startTime
|
||
const materialCount = data.materials?.length || data.length || 0
|
||
|
||
return {
|
||
type: 'api-materials-list',
|
||
status: totalTime > 1000 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, 返回${materialCount}条数据`,
|
||
details: {
|
||
totalTime,
|
||
materialCount,
|
||
url: '/api/materials'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'api-materials-list',
|
||
status: 'error',
|
||
message: `素材列表API测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 17. API: 素材详情测试
|
||
const testApiMaterialDetail = async (): Promise<TestResult> => {
|
||
try {
|
||
// 先获取一个素材ID
|
||
const listResponse = await fetch('/api/materials?pageSize=1', { cache: 'no-store' })
|
||
if (!listResponse.ok) throw new Error('获取素材列表失败')
|
||
|
||
const listData = await listResponse.json()
|
||
const materials = listData.materials || listData
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'api-material-detail',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
const materialId = materials[0].id
|
||
const startTime = performance.now()
|
||
const response = await fetch(`/api/materials/${materialId}`, { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!response.ok) throw new Error('素材详情API失败')
|
||
|
||
const data = await response.json()
|
||
const totalTime = endTime - startTime
|
||
|
||
return {
|
||
type: 'api-material-detail',
|
||
status: totalTime > 500 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms`,
|
||
details: {
|
||
totalTime,
|
||
materialId,
|
||
url: `/api/materials/${materialId}`
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'api-material-detail',
|
||
status: 'error',
|
||
message: `素材详情API测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 18. API: 素材统计测试
|
||
const testApiMaterialStats = async (): Promise<TestResult> => {
|
||
try {
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/materials/stats', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!response.ok) throw new Error('素材统计API失败')
|
||
|
||
const data = await response.json()
|
||
const totalTime = endTime - startTime
|
||
|
||
return {
|
||
type: 'api-material-stats',
|
||
status: totalTime > 1000 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms`,
|
||
details: {
|
||
totalTime,
|
||
stats: data,
|
||
url: '/api/materials/stats'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'api-material-stats',
|
||
status: 'error',
|
||
message: `素材统计API测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 19. API: 上传初始化测试
|
||
const testApiUploadInit = async (): Promise<TestResult> => {
|
||
try {
|
||
// 上传初始化API需要JSON格式,不是FormData
|
||
const testData = {
|
||
fileName: 'test.txt',
|
||
fileSize: 4,
|
||
totalChunks: 1
|
||
}
|
||
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/materials/upload/init', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(testData),
|
||
cache: 'no-store'
|
||
})
|
||
const endTime = performance.now()
|
||
|
||
const totalTime = endTime - startTime
|
||
|
||
// 如果失败,检查错误原因
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({}))
|
||
const isUnauthorized = response.status === 401 || response.status === 403
|
||
const isBadRequest = response.status === 400
|
||
|
||
// 根据错误类型返回不同的状态
|
||
let status: 'success' | 'warning' | 'error' = 'warning'
|
||
let message = `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status}`
|
||
|
||
if (isUnauthorized) {
|
||
status = 'success'
|
||
message += ' (需要登录,这是正常的)'
|
||
} else if (isBadRequest) {
|
||
status = 'warning'
|
||
message += ` (${errorData.error || '参数错误'})`
|
||
} else {
|
||
status = 'error'
|
||
message += ` (${errorData.error || '服务器错误'})`
|
||
}
|
||
|
||
return {
|
||
type: 'api-upload-init',
|
||
status,
|
||
message,
|
||
details: {
|
||
totalTime,
|
||
statusCode: response.status,
|
||
error: errorData.error,
|
||
url: '/api/materials/upload/init'
|
||
}
|
||
}
|
||
}
|
||
|
||
const data = await response.json()
|
||
|
||
// 跟踪uploadId以便后续清理
|
||
if (data.uploadId) {
|
||
createdUploadIds.current.push(data.uploadId)
|
||
}
|
||
|
||
return {
|
||
type: 'api-upload-init',
|
||
status: totalTime > 1000 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms`,
|
||
details: {
|
||
totalTime,
|
||
uploadId: data.uploadId,
|
||
url: '/api/materials/upload/init'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'api-upload-init',
|
||
status: 'error',
|
||
message: `上传初始化API测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 20. API: 下载接口测试
|
||
const testApiDownload = async (): Promise<TestResult> => {
|
||
try {
|
||
// 先获取一个素材ID
|
||
const listResponse = await fetch('/api/materials?pageSize=1', { cache: 'no-store' })
|
||
if (!listResponse.ok) throw new Error('获取素材列表失败')
|
||
|
||
const listData = await listResponse.json()
|
||
const materials = listData.materials || listData
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'api-download',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
const materialId = materials[0].id
|
||
const startTime = performance.now()
|
||
const response = await fetch(`/api/materials/download?id=${materialId}`, {
|
||
method: 'GET',
|
||
cache: 'no-store'
|
||
})
|
||
const endTime = performance.now()
|
||
|
||
const totalTime = endTime - startTime
|
||
|
||
// 即使失败也记录(可能是权限问题)
|
||
if (!response.ok) {
|
||
return {
|
||
type: 'api-download',
|
||
status: 'warning',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录或无权下载)`,
|
||
details: {
|
||
totalTime,
|
||
statusCode: response.status,
|
||
materialId,
|
||
url: `/api/materials/download?id=${materialId}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查是否是重定向或文件流
|
||
const contentType = response.headers.get('content-type') || ''
|
||
const isFile = contentType.includes('application/zip') || contentType.includes('application/octet-stream')
|
||
|
||
return {
|
||
type: 'api-download',
|
||
status: totalTime > 2000 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms${isFile ? ', 返回文件流' : ', 返回数据'}`,
|
||
details: {
|
||
totalTime,
|
||
materialId,
|
||
contentType,
|
||
url: `/api/materials/download?id=${materialId}`
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'api-download',
|
||
status: 'error',
|
||
message: `下载API测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 21. API: 用户信息测试
|
||
const testApiUserProfile = async (): Promise<TestResult> => {
|
||
try {
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/user/profile', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
const totalTime = endTime - startTime
|
||
|
||
if (!response.ok) {
|
||
return {
|
||
type: 'api-user-profile',
|
||
status: 'warning',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录)`,
|
||
details: {
|
||
totalTime,
|
||
statusCode: response.status,
|
||
url: '/api/user/profile'
|
||
}
|
||
}
|
||
}
|
||
|
||
const data = await response.json()
|
||
|
||
return {
|
||
type: 'api-user-profile',
|
||
status: totalTime > 500 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms${data.username ? `, 用户: ${data.username}` : ''}`,
|
||
details: {
|
||
totalTime,
|
||
userId: data.id,
|
||
url: '/api/user/profile'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'api-user-profile',
|
||
status: 'error',
|
||
message: `用户信息API测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 22. API: 系统设置测试
|
||
const testApiSystemSettings = async (): Promise<TestResult> => {
|
||
try {
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/system-settings', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!response.ok) throw new Error('系统设置API失败')
|
||
|
||
const data = await response.json()
|
||
const totalTime = endTime - startTime
|
||
|
||
return {
|
||
type: 'api-system-settings',
|
||
status: totalTime > 500 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms`,
|
||
details: {
|
||
totalTime,
|
||
url: '/api/system-settings'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'api-system-settings',
|
||
status: 'error',
|
||
message: `系统设置API测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 23. API: 健康检查测试
|
||
const testApiHealth = async (): Promise<TestResult> => {
|
||
try {
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/health', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!response.ok) throw new Error('健康检查API失败')
|
||
|
||
const data = await response.json()
|
||
const totalTime = endTime - startTime
|
||
|
||
return {
|
||
type: 'api-health',
|
||
status: totalTime > 200 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${data.status || 'ok'}`,
|
||
details: {
|
||
totalTime,
|
||
health: data,
|
||
url: '/api/health'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'api-health',
|
||
status: 'error',
|
||
message: `健康检查API测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 24. 数据库查询性能测试
|
||
const testDatabaseQuery = async (): Promise<TestResult> => {
|
||
try {
|
||
// 通过素材列表API间接测试数据库查询性能
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/materials?pageSize=1', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!response.ok) throw new Error('数据库查询测试失败')
|
||
|
||
const totalTime = endTime - startTime
|
||
|
||
// 获取Resource Timing
|
||
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||
const entry = entries.find(e => e.name.includes('/api/materials'))
|
||
|
||
let ttfb = 0
|
||
if (entry) {
|
||
ttfb = entry.responseStart - entry.requestStart
|
||
}
|
||
|
||
return {
|
||
type: 'database-query',
|
||
status: ttfb > 500 ? 'warning' : 'success',
|
||
message: `查询时间: ${ttfb > 0 ? ttfb.toFixed(0) : totalTime.toFixed(0)}ms (总耗时: ${totalTime.toFixed(0)}ms)`,
|
||
details: {
|
||
totalTime,
|
||
ttfb: ttfb > 0 ? ttfb : totalTime,
|
||
url: '/api/materials'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'database-query',
|
||
status: 'error',
|
||
message: `数据库查询测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 25. Redis连接测试(通过系统状态API)
|
||
const testRedisConnection = async (): Promise<TestResult> => {
|
||
try {
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/admin/system-status', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
const totalTime = endTime - startTime
|
||
|
||
if (!response.ok) {
|
||
return {
|
||
type: 'redis-connection',
|
||
status: 'warning',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录或无权访问)`,
|
||
details: {
|
||
totalTime,
|
||
statusCode: response.status,
|
||
url: '/api/admin/system-status'
|
||
}
|
||
}
|
||
}
|
||
|
||
const data = await response.json()
|
||
const redisStatus = data.redis || data.redisStatus || 'unknown'
|
||
|
||
return {
|
||
type: 'redis-connection',
|
||
status: totalTime > 500 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, Redis: ${redisStatus === 'connected' || redisStatus === 'ok' ? '已连接' : '状态未知'}`,
|
||
details: {
|
||
totalTime,
|
||
redisStatus,
|
||
url: '/api/admin/system-status'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'redis-connection',
|
||
status: 'error',
|
||
message: `Redis连接测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 26. 页面: 首页加载测试
|
||
const testPageHomeLoad = async (): Promise<TestResult> => {
|
||
try {
|
||
const startTime = performance.now()
|
||
const response = await fetch('/', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!response.ok) throw new Error('首页加载失败')
|
||
|
||
const html = await response.text()
|
||
const totalTime = endTime - startTime
|
||
const htmlSize = new Blob([html]).size
|
||
|
||
// 获取Resource Timing
|
||
const entries = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[]
|
||
const entry = entries[0]
|
||
|
||
let domContentLoaded = 0
|
||
let loadComplete = 0
|
||
if (entry) {
|
||
domContentLoaded = entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart
|
||
loadComplete = entry.loadEventEnd - entry.loadEventStart
|
||
}
|
||
|
||
return {
|
||
type: 'page-home-load',
|
||
status: totalTime > 2000 ? 'warning' : 'success',
|
||
message: `加载时间: ${totalTime.toFixed(0)}ms, HTML大小: ${formatFileSize(htmlSize)}`,
|
||
details: {
|
||
totalTime,
|
||
htmlSize,
|
||
domContentLoaded: domContentLoaded > 0 ? domContentLoaded : undefined,
|
||
loadComplete: loadComplete > 0 ? loadComplete : undefined,
|
||
url: '/'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'page-home-load',
|
||
status: 'error',
|
||
message: `首页加载测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 27. 页面: 详情页加载测试
|
||
const testPageDetailLoad = async (): Promise<TestResult> => {
|
||
try {
|
||
// 先获取一个素材ID
|
||
const listResponse = await fetch('/api/materials?pageSize=1', { cache: 'no-store' })
|
||
if (!listResponse.ok) throw new Error('获取素材列表失败')
|
||
|
||
const listData = await listResponse.json()
|
||
const materials = listData.materials || listData
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'page-detail-load',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
const materialId = materials[0].id
|
||
const startTime = performance.now()
|
||
const response = await fetch(`/share/${materialId}`, { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
if (!response.ok) throw new Error('详情页加载失败')
|
||
|
||
const html = await response.text()
|
||
const totalTime = endTime - startTime
|
||
const htmlSize = new Blob([html]).size
|
||
|
||
return {
|
||
type: 'page-detail-load',
|
||
status: totalTime > 2000 ? 'warning' : 'success',
|
||
message: `加载时间: ${totalTime.toFixed(0)}ms, HTML大小: ${formatFileSize(htmlSize)}`,
|
||
details: {
|
||
totalTime,
|
||
htmlSize,
|
||
materialId,
|
||
url: `/share/${materialId}`
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'page-detail-load',
|
||
status: 'error',
|
||
message: `详情页加载测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 28. 上传流程测试(完整流程)
|
||
const testUploadFlow = async (): Promise<TestResult> => {
|
||
try {
|
||
// 上传初始化API需要JSON格式
|
||
const testData = {
|
||
fileName: 'test-upload.txt',
|
||
fileSize: 13, // 'test content'的长度
|
||
totalChunks: 1
|
||
}
|
||
|
||
const startTime = performance.now()
|
||
|
||
// 步骤1: 初始化上传
|
||
const initResponse = await fetch('/api/materials/upload/init', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(testData),
|
||
cache: 'no-store'
|
||
})
|
||
|
||
const initTime = performance.now() - startTime
|
||
|
||
if (!initResponse.ok) {
|
||
const errorData = await initResponse.json().catch(() => ({}))
|
||
const isUnauthorized = initResponse.status === 401 || initResponse.status === 403
|
||
const isBadRequest = initResponse.status === 400
|
||
|
||
let status: 'success' | 'warning' | 'error' = 'warning'
|
||
let message = `初始化失败: ${initTime.toFixed(0)}ms, 状态: ${initResponse.status}`
|
||
|
||
if (isUnauthorized) {
|
||
status = 'success'
|
||
message += ' (需要登录,这是正常的)'
|
||
} else if (isBadRequest) {
|
||
status = 'warning'
|
||
message += ` (${errorData.error || '参数错误'})`
|
||
} else {
|
||
status = 'error'
|
||
message += ` (${errorData.error || '服务器错误'})`
|
||
}
|
||
|
||
return {
|
||
type: 'upload-flow',
|
||
status,
|
||
message,
|
||
details: {
|
||
totalTime: initTime,
|
||
step: 'init',
|
||
statusCode: initResponse.status,
|
||
error: errorData.error,
|
||
url: '/api/materials/upload/init'
|
||
}
|
||
}
|
||
}
|
||
|
||
const initData = await initResponse.json()
|
||
const totalTime = performance.now() - startTime
|
||
|
||
// 跟踪uploadId以便后续清理
|
||
if (initData.uploadId) {
|
||
createdUploadIds.current.push(initData.uploadId)
|
||
}
|
||
|
||
return {
|
||
type: 'upload-flow',
|
||
status: totalTime > 2000 ? 'warning' : 'success',
|
||
message: `初始化完成: ${initTime.toFixed(0)}ms (总耗时: ${totalTime.toFixed(0)}ms)`,
|
||
details: {
|
||
totalTime,
|
||
initTime,
|
||
uploadId: initData.uploadId,
|
||
step: 'init',
|
||
url: '/api/materials/upload/init'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'upload-flow',
|
||
status: 'error',
|
||
message: `上传流程测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 29. 下载流程测试
|
||
const testDownloadFlow = async (): Promise<TestResult> => {
|
||
try {
|
||
// 先获取一个素材ID
|
||
const listResponse = await fetch('/api/materials?pageSize=1', { cache: 'no-store' })
|
||
if (!listResponse.ok) throw new Error('获取素材列表失败')
|
||
|
||
const listData = await listResponse.json()
|
||
const materials = listData.materials || listData
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'download-flow',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
const materialId = materials[0].id
|
||
const startTime = performance.now()
|
||
|
||
// 测试下载接口响应
|
||
const response = await fetch(`/api/materials/download?id=${materialId}`, {
|
||
method: 'GET',
|
||
cache: 'no-store'
|
||
})
|
||
|
||
const totalTime = performance.now() - startTime
|
||
|
||
if (!response.ok) {
|
||
return {
|
||
type: 'download-flow',
|
||
status: 'warning',
|
||
message: `下载接口响应: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录或无权下载)`,
|
||
details: {
|
||
totalTime,
|
||
statusCode: response.status,
|
||
materialId,
|
||
url: `/api/materials/download?id=${materialId}`
|
||
}
|
||
}
|
||
}
|
||
|
||
const contentType = response.headers.get('content-type') || ''
|
||
const isFile = contentType.includes('application/zip') || contentType.includes('application/octet-stream')
|
||
|
||
return {
|
||
type: 'download-flow',
|
||
status: totalTime > 2000 ? 'warning' : 'success',
|
||
message: `下载接口响应: ${totalTime.toFixed(0)}ms${isFile ? ', 返回文件流' : ', 返回数据'}`,
|
||
details: {
|
||
totalTime,
|
||
contentType,
|
||
materialId,
|
||
url: `/api/materials/download?id=${materialId}`
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'download-flow',
|
||
status: 'error',
|
||
message: `下载流程测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 30. 转码服务测试
|
||
const testTranscodeService = async (): Promise<TestResult> => {
|
||
try {
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/admin/video-transcode-stats', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
const totalTime = endTime - startTime
|
||
|
||
if (!response.ok) {
|
||
return {
|
||
type: 'transcode-service',
|
||
status: 'warning',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (可能未登录或无权访问)`,
|
||
details: {
|
||
totalTime,
|
||
statusCode: response.status,
|
||
url: '/api/admin/video-transcode-stats'
|
||
}
|
||
}
|
||
}
|
||
|
||
const data = await response.json()
|
||
const queueSize = data.queueSize || data.pending || 0
|
||
const processing = data.processing || 0
|
||
|
||
return {
|
||
type: 'transcode-service',
|
||
status: totalTime > 1000 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, 队列: ${queueSize}, 处理中: ${processing}`,
|
||
details: {
|
||
totalTime,
|
||
queueSize,
|
||
processing,
|
||
stats: data,
|
||
url: '/api/admin/video-transcode-stats'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'transcode-service',
|
||
status: 'error',
|
||
message: `转码服务测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 31. 认证Session测试
|
||
const testAuthSession = async (): Promise<TestResult> => {
|
||
try {
|
||
const startTime = performance.now()
|
||
const response = await fetch('/api/user/profile', { cache: 'no-store' })
|
||
const endTime = performance.now()
|
||
|
||
const totalTime = endTime - startTime
|
||
|
||
if (response.status === 401 || response.status === 403) {
|
||
return {
|
||
type: 'auth-session',
|
||
status: 'warning',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, 状态: ${response.status} (未登录或Session过期)`,
|
||
details: {
|
||
totalTime,
|
||
statusCode: response.status,
|
||
isAuthenticated: false,
|
||
url: '/api/user/profile'
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!response.ok) throw new Error('认证测试失败')
|
||
|
||
const data = await response.json()
|
||
const isAuthenticated = !!data.id
|
||
|
||
return {
|
||
type: 'auth-session',
|
||
status: totalTime > 500 ? 'warning' : 'success',
|
||
message: `响应时间: ${totalTime.toFixed(0)}ms, 认证状态: ${isAuthenticated ? '已登录' : '未登录'}`,
|
||
details: {
|
||
totalTime,
|
||
isAuthenticated,
|
||
userId: data.id,
|
||
url: '/api/user/profile'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'auth-session',
|
||
status: 'error',
|
||
message: `认证Session测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 开始诊断(完整版)
|
||
const startDiagnose = async () => {
|
||
setTesting(true)
|
||
setResults([])
|
||
setNetworkInfo(null)
|
||
|
||
// 清空资源跟踪数组
|
||
createdBlobs.current = []
|
||
createdUrls.current = []
|
||
createdUploadIds.current = []
|
||
|
||
try {
|
||
// 1. 网络环境检测
|
||
setResults([{ type: 'network-env', status: 'testing' }])
|
||
const networkEnvResult = await testNetworkEnvironment()
|
||
setResults([networkEnvResult])
|
||
|
||
// 从网络环境检测结果中获取网络信息(用于后续测试判断)
|
||
const isIntranet = networkEnvResult.details?.isIntranet ?? false
|
||
|
||
// 2. DNS和连接测试
|
||
setResults(prev => [...prev, { type: 'connection', status: 'testing' }])
|
||
const connectionResult = await testDNSAndConnection()
|
||
setResults(prev => [...prev.slice(0, -1), connectionResult])
|
||
|
||
// 3. 下载测速
|
||
setResults(prev => [...prev, { type: 'download', status: 'testing' }])
|
||
const downloadResult = await testDownloadSpeed()
|
||
setResults(prev => [...prev.slice(0, -1), downloadResult])
|
||
|
||
// 4. 上传测速
|
||
setResults(prev => [...prev, { type: 'upload', status: 'testing' }])
|
||
const uploadResult = await testUploadSpeed()
|
||
setResults(prev => [...prev.slice(0, -1), uploadResult])
|
||
|
||
// 5. 存储连通性
|
||
setResults(prev => [...prev, { type: 'storage', status: 'testing' }])
|
||
const storageResult = await testStorageConnectivity()
|
||
setResults(prev => [...prev.slice(0, -1), storageResult])
|
||
|
||
// 6. 预览图片加载测试
|
||
setResults(prev => [...prev, { type: 'preview-image', status: 'testing' }])
|
||
const imageResult = await testPreviewImageLoad()
|
||
setResults(prev => [...prev.slice(0, -1), imageResult])
|
||
|
||
// 7. 预览视频加载测试
|
||
setResults(prev => [...prev, { type: 'preview-video', status: 'testing' }])
|
||
const videoResult = await testPreviewVideoLoad()
|
||
setResults(prev => [...prev.slice(0, -1), videoResult])
|
||
|
||
// 8. 音频文件加载测试
|
||
setResults(prev => [...prev, { type: 'preview-audio', status: 'testing' }])
|
||
const audioResult = await testPreviewAudioLoad()
|
||
setResults(prev => [...prev.slice(0, -1), audioResult])
|
||
|
||
// 9. 代理性能测试
|
||
setResults(prev => [...prev, { type: 'proxy-performance', status: 'testing' }])
|
||
const proxyResult = await testProxyPerformance()
|
||
setResults(prev => [...prev.slice(0, -1), proxyResult])
|
||
|
||
// 10. 视频Range请求测试
|
||
setResults(prev => [...prev, { type: 'video-range', status: 'testing' }])
|
||
const rangeResult = await testVideoRangeRequest()
|
||
setResults(prev => [...prev.slice(0, -1), rangeResult])
|
||
|
||
// 11. 内网MinIO直连测试(仅内网用户)
|
||
if (isIntranet) {
|
||
setResults(prev => [...prev, { type: 'intranet-minio', status: 'testing' }])
|
||
const intranetResult = await testIntranetMinIO()
|
||
setResults(prev => [...prev.slice(0, -1), intranetResult])
|
||
}
|
||
|
||
// 12. 公网S3直连测试
|
||
setResults(prev => [...prev, { type: 'public-s3', status: 'testing' }])
|
||
const publicS3Result = await testPublicS3()
|
||
setResults(prev => [...prev.slice(0, -1), publicS3Result])
|
||
|
||
// 13. 并发测试
|
||
setResults(prev => [...prev, { type: 'concurrent', status: 'testing' }])
|
||
const concurrentResult = await testConcurrent()
|
||
setResults(prev => [...prev.slice(0, -1), concurrentResult])
|
||
|
||
// 14. 缓存测试
|
||
setResults(prev => [...prev, { type: 'cache', status: 'testing' }])
|
||
const cacheResult = await testCache()
|
||
setResults(prev => [...prev.slice(0, -1), cacheResult])
|
||
|
||
// 15. S3存储性能分析
|
||
setResults(prev => [...prev, { type: 's3-performance', status: 'testing' }])
|
||
const s3Result = await testS3Performance()
|
||
setResults(prev => [...prev.slice(0, -1), s3Result])
|
||
|
||
// 16. API: 素材列表测试
|
||
setResults(prev => [...prev, { type: 'api-materials-list', status: 'testing' }])
|
||
const apiMaterialsListResult = await testApiMaterialsList()
|
||
setResults(prev => [...prev.slice(0, -1), apiMaterialsListResult])
|
||
|
||
// 17. API: 素材详情测试
|
||
setResults(prev => [...prev, { type: 'api-material-detail', status: 'testing' }])
|
||
const apiMaterialDetailResult = await testApiMaterialDetail()
|
||
setResults(prev => [...prev.slice(0, -1), apiMaterialDetailResult])
|
||
|
||
// 18. API: 素材统计测试
|
||
setResults(prev => [...prev, { type: 'api-material-stats', status: 'testing' }])
|
||
const apiMaterialStatsResult = await testApiMaterialStats()
|
||
setResults(prev => [...prev.slice(0, -1), apiMaterialStatsResult])
|
||
|
||
// 19. API: 上传初始化测试
|
||
setResults(prev => [...prev, { type: 'api-upload-init', status: 'testing' }])
|
||
const apiUploadInitResult = await testApiUploadInit()
|
||
setResults(prev => [...prev.slice(0, -1), apiUploadInitResult])
|
||
|
||
// 20. API: 下载接口测试
|
||
setResults(prev => [...prev, { type: 'api-download', status: 'testing' }])
|
||
const apiDownloadResult = await testApiDownload()
|
||
setResults(prev => [...prev.slice(0, -1), apiDownloadResult])
|
||
|
||
// 21. API: 用户信息测试
|
||
setResults(prev => [...prev, { type: 'api-user-profile', status: 'testing' }])
|
||
const apiUserProfileResult = await testApiUserProfile()
|
||
setResults(prev => [...prev.slice(0, -1), apiUserProfileResult])
|
||
|
||
// 22. API: 系统设置测试
|
||
setResults(prev => [...prev, { type: 'api-system-settings', status: 'testing' }])
|
||
const apiSystemSettingsResult = await testApiSystemSettings()
|
||
setResults(prev => [...prev.slice(0, -1), apiSystemSettingsResult])
|
||
|
||
// 23. API: 健康检查测试
|
||
setResults(prev => [...prev, { type: 'api-health', status: 'testing' }])
|
||
const apiHealthResult = await testApiHealth()
|
||
setResults(prev => [...prev.slice(0, -1), apiHealthResult])
|
||
|
||
// 24. 数据库查询性能测试
|
||
setResults(prev => [...prev, { type: 'database-query', status: 'testing' }])
|
||
const databaseQueryResult = await testDatabaseQuery()
|
||
setResults(prev => [...prev.slice(0, -1), databaseQueryResult])
|
||
|
||
// 25. Redis连接测试
|
||
setResults(prev => [...prev, { type: 'redis-connection', status: 'testing' }])
|
||
const redisConnectionResult = await testRedisConnection()
|
||
setResults(prev => [...prev.slice(0, -1), redisConnectionResult])
|
||
|
||
// 26. 页面: 首页加载测试
|
||
setResults(prev => [...prev, { type: 'page-home-load', status: 'testing' }])
|
||
const pageHomeLoadResult = await testPageHomeLoad()
|
||
setResults(prev => [...prev.slice(0, -1), pageHomeLoadResult])
|
||
|
||
// 27. 页面: 详情页加载测试
|
||
setResults(prev => [...prev, { type: 'page-detail-load', status: 'testing' }])
|
||
const pageDetailLoadResult = await testPageDetailLoad()
|
||
setResults(prev => [...prev.slice(0, -1), pageDetailLoadResult])
|
||
|
||
// 28. 上传流程测试
|
||
setResults(prev => [...prev, { type: 'upload-flow', status: 'testing' }])
|
||
const uploadFlowResult = await testUploadFlow()
|
||
setResults(prev => [...prev.slice(0, -1), uploadFlowResult])
|
||
|
||
// 29. 下载流程测试
|
||
setResults(prev => [...prev, { type: 'download-flow', status: 'testing' }])
|
||
const downloadFlowResult = await testDownloadFlow()
|
||
setResults(prev => [...prev.slice(0, -1), downloadFlowResult])
|
||
|
||
// 30. 转码服务测试
|
||
setResults(prev => [...prev, { type: 'transcode-service', status: 'testing' }])
|
||
const transcodeServiceResult = await testTranscodeService()
|
||
setResults(prev => [...prev.slice(0, -1), transcodeServiceResult])
|
||
|
||
// 31. 认证Session测试
|
||
setResults(prev => [...prev, { type: 'auth-session', status: 'testing' }])
|
||
const authSessionResult = await testAuthSession()
|
||
setResults(prev => [...prev.slice(0, -1), authSessionResult])
|
||
|
||
// 32. 内外网URL重写测试
|
||
if (isIntranet) {
|
||
// 内网用户的URL重写测试
|
||
setResults(prev => [...prev, { type: 'intranet-url-rewrite', status: 'testing' }])
|
||
const intranetUrlRewriteResult = await testIntranetUrlRewrite()
|
||
setResults(prev => [...prev.slice(0, -1), intranetUrlRewriteResult])
|
||
} else {
|
||
// 外网用户的URL重写测试
|
||
setResults(prev => [...prev, { type: 'public-url-rewrite', status: 'testing' }])
|
||
const publicUrlRewriteResult = await testPublicUrlRewrite()
|
||
setResults(prev => [...prev.slice(0, -1), publicUrlRewriteResult])
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('诊断过程出错:', error)
|
||
showToast('诊断过程出错', { variant: 'error' })
|
||
} finally {
|
||
// 清理诊断过程中创建的所有资源
|
||
await cleanupDiagnosticResources()
|
||
setTesting(false)
|
||
}
|
||
}
|
||
|
||
// 32. 内网用户URL重写测试
|
||
const testIntranetUrlRewrite = async (): Promise<TestResult> => {
|
||
try {
|
||
// 增加pageSize提高找到有效素材的概率(内网用户专用测试)
|
||
const response = await fetch('/api/materials?pageSize=10', { cache: 'no-store' })
|
||
if (!response.ok) throw new Error('获取测试素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'intranet-url-rewrite',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
// 查找第一个有previewUrl的素材
|
||
const materialWithPreview = materials.find((m: any) => m.previewUrl)
|
||
if (!materialWithPreview || !materialWithPreview.previewUrl) {
|
||
return {
|
||
type: 'intranet-url-rewrite',
|
||
status: 'warning',
|
||
message: '没有找到有预览URL的素材'
|
||
}
|
||
}
|
||
|
||
const testUrl = materialWithPreview.previewUrl
|
||
|
||
// 检查URL是否被正确重写为内网MinIO地址
|
||
const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1')
|
||
const isProxyUrl = testUrl.includes('/api/proxy-media')
|
||
const isPublicS3 = isPublicS3Url(testUrl)
|
||
|
||
let status: 'success' | 'warning' | 'error' = 'success'
|
||
let message = ''
|
||
const issues: string[] = []
|
||
|
||
if (isIntranetMinIO && !isProxyUrl) {
|
||
message = 'URL已正确重写为内网MinIO地址(直连,不走代理)'
|
||
} else if (isProxyUrl && isPublicS3) {
|
||
status = 'warning'
|
||
issues.push('内网用户仍通过代理访问公网S3,应直接访问内网MinIO')
|
||
message = `URL通过代理访问: ${issues.join(', ')}`
|
||
} else if (isPublicS3 && !isProxyUrl) {
|
||
status = 'warning'
|
||
issues.push('内网用户直接访问公网S3,应重写为内网MinIO')
|
||
message = `URL未重写: ${issues.join(', ')}`
|
||
} else {
|
||
message = `URL格式: ${isProxyUrl ? '代理URL' : isIntranetMinIO ? '内网MinIO' : '其他'}`
|
||
}
|
||
|
||
return {
|
||
type: 'intranet-url-rewrite',
|
||
status,
|
||
message,
|
||
details: {
|
||
url: testUrl,
|
||
isIntranetMinIO,
|
||
isProxyUrl,
|
||
isPublicS3,
|
||
issues: issues.length > 0 ? issues : undefined
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'intranet-url-rewrite',
|
||
status: 'error',
|
||
message: `内网URL重写测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 33. 外网用户URL重写测试
|
||
const testPublicUrlRewrite = async (): Promise<TestResult> => {
|
||
try {
|
||
// 增加pageSize提高找到有效素材的概率(外网用户专用测试)
|
||
const response = await fetch('/api/materials?pageSize=10', { cache: 'no-store' })
|
||
if (!response.ok) throw new Error('获取测试素材失败')
|
||
|
||
const data = await response.json()
|
||
const materials = data.materials || data
|
||
if (!materials || materials.length === 0) {
|
||
return {
|
||
type: 'public-url-rewrite',
|
||
status: 'warning',
|
||
message: '没有可测试的素材'
|
||
}
|
||
}
|
||
|
||
// 查找第一个有previewUrl的素材
|
||
const materialWithPreview = materials.find((m: any) => m.previewUrl)
|
||
if (!materialWithPreview || !materialWithPreview.previewUrl) {
|
||
return {
|
||
type: 'public-url-rewrite',
|
||
status: 'warning',
|
||
message: '没有找到有预览URL的素材'
|
||
}
|
||
}
|
||
|
||
const testUrl = materialWithPreview.previewUrl
|
||
|
||
// 检查URL是否正确(外网用户应使用公网S3或代理)
|
||
const isIntranetMinIO = testUrl.includes('192.168.') || testUrl.includes('localhost') || testUrl.includes('127.0.0.1')
|
||
const isProxyUrl = testUrl.includes('/api/proxy-media')
|
||
const isPublicS3 = isPublicS3Url(testUrl)
|
||
|
||
let status: 'success' | 'warning' | 'error' = 'success'
|
||
let message = ''
|
||
const issues: string[] = []
|
||
|
||
if (isIntranetMinIO) {
|
||
status = 'error'
|
||
issues.push('外网用户不应访问内网MinIO地址')
|
||
message = `URL错误: ${issues.join(', ')}`
|
||
} else if (isProxyUrl || isPublicS3) {
|
||
message = `URL格式正确: ${isProxyUrl ? '通过代理' : '直接访问公网S3'}`
|
||
} else {
|
||
status = 'warning'
|
||
message = 'URL格式未知'
|
||
}
|
||
|
||
return {
|
||
type: 'public-url-rewrite',
|
||
status,
|
||
message,
|
||
details: {
|
||
url: testUrl,
|
||
isIntranetMinIO,
|
||
isProxyUrl,
|
||
isPublicS3,
|
||
issues: issues.length > 0 ? issues : undefined
|
||
}
|
||
}
|
||
} catch (error) {
|
||
return {
|
||
type: 'public-url-rewrite',
|
||
status: 'error',
|
||
message: `外网URL重写测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
}
|
||
|
||
// 复制结果(增强版:包含所有详细信息)
|
||
const copyResults = async () => {
|
||
if (results.length === 0) return
|
||
|
||
const timestamp = new Date().toLocaleString('zh-CN')
|
||
const browserInfo = navigator.userAgent
|
||
const currentUrl = window.location.href
|
||
|
||
// 获取浏览器详细信息
|
||
const connection = (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection
|
||
const networkType = connection?.effectiveType || 'unknown'
|
||
const downlink = connection?.downlink || 'unknown'
|
||
const rtt = connection?.rtt || 'unknown'
|
||
const saveData = connection?.saveData || false
|
||
const browserDownlinkSpeed = typeof downlink === 'number'
|
||
? formatHumanSpeed((downlink * 1000 * 1000) / 8)
|
||
: '未知'
|
||
|
||
const report = [
|
||
'=== 网络诊断完整报告 ===',
|
||
`时间: ${timestamp}`,
|
||
`浏览器: ${browserInfo}`,
|
||
`当前URL: ${currentUrl}`,
|
||
'',
|
||
'=== 网络环境信息 ===',
|
||
`在线状态: ${navigator.onLine ? '在线' : '离线'}`,
|
||
`网络类型: ${networkType}`,
|
||
`浏览器估算下行: ${browserDownlinkSpeed}`,
|
||
`RTT延迟: ${rtt !== 'unknown' ? `${rtt} ms` : '未知'}`,
|
||
`省流模式: ${saveData ? '开启' : '关闭'}`,
|
||
networkInfo ? [
|
||
`内网/外网: ${networkInfo.isIntranet ? '内网' : '外网'}`,
|
||
`访问域名: ${networkInfo.hostname}`,
|
||
`客户端IP: ${networkInfo.clientIp}`,
|
||
`公网域名: ${networkInfo.isPublicDomain ? '是' : '否'}`
|
||
].join('\n') : '',
|
||
'',
|
||
'=== 测试结果 ===',
|
||
...results.map(r => {
|
||
const typeMap: Record<string, string> = {
|
||
'network-env': '网络环境检测',
|
||
'connection': 'DNS和连接测试',
|
||
'download': '下载测速',
|
||
'upload': '上传测速',
|
||
'storage': '存储连通性',
|
||
'preview-image': '预览图片加载',
|
||
'preview-video': '预览视频加载',
|
||
'preview-audio': '音频文件加载',
|
||
'proxy-performance': '代理性能测试',
|
||
'video-range': '视频Range请求测试',
|
||
'intranet-minio': '内网MinIO直连测试',
|
||
'public-s3': '公网S3直连测试',
|
||
'concurrent': '并发测试',
|
||
'cache': '缓存测试',
|
||
's3-performance': 'S3存储性能分析',
|
||
'api-materials-list': 'API: 素材列表',
|
||
'api-material-detail': 'API: 素材详情',
|
||
'api-material-stats': 'API: 素材统计',
|
||
'api-upload-init': 'API: 上传初始化',
|
||
'api-download': 'API: 下载接口',
|
||
'api-user-profile': 'API: 用户信息',
|
||
'api-system-settings': 'API: 系统设置',
|
||
'api-health': 'API: 健康检查',
|
||
'database-query': '数据库查询性能',
|
||
'redis-connection': 'Redis连接测试',
|
||
'page-home-load': '页面: 首页加载',
|
||
'page-detail-load': '页面: 详情页加载',
|
||
'upload-flow': '上传流程测试',
|
||
'download-flow': '下载流程测试',
|
||
'transcode-service': '转码服务测试',
|
||
'auth-session': '认证Session测试',
|
||
'intranet-url-rewrite': '内网URL重写测试',
|
||
'public-url-rewrite': '外网URL重写测试'
|
||
}
|
||
const typeText = typeMap[r.type] || r.type
|
||
|
||
const statusText = r.status === 'success' ? '✓ 成功' : r.status === 'error' ? '✗ 失败' : r.status === 'warning' ? '⚠ 警告' : '⋯ 测试中'
|
||
|
||
let detailsText = ''
|
||
if (r.details) {
|
||
const details: string[] = []
|
||
if (r.details.dnsTime !== undefined) details.push(`DNS解析: ${r.details.dnsTime.toFixed(0)}ms`)
|
||
if (r.details.connectTime !== undefined) details.push(`TCP连接: ${r.details.connectTime.toFixed(0)}ms`)
|
||
if (r.details.sslTime !== undefined) details.push(`SSL握手: ${r.details.sslTime.toFixed(0)}ms`)
|
||
if (r.details.ttfb !== undefined) details.push(`首字节时间: ${r.details.ttfb.toFixed(0)}ms`)
|
||
if (r.details.requestTime !== undefined) details.push(`请求时间: ${r.details.requestTime.toFixed(0)}ms`)
|
||
if (r.details.responseTime !== undefined) details.push(`响应时间: ${r.details.responseTime.toFixed(0)}ms`)
|
||
if (r.details.totalTime !== undefined) details.push(`总耗时: ${r.details.totalTime.toFixed(0)}ms`)
|
||
if (r.details.fileSize !== undefined) details.push(`文件大小: ${formatFileSize(r.details.fileSize)}`)
|
||
if (r.details.downloadSpeed !== undefined) details.push(`下载速度: ${formatHumanSpeed(r.details.downloadSpeed)}`)
|
||
if (r.details.url) details.push(`URL: ${r.details.url}`)
|
||
if (r.details.isFrp !== undefined) details.push(`FRP穿透: ${r.details.isFrp ? '是' : '否'}`)
|
||
if (r.details.supportsRange !== undefined) details.push(`Range支持: ${r.details.supportsRange ? '是' : '否'}`)
|
||
if (r.details.cacheSpeedup !== undefined) details.push(`缓存加速: ${r.details.cacheSpeedup.toFixed(1)}%`)
|
||
if (r.details.fileCount !== undefined) details.push(`并发文件数: ${r.details.fileCount}`)
|
||
if (r.details.avgTimePerFile !== undefined) details.push(`平均时间/文件: ${r.details.avgTimePerFile.toFixed(0)}ms`)
|
||
if (r.details.materialCount !== undefined) details.push(`素材数量: ${r.details.materialCount}`)
|
||
if (r.details.materialId !== undefined) details.push(`素材ID: ${r.details.materialId}`)
|
||
if (r.details.uploadId !== undefined) details.push(`上传ID: ${r.details.uploadId}`)
|
||
if (r.details.userId !== undefined) details.push(`用户ID: ${r.details.userId}`)
|
||
if (r.details.statusCode !== undefined) details.push(`HTTP状态: ${r.details.statusCode}`)
|
||
if (r.details.contentType !== undefined) details.push(`内容类型: ${r.details.contentType}`)
|
||
if (r.details.htmlSize !== undefined) details.push(`HTML大小: ${formatFileSize(r.details.htmlSize)}`)
|
||
if (r.details.queueSize !== undefined) details.push(`队列大小: ${r.details.queueSize}`)
|
||
if (r.details.processing !== undefined) details.push(`处理中: ${r.details.processing}`)
|
||
if (r.details.isAuthenticated !== undefined) details.push(`认证状态: ${r.details.isAuthenticated ? '已登录' : '未登录'}`)
|
||
if (r.details.redisStatus !== undefined) details.push(`Redis状态: ${r.details.redisStatus}`)
|
||
if (r.details.step !== undefined) details.push(`步骤: ${r.details.step}`)
|
||
if (r.details.stats !== undefined) details.push(`统计信息: ${JSON.stringify(r.details.stats).substring(0, 100)}`)
|
||
if (r.details.health !== undefined) details.push(`健康状态: ${JSON.stringify(r.details.health).substring(0, 100)}`)
|
||
if (r.details.error !== undefined) details.push(`错误信息: ${r.details.error}`)
|
||
if (r.details.issues && Array.isArray(r.details.issues) && r.details.issues.length > 0) {
|
||
details.push(`问题: ${r.details.issues.join(', ')}`)
|
||
}
|
||
if (details.length > 0) {
|
||
detailsText = '\n 详细信息: ' + details.join(', ')
|
||
}
|
||
}
|
||
|
||
return `${typeText}: ${statusText}\n ${r.message || '测试中...'}${detailsText}`
|
||
}),
|
||
'',
|
||
'=== 性能分析 ===',
|
||
...results.filter(r => r.status === 'warning' || r.status === 'error').map(r => {
|
||
const typeMap: Record<string, string> = {
|
||
'preview-image': '预览图片',
|
||
'preview-video': '预览视频',
|
||
'preview-audio': '音频文件',
|
||
'connection': '网络连接',
|
||
'proxy-performance': '代理性能',
|
||
'video-range': '视频Range请求',
|
||
'intranet-minio': '内网MinIO',
|
||
'public-s3': '公网S3',
|
||
'concurrent': '并发性能',
|
||
'cache': '缓存效果',
|
||
's3-performance': 'S3存储性能',
|
||
'api-materials-list': '素材列表API',
|
||
'api-material-detail': '素材详情API',
|
||
'api-material-stats': '素材统计API',
|
||
'api-upload-init': '上传初始化API',
|
||
'api-download': '下载API',
|
||
'api-user-profile': '用户信息API',
|
||
'api-system-settings': '系统设置API',
|
||
'api-health': '健康检查API',
|
||
'database-query': '数据库查询',
|
||
'redis-connection': 'Redis连接',
|
||
'page-home-load': '首页加载',
|
||
'page-detail-load': '详情页加载',
|
||
'upload-flow': '上传流程',
|
||
'download-flow': '下载流程',
|
||
'transcode-service': '转码服务',
|
||
'auth-session': '认证Session',
|
||
'intranet-url-rewrite': '内网URL重写',
|
||
'public-url-rewrite': '外网URL重写'
|
||
}
|
||
const typeText = typeMap[r.type] || r.type
|
||
return `⚠ ${typeText}${r.status === 'error' ? '失败' : '性能较差'},可能影响用户体验`
|
||
}),
|
||
'',
|
||
'=== 优化建议 ===',
|
||
...results
|
||
.filter(r => r.details?.suggestions && Array.isArray(r.details.suggestions))
|
||
.flatMap(r => r.details!.suggestions as string[]),
|
||
'',
|
||
'提示: 请将此报告提供给技术支持以便快速定位问题'
|
||
].filter(Boolean).join('\n')
|
||
|
||
try {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
await navigator.clipboard.writeText(report)
|
||
setCopied(true)
|
||
setTimeout(() => setCopied(false), 2000)
|
||
return
|
||
}
|
||
throw new Error('Clipboard API not available')
|
||
} catch (error) {
|
||
console.error('复制失败,尝试降级方案:', error)
|
||
try {
|
||
const textarea = document.createElement('textarea')
|
||
textarea.value = report
|
||
textarea.style.position = 'fixed'
|
||
textarea.style.left = '-999999px'
|
||
textarea.style.top = '-999999px'
|
||
textarea.style.opacity = '0'
|
||
textarea.setAttribute('readonly', '')
|
||
document.body.appendChild(textarea)
|
||
|
||
if (navigator.userAgent.match(/ipad|iphone/i)) {
|
||
const range = document.createRange()
|
||
range.selectNodeContents(textarea)
|
||
const selection = window.getSelection()
|
||
selection?.removeAllRanges()
|
||
selection?.addRange(range)
|
||
textarea.setSelectionRange(0, 999999)
|
||
} else {
|
||
textarea.select()
|
||
}
|
||
|
||
const successful = document.execCommand('copy')
|
||
document.body.removeChild(textarea)
|
||
|
||
if (successful) {
|
||
setCopied(true)
|
||
setTimeout(() => setCopied(false), 2000)
|
||
} else {
|
||
throw new Error('execCommand failed')
|
||
}
|
||
} catch (err) {
|
||
console.error('降级复制也失败:', err)
|
||
showToast('复制失败,请手动复制结果', { variant: 'error' })
|
||
}
|
||
}
|
||
}
|
||
|
||
const getStatusIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'testing':
|
||
return (
|
||
<svg className="w-5 h-5 text-blue-500 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||
</svg>
|
||
)
|
||
case 'success':
|
||
return (
|
||
<svg className="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
)
|
||
case 'warning':
|
||
return (
|
||
<svg className="w-5 h-5 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||
</svg>
|
||
)
|
||
case 'error':
|
||
return (
|
||
<svg className="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
)
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
const getTypeText = (type: string) => {
|
||
switch (type) {
|
||
case 'network-env':
|
||
return '网络环境检测'
|
||
case 'connection':
|
||
return 'DNS和连接'
|
||
case 'download':
|
||
return '下载测速'
|
||
case 'upload':
|
||
return '上传测速'
|
||
case 'storage':
|
||
return '存储连通性'
|
||
case 'preview-image':
|
||
return '预览图片加载'
|
||
case 'preview-video':
|
||
return '预览视频加载'
|
||
case 'preview-audio':
|
||
return '音频文件加载'
|
||
case 'proxy-performance':
|
||
return '代理性能测试'
|
||
case 'video-range':
|
||
return '视频Range请求'
|
||
case 'intranet-minio':
|
||
return '内网MinIO直连'
|
||
case 'public-s3':
|
||
return '公网S3直连'
|
||
case 'concurrent':
|
||
return '并发测试'
|
||
case 'cache':
|
||
return '缓存测试'
|
||
case 's3-performance':
|
||
return 'S3存储性能'
|
||
case 'api-materials-list':
|
||
return 'API: 素材列表'
|
||
case 'api-material-detail':
|
||
return 'API: 素材详情'
|
||
case 'api-material-stats':
|
||
return 'API: 素材统计'
|
||
case 'api-upload-init':
|
||
return 'API: 上传初始化'
|
||
case 'api-download':
|
||
return 'API: 下载接口'
|
||
case 'api-user-profile':
|
||
return 'API: 用户信息'
|
||
case 'api-system-settings':
|
||
return 'API: 系统设置'
|
||
case 'api-health':
|
||
return 'API: 健康检查'
|
||
case 'database-query':
|
||
return '数据库查询'
|
||
case 'redis-connection':
|
||
return 'Redis连接'
|
||
case 'page-home-load':
|
||
return '页面: 首页加载'
|
||
case 'page-detail-load':
|
||
return '页面: 详情页加载'
|
||
case 'upload-flow':
|
||
return '上传流程'
|
||
case 'download-flow':
|
||
return '下载流程'
|
||
case 'transcode-service':
|
||
return '转码服务'
|
||
case 'auth-session':
|
||
return '认证Session'
|
||
case 'intranet-url-rewrite':
|
||
return '内网URL重写'
|
||
case 'public-url-rewrite':
|
||
return '外网URL重写'
|
||
default:
|
||
return ''
|
||
}
|
||
}
|
||
|
||
if (!isOpen) return null
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white dark:bg-gray-800 rounded-2xl max-w-2xl w-full shadow-2xl max-h-[90vh] flex flex-col">
|
||
{/* Header */}
|
||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||
<div className="flex items-center justify-between">
|
||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">网络诊断(完整版)</h2>
|
||
<button
|
||
onClick={onClose}
|
||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-2xl leading-none"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||
全面测试网络连接、存储服务和文件加载性能
|
||
</p>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="p-6 space-y-3 overflow-y-auto flex-1">
|
||
{results.length === 0 ? (
|
||
<div className="text-center py-8">
|
||
<svg className="w-16 h-16 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<p className="text-gray-500 dark:text-gray-400 mb-2">
|
||
点击下方按钮开始完整诊断
|
||
</p>
|
||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||
将测试33项网络、API、数据库和业务流程指标,预计耗时60-120秒
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{results.map((result, index) => (
|
||
<div
|
||
key={index}
|
||
className={`flex items-start gap-3 p-3 rounded-lg transition-all ${
|
||
result.status === 'warning'
|
||
? 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800'
|
||
: result.status === 'error'
|
||
? 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
|
||
: result.status === 'testing'
|
||
? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
|
||
: 'bg-gray-50 dark:bg-gray-700'
|
||
}`}
|
||
>
|
||
<div className="flex-shrink-0 mt-0.5">
|
||
{getStatusIcon(result.status)}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||
{getTypeText(result.type)}
|
||
</p>
|
||
<p className="text-xs text-gray-600 dark:text-gray-300 mt-1 whitespace-pre-line">
|
||
{result.message || '测试中...'}
|
||
</p>
|
||
{result.details && (
|
||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400 space-y-0.5">
|
||
{result.details.dnsTime !== undefined && (
|
||
<div>DNS解析: {result.details.dnsTime.toFixed(0)}ms</div>
|
||
)}
|
||
{result.details.connectTime !== undefined && (
|
||
<div>TCP连接: {result.details.connectTime.toFixed(0)}ms</div>
|
||
)}
|
||
{result.details.sslTime !== undefined && (
|
||
<div>SSL握手: {result.details.sslTime.toFixed(0)}ms</div>
|
||
)}
|
||
{result.details.ttfb !== undefined && (
|
||
<div>首字节时间: {result.details.ttfb.toFixed(0)}ms</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div className="p-6 border-t border-gray-200 dark:border-gray-700 space-y-3 flex-shrink-0">
|
||
<button
|
||
onClick={startDiagnose}
|
||
disabled={testing}
|
||
className="w-full btn-primary disabled:opacity-50"
|
||
>
|
||
{testing ? `诊断中... (${results.length}/33)` : results.length > 0 ? '重新诊断' : '开始完整诊断'}
|
||
</button>
|
||
|
||
{results.length > 0 && !testing && (
|
||
<button
|
||
onClick={copyResults}
|
||
className="w-full py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||
>
|
||
{copied ? '已复制到剪贴板' : '复制完整报告'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 统一的Toast提示 */}
|
||
{toast && toast.show && (
|
||
<ToastBubble message={toast.message} variant={toast.variant} />
|
||
)}
|
||
</div>
|
||
)
|
||
}
|