文件
Henglianshutong/tests/verify_active_countdown_spec.js

240 行
10 KiB
JavaScript

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

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

/**
* 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);
}