/** * CFC API Integration Test Runner v3 * 环境: http://cfc.iwintrue.com:80 * 覆盖: NEW-01/02/03修复验证 + ISSUE-001/002/003回归 + 新增API测试 * * 运行: node tests/run-api-tests-v3.js */ const BASE = 'http://cfc.iwintrue.com:80'; const ADMIN = { phone: '13800138000', userId: 7, role: 'admin', name: '管理员' }; const PARENT = { phone: '13701366188', userId: 8, familyId: 2, role: 'parent', name: '家长' }; let adminToken = null; let testResults = []; let testStartTime = Date.now(); function log(msg) { console.log(`[${new Date().toISOString()}] ${msg}`); } function pass(name, detail = '') { testResults.push({ name, result: 'PASS', detail, time: Date.now() }); console.log(` ✅ PASS | ${name}${detail ? ' | ' + detail : ''}`); } function fail(name, err, detail = '') { testResults.push({ name, result: 'FAIL', detail: detail || err.message, time: Date.now() }); console.log(` ❌ FAIL | ${name} | ${err.message}${detail ? ' | ' + detail : ''}`); } function skip(name, reason) { testResults.push({ name, result: 'SKIP', detail: reason, time: Date.now() }); console.log(` ⏭️ SKIP | ${name} | ${reason}`); } async function api(path, body, headers = {}) { const url = `${BASE}${path}`; const opts = { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: body ? JSON.stringify(body) : undefined, }; try { const resp = await fetch(url, opts); const text = await resp.text(); let data; try { data = JSON.parse(text); } catch { data = null; } return { status: resp.status, data, ok: resp.ok, raw: text.substring(0, 300) }; } catch (err) { throw new Error(`网络错误: ${err.message}`); } } async function adminApi(path, body) { if (!adminToken) throw new Error('无admin token'); return api(path, body, { Authorization: `Bearer ${adminToken}` }); } async function publicApi(path, body) { return api(path, body, {}); } // ───────────────────────────────────────── // 1. 登录 // ───────────────────────────────────────── async function login() { log('=== 1. 登录测试 ==='); try { // 发送验证码(skip-captcha 已启用,任意验证码均可) await api('/api/admin-auth/send-code', { phone: ADMIN.phone }); // 登录获取 token const resp = await api('/api/admin-auth/login', { phone: ADMIN.phone, code: '123456' }); if (resp.data?.data?.token) { adminToken = resp.data.data.token; pass('Admin登录', `userId=${resp.data.data.adminId || ADMIN.userId}`); } else { fail('Admin登录', new Error('未获取到token'), `raw: ${resp.raw}`); } } catch (err) { fail('Admin登录', err); } skip('Parent登录(手机验证码)', '测试环境无法发短信,parent端需手动测试'); } // ───────────────────────────────────────── // 2. 公共端点(无需认证) // ───────────────────────────────────────── async function testPublicEndpoints() { log('=== 2. 公共端点测试 ==='); // 文章列表 try { const resp = await publicApi('/api/articles/list', { page: 1, size: 5 }); if (resp.ok || resp.data?.code === 200) { pass('文章列表(公开)', 'ok'); } else { fail('文章列表(公开)', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('文章列表(公开)', err); } // 活动列表 try { const resp = await publicApi('/api/activity/list', { page: 1, size: 5 }); if (resp.ok || resp.data?.code === 200) { pass('活动列表(公开)', 'ok'); } else { fail('活动列表(公开)', new Error(`status=${resp.status}`)); } } catch (err) { fail('活动列表(公开)', err); } } // ───────────────────────────────────────── // 3. NEW-01 验证:能量规则创建 // 修复: AdminEnergyRuleController.create() 改用 Map // ───────────────────────────────────────── async function testNEW01_EnergyRuleCreate() { log('=== 3. NEW-01 验证:能量规则创建 ==='); if (!adminToken) { skip('NEW-01 能量规则创建', '无admin token'); return; } // 先查看能量规则列表(确保维度存在) let dimensions = []; try { const resp = await adminApi('/api/admin/energy-rule/dimensions', {}); if (resp.ok || resp.data?.code === 200) { dimensions = resp.data?.data || []; pass('能量维度列表', `count=${dimensions.length}`); } else { fail('能量维度列表', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('能量维度列表', err); return; } // 创建能量规则(使用正确的 Map 参数格式) let ruleId = null; try { const resp = await adminApi('/api/admin/energy-rule/create', { ruleName: '自动化测试规则_NEW01_' + Date.now(), dimensionCode: 'body', eventType: 'checkin', energyValue: 5, status: 1, action: 'increase', priority: 0, }); if (resp.ok || resp.data?.code === 200) { // 插入后 EnergyRule.id 会回填到 data ruleId = resp.data?.data?.id || resp.data?.id; pass('NEW-01 创建能量规则', `id=${ruleId} ✅ NEW-01已修复`); } else { fail('NEW-01 创建能量规则', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('NEW-01 创建能量规则', err); } if (!ruleId) return; // 切换状态 try { const resp = await adminApi('/api/admin/energy-rule/toggle', { id: ruleId }); if (resp.ok || resp.data?.code === 200) { pass('能量规则切换状态', `id=${ruleId}`); } else { fail('能量规则切换状态', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('能量规则切换状态', err); } } // ───────────────────────────────────────── // 4. NEW-02 验证:SKU列表 // 修复: ProductSkuController.listByProduct 改用 Map + null检查 // ───────────────────────────────────────── async function testNEW02_SkuList() { log('=== 4. NEW-02 验证:SKU列表 ==='); if (!adminToken) { skip('NEW-02 SKU列表', '无admin token'); return; } // productId=1 在之前测试中应该是已存在的商品 try { const resp = await adminApi('/api/admin/product/sku/list', { productId: 1 }); if (resp.ok || resp.data?.code === 200) { const skus = resp.data?.data || []; pass('NEW-02 SKU列表', `count=${skus.length} ✅ NEW-02已修复`); } else if (resp.data?.code === 500) { fail('NEW-02 SKU列表', new Error('500 Internal Server Error'), '⚠️ NEW-02可能回归'); } else { fail('NEW-02 SKU列表', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('NEW-02 SKU列表', err); } } // ───────────────────────────────────────── // 5. NEW-03 验证:活动发布 // 修复: ActivityController.publish() 改用 Map + try-catch // ───────────────────────────────────────── async function testNEW03_ActivityPublish() { log('=== 5. NEW-03 验证:活动发布 ==='); if (!adminToken) { skip('NEW-03 活动发布', '无admin token'); return; } // 创建活动 let activityId = null; try { const resp = await adminApi('/api/activity/create', { title: '自动化E2E测试活动_NEW03_' + Date.now(), description: 'NEW-03修复验证测试', dimensionCode: 'body', startTime: new Date(Date.now() + 86400000 * 7).toISOString().replace('T', ' ').split('.')[0], endTime: new Date(Date.now() + 86400000 * 8).toISOString().replace('T', ' ').split('.')[0], location: '线上直播', maxParticipants: 30, }); if (resp.ok || resp.data?.code === 200) { activityId = resp.data?.data?.id || resp.data?.id; pass('创建测试活动', `id=${activityId}`); } else { fail('创建测试活动', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('创建测试活动', err); } if (!activityId) return; // 发布活动(NEW-03 核心测试) try { const resp = await adminApi('/api/activity/publish', { id: activityId }); if (resp.ok || resp.data?.code === 200) { pass('NEW-03 发布活动', `id=${activityId} ✅ NEW-03已修复`); } else if (resp.data?.code === 500) { fail('NEW-03 发布活动', new Error('500 Internal Server Error'), '⚠️ NEW-03可能回归'); } else { fail('NEW-03 发布活动', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('NEW-03 发布活动', err); } } // ───────────────────────────────────────── // 6. ISSUE-001 验证:供应商下架商品 // ───────────────────────────────────────── async function testISSUE001() { log('=== 6. ISSUE-001 验证:供应商下架商品 ==='); if (!adminToken) { skip('ISSUE-001 供应商下架', '无admin token'); return; } // 1) 审核供应商 81069(如尚未批准) try { const resp = await adminApi('/api/admin/vendor/review', { userId: 81069, action: 'approve', reason: '自动化测试', }); if (resp.ok || resp.data?.code === 200) { pass('审核供应商81069', 'approved'); } else { fail('审核供应商81069', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('审核供应商81069', err); } // 2) 审核商品(如尚未批准) try { const resp = await adminApi('/api/admin/product/review', { productId: 7, action: 'approve', reason: '自动化测试', }); if (resp.ok || resp.data?.code === 200) { pass('审核商品7', 'approved'); } else { fail('审核商品7', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('审核商品7', err); } // 3) 上架商品 try { const resp = await adminApi('/api/admin/product/shelve', { productId: 7, shelve: true }); if (resp.ok || resp.data?.code === 200) { pass('上架商品7', 'on_shelf'); } else { fail('上架商品7', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('上架商品7', err); } // 4) 供应商下架商品(核心验证) try { const resp = await api('/api/product/shelve', { productId: 7, action: 'unshelve' }, { 'X-User-Id': '81069' }); if (resp.ok || resp.data?.code === 200) { pass('ISSUE-001 供应商下架商品', 'unshelved ✅ ISSUE-001已修复'); } else if (resp.data?.message?.includes('未通过审核')) { fail('ISSUE-001 供应商下架商品', new Error('商品未通过审核/状态异常'), '需检查商品状态'); } else { fail('ISSUE-001 供应商下架商品', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('ISSUE-001 供应商下架商品', err); } } // ───────────────────────────────────────── // 7. ISSUE-002/003 回归验证 // ───────────────────────────────────────── async function testIssue002_003() { log('=== 7. ISSUE-002/003 回归验证 ==='); if (!adminToken) { skip('ISSUE-002/003', '无admin token'); return; } // ISSUE-003: 创建用户 const phone = '139' + String(Math.floor(Math.random() * 1e9)).padStart(9, '0'); let newUserId = null; try { const resp = await adminApi('/api/admin/users/create', { phone: phone, password: 'Test123456', name: '自动化测试用户_' + Date.now(), role: 'parent', }); if (resp.ok || resp.data?.code === 200) { newUserId = resp.data?.data?.id || resp.data?.id; pass('ISSUE-003 创建用户', `userId=${newUserId} ✅ ISSUE-003稳定`); } else if (resp.data?.code === 500) { fail('ISSUE-003 创建用户', new Error('500 - ISSUE-003回归'), '⚠️ 需紧急修复'); } else { fail('ISSUE-003 创建用户', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('ISSUE-003 创建用户', err); } // ISSUE-002: 文章发布(检查 articleId=13 是否仍可发布) try { const resp = await adminApi('/api/article/13/publish', {}); if (resp.ok || resp.data?.code === 200) { pass('ISSUE-002 文章发布', 'articleId=13 ✅ ISSUE-002稳定'); } else { fail('ISSUE-002 文章发布', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('ISSUE-002 文章发布', err); } } // ───────────────────────────────────────── // 8. 新增API测试(来自 recent commits) // dfcc285: 心愿兑换积分检查和扣除 // 3e44f18: grantEnergy + deductForWish 接口 // 8212e25: getRecordsByChild 成长记录 // ───────────────────────────────────────── async function testNewAPIs() { log('=== 8. 新增API测试(recent commits)==='); if (!adminToken) { skip('新增API测试', '无admin token'); return; } // 8a. EnergyService.grantEnergy + deductForWish // POST /api/energy/grantEnergy try { const resp = await adminApi('/api/energy/grantEnergy', { childId: 1, taskId: 1, amount: 10, dimensionCode: 'body', }); if (resp.ok || resp.data?.code === 200) { pass('energy.grantEnergy', 'ok'); } else { fail('energy.grantEnergy', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('energy.grantEnergy', err); } // POST /api/energy/deductForWish try { const resp = await adminApi('/api/energy/deductForWish', { childId: 1, wishId: 1, amount: 5, }); if (resp.ok || resp.data?.code === 200) { pass('energy.deductForWish', 'ok'); } else { fail('energy.deductForWish', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('energy.deductForWish', err); } // 8b. PointsService.checkAndGrantReward + deductForWish (dfcc285) // POST /api/points/checkAndGrantReward try { const resp = await adminApi('/api/points/checkAndGrantReward', { childId: 1, amount: 5, }); // 可能返回 -1(余额不足)或成功 if (resp.ok || resp.data?.code === 200 || resp.data?.data === -1) { pass('points.checkAndGrantReward', `result=${resp.data?.data}`); } else { fail('points.checkAndGrantReward', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('points.checkAndGrantReward', err); } // POST /api/points/deductForWish try { const resp = await adminApi('/api/points/deductForWish', { childId: 1, amount: 5, }); if (resp.ok || resp.data?.code === 200) { pass('points.deductForWish', `result=${resp.data?.data}`); } else { fail('points.deductForWish', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('points.deductForWish', err); } // 8c. GrowthRecord - getRecordsByChild (8212e25) // GET /api/growth/record/child/{childId} 或 POST try { const resp = await adminApi('/api/growth/record/child/1', {}); if (resp.ok || resp.data?.code === 200) { const records = resp.data?.data || []; pass('growth.getRecordsByChild', `count=${Array.isArray(records) ? records.length : 0}`); } else { fail('growth.getRecordsByChild', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('growth.getRecordsByChild', err); } // 8d. Activity end try { const resp = await adminApi('/api/activity/end', { id: 18 }); if (resp.ok || resp.data?.code === 200 || resp.data?.message?.includes('不存在')) { pass('activity.end', 'ok'); } else { fail('activity.end', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`)); } } catch (err) { fail('activity.end', err); } } // ───────────────────────────────────────── // 9. 能量规则维度查询 // ───────────────────────────────────────── async function testEnergyRuleDimensions() { log('=== 9. 能量规则维度查询 ==='); if (!adminToken) { skip('能量规则维度查询', '无admin token'); return; } try { const resp = await adminApi('/api/admin/energy-rule/dimensions', {}); if (resp.ok || resp.data?.code === 200) { const dims = resp.data?.data || []; pass('能量规则维度列表', `count=${dims.length}`); dims.forEach(d => { console.log(` - ${d.dimensionCode || d.code}: ${d.name || d.dimensionName || JSON.stringify(d)}`); }); } else { fail('能量规则维度列表', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('能量规则维度列表', err); } } // ───────────────────────────────────────── // 10. 后台管理核心接口 // ───────────────────────────────────────── async function testAdminCore() { log('=== 10. 后台管理核心接口 ==='); if (!adminToken) { skip('后台管理核心接口', '无admin token'); return; } // 用户列表 try { const resp = await adminApi('/api/admin/users', { page: 1, size: 10 }); if (resp.ok || resp.data?.code === 200) { pass('用户列表', 'ok'); } else { fail('用户列表', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('用户列表', err); } // 供应商列表 try { const resp = await adminApi('/api/admin/vendor/list', {}); if (resp.ok || resp.data?.code === 200) { pass('供应商列表', 'ok'); } else { fail('供应商列表', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('供应商列表', err); } // 规划师申请列表 try { const resp = await adminApi('/api/admin/guide/applications/pending', {}); if (resp.ok || resp.data?.code === 200) { const apps = resp.data?.data || []; pass('规划师申请列表', `pending=${apps.length}`); } else { fail('规划师申请列表', new Error(`code=${resp.data?.code}`)); } } catch (err) { fail('规划师申请列表', err); } } // ───────────────────────────────────────── // 报告生成 // ───────────────────────────────────────── async function generateReport() { const duration = Date.now() - testStartTime; const passed = testResults.filter(r => r.result === 'PASS').length; const failed = testResults.filter(r => r.result === 'FAIL').length; const skipped = testResults.filter(r => r.result === 'SKIP').length; const total = testResults.length; console.log('\n' + '='.repeat(60)); console.log(` API 测试报告 v3 — ${new Date().toLocaleString()}`); console.log(` 耗时: ${Math.round(duration / 1000)}s`); console.log(` 结果: ${passed} PASS / ${failed} FAIL / ${skipped} SKIP / ${total} TOTAL`); console.log('='.repeat(60)); if (failed > 0) { console.log('\n❌ FAIL 明细:'); testResults.filter(r => r.result === 'FAIL').forEach(r => { console.log(` - ${r.name}: ${r.detail}`); }); } const report = { version: 'v3', timestamp: new Date().toISOString(), duration_ms: duration, summary: { total, passed, failed, skipped }, results: testResults, }; const fs = require('fs'); const path = require('path'); const reportPath = path.join(__dirname, 'TEST-RESULTS-v3.md'); let md = `# API 测试报告 v3 — ${new Date().toLocaleString()}\n\n`; md += `| 结果 | 数量 |\n|------|------|\n`; md += `| ✅ PASS | ${passed} |\n| ❌ FAIL | ${failed} |\n| ⏭️ SKIP | ${skipped} |\n| 总计 | ${total} |\n\n`; md += `## FAIL 明细\n\n`; testResults.filter(r => r.result === 'FAIL').forEach(r => { md += `- **${r.name}**: ${r.detail}\n`; }); md += `\n## 完整结果\n\n`; md += `| 用例 | 结果 | 详情 |\n|------|------|------|\n`; testResults.forEach(r => { md += `| ${r.name} | ${r.result} | ${r.detail} |\n`; }); fs.writeFileSync(reportPath, md, 'utf8'); console.log(`\n报告已保存: ${reportPath}`); } // ───────────────────────────────────────── // 主流程 // ───────────────────────────────────────── async function main() { log('CFC API 测试 Runner v3 开始'); log(`目标环境: ${BASE}`); await login(); await testPublicEndpoints(); await testNEW01_EnergyRuleCreate(); await testNEW02_SkuList(); await testNEW03_ActivityPublish(); await testISSUE001(); await testIssue002_003(); await testEnergyRuleDimensions(); await testAdminCore(); await testNewAPIs(); await generateReport(); } main().catch(console.error);