/** * ============================================================ * 考核并发性能实验 * * 场景:多人同时参加考核,验证系统是否产生数据竞争 * - 并发启动考核会话 → 验证出题不冲突、会话唯一 * - 并发提交答案 → 验证评分不串号 * - 验证最终分数合理性 * * 检查指标: * 1. 并发创建考生是否冲突 * 2. Session ID 是否唯一 * 3. 异步出题是否每个会话都拿到正确题数 * 4. 跨会话题目是否有重叠(去重问题) * 5. 维度分布是否合理 * 6. 最终分数是否正常 * ============================================================ */ const API = 'http://localhost:3001'; const TENANT_ID = 'a140a68e-f70a-44d3-b753-fa33d48cf234'; let pass = 0, fail = 0, warn = 0; function ok(l, d) { pass++; console.log(` ✅ ${l}${d?' — '+d:''}`); } function no(l, d) { fail++; console.log(` ❌ ${l}${d?' — '+d:''}`); } function soft(l, d) { warn++; console.log(` ⚠️ ${l}${d?' — '+d:''}`); } async function login(u, p) { const r = await fetch(`${API}/api/auth/login`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:u,password:p})}); return r.ok ? (await r.json()).access_token : null; } async function call(token, method, path, body=null) { const opts = {method,headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json'}}; if(body) opts.body = JSON.stringify(body); const r = await fetch(`${API}/api${path}`,opts); return {status:r.status,data:await r.json().catch(()=>null)}; } async function run() { console.log('\n' + '█'.repeat(70)); console.log(' 🔬 考核并发性能实验'); console.log('█'.repeat(70)); const t0 = Date.now(); const adminT = await login('admin','admin123'); ok('管理员登录', !!adminT); // 1. 创建/获取 20 个考生 console.log('\n─── 1. 创建 20 个考生(或获取已有)───'); const N = 20; const candidates = []; // 先查已有用户 const allUsers = await fetch(`${API}/api/users`,{headers:{Authorization:`Bearer ${adminT}`}}).then(r=>r.json()); const userList = Array.isArray(allUsers) ? allUsers : (allUsers.data||[]); for (let i = 0; i < N; i++) { const uname = 'z-perf-' + String(i+1).padStart(2,'0'); let existing = userList.find(u => u.username === uname); if (existing) { candidates.push({name:uname, id:existing.id}); } else { const r = await call(adminT,'POST','/users',{username:uname,password:'conc123',displayName:'考生'+i}); const id = r.data?.user?.id || r.data?.id; if (id) { candidates.push({name:uname,id}); await call(adminT,'POST',`/v1/tenants/${TENANT_ID}/members`,{userId:id,role:'USER'}); } } } ok(`就绪 ${candidates.length}/${N} 考生`, `${Date.now()-t0}ms`); // 2. 并发启动考核 console.log('\n─── 2. 并发启动考核(异步出题)───'); const starts = candidates.map(c => login(c.name,'conc123').then(token => { if (!token) return {name:c.name,err:'login_fail'}; return fetch(`${API}/api/assessment/start`,{method:'POST',headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json'},body:JSON.stringify({templateId:'eefe8c6c-d082-4a8c-b884-76577dde3249',language:'zh'})}) .then(r => r.json().then(d => ({name:c.name,token,sessionId:d.id,status:r.status,data:d}))) .catch(e => ({name:c.name,token,err:e.message})); })); const started = await Promise.all(starts); const sOk = started.filter(r => r.sessionId && r.status < 300); const sFail = started.filter(r => !r.sessionId); ok(`启动考核 ${sOk.length}/${N}`, `失败${sFail.length}`); sFail.forEach(r => soft(`${r.name} 启动失败`, r.err||'?')); // Session ID 唯一性 const ids = sOk.map(r => r.sessionId); ok('Session ID 唯一', new Set(ids).size === ids.length); // 3. 等待异步出题 + 验证 console.log('\n─── 3. 等待异步出题并验证 ───'); const sessions = []; let timedOut = 0; for (const r of sOk) { let questions = []; for (let w = 0; w < 45; w++) { try { const sr = await fetch(`${API}/api/assessment/${r.sessionId}/state`,{headers:{Authorization:`Bearer ${r.token}`}}); if (sr.ok) { const st = await sr.json(); questions = st.questions || []; if (questions.length > 0) break; } } catch(e) {} await new Promise(r => setTimeout(r,2000)); } if (questions.length === 0) timedOut++; sessions.push({name:r.name, sessionId:r.sessionId, token:r.token, questions, data:r.data}); } ok(`异步出题完成 ${sessions.length - timedOut}/${sessions.length}`, `超时 ${timedOut}`); // 题数检查 const qNums = sessions.filter(s => s.questions.length > 0).map(s => s.questions.length); if (qNums.length > 0) { const allSame = qNums.every(n => n === qNums[0]); ok(`会话题数一致`, allSame ? `均为 ${qNums[0]} 题` : `不一致: ${qNums.slice(0,10).join(',')}`); } // 维度分布 const hasQuestions = sessions.filter(s => s.questions.length > 0); for (const s of hasQuestions.slice(0,3)) { const dims = {}; s.questions.forEach(q => { dims[q.dimension] = (dims[q.dimension]||0) + 1; }); ok(`${s.name} 维度`, Object.entries(dims).map(([k,v])=>`${k}:${v}`).join(',')); } ok('都有 PROMPT', hasQuestions.every(s => s.questions.some(q => q.dimension === 'PROMPT'))); ok('都有 LLM', hasQuestions.every(s => s.questions.some(q => q.dimension === 'LLM'))); // 查询总题库大小 let allItemsCount = '?'; try { const bankR = await fetch(`${API}/api/question-banks`,{headers:{Authorization:`Bearer ${adminT}`}}); if (bankR.ok) { const bankD = await bankR.json(); // 查找关联 AI 协作模板的题库 const targetBank = (Array.isArray(bankD)?bankD:bankD.data||[]).find(b => b.templateId === 'eefe8c6c-d082-4a8c-b884-76577dde3249'); if (targetBank) allItemsCount = targetBank.items?.length || targetBank._count?.items || targetBank.itemCount || '?'; } } catch(e) {} // 题目重叠检查(计算概率) if (hasQuestions.length >= 5) { let totalPairs = 0, overlappedPairs = 0, totalOverlapCount = 0; for (let i = 0; i < 5; i++) { for (let j = i+1; j < 5; j++) { totalPairs++; const a = new Set(hasQuestions[i].questions.map(q => q.id)); const b = new Set(hasQuestions[j].questions.map(q => q.id)); const o = [...a].filter(id => b.has(id)).length; if (o > 0) { overlappedPairs++; totalOverlapCount += o; } } } const overlapRate = totalPairs > 0 ? (totalOverlapCount / (totalPairs * 20) * 100).toFixed(1) : '0'; ok('题目重叠检查完成', `题库 ${allItemsCount} 题, 20人×20题需400, 重叠率 ${overlapRate}%`); if (overlappedPairs === 0) ok('零重叠', ''); else soft(`${overlappedPairs}/${totalPairs} 对重叠`, `共 ${totalOverlapCount} 题次`); } // 4. 并发提交答案 console.log('\n─── 4. 并发提交答案 ───'); const qGroups = sessions.filter(s => s.questions && s.questions.length >= 4).slice(0,6); if (qGroups.length === 0) { soft('无足够题目的会话', `总会话${sessions.length}个`); } else { const submits = qGroups.map(s => (async () => { const results = []; for (let qi = 0; qi < 4; qi++) { const q = s.questions[qi]; if (!q) continue; const isChoice = q.questionType === 'MULTIPLE_CHOICE' || q.questionType === 'TRUE_FALSE'; await new Promise(r => setTimeout(r, 500 + Math.random() * 1500)); try { const r = await fetch(`${API}/api/assessment/${s.sessionId}/answer`,{method:'POST',headers:{Authorization:`Bearer ${s.token}`,'Content-Type':'application/json'},body:JSON.stringify({answer:isChoice?'A':'并发测试回答',language:'zh'})}); results.push({qi, ok: r.ok, status: r.status}); } catch(e) { results.push({qi, ok: false, error: e.message}); } } return {name:s.name, results}; })() ); const subResults = (await Promise.all(submits)).filter(Boolean); ok(`并发答题 ${subResults.length}/${qGroups.length} 完成`, ''); for (const sr of subResults) { const okAll = sr.results.every(r => r.ok); const n = sr.results.filter(r => r.ok).length; if (okAll) ok(`${sr.name} 全部提交成功`); else soft(`${sr.name} ${n}/4 成功`); } } // 5. 等待评分并检查最终分数 console.log('\n─── 5. 最终分数检查 ───'); let scored = 0, totalScoreCheck = 0; for (const s of qGroups) { await new Promise(r => setTimeout(r, 15000)); try { const st = await fetch(`${API}/api/assessment/${s.sessionId}/state`,{headers:{Authorization:`Bearer ${s.token}`}}).then(r=>r.json()); // state 返回的是 evaluation state const isDone = st.currentQuestionIndex >= (st.questionCount || st.questions?.length || 20); // 也查一下 session 的状态 const sessionR = await fetch(`${API}/api/assessment/${s.sessionId}`,{headers:{Authorization:`Bearer ${s.token}`}}).catch(()=>null); const session = sessionR?.ok ? await sessionR.json() : null; const status = session?.status || st._sessionStatus || (isDone ? '猜测完成' : '进行中'); const finalScore = session?.finalScore ?? st.finalScore; const passed = session?.passed ?? st.passed; if (status === 'COMPLETED' || (isDone && finalScore !== undefined)) { totalScoreCheck++; if (finalScore !== undefined && finalScore !== null && !isNaN(finalScore)) { scored++; ok(`${s.name} 已完成`, `分数=${finalScore}, 合格=${!!passed}`); } else { soft(`${s.name} 分数待定`, `status=${status}, score=${finalScore}`); } } else { soft(`${s.name} ${status}`, `分数=${finalScore}`); } } catch(e) { soft(`${s.name} 查询失败`, e.message); } } ok(`评分完成 ${scored}/${totalScoreCheck}`, '已评分/已检查'); // 6. 清理 console.log('\n─── 6. 清理 ───'); let cleaned = 0; for (const c of candidates) { await call(adminT,'DELETE',`/users/${c.id}`).catch(()=>{}); cleaned++; } ok(`清理 ${cleaned} 个考生`, ''); // 报告 const elapsed = Math.round((Date.now()-t0)/1000); console.log('\n' + '█'.repeat(70)); console.log(` 📊 并发测试报告 (${elapsed}秒)`); console.log(` ✅ ${pass} ⚠️ ${warn} ❌ ${fail}`); console.log('█'.repeat(70)); if (fail > 0) process.exit(1); else if (warn > 0) console.log('\n ⚠️ 有警告(非致命)'); else console.log('\n 🎉 全部通过!并发表现正常'); } run().catch(e => { console.error('\n💥', e.message); process.exit(1); });