/** * Phase 3 验证:商家(企业账号)注册 / 登录 / 全业务流程 * 运行:node tests/phase3_merchant.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 mobile = '138' + stamp; console.log('═══════════════════════════════════════════════════'); console.log(' Phase 3 验证:商家注册 / 登录 / 全业务流程'); console.log('═══════════════════════════════════════════════════'); sec('账号与身份'); const login = await api('/auth/login', { method: 'POST', data: { mobile: mobile, code: '123456' } }); const token = login.data.token; chk(login.code === 1, '手机号登录(微信不可用时的兜底通道)'); chk(login.data.userinfo.is_merchant === false, '新账号默认个人用户'); let r = await api('/auth/switchRole', { method: 'POST', token: token, data: { role: 'merchant' } }); chk(r.code === 2001 && r.data.need_apply === 1, '未入驻切商家被拦截(2001)', r.msg); sec('商家入驻'); r = await api('/merchant/apply', { method: 'POST', token: token, data: { name: '遵义优匠物业服务有限公司', license_no: '91520300MA6XXXXX1A', legal_person: '王老板', contact: '李经理', mobile: mobile, province: '贵州省', city: '遵义市', district: '汇川区', address: '大连路 100 号 3 楼', invoice_title: '遵义优匠物业服务有限公司', invoice_tax_no: '91520300MA6XXXXX1A', settle_type: 'monthly' } }); chk(r.code === 1 && r.data.merchant_id > 0, '提交企业资料', 'merchant_id=' + r.data.merchant_id); const merchantId = r.data.merchant_id; r = await api('/merchant/info', { token: token }); chk(r.data.applied && r.data.merchant.verify_status === 'pending', '入驻后状态=待审核'); chk(r.data.merchant.settle_type === 'monthly', '结算方式保存为月结'); r = await api('/auth/switchRole', { method: 'POST', token: token, data: { role: 'merchant' } }); chk(r.code === 2002, '审核中切商家被拦截(2002)', r.msg); sql(`UPDATE fa_merchant SET verify_status='pass' WHERE id=${merchantId}`); r = await api('/auth/switchRole', { method: 'POST', token: token, data: { role: 'merchant' } }); chk(r.code === 1 && r.data.redirect === '/pages/merchant/workbench', '审核通过 → 切换商家端', r.data && r.data.redirect); sec('工作台数据'); r = await api('/merchant/info', { token: token }); const m = r.data.merchant; chk(m && Number(m.order_count) === 0 && Number(m.total_amount) === 0, '初始经营数据为 0', '订单 ' + m.order_count + ' / 金额 ' + m.total_amount); chk(m.invoiceable !== undefined, '可开票金额字段就绪', '¥' + m.invoiceable); sec('批量下单'); const svc = (await api('/home/services?cat_id=1')).data.list.find(s => s.price_type === 'fixed'); const orderIds = []; for (const i of [1, 2]) { const addr = await api('/address/save', { method: 'POST', token: token, data: { contact: '门店' + i, mobile: mobile, province: '贵州省', city: '遵义市', district: '汇川区', address: '批量测试地址 ' + i, floor: 3, has_elevator: 1, is_default: 0 } }); const o = await api('/order/create', { method: 'POST', token: token, data: { service_id: svc.id, address_id: addr.data.id, goods_num: 1, remark: '商家批量下单' } }); await api('/order/pay', { method: 'POST', token: token, data: { order_id: o.data.order_id } }); orderIds.push(o.data.order_id); } chk(orderIds.length === 2, '逐单创建并支付', orderIds.join(',')); r = await api('/merchant/bindBatch', { method: 'POST', token: token, data: { order_ids: orderIds.join(','), batch_no: '' } }); chk(r.code === 1 && r.data.count === 2 && r.data.batch_no, '归属到同一批次', 'batch_no=' + r.data.batch_no + ' count=' + r.data.count); r = await api('/merchant/bindBatch', { method: 'POST', token: token, data: { order_ids: orderIds.join(','), batch_no: '' } }); chk(r.code === 1 && r.data.count === 0, '重复归属被忽略(幂等)', 'count=' + r.data.count); sec('账单中心'); r = await api('/merchant/bills', { token: token }); chk(r.code === 1 && r.data.months.length >= 1, '按月汇总生成', r.data.months[0] && (r.data.months[0].month + ' / ' + r.data.months[0].cnt + ' 单 / ¥' + r.data.months[0].amount)); chk(r.data.orders.length === 2, '近期订单明细', r.data.orders.length + ' 条'); sec('开票'); r = await api('/merchant/info', { token: token }); const available = Number(r.data.merchant.invoiceable); chk(available > 0, '可开票金额已累计', '¥' + available); r = await api('/merchant/invoice', { method: 'POST', token: token, data: { amount: available + 100, type: 'normal' } }); chk(r.code !== 1, '超额开票被拦截', r.msg); r = await api('/merchant/invoice', { method: 'POST', token: token, data: { amount: 50, type: 'special', email: 'fin@example.com' } }); chk(r.code === 1, '提交开票申请(专用发票)', r.msg); r = await api('/merchant/invoices', { 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); r = await api('/merchant/invoice', { method: 'POST', token: token, data: { amount: available, type: 'normal' } }); chk(r.code !== 1, '已申请金额占用额度(防重复开票)', r.msg); sec('资料维护与身份切回'); r = await api('/merchant/update', { method: 'POST', token: token, data: { contact: '新经理', mobile: mobile } }); chk(r.code === 1, '维护联系人', r.msg); r = await api('/merchant/info', { token: token }); chk(r.data.merchant.contact === '新经理', '资料已更新'); 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/profile', { token: token }); chk(r.data.is_merchant === true && Number(r.data.merchant.id) === Number(merchantId), 'profile 携带商家身份', 'merchant_id=' + r.data.merchant.id); 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; });