文件
yjyf/tests/phase5_data_sync.js
linda 681b83a762 test(banner): 适配首页多轮播停用与兜底逻辑
单条停用仅移除对应轮播;全部停用才回落到内置轮播,覆盖活动/平台实力/服务主题多张配置
2026-10-28 19:41:00 +08:00

136 行
8.4 KiB
JavaScript

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

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

/**
* Phase 5 验证:后台数据 ↔ 前端接口 同源联动
* 通过直接改后台表(后台 CRUD 的本质操作),断言前端接口立刻反映
* 运行:node tests/phase5_data_sync.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)); }
}
// 直接用 SQL 改后台表,等价于后台 CRUD 的保存动作(强制 utf8mb4,避免中文二次编码)
const sql = (s) => execFileSync('ssh', ['-o', 'BatchMode=yes', 'root@8.137.181.63',
`mysql --default-character-set=utf8mb4 -uyjyf -pyjyf yjyf -e "${s}"`], { encoding: 'utf8' });
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
(async () => {
console.log('═══════════════════════════════════════════════════');
console.log(' Phase 5 验证:后台数据 ↔ 前端接口 同源联动');
console.log('═══════════════════════════════════════════════════');
const mobile = '135' + String(Date.now()).slice(-8);
const login = await api('/auth/login', { method: 'POST', data: { mobile: mobile, code: '123456' } });
const token = login.data.token;
sec('1. 服务上下架(后台 fa_service.status ↔ 前端服务列表)');
const svc = (await api('/home/services?cat_id=1')).data.list[0];
chk(!!svc, '取到一个在售服务', svc && svc.name);
sql(`UPDATE fa_service SET status='hidden' WHERE id=${svc.id}`);
await sleep(1500);
let r = await api('/home/services?cat_id=1');
chk(!(r.data.list || []).some(s => Number(s.id) === Number(svc.id)), '后台下架 → 前端列表不再返回该服务');
sql(`UPDATE fa_service SET status='normal' WHERE id=${svc.id}`);
await sleep(1500);
r = await api('/home/services?cat_id=1');
chk((r.data.list || []).some(s => Number(s.id) === Number(svc.id)), '后台重新上架 → 前端恢复显示');
sec('2. 首页 Banner(后台 fa_banner ↔ /home/index)');
r = await api('/home/index');
const b0 = r.data.banners[0];
chk(!!b0 && !!b0.badge && !!b0.title && !!b0.subtitle && !!b0.date_text, 'Banner 五个字段均由后端下发', b0 && b0.title);
sql(`UPDATE fa_banner SET title='测试同步标题', badge='测试角标' WHERE id=1`);
await sleep(1500);
r = await api('/home/index');
chk(r.data.banners[0].title === '测试同步标题' && r.data.banners[0].badge === '测试角标', '后台改 Banner → 前端立即变化');
sql(`UPDATE fa_banner SET title='双11 狂欢大促', badge='家装季' WHERE id=1`);
await sleep(1500);
r = await api('/home/index');
chk(r.data.banners[0].title === '双11 狂欢大促', '还原成功');
sql(`UPDATE fa_banner SET status='hidden' WHERE id=1`);
await sleep(1500);
r = await api('/home/index');
chk(!r.data.banners.some(b => Number(b.id) === 1) && r.data.banners.length >= 1, '单条停用 → 仅移除该轮播,保留其他主题', '剩余 ' + r.data.banners.length + ' 条');
sql(`UPDATE fa_banner SET status='hidden' WHERE id IN (1,2,3,4)`);
await sleep(1500);
r = await api('/home/index');
chk(r.data.banners.length >= 1 && Number(r.data.banners[0].id) === 0, '全部停用 → 回落到内置兜底轮播', '兜底 title=' + r.data.banners[0].title);
sql(`UPDATE fa_banner SET status='normal' WHERE id IN (1,2,3,4)`);
await sleep(1500);
sec('3. 服务保障条(后台 fa_config ↔ /home/index)');
sql(`UPDATE fa_config SET value='平台认证,先行赔付,极速上门,售后无忧,价格透明' WHERE name='home_assure'`);
await sleep(1500);
r = await api('/home/index');
chk(r.data.assures.length === 5 && r.data.assures[4] === '价格透明', '后台加一条保障 → 前端展示 5 条');
sql(`UPDATE fa_config SET value='平台认证,先行赔付,极速上门,售后无忧' WHERE name='home_assure'`);
await sleep(1500);
r = await api('/home/index');
chk(r.data.assures.length === 4, '还原为 4 条');
sec('4. 教程卡(后台 fa_config ↔ /home/index)');
chk(!!r.data.tutorial && !!r.data.tutorial.title, '教程卡标题由后端下发', r.data.tutorial.title);
sql(`UPDATE fa_config SET value='新手指南|从下单到验收全流程' WHERE name='home_tutorial'`);
await sleep(1500);
r = await api('/home/index');
chk(r.data.tutorial.title === '新手指南' && r.data.tutorial.sub === '从下单到验收全流程', '后台改标题 → 前端立即变化');
sql(`UPDATE fa_config SET value='下单找师傅教程|3 分钟看懂,下单不踩坑' WHERE name='home_tutorial'`);
await sleep(1500);
sec('5. 优惠券(后台 fa_coupon ↔ 前端领券中心)');
sql(`DELETE FROM fa_coupon WHERE name='同步测试券'`);
sql(`INSERT INTO fa_coupon (name,type,amount,discount,min_amount,total,received,start_time,end_time,status,createtime) VALUES ('同步测试券','cash',50.00,1.00,200.00,100,0,0,0,'normal',UNIX_TIMESTAMP())`);
await sleep(1500);
r = await api('/coupon/center', { token: token });
const t = (r.data.list || []).find(c => c.name === '同步测试券');
chk(!!t && String(t.value_text).indexOf('50') > -1, '后台发券 → 前端领券中心出现', t && (t.value_text + ' / ' + t.rule_text));
sql(`UPDATE fa_coupon SET status='hidden' WHERE name='同步测试券'`);
await sleep(1500);
r = await api('/coupon/center', { token: token });
chk(!(r.data.list || []).some(c => c.name === '同步测试券'), '后台下架券 → 前端不再展示');
sql(`DELETE FROM fa_coupon WHERE name='同步测试券'`);
sec('6. 订单状态提示(后端 OrderStatus ↔ 订单详情)');
const addr = await api('/address/save', {
method: 'POST', token: token,
data: { contact: '同步验证', mobile: mobile, province: '贵州省', city: '遵义市', district: '汇川区', address: '同步验证地址', floor: 1, has_elevator: 1, is_default: 1 }
});
const o = await api('/order/create', { method: 'POST', token: token, data: { service_id: svc.id, address_id: addr.data.id } });
r = await api('/order/detail?id=' + o.data.order_id, { token: token });
chk(r.data.order.status_text && r.data.order.status_tip, '状态文案与提示均由后端统一下发',
r.data.order.status_text + ' / ' + r.data.order.status_tip);
sec('7. 后台管理模块完整性');
const ls = execFileSync('ssh', ['-o', 'BatchMode=yes', 'root@8.137.181.63',
"ls /www/wwwroot/yjyf.gzyakj.com.cn/application/admin/controller/"], { encoding: 'utf8' });
const need = ['Banner.php', 'Order.php', 'Master.php', 'Merchant.php', 'Coupon.php', 'Review.php', 'Message.php', 'Service.php', 'Appeal.php', 'Invoice.php', 'Withdraw.php', 'Settle.php'];
const missing = need.filter(n => !ls.includes(n));
chk(missing.length === 0, '12 个业务后台控制器全部就绪', missing.length ? ('缺 ' + missing.join(',')) : '全部生成');
chk(ls.includes('service') || ls.includes('Cat.php'), '服务类目后台控制器就绪');
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; });