/** * 30天收益保活倒计时看板与状态机综合测试用例 * 覆盖: * 1. 后端 IdentityService 保活到期时间与状态判定(正常活跃、临期预警、奖励暂停) * 2. 前端 30 天秒级倒计时时分秒换算逻辑 * 3. 前端 index.vue 模板节点及样式定义完整性 * 4. mp-weixin 编译产物 index.wxml / index.wxss / index.js 完整性 */ const fs = require('fs'); const path = require('path'); const assert = require('assert'); let passCount = 0; let failCount = 0; function it(desc, fn) { try { fn(); console.log(` [PASS] ${desc}`); passCount++; } catch (err) { console.error(` [FAIL] ${desc}`); console.error(` Error: ${err.message}`); failCount++; } } console.log('========================================================'); console.log(' 30天收益保活倒计时看板与状态机覆盖性测试'); console.log('========================================================\n'); // 1. 测试 IdentityService.php const identityServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/IdentityService.php'); const identityContent = fs.readFileSync(identityServicePath, 'utf8'); it('IdentityService.php 存在并包含 active_status 字段逻辑', () => { assert(fs.existsSync(identityServicePath), 'IdentityService.php 不存在'); assert(identityContent.includes("'active_status'"), "未在 attachAgent 中计算 'active_status'"); }); it('IdentityService.php 正确区分 active / expiring / inactive 三种状态', () => { assert(identityContent.includes("'inactive'"), "缺少 inactive 状态判定"); assert(identityContent.includes("'expiring'"), "缺少 expiring 状态判定"); assert(identityContent.includes("'active'"), "缺少 active 状态判定"); }); it('IdentityService.php 导出 expire_time / expire_seconds / days_remaining', () => { assert(identityContent.includes("'expire_time'"), "缺少 expire_time"); assert(identityContent.includes("'expire_seconds'"), "缺少 expire_seconds"); assert(identityContent.includes("'days_remaining'"), "缺少 days_remaining"); }); it('IdentityService.php 符合方案 A:以订单时间戳精确顺延30天,严禁抹平为零点', () => { assert(identityContent.includes('$expireTime = $lastTime + $windowDays * 86400;'), '未采用方案A精确顺延'); assert(!identityContent.includes("date('Y-m-d 00:00:00', $lastTime)"), '仍残留错误抹平为零点的逻辑'); }); const activeServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/ActiveService.php'); const activeContent = fs.readFileSync(activeServicePath, 'utf8'); it('ActiveService.php 活跃判定支持方案 A 秒级 30 天有效', () => { assert(activeContent.includes('$at <= $lastTime + $days * 86400'), 'ActiveService 缺少方案A秒级判定'); }); // 2. 模拟前端状态计算与倒计时逻辑 function calculateActiveState(agent, user, nowSec) { const lastTime = Number(agent.last_self_order_time || user.last_self_order_time || 0); const isExplicitInactive = agent.active_30d === false || user.active_30d === false || agent.is_active_30d === false || user.is_active_30d === false; if (isExplicitInactive || (lastTime <= 0 && (agent.active_30d === false || user.active_30d === false))) { return { status: 'inactive', title: '收益奖励已暂停', badge: '暂缓计奖', btnText: '去选购恢复' }; } let expireTime = Number(agent.expire_time || 0); if (!expireTime) { expireTime = lastTime > 0 ? (lastTime + 30 * 86400) : (nowSec + 30 * 86400); } const remainingSeconds = Math.max(0, expireTime - nowSec); const remainingDays = Math.ceil(remainingSeconds / 86400); if (remainingSeconds <= 0) { return { status: 'inactive', title: '收益奖励已暂停', badge: '暂缓计奖', btnText: '去选购恢复' }; } if (remainingDays <= 5) { return { status: 'expiring', title: '30天保活临期预警', badge: `仅剩 ${remainingDays} 天`, btnText: '立即自购保活' }; } return { status: 'active', title: '30天收益资格生效中', badge: '正常活跃', btnText: '自购或分享续期' }; } function calculateCountdown(expireTime, nowSec) { const remaining = Math.max(0, expireTime - nowSec); const d = Math.floor(remaining / 86400); const h = Math.floor((remaining % 86400) / 3600); const m = Math.floor((remaining % 3600) / 60); const s = remaining % 60; return { days: d < 10 ? '0' + d : String(d), hours: h < 10 ? '0' + h : String(h), minutes: m < 10 ? '0' + m : String(m), seconds: s < 10 ? '0' + s : String(s), totalSeconds: remaining }; } // 3. 正常场景:自购在第 2 天(剩余 28 天) -> 正常活跃 it('正常场景:剩余28天 -> 状态为 active 且倒计时天数格式为 28,按钮为自购或分享续期', () => { const now = 1700000000; const expire = now + 28 * 86400 + 3600 * 5 + 60 * 12 + 30; // 28天5小时12分30秒 const state = calculateActiveState({ expire_time: expire, last_self_order_time: now - 2 * 86400 }, {}, now); const cd = calculateCountdown(expire, now); assert.strictEqual(state.status, 'active'); assert.strictEqual(state.badge, '正常活跃'); assert.strictEqual(state.btnText, '自购或分享续期'); assert.strictEqual(cd.days, '28'); assert.strictEqual(cd.hours, '05'); assert.strictEqual(cd.minutes, '12'); assert.strictEqual(cd.seconds, '30'); }); // 3.1 真实业务场景复现(用户截图:09-19 14:39:00 下单,09-26 13:42:14 查看) it('真实截图场景:09-19 14:39 下单,09-26 13:42:14 查看 -> 准确剩余 23天 00时 56分 46秒', () => { const orderTime = new Date('2026-09-19T14:39:00+08:00').getTime() / 1000; const viewTime = new Date('2026-09-26T13:42:14+08:00').getTime() / 1000; // 方案 A:精确顺延 30 天 const expire = orderTime + 30 * 86400; const state = calculateActiveState({ expire_time: expire, last_self_order_time: orderTime }, {}, viewTime); const cd = calculateCountdown(expire, viewTime); assert.strictEqual(state.status, 'active'); assert.strictEqual(state.badge, '正常活跃'); assert.strictEqual(cd.days, '23', '方案A下天数应准确为 23 天,绝不能是 22 天'); assert.strictEqual(cd.hours, '00', '方案A下小时应准确为 00 时,绝不能是 10 时'); assert.strictEqual(cd.minutes, '56', '方案A下分钟应准确为 56 分'); assert.strictEqual(cd.seconds, '46', '方案A下秒数应准确为 46 秒'); }); // 4. 临期场景:剩余 3 天(<= 5 天) -> 临期预警 it('临期预警场景:剩余3天 -> 状态为 expiring,提示立即自购保活', () => { const now = 1700000000; const expire = now + 3 * 86400 + 3600 * 2 + 60 * 1 + 5; const state = calculateActiveState({ expire_time: expire, last_self_order_time: now - 27 * 86400 }, {}, now); const cd = calculateCountdown(expire, now); assert.strictEqual(state.status, 'expiring'); assert.strictEqual(state.badge, '仅剩 4 天'); // ceil assert.strictEqual(state.btnText, '立即自购保活'); assert.strictEqual(cd.days, '03'); assert.strictEqual(cd.hours, '02'); assert.strictEqual(cd.minutes, '01'); assert.strictEqual(cd.seconds, '05'); }); // 5. 业务失败/过期场景:超过30天未自购 -> 奖励暂停 it('过期场景:剩余时间为0 -> 状态为 inactive,按钮为去选购恢复', () => { const now = 1700000000; const expire = now - 100; const state = calculateActiveState({ expire_time: expire, last_self_order_time: now - 31 * 86400 }, {}, now); const cd = calculateCountdown(expire, now); assert.strictEqual(state.status, 'inactive'); assert.strictEqual(state.title, '收益奖励已暂停'); assert.strictEqual(state.btnText, '去选购恢复'); assert.strictEqual(cd.totalSeconds, 0); assert.strictEqual(cd.days, '00'); }); // 6. 前端源码 index.vue 结构检查 const indexVuePath = path.resolve(__dirname, '../uniapp/pages/commission/index.vue'); const indexVueContent = fs.readFileSync(indexVuePath, 'utf8'); it('uniapp/pages/commission/index.vue 包含 active-countdown-card 卡片', () => { assert(indexVueContent.includes('active-countdown-card'), '未找到 active-countdown-card 类'); assert(indexVueContent.includes('countdown.days'), '未找到倒计时天数绑定'); assert(indexVueContent.includes('countdown.hours'), '未找到倒计时小时绑定'); assert(indexVueContent.includes('countdown.minutes'), '未找到倒计时分钟绑定'); assert(indexVueContent.includes('countdown.seconds'), '未找到倒计时秒数绑定'); }); it('uniapp/pages/commission/index.vue 包含定时器生命周期管理', () => { assert(indexVueContent.includes('setInterval(updateCountdown, 1000)'), '缺少 1 秒定时器'); assert(indexVueContent.includes('clearInterval(timer)'), '缺少定时器清理'); assert(indexVueContent.includes('onShow'), '缺少 onShow 挂载'); assert(indexVueContent.includes('onHide'), '缺少 onHide 清理'); assert(indexVueContent.includes('onUnload'), '缺少 onUnload 清理'); }); // 7. 微信小程序已编译产物检查 const wxmlPath = path.resolve(__dirname, '../uniapp/unpackage/dist/dev/mp-weixin/pages/commission/index.wxml'); const wxssPath = path.resolve(__dirname, '../uniapp/unpackage/dist/dev/mp-weixin/pages/commission/index.wxss'); const jsPath = path.resolve(__dirname, '../uniapp/unpackage/dist/dev/mp-weixin/pages/commission/index.js'); it('mp-weixin 产物完整且包含 active-countdown-card 节点与样式', () => { const wxml = fs.readFileSync(wxmlPath, 'utf8'); const wxss = fs.readFileSync(wxssPath, 'utf8'); const js = fs.readFileSync(jsPath, 'utf8'); assert(wxml.includes('active-countdown-card'), 'index.wxml 缺少 active-countdown-card'); assert(wxml.includes('timer-digits'), 'index.wxml 缺少 timer-digits 倒计时节点'); assert(wxss.includes('.active-countdown-card'), 'index.wxss 缺少 .active-countdown-card'); assert(wxss.includes('.status-active'), 'index.wxss 缺少 .status-active'); assert(wxss.includes('.status-expiring'), 'index.wxss 缺少 .status-expiring'); assert(wxss.includes('.status-inactive'), 'index.wxss 缺少 .status-inactive'); assert(js.includes('countdown'), 'index.js 缺少 countdown 响应式对象'); assert(js.includes('activeState'), 'index.js 缺少 activeState 响应式计算属性'); }); console.log(`\n--------------------------------------------------------`); console.log(`测试结果统计: 通过 ${passCount} 项, 失败 ${failCount} 项`); console.log(`========================================================\n`); if (failCount > 0) { process.exit(1); } else { process.exit(0); }