2 Commits

Author SHA1 Message Date
luo
e084cdeb90 fix: some
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m14s
2026-10-26 15:00:05 +08:00
luo
b5f89b744c ci: switch production deployment to iteration/october
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m3s
2026-10-26 14:29:18 +08:00
7 changed files with 322 additions and 143 deletions

View File

@@ -3,6 +3,7 @@ name: 构建并部署前端到生产环境
on:
push:
branches:
- iteration/october
- dev
- test
@@ -27,7 +28,7 @@ jobs:
- name: 设置镜像标签
id: tag
run: |
if [ "${{ github.ref }}" = "refs/heads/iteration/august" ]; then
if [ "${{ github.ref }}" = "refs/heads/iteration/october" ]; then
echo "tag=latest" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
echo "tag=dev" >> $GITHUB_OUTPUT
@@ -51,8 +52,8 @@ jobs:
docker push ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
docker push ${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: 部署到本地(仅 iteration/august 分支)
if: github.ref == 'refs/heads/iteration/august'
- name: 部署到本地(仅 iteration/october 分支)
if: github.ref == 'refs/heads/iteration/october'
run: |
mkdir -p ${{ env.DEPLOY_DIR }}

View File

@@ -3,24 +3,30 @@ import { useAssetStore } from '@/store/asset.js';
import { useUserStore } from '@/store/user.js';
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
export const normalizeAssetInfo = (data = {}) => ({
...data,
allowed_payment_methods: Array.isArray(data.allowed_payment_methods)
? data.allowed_payment_methods
: [],
real_name_status: data.real_name_status === undefined || data.real_name_status === null
? data.real_name_status
: Number(data.real_name_status),
realname_required: data.realname_required === undefined || data.realname_required === null
? data.realname_required
: data.realname_required === true || data.realname_required === 1,
days_until_final_expiry: data.days_until_final_expiry === null || data.days_until_final_expiry === undefined
? data.days_until_final_expiry
: Number(data.days_until_final_expiry),
is_expiring: data.is_expiring === undefined || data.is_expiring === null
? data.is_expiring
: data.is_expiring === true || data.is_expiring === 1
});
let assetInfoRequestId = 0;
export const normalizeAssetInfo = (data = {}) => {
// 保护期已下线:不向展示和业务判断透传废弃字段,兼容 none 或字段缺失。
const { device_protect_status, ...info } = data;
return {
...info,
allowed_payment_methods: Array.isArray(data.allowed_payment_methods)
? data.allowed_payment_methods
: [],
real_name_status: data.real_name_status === undefined || data.real_name_status === null
? data.real_name_status
: Number(data.real_name_status),
realname_required: data.realname_required === undefined || data.realname_required === null
? data.realname_required
: data.realname_required === true || data.realname_required === 1,
days_until_final_expiry: data.days_until_final_expiry === null || data.days_until_final_expiry === undefined
? data.days_until_final_expiry
: Number(data.days_until_final_expiry),
is_expiring: data.is_expiring === undefined || data.is_expiring === null
? data.is_expiring
: data.is_expiring === true || data.is_expiring === 1
};
};
const normalizePackageHistoryNode = (node = {}) => ({
...node,
@@ -69,6 +75,7 @@ export const assetApi = {
return Promise.reject({ msg: '登录和微信配置尚未完成,请稍后重试' });
}
const phoneRevision = userStore.state.phoneRevision;
const requestId = identifier === context.identifier ? ++assetInfoRequestId : null;
return request({
url: '/api/c/v1/asset/info',
method: 'GET',
@@ -76,7 +83,10 @@ export const assetApi = {
}).then((response) => {
const data = normalizeAssetInfo(response);
if (identifier === context.identifier && isCurrentSession(context)) {
useAssetStore().receiveExpiryReminder(context, data.expiry_reminder);
const assetStore = useAssetStore();
// 替换整个快照,不合并旧轮询开关;迟到的旧请求不得覆盖新状态。
if (requestId === assetInfoRequestId) assetStore.setAssetInfo(data);
assetStore.receiveExpiryReminder(context, data.expiry_reminder);
if (phoneRevision === userStore.state.phoneRevision && typeof data.bound_phone === 'string') {
userStore.setCustomerPhone(data.bound_phone);
}

View File

@@ -40,32 +40,23 @@
<script setup>
import {
ref,
computed,
watch
computed
} from 'vue';
import {
assetApi
} from '@/api/index.js';
import {
useUserStore
} from '@/store/index.js';
const props = defineProps({
identifier: {
type: String,
default: ''
assetInfo: {
type: Object,
default: null
}
});
const emit = defineEmits(['packageLoaded']);
const userStore = useUserStore();
// 流量数据
const usedMB = ref(0);
const totalMB = ref(0);
const remainMB = ref(0);
// 和首页共用最新资产快照,不保留只在首次进入时加载的套餐缓存。
const metrics = computed(() => props.assetInfo?.current_package_usage_id
? getTrafficMetrics(props.assetInfo)
: { used: 0, total: 0, remain: 0 });
const usedMB = computed(() => metrics.value.used);
const totalMB = computed(() => metrics.value.total);
const remainMB = computed(() => metrics.value.remain);
const toMB = (value) => Number(value) || 0;
@@ -125,40 +116,6 @@
};
};
// 加载套餐流量数据
const loadPackageData = async () => {
if (userStore.state.loginPending || !userStore.state.token) return;
const identifier = props.identifier || userStore.state.identifier;
if (!identifier) return;
try {
const data = await assetApi.getInfo(identifier);
if (data.current_package_usage_id) {
const metrics = getTrafficMetrics(data);
usedMB.value = metrics.used;
totalMB.value = metrics.total;
remainMB.value = metrics.remain;
emit('packageLoaded', data);
} else {
usedMB.value = 0;
totalMB.value = 0;
remainMB.value = 0;
emit('packageLoaded', null);
}
} catch (e) {
console.error('加载套餐流量数据失败', e);
}
};
// 监听 identifier 变化
watch(() => [props.identifier || userStore.state.identifier, userStore.state.token, userStore.state.loginPending], ([newVal, token, loginPending]) => {
if (newVal && token && !loginPending) {
loadPackageData();
}
}, {
immediate: true
});
</script>
<style scoped lang="scss">

View File

@@ -10,7 +10,7 @@
<!-- <VoiceCard v-if="!userInfo.isDevice" :voiceStats="voiceStats" :dateRangeText="dateRangeText"
@openDatePicker="openDateRangePicker" /> -->
<TrafficCard :identifier="userStore.state.identifier" />
<TrafficCard :assetInfo="assetStore.state.assetInfo" />
<!-- <WhitelistCard v-if="!userInfo.isDevice" :whitelistData="whitelistData" @refresh="refreshWhitelist"
@add="showAddWhitelistDialog" @showSms="showSmsCodeDialog" /> -->
@@ -205,6 +205,8 @@
let recoverShow = ref(false);
let logoutShow = ref(false);
let loading = ref(false);
let syncing = ref(false);
let assetRequestId = 0;
let userInfo = reactive({
isDevice: true,
@@ -284,7 +286,6 @@
let showSmsCode = ref(false);
let smsCodePhone = ref('');
let smsCode = ref('');
let indexEntryChecking = ref(false);
let notificationUnreadCount = ref(0);
let notificationPopupShow = ref(false);
let notificationItems = ref([]);
@@ -309,11 +310,12 @@
if (!identifier) return;
if (userStore.state.identifier !== identifier) userStore.setIdentifier(identifier);
const context = getSessionContext();
const requestId = ++assetRequestId;
loading.value = true;
try {
const data = await assetApi.getInfo(identifier);
if (!isCurrentSession(context)) return null;
if (!isCurrentSession(context) || requestId !== assetRequestId) return null;
// 基本信息
userInfo.isDevice = data.asset_type === 'device';
@@ -338,7 +340,7 @@
effectiveRealnamePolicy.value = data.effective_realname_policy || 'none';
userStore.setRealNameStatus(data.real_name_status);
// 流量信息已由 TrafficCard 组件独立获取,这里不再处理
// 流量卡片消费同一次 asset/info 快照,支付和有效期变化后一起更新。
// 当前卡信息
if (data.cards && data.cards.length > 0) {
@@ -397,7 +399,7 @@
console.error('加载资产信息失败', e);
return null;
} finally {
loading.value = false;
if (requestId === assetRequestId) loading.value = false;
}
};
@@ -415,14 +417,7 @@
}
if (userStore.state.identifier !== identifier) userStore.setIdentifier(identifier);
if (indexEntryChecking.value) return;
indexEntryChecking.value = true;
try {
await loadAssetInfo();
} finally {
indexEntryChecking.value = false;
}
await loadAssetInfo();
};
const onRenewalVisibilityChange = (visible) => {
@@ -738,6 +733,7 @@
};
const onSync = async () => {
if (syncing.value) return;
const identifier = userStore.state.identifier;
if (!identifier) {
uni.showToast({
@@ -747,11 +743,13 @@
return;
}
const context = getSessionContext();
uni.showModal({
title: '确认同步',
content: '确定要同步运营商数据吗?',
content: '确定要同步运营商数据吗?受理不代表同步或停复机已执行完成,结果以稍后读取的实际状态为准。',
success: (res) => {
if (!res.confirm) return;
if (!res.confirm || syncing.value || !isCurrentSession(context)) return;
syncing.value = true;
uni.showLoading({
title: '同步中...',
mask: true
@@ -759,17 +757,19 @@
assetApi.refresh(identifier).then((data) => {
uni.hideLoading();
if (!isCurrentSession(context)) return;
if (data.accepted) {
if (data?.accepted === true || data?.accepted === 1) {
const refreshTypeText = data.refresh_type === 'device' ? '设备' : '卡';
uni.showToast({
title: `${refreshTypeText}数据同步成功`,
icon: 'success',
duration: 2000
title: `${refreshTypeText}同步请求已受理,请稍后查看状态`,
icon: 'none',
duration: 3000
});
loadAssetInfo();
setTimeout(() => {
loadAssetInfo();
if (homeActive.value && isCurrentSession(context)) loadAssetInfo();
}, 2000);
} else {
uni.showToast({
@@ -779,6 +779,7 @@
}
}).catch((e) => {
uni.hideLoading();
if (!isCurrentSession(context)) return;
console.error('同步数据失败', e);
if (e.msg && e.msg.includes('冷却')) {
@@ -793,7 +794,7 @@
icon: 'none'
});
}
});
}).finally(() => { syncing.value = false; });
}
});
};
@@ -1018,9 +1019,8 @@
handleIndexEntry();
return;
}
if (consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.HOME)) {
loadAssetInfo();
}
// 每次返回首页都重新读取实际状态,支付返回不再重复发起两次请求。
consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.HOME);
loadNotificationUnreadCount();
runPopupCandidate();
await handleIndexEntry();
@@ -1030,6 +1030,7 @@
});
onHide(() => {
homeEntryId++;
assetRequestId++;
homeActive.value = false;
shownExpiryReminder.value = null;
notificationPopupShow.value = false;

View File

@@ -67,14 +67,14 @@
</view>
<RenewalPaymentPopup ref="renewalPopupRef" :identifier="userStore.state.identifier"
paymentRefreshTarget="order-list" @completed="resetOrderListAndLoad" />
paymentRefreshTarget="order-list" @completed="refreshOrderState" />
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { ref, reactive, computed } from 'vue';
import { onReachBottom, onShow } from '@dcloudio/uni-app';
import { orderApi } from '@/api/index.js';
import { assetApi, orderApi } from '@/api/index.js';
import RenewalPaymentPopup from '@/components/RenewalPaymentPopup.vue';
import { useUserStore } from '@/store/index.js';
import {
@@ -121,17 +121,25 @@
loadOrderList();
};
const refreshOrderState = () => {
resetOrderListAndLoad();
// 订单支付、退款及套餐生效后,读取后端实际轮询状态,不推测已恢复。
return assetApi.getInfo(userStore.state.identifier).catch(error => {
console.error('刷新订单关联资产状态失败', error);
});
};
const scheduleOrderStatusRefresh = () => {
uni.showToast({
title: '正在同步支付状态',
icon: 'none'
});
resetOrderListAndLoad();
refreshOrderState();
setTimeout(() => {
resetOrderListAndLoad();
}, 1500);
setTimeout(() => {
resetOrderListAndLoad();
refreshOrderState();
}, 3000);
};
@@ -232,16 +240,13 @@
onShow(() => {
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.ORDER_LIST)) {
refreshOrderState();
return;
}
scheduleOrderStatusRefresh();
});
onMounted(() => {
loadOrderList();
});
onReachBottom(() => {
loadMore();
});

View File

@@ -102,7 +102,7 @@
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { ref, reactive, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { assetApi, isAssetRealNameCompleted, orderApi, walletApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js';
@@ -121,6 +121,7 @@
import { getPaymentErrorMessage, isUnknownPaymentResult } from '@/utils/payment-attempt.js';
import { formatMoney } from '@/utils/display.js';
import { getDefaultPaymentMethod, getPaymentMethodOptions, normalizePaymentMethods } from '@/utils/payment-methods.js';
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
const userStore = useUserStore();
const popupCandidateShow = ref(false);
@@ -147,7 +148,7 @@
{ label: '正式套餐', value: 'formal' },
{ label: '加油包', value: 'addon' }
];
let assetTypePromise = null;
let assetRequestId = 0;
let packageRequestId = 0;
const paymentMethodOptions = computed(() => getPaymentMethodOptions(allowedPaymentMethods.value));
@@ -177,22 +178,19 @@
const loadAssetType = async () => {
if (!userStore.state.identifier) return;
if (assetTypePromise) return assetTypePromise;
assetTypePromise = assetApi.getInfo(userStore.state.identifier)
.then((data) => {
assetInfo.value = data;
allowedPaymentMethods.value = normalizePaymentMethods(data.allowed_payment_methods);
normalizePaymentMethod();
})
.catch((error) => {
console.error('加载资产类型失败', error);
})
.finally(() => {
assetTypePromise = null;
});
return assetTypePromise;
const context = getSessionContext();
const requestId = ++assetRequestId;
try {
const data = await assetApi.getInfo(userStore.state.identifier);
if (!isCurrentSession(context) || requestId !== assetRequestId) return null;
assetInfo.value = data;
allowedPaymentMethods.value = normalizePaymentMethods(data.allowed_payment_methods);
normalizePaymentMethod();
return data;
} catch (error) {
console.error('加载资产类型失败', error);
return null;
}
};
const loadPackages = async (packageType = activePackageType.value) => {
@@ -261,12 +259,21 @@
};
const syncPackagePageState = async () => {
await loadAssetType();
if (!await loadAssetType()) return;
await Promise.all([loadPackages(), loadWalletBalance()]);
};
const refreshAfterPayment = async () => {
const context = getSessionContext();
await syncPackagePageState();
// 充值自动购包可能稍后生效,再确认一次,仍以实际响应为准。
setTimeout(() => {
if (isCurrentSession(context)) syncPackagePageState();
}, 1500);
};
const buyPackage = async (item) => {
await loadAssetType();
if (!await loadAssetType()) return;
if (assetInfo.value.effective_realname_policy === 'before_order' &&
assetInfo.value.realname_required && !isAssetRealNameCompleted(assetInfo.value)) {
uni.showModal({
@@ -309,9 +316,7 @@
} else {
showPaymentToast(true, isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功');
}
setTimeout(() => {
loadWalletBalance();
}, 1500);
await refreshAfterPayment();
} catch (error) {
handlePaymentError(error);
}
@@ -464,9 +469,7 @@
} catch (error) {
showPaymentToast(false, '支付结果确认失败,请稍后查看订单');
}
setTimeout(() => {
loadWalletBalance();
}, 1500);
await refreshAfterPayment();
return;
}
@@ -505,11 +508,12 @@
};
onShow(() => {
// 后台也可能修改套餐有效期,返回页面时始终重读,不只处理支付宝返回。
syncPackagePageState();
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.PACKAGE_ORDER)) {
return;
}
syncPackagePageState();
uni.showModal({
title: '支付状态确认',
content: '已返回页面,支付结果以订单状态为准。是否前往我的订单查看最新状态?',
@@ -539,9 +543,6 @@
popupCandidateShow.value = true;
});
onMounted(() => {
syncPackagePageState();
});
</script>
<style lang="scss" scoped>

View File

@@ -29,6 +29,13 @@ function runtime(initial = {}) {
const timers = [];
const sessionStorage = new Map();
const api = {};
const props = vue.reactive({});
const payment = {
consumePendingPaymentRefresh: () => false,
PAYMENT_REFRESH_TARGETS: { HOME: 'home', PACKAGE_ORDER: 'package-order', ORDER_LIST: 'order-list', MY_WALLET: 'my-wallet' },
handlePaymentError() {}, isValidAlipayPaymentLink: () => false, isValidWechatPayConfig: () => false,
markPendingPaymentRefresh() {}, openAlipayPayment: async () => {}, showPaymentToast() {}, wechatH5Pay: async () => {}
};
let inWechat = false;
let sdkInit = async () => {};
let options = {};
@@ -40,6 +47,8 @@ function runtime(initial = {}) {
request: request => { requests.push(request); },
showToast: value => calls.push({ kind: 'toast', ...value }),
showModal: value => calls.push({ kind: 'modal', ...value }),
showLoading: value => calls.push({ kind: 'loading', ...value }),
hideLoading: () => calls.push({ kind: 'hideLoading' }),
reLaunch: value => calls.push({ kind: 'reLaunch', ...value }),
redirectTo: value => calls.push({ kind: 'redirectTo', ...value }),
navigateTo: value => calls.push({ kind: 'navigateTo', ...value }),
@@ -53,7 +62,7 @@ function runtime(initial = {}) {
getCurrentPages: () => [{ options }],
sessionStorage: { getItem: key => sessionStorage.get(key) || '', removeItem: key => sessionStorage.delete(key), setItem: (key, value) => sessionStorage.set(key, value) },
window: { location: { search: '', pathname: '/', hash: '', origin: 'https://example.invalid' }, history: { replaceState() {} } },
defineProps: () => ({}), defineEmits: () => () => {}
defineProps: () => props, defineEmits: () => () => {}, defineExpose() {}
});
const cache = new Map();
const synthetic = (key, values) => {
@@ -81,13 +90,14 @@ function runtime(initial = {}) {
onMounted: cb => mounted.push(cb), onUnmounted: cb => unmounted.push(cb)
});
if (specifier === '@dcloudio/uni-app') return synthetic('uni-app', {
onShow: cb => shown.push(cb), onHide: cb => hidden.push(cb), onUnload: cb => unmounted.push(cb)
onShow: cb => shown.push(cb), onHide: cb => hidden.push(cb), onUnload: cb => unmounted.push(cb),
onLoad() {}, onReachBottom() {}
});
if (specifier === '@/api/index.js') return synthetic('api-index', {
...api, deviceApi: {}, hasActiveOrPendingPackage: async () => true
});
if (specifier === '@/utils/payment.js') return synthetic('payment', {
consumePendingPaymentRefresh: () => false, PAYMENT_REFRESH_TARGETS: { HOME: 'home' }
...payment, consumePendingPaymentRefresh: (...args) => payment.consumePendingPaymentRefresh(...args)
});
if (specifier === '@/utils/wxsdk.js') return synthetic('wxsdk', {
initWxConfig: () => sdkInit(), wxScan: async () => {}, isInWechat: () => inWechat
@@ -106,7 +116,7 @@ function runtime(initial = {}) {
const reply = (request, data = {}, code = 0, statusCode = 200, msg = 'success') => {
request.success({ statusCode, data: { code, msg, data } });
};
return { storage, sessionStorage, context, calls, requests, mounted, shown, hidden, unmounted, timers, api, load, reply,
return { storage, sessionStorage, context, calls, requests, mounted, shown, hidden, unmounted, timers, api, props, payment, load, reply,
setOptions: value => { options = value; }, tick: () => vue.nextTick(),
setWechatSdk: init => { inWechat = true; sdkInit = init; } };
}
@@ -727,3 +737,197 @@ test('401 clears phone/reminder state, but late 401 cannot log out a new session
assert.equal(user.state.assetPhoneBound, null);
assert.equal(r.storage.has('customerAssetPhoneState'), false);
});
test('asset info ignores deprecated protection values and preserves actual polling states', async () => {
const r = runtime();
const { normalizeAssetInfo } = await r.load('api/modules/asset.js');
for (const protection of [undefined, 'none', 'protecting']) {
const input = { enable_polling: false, network_status: 0, cards: [{ enable_polling: false }] };
if (protection !== undefined) input.device_protect_status = protection;
const result = normalizeAssetInfo(input);
assert.equal(Object.hasOwn(result, 'device_protect_status'), false);
assert.equal(result.enable_polling, false);
assert.equal(result.network_status, 0);
assert.equal(result.cards[0].enable_polling, false);
}
assert.equal(normalizeAssetInfo({}).enable_polling, undefined);
for (const path of ['pages/index/index.vue', 'components/UserInfoCard.vue', 'components/DeviceStatusCard.vue', 'components/TrafficCard.vue']) {
assert.doesNotMatch(readFileSync(resolve(root, path), 'utf8'), /device_protect_status|保护期/);
}
});
test('fresh asset snapshots replace polling cache and late responses cannot roll it back', async () => {
const r = runtime();
const { assetApi } = await r.load('api/modules/asset.js');
const asset = (await r.load('store/asset.js')).useAssetStore();
asset.setAssetInfo({ enable_polling: false, current_package_id: 1 });
const old = assetApi.getInfo('A');
const fresh = assetApi.getInfo('A');
r.reply(r.requests[1], { enable_polling: true, current_package_id: 2, cards: [{ enable_polling: true }] });
await fresh;
r.reply(r.requests[0], { enable_polling: false, current_package_id: 1 });
await old;
assert.equal(asset.state.assetInfo.enable_polling, true);
assert.equal(asset.state.assetInfo.current_package_id, 2);
assert.equal(asset.state.assetInfo.cards[0].enable_polling, true);
const missing = assetApi.getInfo('A');
r.reply(r.requests[2], { current_package_id: 3 }); await missing;
assert.equal(asset.state.assetInfo.enable_polling, undefined);
assert.equal(asset.state.assetInfo.cards, undefined);
const late = assetApi.getInfo('A');
(await r.load('store/user.js')).useUserStore().setIdentifier('B');
r.reply(r.requests[3], { enable_polling: true }); await late;
assert.equal(asset.state.assetInfo, null);
const b = assetApi.getInfo('B');
r.reply(r.requests[4], { enable_polling: false }); await b;
assert.equal(asset.state.assetInfo.enable_polling, false);
});
test('traffic uses the same fresh asset snapshot and clears expired or missing package data', async () => {
const r = runtime();
const card = await r.load('components/TrafficCard.vue', ['usedMB', 'totalMB', 'remainMB']);
assert.equal(card.totalMB.value, 0);
r.props.assetInfo = { current_package_usage_id: 1, real_used_mb: 100, real_total_mb: 1000 };
assert.equal(card.usedMB.value, 100);
assert.equal(card.remainMB.value, 900);
r.props.assetInfo = { current_package_usage_id: 2, real_used_mb: 200, virtual_used_mb: 150, enable_virtual_data: true, real_total_mb: 2000 };
assert.equal(card.usedMB.value, 150);
assert.equal(card.totalMB.value, 2000);
assert.equal(card.remainMB.value, 1850);
r.props.assetInfo = { real_used_mb: 200, real_total_mb: 2000 };
assert.equal(card.usedMB.value, 0);
assert.equal(card.totalMB.value, 0);
r.props.assetInfo = null;
assert.equal(card.remainMB.value, 0);
assert.equal(r.requests.length, 0);
});
test('returning home reloads actual polling, network and expiry with one asset request per entry', async () => {
const r = runtime();
r.api.assetApi = (await r.load('api/modules/asset.js')).assetApi;
r.api.notificationApi = { getUnreadCount: async () => ({ count: 0 }) };
r.payment.consumePendingPaymentRefresh = () => true;
const page = await r.load('pages/index/index.vue', ['deviceInfo']);
const first = r.shown[0]();
assert.equal(r.requests.length, 1);
r.reply(r.requests[0], { enable_polling: false, network_status: 0, estimated_final_expires_at: '2026-10-26T00:00:00Z' });
await first;
const asset = (await r.load('store/asset.js')).useAssetStore();
assert.equal(asset.state.assetInfo.enable_polling, false);
r.hidden[0]();
const next = r.shown[0]();
assert.equal(r.requests.length, 2);
r.reply(r.requests[1], { enable_polling: true, network_status: 1, estimated_final_expires_at: '2026-11-26T00:00:00Z' });
await next;
assert.equal(asset.state.assetInfo.enable_polling, true);
assert.equal(page.deviceInfo.network_status, 1);
assert.equal(page.deviceInfo.expireDate, '2026-11-26');
});
test('home rejects an old asset request after a new refresh or hide-and-return', async () => {
const r = runtime();
r.api.notificationApi = {};
const old = deferred();
const fresh = deferred();
let reads = 0;
r.api.assetApi = { getInfo: () => (++reads === 1 ? old.promise : fresh.promise) };
const page = await r.load('pages/index/index.vue', ['loadAssetInfo', 'deviceInfo']);
const a = page.loadAssetInfo();
r.hidden[0]();
const b = page.loadAssetInfo();
fresh.resolve({ network_status: 0, current_package: '新套餐' }); await b;
old.resolve({ network_status: 1, current_package: '旧套餐' }); await a;
assert.equal(page.deviceInfo.network_status, 0);
assert.equal(page.deviceInfo.packageName, '新套餐');
});
test('sync acceptance is not completion and never optimistically resumes the network', async () => {
for (const accepted of [true, false, undefined, 'false']) {
const r = runtime();
r.api.notificationApi = {};
let reads = 0;
r.api.assetApi = {
refresh: async () => ({ accepted, refresh_type: 'device' }),
getInfo: async () => { reads++; return { network_status: 0 }; }
};
const page = await r.load('pages/index/index.vue', ['onSync', 'deviceInfo', 'homeActive']);
page.homeActive.value = true;
await page.onSync();
const modal = r.calls.find(call => call.kind === 'modal');
assert.match(modal.content, /受理不代表.*停复机已执行完成/);
modal.success({ confirm: true });
await new Promise(resolve => setImmediate(resolve));
const toast = r.calls.find(call => call.kind === 'toast');
assert.equal(toast.icon, 'none');
assert.doesNotMatch(toast.title, /同步成功|复机成功|停机成功/);
assert.equal(reads, accepted === true ? 1 : 0);
if (accepted === true) {
assert.match(toast.title, /已受理/);
assert.equal(page.deviceInfo.network_status, 0);
await r.timers[0]();
assert.equal(reads, 2);
(await r.load('store/user.js')).useUserStore().setIdentifier('B');
await r.timers[0]();
assert.equal(reads, 2);
}
}
});
test('package page reloads asset state on every entry, after wallet payment and after wechat payment', async () => {
for (const method of ['wallet', 'wechat']) {
const r = runtime();
let polling = false;
let reads = 0;
r.api.assetApi = {
getInfo: async () => { reads++; return { enable_polling: polling, allowed_payment_methods: [method] }; },
getPackages: async () => ({ packages: [] })
};
r.api.isAssetRealNameCompleted = () => true;
r.api.walletApi = { getDetail: async () => ({ balance: 0 }) };
r.api.orderApi = {
create: async () => ({ order: { order_id: 12, payment_status: 2 } }),
pay: async () => ({}), getDetail: async () => ({ payment_status: 2 })
};
const page = await r.load('pages/package-order/package-order.vue', ['syncPackagePageState', 'confirmPay', 'currentPackage', 'paymentMethod', 'assetInfo', 'handleWechatPay']);
await page.syncPackagePageState();
assert.equal(page.assetInfo.value.enable_polling, false);
polling = true;
if (method === 'wallet') {
page.currentPackage.value = { package_id: 1 };
page.paymentMethod.value = 'wallet';
await page.confirmPay();
} else {
await page.handleWechatPay({}, false, 12);
}
assert.equal(page.assetInfo.value.enable_polling, true);
assert.equal(reads, 2);
polling = false;
await r.timers[0]();
await new Promise(resolve => setImmediate(resolve));
assert.equal(page.assetInfo.value.enable_polling, false);
polling = true;
r.shown[0]();
await new Promise(resolve => setImmediate(resolve));
assert.equal(page.assetInfo.value.enable_polling, true);
assert.equal(reads, 4);
}
});
test('order completion and payment return reread actual asset state, not a presumed polling value', async () => {
const r = runtime();
let reads = 0;
r.api.assetApi = { getInfo: async () => { reads++; return { enable_polling: false }; } };
r.api.orderApi = { getList: async () => ({ items: [] }) };
const page = await r.load('pages/order-list/order-list.vue', ['refreshOrderState']);
await page.refreshOrderState();
assert.equal(reads, 1);
r.shown[0]();
await new Promise(resolve => setImmediate(resolve));
assert.equal(reads, 2);
r.payment.consumePendingPaymentRefresh = () => true;
r.shown[0]();
await new Promise(resolve => setImmediate(resolve));
assert.equal(reads, 3);
await r.timers[1]();
assert.equal(reads, 4);
});