Files
device-voice-h5/utils/silence-guard.js
邹进 37b650d41e
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m3s
feat: 沉默期已过期待延期时全局拦截,除绑定手机号与延期支付页外都跳转延期页
2026-11-08 16:41:53 +09:00

145 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 沉默期强制延期拦截:当前资产有"沉默期已过期待延期"的套餐时,
* 除首页(强制延期弹窗)、登录/授权、绑定手机号、延期与支付相关页面外,其他页面一律不能进入,
* 统一引导到延期页。
*/
import { isSilenceReminderBlocking, pickSilenceReminder } from './silence.js';
export const SILENCE_EXTENSION_PAGE = '/pages/silence-extension/silence-extension';
// 不拦截的页面:登录授权、绑定手机号、首页(自带强制弹窗)、延期办理与支付相关页面
export const SILENCE_GUARD_ALLOWED_PAGES = Object.freeze([
'pages/index/index',
'pages/login/login',
'pages/auth/auth',
'pages/error/error',
'pages/bind/bind',
'pages/silence-extension/silence-extension',
'pages/silence-payment/silence-payment',
'pages/silence-orders/silence-orders',
'pages/silence-orders/detail',
'pages/silence-records/silence-records',
'pages/alipay-payment/alipay-payment'
]);
// 未被拦截的结果缓存时长;被拦截时每次都重新查询,延期完成后可立即放行
const PASS_CACHE_MS = 5 * 60 * 1000;
const NAVIGATION_METHODS = ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'];
const CHECKED_FLAG = '__silenceGuardChecked';
export const pagePathOf = (url) => String(url || '').split('?')[0].replace(/^\/+/, '');
export const isSilenceGuardAllowed = (url) => SILENCE_GUARD_ALLOWED_PAGES.includes(pagePathOf(url));
export const silenceExtensionUrl = (reminder) =>
`${SILENCE_EXTENSION_PAGE}?package_usage_id=${reminder.packageUsageId}`;
/**
* 创建拦截器核心(依赖可注入,便于测试)。
* deps.getSession(): { token, identifier }
* deps.loadPackageHistory(identifier): Promise<{ items }>
* deps.now(): number
*/
export const createSilenceGuard = ({ getSession, loadPackageHistory, now = () => Date.now() }) => {
let passCache = null;
let pending = null;
const sessionKey = () => {
const { token, identifier } = getSession();
const id = String(identifier || '').trim();
return token && id ? { key: `${token}|${id}`, identifier: id } : null;
};
/** 命中"未拦截"缓存时同步返回 true,可直接放行 */
const isKnownPass = () => {
const session = sessionKey();
if (!session) return true;
return !!passCache && passCache.key === session.key && now() - passCache.at < PASS_CACHE_MS;
};
/** 查询当前资产是否需要强制延期;返回需要延期的提醒,或 null(放行) */
const resolveBlock = () => {
const session = sessionKey();
if (!session) return Promise.resolve(null);
if (isKnownPass()) return Promise.resolve(null);
if (pending?.key === session.key) return pending.promise;
const promise = loadPackageHistory(session.identifier)
.then((data) => {
const reminder = pickSilenceReminder(data?.items, now());
if (isSilenceReminderBlocking(reminder)) {
passCache = null;
return reminder;
}
passCache = { key: session.key, at: now() };
return null;
})
.catch((error) => {
// 查询失败不锁死用户,放行但不缓存,下次跳转再查
console.error('沉默期拦截检查失败', error);
return null;
})
.finally(() => {
if (pending?.promise === promise) pending = null;
});
pending = { key: session.key, promise };
return promise;
};
const reset = () => {
passCache = null;
pending = null;
};
return { isKnownPass, resolveBlock, reset };
};
const notifyBlocked = () => {
uni.showToast({ title: '套餐沉默期已过期,请先办理延期', icon: 'none', duration: 2000 });
};
/**
* 安装全局拦截:
* 1. 拦截 navigateTo/redirectTo/reLaunch/switchTab:目标页不在白名单时先检查,需要延期则改去延期页;
* 2. 页面显示时(直接打开链接、刷新、浏览器后退)再检查一次,需要延期则替换为延期页。
*/
export const installSilenceGuard = (app, guard) => {
NAVIGATION_METHODS.forEach((method) => {
uni.addInterceptor(method, {
invoke(args) {
if (args && args[CHECKED_FLAG]) {
delete args[CHECKED_FLAG];
return;
}
if (isSilenceGuardAllowed(args?.url) || guard.isKnownPass()) return;
guard.resolveBlock().then((reminder) => {
if (reminder) {
notifyBlocked();
uni.navigateTo({ url: silenceExtensionUrl(reminder) });
return;
}
uni[method]({ ...args, [CHECKED_FLAG]: true });
});
return false;
}
});
});
app.mixin({
onShow() {
const route = this.$page?.route || this.route;
if (!route || isSilenceGuardAllowed(route)) return;
const known = guard.isKnownPass();
if (known) return;
uni.showLoading({ title: '加载中', mask: true });
guard.resolveBlock().then((reminder) => {
uni.hideLoading();
const pages = getCurrentPages();
const current = pages[pages.length - 1];
if (!reminder || (current?.route && current.route !== route)) return;
notifyBlocked();
uni.redirectTo({ url: silenceExtensionUrl(reminder), [CHECKED_FLAG]: true });
});
}
});
};