微信登录修复: - 根因:manifest.json 的 mp-weixin.appid 为空 → 开发者工具用测试号,code 与服务端 appid 不匹配 - 修复:填入 wx40ca4b02d4d1cb58 - WxLogin 增加错误码语义映射(40029 appid 不一致 / 40125 secret 错 / 45011 频繁 / 40226 风控) - 登录页新增手机号登录兜底通道(微信不可用时业务不被卡住) 商家(企业账号): - 新表 fa_merchant / fa_merchant_order / fa_invoice(纯新增,DB 40→45 表) - Merchant 控制器:apply/info/update/bills/invoices/invoice/bindBatch 入驻含企业信息+营业执照+开票信息+结算方式(预付/月结) 账单按月汇总、开票额度按已付-已开票校验、批量下单按批次归属 - Auth::switchRole 支持 merchant(2001/2002/2003 语义码 + redirect) - Auth::profile 增加 is_merchant / merchant 前端: - 登录页角色扩为 业主/师傅/商家 三类 - pages/merchant/apply 企业入驻;workbench 商家工作台(经营数据+按月账单+近期订单+六宫格) - pages/merchant/batch 批量下单(多地址×多数量)→ 逐单建单支付 → 绑定批次 - pages/merchant/invoice 开票申请与记录 - pages/mine 增加商家入驻/切换商家端入口;角色徽章三态 验证:163 项断言全通过(page_flow 47 + e2e 65 + phase2 27 + phase3 24)
141 行
8.6 KiB
JavaScript
141 行
8.6 KiB
JavaScript
/**
|
||
* 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; });
|