/** * 全面端到端测试脚本 * * 流程: * 1. 后台管理系统:admin 创建团课(无封面、张教练)→ 保存二维码 * 2. 会员端:预约团课(开课前30分钟以上) * 3. 调整时间 → 扫码签到(开课前2小时内) * 4. 调整时间 → 教练端:手动开课 → 手动结课 * * API Base: http://192.168.110.64:8084 * 认证: HMAC-SHA256 + JWT * * 业务规则(来自后端代码): * - 预约: 距开课 >= 30分钟 * - 签到: 开课前2小时 ~ 课程结束 * - 开课(>=60min课程): 开课时间后10分钟内正常, 10-30分钟迟到, >30分钟拒绝 * - 结课: 结束时间后10分钟内 */ const http = require('http'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const API_BASE = 'http://192.168.110.64:8084'; const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026'; const QRCODE_DIR = path.resolve(__dirname, 'QRCODE'); // 测试账号 const ADMIN = { username: 'admin', password: 'Test@123' }; const MEMBER_CODE = 'dev-test-fullflow-' + Date.now(); const COACH = { username: 'coach_zhang', password: 'Test@123' }; // 结果记录 const results = { steps: [], startTime: new Date().toISOString(), endTime: null, courseId: null, courseName: null, memberId: null, bookingId: null, qrPath: null, passed: 0, failed: 0 }; function record(step, status, detail) { const entry = { step, status, detail, time: new Date().toISOString() }; results.steps.push(entry); const icon = status === 'PASS' ? '✓' : status === 'SKIP' ? '○' : '✗'; console.log(` ${icon} [${status}] ${step}${detail ? ': ' + detail : ''}`); if (status === 'PASS') results.passed++; else if (status === 'FAIL') results.failed++; } // ── 本地时间格式化(发送给服务端 LocalDateTime,不带时区)── function formatLocalTime(d) { const pad = (n) => String(n).padStart(2, '0'); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); } // ── HTTP 请求工具 ── function apiRequest(method, urlPath, body, token) { return new Promise((resolve, reject) => { const u = new URL(API_BASE + urlPath); const timestamp = Date.now(); const nonce = timestamp.toString(36) + '-' + Math.random().toString(36).substring(2, 8); const bodyStr = body ? JSON.stringify(body) : ''; const stringToSign = [method.toUpperCase(), u.pathname + u.search, '', bodyStr, String(timestamp), nonce].join('\n'); const sig = crypto.createHmac('sha256', SIGNATURE_SECRET).update(stringToSign).digest('base64'); const headers = { 'Content-Type': 'application/json', 'X-Signature': sig, 'X-Timestamp': String(timestamp), 'X-Nonce': nonce }; if (token) headers['Authorization'] = 'Bearer ' + token; const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname + u.search, method, headers, timeout: 15000 }, (res) => { let data = ''; res.on('data', c => data += c); res.on('end', () => { let parsed; try { parsed = JSON.parse(data); } catch (e) { parsed = data; } resolve({ status: res.statusCode, data: parsed }); }); }); req.on('error', e => reject(e)); if (bodyStr) req.write(bodyStr); req.end(); }); } // 下载文件到本地 function downloadFile(urlPath, token, destPath) { return new Promise((resolve, reject) => { const u = new URL(API_BASE + urlPath); const timestamp = Date.now(); const nonce = timestamp.toString(36) + '-' + Math.random().toString(36).substring(2, 8); const stringToSign = ['GET', u.pathname + u.search, '', '', String(timestamp), nonce].join('\n'); const sig = crypto.createHmac('sha256', SIGNATURE_SECRET).update(stringToSign).digest('base64'); const headers = { 'X-Signature': sig, 'X-Timestamp': String(timestamp), 'X-Nonce': nonce }; if (token) headers['Authorization'] = 'Bearer ' + token; const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname + u.search, method: 'GET', headers, timeout: 15000 }, (res) => { const chunks = []; res.on('data', c => chunks.push(c)); res.on('end', () => { const buf = Buffer.concat(chunks); fs.writeFileSync(destPath, buf); resolve(destPath); }); }); req.on('error', e => reject(e)); req.end(); }); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } // ── 主流程 ── async function main() { console.log('═══════════════════════════════════════════'); console.log(' 全面端到端测试 - 全流程'); console.log(' 包含: 后台管理 / 会员端 / 教练端'); console.log(` 时间: ${results.startTime}`); console.log(` 本地时间: ${formatLocalTime(new Date())}`); console.log('═══════════════════════════════════════════\n'); // 确保 QRCODE 目录存在 if (!fs.existsSync(QRCODE_DIR)) fs.mkdirSync(QRCODE_DIR, { recursive: true }); let adminToken, coachToken, memberToken; // ═══════════════════════════════════════════ // 阶段1:后台管理系统 // ═══════════════════════════════════════════ console.log('── 阶段1:后台管理系统 ──'); // 1a. 管理员登录 try { const loginRes = await apiRequest('POST', '/api/auth/login', ADMIN); if (loginRes.status === 200 && loginRes.data.token) { adminToken = loginRes.data.token; record('1a. 管理员登录', 'PASS', `${ADMIN.username} / userId=${loginRes.data.userId}`); } else { record('1a. 管理员登录', 'FAIL', `status=${loginRes.status}, body=${JSON.stringify(loginRes.data).substring(0, 200)}`); throw new Error('Admin login failed, cannot continue'); } } catch (e) { record('1a. 管理员登录', 'FAIL', e.message); if (!adminToken) { generateMarkdownReport(); return; } } // 1b. 获取教练列表 let coachId = null; if (adminToken) { try { const coachRes = await apiRequest('GET', '/api/coach/list', null, adminToken); if (coachRes.status === 200) { const coaches = Array.isArray(coachRes.data) ? coachRes.data : (coachRes.data?.data || []); const zhang = coaches.find(c => c.username === 'coach_zhang' || (c.nickname && c.nickname.includes('张'))); if (zhang) { coachId = zhang.id; record('1b. 获取教练列表', 'PASS', `找到 ${COACH.username}, id=${coachId}, nickname=${zhang.nickname || '-'}`); } else { record('1b. 获取教练列表', 'FAIL', `未找到 ${COACH.username}, 列表: ${coaches.map(c => c.username || c.nickname).join(', ')}`); } } else { record('1b. 获取教练列表', 'FAIL', `status=${coachRes.status}`); } } catch (e) { record('1b. 获取教练列表', 'FAIL', e.message); } } // 1c. 创建团课(无封面、张教练负责) let courseId = null; const courseName = '全流程测试-' + Date.now().toString(36); // 初始时间:5小时后开课,留足预约时间(>=30分钟) const createStartTime = new Date(Date.now() + 5 * 60 * 60 * 1000); const createEndTime = new Date(createStartTime.getTime() + 60 * 60 * 1000); if (adminToken && coachId) { try { const createBody = { courseName: courseName, coachId: parseInt(coachId), courseType: 1, startTime: formatLocalTime(createStartTime), endTime: formatLocalTime(createEndTime), maxMembers: 20, location: '测试场地-全流程', coverImage: '', // 无封面 description: '自动化全流程端到端测试团课' }; console.log(' 创建请求:', JSON.stringify(createBody)); const createRes = await apiRequest('POST', '/api/groupCourse', createBody, adminToken); console.log(' 创建响应: status=' + createRes.status + ', body=' + JSON.stringify(createRes.data).substring(0, 300)); const createdCourse = createRes.data?.data || createRes.data; if (createRes.status === 200 && createdCourse?.id) { courseId = createdCourse.id; results.courseId = courseId; results.courseName = courseName; record('1c. 创建团课', 'PASS', `id=${courseId}, name="${courseName}", coachId=${coachId}, 无封面, startTime=${formatLocalTime(createStartTime)}`); } else { // 尝试从message中提取信息 const msg = createRes.data?.message || ''; record('1c. 创建团课', 'FAIL', `status=${createRes.status}, msg=${msg}, body=${JSON.stringify(createRes.data).substring(0, 300)}`); } } catch (e) { record('1c. 创建团课', 'FAIL', e.message); } } // 1d. 获取并保存二维码 if (adminToken && courseId) { try { const detailRes = await apiRequest('GET', `/api/groupCourse/${courseId}/detail`, null, adminToken); const course = detailRes.data; if (course.qrCodePath) { const destFile = path.join(QRCODE_DIR, courseName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_') + '.png'); try { await downloadFile(course.qrCodePath, adminToken, destFile); results.qrPath = destFile; const size = fs.statSync(destFile).size; record('1d. 保存二维码', 'PASS', `已保存: ${destFile} (${size} bytes)`); } catch (e) { record('1d. 保存二维码', 'FAIL', `下载失败: ${e.message}`); } } else { record('1d. 保存二维码', 'FAIL', 'qrCodePath 为空(可能异步生成中)'); } } catch (e) { record('1d. 保存二维码', 'FAIL', e.message); } } // ═══════════════════════════════════════════ // 阶段2:会员端 - 预约 + 签到 // ═══════════════════════════════════════════ console.log('\n── 阶段2:会员端 ──'); // 2a. 会员登录 if (courseId) { try { const memberLoginRes = await apiRequest('POST', '/api/member/auth/miniapp/login', { code: MEMBER_CODE }); if (memberLoginRes.status === 200 && memberLoginRes.data.accessToken) { memberToken = memberLoginRes.data.accessToken; results.memberId = memberLoginRes.data.memberId; record('2a. 会员登录', 'PASS', `memberId=${results.memberId}`); } else { record('2a. 会员登录', 'FAIL', `status=${memberLoginRes.status}, ${JSON.stringify(memberLoginRes.data).substring(0, 200)}`); } } catch (e) { record('2a. 会员登录', 'FAIL', e.message); } } // 2b. 预约团课(现在距开课5小时 > 30分钟,满足预约条件) if (memberToken && courseId) { try { const bookBody = { courseId: parseInt(courseId), memberId: parseInt(results.memberId) }; console.log(' 预约请求:', JSON.stringify(bookBody)); const bookRes = await apiRequest('POST', '/api/groupCourse/book', bookBody, memberToken); console.log(' 预约响应: status=' + bookRes.status + ', body=' + JSON.stringify(bookRes.data).substring(0, 300)); if (bookRes.status === 200) { results.bookingId = bookRes.data?.data?.id || bookRes.data?.id; record('2b. 预约团课', 'PASS', `courseId=${courseId}, bookingId=${results.bookingId}, 距开课约5h(>=30min要求)`); } else if (bookRes.status === 400) { const msg = bookRes.data?.message || ''; if (msg.includes('已预约')) { record('2b. 预约团课', 'SKIP', '已预约过该课程(可能是重复测试)'); } else { record('2b. 预约团课', 'FAIL', `status=400, msg=${msg}`); } } else { record('2b. 预约团课', 'FAIL', `status=${bookRes.status}, ${JSON.stringify(bookRes.data).substring(0, 200)}`); } } catch (e) { record('2b. 预约团课', 'FAIL', e.message); } } // 2c. 调整团课时间:签到需要 startTime 在 2 小时内(规则: now >= startTime - 2h) const signInStart = new Date(Date.now() + 60 * 60 * 1000); // 1小时后开课(在2h窗口内) if (adminToken && courseId) { try { const updateBody = { startTime: formatLocalTime(signInStart), endTime: formatLocalTime(new Date(signInStart.getTime() + 60 * 60 * 1000)) }; const updateRes = await apiRequest('PUT', `/api/groupCourse/${courseId}`, updateBody, adminToken); console.log(' 调整时间响应: status=' + updateRes.status); if (updateRes.status === 200) { record('2c. 调整课程时间(签到用)', 'PASS', `startTime→${formatLocalTime(signInStart)}(1h后→满足签到2h窗口)`); } else { record('2c. 调整课程时间(签到用)', 'FAIL', `status=${updateRes.status}, ${JSON.stringify(updateRes.data).substring(0, 200)}`); } } catch (e) { record('2c. 调整课程时间(签到用)', 'FAIL', e.message); } } // 2d. 签到(扫码签到 = 调用 groupCourse/signin API) if (memberToken && courseId) { try { const signinBody = { courseId: parseInt(courseId) }; const signinRes = await apiRequest('POST', `/api/groupCourse/signin/${results.memberId}`, signinBody, memberToken); console.log(' 签到响应: status=' + signinRes.status + ', body=' + JSON.stringify(signinRes.data).substring(0, 300)); if (signinRes.status === 200) { record('2d. 扫码签到', 'PASS', `courseId=${courseId}, memberId=${results.memberId}, 签到时间距开课约1h(满足2h窗口)`); } else if (signinRes.status === 400) { const msg = signinRes.data?.message || ''; if (msg.includes('已出席') || msg.includes('已签到')) { record('2d. 扫码签到', 'SKIP', '已签到过'); } else { record('2d. 扫码签到', 'FAIL', `status=400, msg=${msg}`); } } else { record('2d. 扫码签到', 'FAIL', `status=${signinRes.status}, ${JSON.stringify(signinRes.data).substring(0, 200)}`); } } catch (e) { record('2d. 扫码签到', 'FAIL', e.message); } } // ═══════════════════════════════════════════ // 阶段3:教练端 - 开课 + 结课 // ═══════════════════════════════════════════ console.log('\n── 阶段3:教练端 ──'); // 3a. 教练登录 try { const coachLoginRes = await apiRequest('POST', '/api/auth/login', COACH); if (coachLoginRes.status === 200 && coachLoginRes.data.token) { coachToken = coachLoginRes.data.token; record('3a. 教练登录', 'PASS', `${COACH.username}, userId=${coachLoginRes.data.userId}`); } else { record('3a. 教练登录', 'FAIL', `status=${coachLoginRes.status}, ${JSON.stringify(coachLoginRes.data).substring(0, 200)}`); } } catch (e) { record('3a. 教练登录', 'FAIL', e.message); } // 3b. 调整课程时间: 开课需要已过 startTime(规则: 10分钟内正常开课) const coachStartTime = new Date(Date.now() - 3 * 60 * 1000); // 3分钟前 if (adminToken && courseId) { try { const updateBody = { startTime: formatLocalTime(coachStartTime), endTime: formatLocalTime(new Date(coachStartTime.getTime() + 60 * 60 * 1000)) }; const updateRes = await apiRequest('PUT', `/api/groupCourse/${courseId}`, updateBody, adminToken); console.log(' 调整时间为过去响应: status=' + updateRes.status); if (updateRes.status === 200) { record('3b. 调整课程时间(开课用)', 'PASS', `startTime→${formatLocalTime(coachStartTime)}(3分钟前→教练可正常开课)`); } else { record('3b. 调整课程时间(开课用)', 'FAIL', `status=${updateRes.status}, ${JSON.stringify(updateRes.data).substring(0, 200)}`); } } catch (e) { record('3b. 调整课程时间(开课用)', 'FAIL', e.message); } } // 3c. 教练手动开课 if (coachToken && courseId) { try { const startRes = await apiRequest('POST', `/api/coach/courses/${courseId}/start`, {}, coachToken); console.log(' 开课响应: status=' + startRes.status + ', body=' + JSON.stringify(startRes.data).substring(0, 300)); if (startRes.status === 200) { const respData = startRes.data?.data || startRes.data; record('3c. 手动开课', 'PASS', `courseId=${courseId}, status=${respData?.status}, msg=${startRes.data?.message || ''}`); } else if (startRes.status === 400) { const msg = startRes.data?.message || ''; record('3c. 手动开课', 'FAIL', `status=400, msg=${msg}`); } else { record('3c. 手动开课', 'FAIL', `status=${startRes.status}, ${JSON.stringify(startRes.data).substring(0, 200)}`); } } catch (e) { record('3c. 手动开课', 'FAIL', e.message); } } // 3d. 调整课程结束时间:结课需要 endTime 在10分钟内(规则: now <= endTime + 10min) // 把 endTime 设为2分钟前 const coachEndTime = new Date(Date.now() - 2 * 60 * 1000); if (adminToken && courseId) { try { const updateBody = { endTime: formatLocalTime(coachEndTime) }; const updateRes = await apiRequest('PUT', `/api/groupCourse/${courseId}`, updateBody, adminToken); console.log(' 调整结束时间响应: status=' + updateRes.status); if (updateRes.status === 200) { record('3d. 调整结束时间(结课用)', 'PASS', `endTime→${formatLocalTime(coachEndTime)}(2分钟前→教练可结课)`); } else { record('3d. 调整结束时间(结课用)', 'FAIL', `status=${updateRes.status}, ${JSON.stringify(updateRes.data).substring(0, 200)}`); } } catch (e) { record('3d. 调整结束时间(结课用)', 'FAIL', e.message); } } // 3e. 教练手动结课 if (coachToken && courseId) { try { const endRes = await apiRequest('POST', `/api/coach/courses/${courseId}/end`, {}, coachToken); console.log(' 结课响应: status=' + endRes.status + ', body=' + JSON.stringify(endRes.data).substring(0, 300)); if (endRes.status === 200) { const respData = endRes.data?.data || endRes.data; record('3e. 手动结课', 'PASS', `courseId=${courseId}, status=${respData?.status}, msg=${endRes.data?.message || ''}`); } else if (endRes.status === 400) { const msg = endRes.data?.message || ''; record('3e. 手动结课', 'FAIL', `status=400, msg=${msg}`); } else { record('3e. 手动结课', 'FAIL', `status=${endRes.status}, ${JSON.stringify(endRes.data).substring(0, 200)}`); } } catch (e) { record('3e. 手动结课', 'FAIL', e.message); } } // ═══════════════════════════════════════════ // 阶段4:验证与总结 // ═══════════════════════════════════════════ console.log('\n── 阶段4:最终验证 ──'); if (adminToken && courseId) { try { const finalRes = await apiRequest('GET', `/api/groupCourse/${courseId}/detail`, null, adminToken); const fc = finalRes.data; record('4. 最终课程状态', 'PASS', `name="${fc.courseName}", status=${fc.status}, members=${fc.currentMembers}, startTime=${fc.startTime}`); } catch (e) { record('4. 最终课程状态', 'FAIL', e.message); } } // ── 输出汇总 ── results.endTime = new Date().toISOString(); console.log('\n═══════════════════════════════════════════'); console.log(' 测试结果汇总'); console.log('═══════════════════════════════════════════'); console.log(` 通过: ${results.passed} | 失败: ${results.failed} | 总计: ${results.steps.length}`); console.log(` 课程ID: ${courseId || 'N/A'} 课程名: ${courseName}`); console.log(` 二维码: ${results.qrPath || 'N/A'}`); console.log(` 开始时间: ${results.startTime}`); console.log(` 结束时间: ${results.endTime}`); console.log('═══════════════════════════════════════════\n'); // 生成 Markdown 报告 generateMarkdownReport(); } // ── 生成 Markdown 报告 ── function generateMarkdownReport() { const reportPath = path.join(__dirname, 'TEST_REPORT_full_flow.md'); const lines = []; lines.push('# 全面端到端测试报告'); lines.push(''); lines.push('## 测试概要'); lines.push(''); lines.push('| 项目 | 值 |'); lines.push('|------|-----|'); lines.push('| 测试时间 | ' + results.startTime + ' ~ ' + results.endTime + ' |'); lines.push('| 总步骤数 | ' + results.steps.length + ' |'); lines.push('| 通过 | ' + results.passed + ' |'); lines.push('| 失败 | ' + results.failed + ' |'); lines.push('| 课程ID | ' + (results.courseId || 'N/A') + ' |'); lines.push('| 课程名称 | ' + (results.courseName || 'N/A') + ' |'); lines.push('| 二维码路径 | ' + (results.qrPath || 'N/A') + ' |'); lines.push('| API地址 | ' + API_BASE + ' |'); lines.push(''); lines.push('## 测试范围'); lines.push(''); lines.push('| 模块 | 项目 | 测试内容 |'); lines.push('|------|------|----------|'); lines.push('| 后台管理系统 | `gym-manage-web` | 管理员登录、创建团课(无封面、张教练)、修改团课时间、保存二维码 |'); lines.push('| 会员端 | `gym-manage-uniapp` | 会员登录、预约团课、扫码签到 |'); lines.push('| 教练端 | `gym-manage-coach-uniapp` | 教练登录、手动开课、手动结课 |'); lines.push('| 后端API | `gym-manage-api` | 所有操作通过REST API完成 |'); lines.push(''); lines.push('## 业务规则验证'); lines.push(''); lines.push('| 规则 | 条件 | 测试策略 |'); lines.push('|------|------|----------|'); lines.push('| 预约时间限制 | 需在开课前 >= 30分钟 | 创建课程startTime为5小时后,预约成功 |'); lines.push('| 签到时间窗口 | 开课前2小时 ~ 课程结束 | 调整startTime为1小时后,签到成功 |'); lines.push('| 教练开课 | 开课时间后10分钟内正常开课 | 调整startTime为3分钟前,开课成功 |'); lines.push('| 教练结课 | 结束时间后10分钟内结课 | 调整endTime为2分钟前,结课 |'); lines.push(''); lines.push('## 测试流程'); lines.push(''); lines.push('```'); lines.push('1. Admin: Login -> Get coach list -> Create course -> Save QR code'); lines.push('2. Member: Login -> Book course'); lines.push('3. Admin: Adjust startTime (for sign-in window)'); lines.push('4. Member: Sign in (scan QR)'); lines.push('5. Admin: Adjust startTime to past (for coach start)'); lines.push('6. Coach: Login -> Start course'); lines.push('7. Admin: Adjust endTime to past (for coach end)'); lines.push('8. Coach: End course'); lines.push('```'); lines.push(''); lines.push('## 详细步骤结果'); lines.push(''); lines.push('| # | 步骤 | 状态 | 详情 | 时间 |'); lines.push('|---|------|------|------|------|'); results.steps.forEach((s, i) => { const icon = s.status === 'PASS' ? 'PASS' : (s.status === 'SKIP' ? 'SKIP' : 'FAIL'); const detail = (s.detail || '-').replace(/\|/g, '\\|'); lines.push('| ' + (i + 1) + ' | ' + s.step + ' | ' + icon + ' | ' + detail + ' | ' + (s.time || '-') + ' |'); }); lines.push(''); const failSteps = results.steps.filter(s => s.status === 'FAIL'); if (failSteps.length > 0) { lines.push('## 失败步骤分析'); lines.push(''); failSteps.forEach(s => { lines.push('- **' + s.step + '**: ' + (s.detail || '无详情')); }); lines.push(''); } lines.push('## 使用的API端点'); lines.push(''); lines.push('| 端点 | 方法 | 用途 | 认证 |'); lines.push('|------|------|------|------|'); lines.push('| `/api/auth/login` | POST | 管理员/教练登录 | HMAC签名 |'); lines.push('| `/api/coach/list` | GET | 获取教练列表 | JWT (Admin) |'); lines.push('| `/api/groupCourse` | POST | 创建团课 | JWT (Admin) |'); lines.push('| `/api/groupCourse/{id}` | PUT | 修改团课时间 | JWT (Admin) |'); lines.push('| `/api/groupCourse/{id}/detail` | GET | 获取课程详情(含二维码) | JWT (Admin) |'); lines.push('| `/api/member/auth/miniapp/login` | POST | 会员登录 | HMAC签名 |'); lines.push('| `/api/groupCourse/book` | POST | 预约团课 | JWT (Member) |'); lines.push('| `/api/groupCourse/signin/{memberId}` | POST | 扫码签到 | JWT (Member) |'); lines.push('| `/api/coach/courses/{courseId}/start` | POST | 教练手动开课 | JWT (Coach) |'); lines.push('| `/api/coach/courses/{courseId}/end` | POST | 教练手动结课 | JWT (Coach) |'); lines.push(''); lines.push('## 认证机制'); lines.push(''); lines.push('- **JWT Token**: `Authorization: Bearer {token}`'); lines.push('- **HMAC-SHA256**: `X-Signature`, `X-Timestamp`, `X-Nonce`'); lines.push('- **Secret Key**: `NovalonManageSystemSecretKey2026`'); lines.push(''); lines.push('## 测试账号'); lines.push(''); lines.push('| 角色 | 用户名 | 密码 |'); lines.push('|------|--------|------|'); lines.push('| 管理员 | admin | Test@123 |'); lines.push('| 会员 | (小程序code登录) | ' + MEMBER_CODE + ' |'); lines.push('| 教练 | coach_zhang | Test@123 |'); lines.push(''); lines.push('## 时间约束处理策略'); lines.push(''); lines.push('本测试通过后台API动态调整课程时间,绕过各步骤的时间限制:'); lines.push(''); lines.push('| 步骤 | 时间约束 | 处理方式 |'); lines.push('|------|----------|----------|'); lines.push('| 预约 | 需 >= 30分钟前 | 创建课程startTime=当前+5h |'); lines.push('| 签到 | 开课前2h ~ 课程结束 | PUT修改startTime=当前+1h |'); lines.push('| 开课 | 开课时间后10分钟内 | PUT修改startTime=当前-3min |'); lines.push('| 结课 | 结束时间后10分钟内 | PUT修改endTime=当前-2min |'); lines.push(''); lines.push('> **注意**: 使用 `formatLocalTime()` 发送本地时间(无时区),确保与服务器 LocalDateTime 一致。'); fs.writeFileSync(reportPath, lines.join('\n'), 'utf-8'); console.log('报告已保存: ' + reportPath); } // ── 运行 ── main().catch(e => { console.error('测试脚本异常:', e); record('脚本异常', 'FAIL', e.message); generateMarkdownReport(); });