feat: 微信授权登录 + 我的中心角色切换 + 师傅工作台全模块 + 图标美化
登录与角色: - Auth::wxlogin 微信 code → jscode2session → 落库发 token(appid/secret 走 .env) - 新增 fa_user_wechat 表,openid 幂等建号(CLI 已验证 is_new 1→0 不重复建号) - Auth::switchRole 角色切换(未入驻 2001 / 审核中 2002 / 未通过 2003) - pages/login 改为微信一键授权;pages/mine 我的中心(师傅入口 + 切换师傅端) 师傅工作台(对齐需求文档): - pages/master/workbench 工作台:接单开关 / 待处理待验收 / 可提现 / 六宫格入口 / 信用四指标 / 技能标签 - pages/master/hall 订单大厅:类型筛选(一口价/报价) + 4 种排序 + 抢单倒计时 + 报价入口 - pages/master/quote 报价竞标:金额/说明/有效期 + 报价记录 - pages/master/wallet 钱包:可提现/今日/累计 + 账单明细 + 提现校验 - pages/master/credit 信用与申诉:五项指标 + 规则 + 申诉提交/记录 - 新增 fa_appeal 表 视觉: - 15 个类目图标重画为双色扁平 SVG,用无头 Chrome 渲染为 192px 透明 PNG - 首页改用 <image src=static/icons/*.png>,移除 200 行 CSS 图标数据 验证:page_flow 47 + e2e_flow 65 + phase2_verify 27 = 139 项断言全通过
这个提交包含在:
140
tests/phase2_verify.js
普通文件
140
tests/phase2_verify.js
普通文件
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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 && /微信登录失败/.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; });
|
||||
在新工单中引用
屏蔽一个用户