文件
yjyf/tests/phase2_verify.js

141 行
8.7 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 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 === '/pages/index/index', '切回业主成功', 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; });