240 行
10 KiB
JavaScript
240 行
10 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|