fix: 沉默期提醒每次登录都弹,有未支付延期订单时提示去支付或作废
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled

- 频率从每天一次改为每次登录一次:重新登录后会再次提醒,同一次登录内回首页不重复
- 已过期套餐有未支付延期订单时,弹窗提示继续支付或作废
This commit is contained in:
邹进
2026-11-07 21:04:12 +09:00
parent 06ca8af218
commit c11d1881ef
5 changed files with 67 additions and 20 deletions

View File

@@ -2,10 +2,13 @@
<view class="silence-overlay" @tap.stop>
<view class="silence-dialog" :class="`silence-${reminder.kind}`" role="dialog" aria-modal="true" aria-label="套餐沉默期提醒">
<button class="silence-close" aria-label="关闭沉默期提醒" @tap="$emit('close')">×</button>
<view class="silence-title">{{ reminder.kind === 'expired' ? '套餐沉默期已过期' : '请尽快完成实名' }}</view>
<view class="silence-title">{{ title }}</view>
<view v-if="reminder.packageName" class="silence-package">{{ reminder.packageName }}</view>
<view class="silence-body">
<template v-if="reminder.kind === 'expired'">
<template v-if="reminder.pendingOrderNo">
套餐沉默期已于 <text class="silence-strong">{{ deadline }}</text> 过期,您有一笔未支付的延期订单({{ reminder.pendingOrderNo }}),请继续支付;不需要了可以作废后重新办理。
</template>
<template v-else-if="reminder.kind === 'expired'">
套餐需在 <text class="silence-strong">{{ deadline }}</text> 前完成实名,现已过期。办理延期后可继续实名激活使用。
</template>
<template v-else>
@@ -13,7 +16,7 @@
<text class="silence-strong">{{ reminder.remainingDays }}</text> 天,过期后需办理延期。
</template>
</view>
<button class="silence-confirm" @tap="$emit('action')">{{ reminder.kind === 'expired' ? '去延期' : '查看套餐' }}</button>
<button class="silence-confirm" @tap="$emit('action')">{{ actionText }}</button>
<button class="silence-later" @tap="$emit('close')">我知道了</button>
</view>
</view>
@@ -26,6 +29,14 @@
const props = defineProps({ reminder: { type: Object, required: true } });
defineEmits(['close', 'action']);
const deadline = computed(() => formatSilenceDate(props.reminder.silence?.silence_deadline_at));
const title = computed(() => {
if (props.reminder.pendingOrderNo) return '延期订单未支付';
return props.reminder.kind === 'expired' ? '套餐沉默期已过期' : '请尽快完成实名';
});
const actionText = computed(() => {
if (props.reminder.pendingOrderNo) return '去支付或作废';
return props.reminder.kind === 'expired' ? '去延期' : '查看套餐';
});
</script>
<style scoped lang="scss">

View File

@@ -151,7 +151,8 @@
assetApi,
deviceApi,
hasActiveOrPendingPackage,
notificationApi
notificationApi,
packageSilenceApi
} from '@/api/index.js';
import {
useUserStore,
@@ -201,11 +202,20 @@
const data = await assetApi.getPackageHistory(identifier, 1, 20, { status: 0 });
if (entryId !== homeEntryId || !homeActive.value || identifier !== String(userStore.state.identifier || '').trim()) return;
const reminder = pickSilenceReminder(data?.items);
if (reminder && !uni.getStorageSync(silenceReminderKey(identifier, reminder, getShanghaiDay()))) {
silenceReminderIdentifier = identifier;
silenceReminder.value = reminder;
advanceHomePopupQueue();
if (!reminder || uni.getStorageSync(silenceReminderKey(identifier, reminder, getSessionContext().token))) return;
// 已过期的套餐再查一次延期资格:有未支付的延期订单时,弹窗改为提醒去支付或作废
if (reminder.kind === 'expired') {
try {
const eligibility = await packageSilenceApi.getEligibility(reminder.packageUsageId);
if (eligibility?.pending_order_no) reminder.pendingOrderNo = eligibility.pending_order_no;
} catch (error) {
// 资格查询失败不影响提醒本身
}
}
if (entryId !== homeEntryId || !homeActive.value || identifier !== String(userStore.state.identifier || '').trim()) return;
silenceReminderIdentifier = identifier;
silenceReminder.value = reminder;
advanceHomePopupQueue();
} catch (error) {
// 提醒失败不影响首页其他功能
console.error('加载沉默期提醒失败', error);
@@ -218,7 +228,7 @@
silenceReminder.value = null;
return false;
}
uni.setStorageSync(silenceReminderKey(silenceReminderIdentifier, reminder, getShanghaiDay()), 1);
uni.setStorageSync(silenceReminderKey(silenceReminderIdentifier, reminder, getSessionContext().token), 1);
silenceReminderShow.value = true;
return true;
};

View File

@@ -94,7 +94,7 @@ function runtime(initial = {}) {
onLoad() {}, onReachBottom() {}
});
if (specifier === '@/api/index.js') return synthetic('api-index', {
...api, deviceApi: {}, hasActiveOrPendingPackage: async () => true
packageSilenceApi: {}, ...api, deviceApi: {}, hasActiveOrPendingPackage: async () => true
});
if (specifier === '@/utils/payment.js') return synthetic('payment', {
...payment, consumePendingPaymentRefresh: (...args) => payment.consumePendingPaymentRefresh(...args)
@@ -934,7 +934,7 @@ test('order completion and payment return reread actual asset state, not a presu
assert.equal(reads, 4);
});
test('home shows the silence reminder after other popups, once per day, and opens the extension page', async () => {
test('home shows the silence reminder after other popups, once per login, and opens the extension page', async () => {
const r = runtime();
r.api.assetApi = {
getInfo: async () => ({ identifier: 'A' }),
@@ -944,20 +944,31 @@ test('home shows the silence reminder after other popups, once per day, and open
children: []
}] })
};
let pendingOrderNo = '';
r.api.packageSilenceApi = { getEligibility: async () => ({ can_extend: !pendingOrderNo, pending_order_no: pendingOrderNo }) };
r.api.notificationApi = { getUnreadCount: async () => ({ count: 0 }), getList: async () => ({ items: [] }) };
const page = await r.load('pages/index/index.vue', ['silenceReminder', 'silenceReminderShow', 'onPopupCandidateClose', 'openSilenceReminderAction', 'closeSilenceReminder']);
await r.shown[0](); await r.tick(); await r.tick();
const settle = async () => { for (let i = 0; i < 6; i++) await r.tick(); };
await r.shown[0](); await settle();
// 运营弹窗仍在时不弹沉默期提醒
assert.equal(page.silenceReminderShow.value, false);
page.onPopupCandidateClose('empty'); await r.tick();
page.onPopupCandidateClose('empty'); await settle();
assert.equal(page.silenceReminderShow.value, true);
assert.equal(page.silenceReminder.value.kind, 'expired');
assert.equal(page.silenceReminder.value.pendingOrderNo, undefined);
page.openSilenceReminderAction();
assert.equal(page.silenceReminderShow.value, false);
assert.equal(r.calls.at(-1).kind, 'navigateTo');
assert.equal(r.calls.at(-1).url, '/pages/silence-extension/silence-extension?package_usage_id=44961');
// 同一天再次回到首页不再弹
r.hidden[0](); await r.shown[0](); await r.tick(); await r.tick();
page.onPopupCandidateClose('empty'); await r.tick();
// 同一次登录内再回到首页不重复弹
r.hidden[0](); await r.shown[0](); await settle();
page.onPopupCandidateClose('empty'); await settle();
assert.equal(page.silenceReminderShow.value, false);
// 重新登录(凭证变化)后再次提醒;有未支付延期订单时提示去支付或作废
pendingOrderNo = 'SE20261107200004975908';
r.storage.set('token', 'session-B');
r.hidden[0](); await r.shown[0](); await settle();
page.onPopupCandidateClose('empty'); await settle();
assert.equal(page.silenceReminderShow.value, true);
assert.equal(page.silenceReminder.value.pendingOrderNo, 'SE20261107200004975908');
});

View File

@@ -90,5 +90,8 @@ test('首页沉默期提醒:优先已过期可延期,其次 7 天内到期
assert.equal(waiting.remainingDays, 3);
assert.equal(pickSilenceReminder([node(7, { silence_status: 'ended' })], now), null);
assert.equal(silenceReminderKey('A', { packageUsageId: 3, kind: 'expired' }, '2026-11-07'), 'silence_reminder:A:3:expired:2026-11-07');
const keyA = silenceReminderKey('A', { packageUsageId: 3, kind: 'expired' }, 'token-1');
assert.match(keyA, /^silence_reminder:A:3:expired:/);
assert.equal(keyA, silenceReminderKey('A', { packageUsageId: 3, kind: 'expired' }, 'token-1'));
assert.notEqual(keyA, silenceReminderKey('A', { packageUsageId: 3, kind: 'expired' }, 'token-2'));
});

View File

@@ -164,6 +164,18 @@ export const pickSilenceReminder = (items, now = Date.now()) => {
return null;
};
/** 每个资产、每笔套餐、每种提醒每天最多弹一次的存储键 */
export const silenceReminderKey = (identifier, reminder, day) =>
`silence_reminder:${identifier}:${reminder.packageUsageId}:${reminder.kind}:${day}`;
const shortHash = (value) => {
let hash = 0;
const text = String(value || '');
for (let index = 0; index < text.length; index++) {
hash = (hash * 31 + text.charCodeAt(index)) | 0;
}
return (hash >>> 0).toString(36);
};
/**
* 每次登录、每个资产、每笔套餐、每种提醒最多弹一次的存储键:
* 带上登录凭证摘要,重新登录后会再次提醒;同一次登录内反复回首页不重复弹。
*/
export const silenceReminderKey = (identifier, reminder, sessionToken) =>
`silence_reminder:${identifier}:${reminder.packageUsageId}:${reminder.kind}:${shortHash(sessionToken)}`;