/** * Phase 2 验证:微信登录、角色切换、师傅工作台各模块 * 运行:node tests/phase2_verify.js */ const { execFileSync } = require('child_process'); const BASE = process.env.API_BASE || 'https://yjyf.gzyakj.com.cn/api'; let pass = 0, fail = 0; const failures = []; const ok = (m, e) => { pass++; console.log(' ✅ ' + m + (e ? ' → ' + e : '')); }; const bad = (m, e) => { fail++; failures.push(m + ' :: ' + e); console.log(' ❌ ' + m + ' → ' + e); }; const chk = (c, m, e) => (c ? ok(m, e) : bad(m, e || '断言失败')); const sec = (t) => console.log('\n▌' + t); async function api(path, opt) { const o = opt || {}; const headers = {}; if (o.token) headers.token = o.token; let body; if (o.data) { headers['content-type'] = 'application/x-www-form-urlencoded'; body = new URLSearchParams(o.data).toString(); } const res = await fetch(BASE + path, { method: o.method || 'GET', headers, body }); const text = await res.text(); try { return JSON.parse(text); } catch (e) { throw new Error('非 JSON: ' + text.slice(0, 120)); } } const sql = (s) => execFileSync('ssh', ['-o', 'BatchMode=yes', 'root@8.137.181.63', `mysql -uyjyf -pyjyf yjyf -e "${s}"`], { encoding: 'utf8' }).trim(); (async () => { const stamp = String(Date.now()).slice(-8); const uM = '139' + stamp; console.log('═══════════════════════════════════════════════════'); console.log(' Phase 2 验证:角色切换 + 师傅工作台模块'); console.log('═══════════════════════════════════════════════════'); sec('微信授权登录'); let r = await api('/auth/wxlogin', { method: 'POST', data: { code: 'invalid_code_x' } }); chk(r.code !== 1 && /微信登录失败|appid/.test(r.msg || ''), '无效 code 被微信侧拒绝并给出排查指引', r.msg); r = await api('/auth/wxlogin', { method: 'POST', data: {} }); chk(r.code !== 1, '缺少 code 时参数校验生效', r.msg); const lu = await api('/auth/login', { method: 'POST', data: { mobile: uM, code: '123456' } }); const token = lu.data.token; sec('角色切换:未入驻不能切师傅'); r = await api('/auth/switchRole', { method: 'POST', token: token, data: { role: 'master' } }); chk(r.code === 2001 && r.data && r.data.need_apply === 1, '未入驻 → 返回 2001 并指向入驻页', r.msg); sec('师傅入驻 → 审核 → 切换师傅身份'); r = await api('/master/apply', { method: 'POST', token: token, data: { realname: 'Phase2师傅', mobile: uM, idcard_no: '520101199505051234', city: '遵义市', 'skills[0]': 1 } }); const masterId = r.data.master_id; chk(r.code === 1, '提交入驻', 'master_id=' + masterId); r = await api('/auth/switchRole', { method: 'POST', token: token, data: { role: 'master' } }); chk(r.code === 2002, '审核中 → 返回 2002', r.msg); sql(`UPDATE fa_master SET verify_status='pass' WHERE id=${masterId}`); r = await api('/auth/switchRole', { method: 'POST', token: token, data: { role: 'master' } }); chk(r.code === 1 && r.data.redirect === '/pages/master/workbench', '审核通过 → 切换成功并跳转工作台', r.data && r.data.redirect); sec('师傅工作台'); r = await api('/master/info', { token: token }); chk(r.code === 1 && r.data.master.verify_status === 'pass', '工作台头部数据就绪'); chk(typeof r.data.master.todo_count === 'number' && typeof r.data.master.verify_count === 'number', '待处理/待验收角标字段'); sec('信用体系'); r = await api('/master/credit', { token: token }); const cr = r.data; chk(r.code === 1, '信用接口可用'); chk(cr && cr.score !== undefined && cr.accept_rate !== undefined && cr.ontime_rate !== undefined && cr.good_rate !== undefined && cr.complaint_rate !== undefined, '五项信用指标齐全', `评分${cr.score} 接单${cr.accept_rate}% 准时${cr.ontime_rate}% 好评${cr.good_rate}% 投诉${cr.complaint_rate}%`); chk(Array.isArray(cr.rules) && cr.rules.length === 5, '信用规则说明 5 条'); sec('申诉中心'); r = await api('/master/appeal', { method: 'POST', token: token, data: { type: 'review', content: '客户恶意差评,实际服务已完成' } }); chk(r.code === 1, '提交申诉成功', r.msg); r = await api('/master/appeal', { method: 'POST', token: token, data: { type: 'review', content: '短' } }); chk(r.code !== 1, '内容过短被拦截', r.msg); r = await api('/master/appeals', { token: token }); chk(r.code === 1 && r.data.list.length === 1 && r.data.list[0].type_text === '恶意评价', '申诉列表回显类型与状态', r.data.list[0] && r.data.list[0].status_text); sec('订单大厅:类型筛选 + 排序 + 倒计时'); const svcFixed = (await api('/home/services?cat_id=1')).data.list.find(s => s.price_type === 'fixed'); const addr = await api('/address/save', { method: 'POST', token: token, data: { contact: 'P2客户', mobile: uM, province: '贵州省', city: '遵义市', district: '汇川区', address: 'P2 测试地址', floor: 2, has_elevator: 1, is_default: 1 } }); const o1 = await api('/order/create', { method: 'POST', token: token, data: { service_id: svcFixed.id, address_id: addr.data.id, urgent: 1 } }); await api('/order/pay', { method: 'POST', token: token, data: { order_id: o1.data.order_id } }); r = await api('/master/hall?type=fixed&sort=amount', { token: token }); chk(r.code === 1, 'hall 支持 type + sort 参数', 'total=' + r.data.total); const hl = (r.data.list || [])[0]; chk(hl && hl.left_seconds !== undefined, '返回抢单倒计时 left_seconds', hl && hl.left_seconds + 's'); chk(hl && hl.quote_count !== undefined, '返回报价数 quote_count'); r = await api('/master/hall?type=quote', { token: token }); chk(r.code === 1 && (r.data.list || []).every(o => o.price_type === 'quote'), '报价单筛选口径正确'); r = await api('/master/hall?sort=urgent', { token: token }); chk(r.code === 1, '急单优先排序可用'); sec('报价竞标'); const svcQuote = (await api('/home/services?cat_id=1')).data.list.find(s => s.price_type === 'quote'); if (svcQuote) { const o2 = await api('/order/create', { method: 'POST', token: token, data: { service_id: svcQuote.id, address_id: addr.data.id } }); await api('/order/pay', { method: 'POST', token: token, data: { order_id: o2.data.order_id } }); r = await api('/master/quote', { method: 'POST', token: token, data: { order_id: o2.data.order_id, amount: 260, description: '含材料与上门', valid_hours: 48 } }); chk(r.code === 1, '提交报价成功', r.msg); r = await api('/master/quote', { method: 'POST', token: token, data: { order_id: o2.data.order_id, amount: 200 } }); chk(r.code !== 1, '重复报价被拦截', r.msg); r = await api('/master/quotes', { token: token }); chk(r.code === 1 && r.data.list.length === 1, '报价记录列表', r.data.list[0] && ('¥' + r.data.list[0].amount + ' / ' + r.data.list[0].status_text)); } else { bad('报价竞标', '未找到报价类服务'); } sec('钱包与提现'); r = await api('/master/income', { token: token }); chk(r.code === 1 && r.data.money !== undefined && r.data.today_income !== undefined, '收入概览字段齐全', '可提现 ' + r.data.money); r = await api('/master/withdraw', { method: 'POST', token: token, data: { amount: 5 } }); chk(r.code !== 1, '低于 10 元被拦截', r.msg); r = await api('/master/withdraw', { method: 'POST', token: token, data: { amount: 9999 } }); chk(r.code !== 1, '超出余额被拦截', r.msg); sec('切回业主身份'); r = await api('/auth/switchRole', { method: 'POST', token: token, data: { role: 'user' } }); chk(r.code === 1 && r.data.redirect === '/YJYF/YJYF', '切回业主成功', r.data && r.data.redirect); r = await api('/auth/switchRole', { method: 'POST', token: token, data: { role: 'xxx' } }); chk(r.code !== 1, '非法角色参数被拦截', r.msg); console.log('\n═══════════════════════════════════════════════════'); console.log(` 结果:通过 ${pass} 项 / 失败 ${fail} 项`); if (fail) { console.log(' 失败明细:'); failures.forEach(f => console.log(' - ' + f)); process.exitCode = 1; } else console.log(' 🎉 角色切换与师傅工作台全模块验证通过'); console.log('═══════════════════════════════════════════════════'); })().catch(e => { console.error('\n💥 中断: ' + e.message); process.exitCode = 1; });