fix(aihr): harden broadcast and mobile assistant flows

This commit is contained in:
2026-07-23 03:35:36 +08:00
parent 62c8b41672
commit 4b31848e3d
30 changed files with 1127 additions and 208 deletions
+42 -41
View File
@@ -147,6 +147,11 @@ import {
type BroadcastStatus
} from '@/api/aihr/broadcast';
import { useUserStore } from '@/store/modules/user';
import {
broadcastPendingStorageKey,
createPendingBroadcastStorage,
type PendingBroadcastPublish
} from '@/utils/broadcast-pending';
const messages = ref<BroadcastAdminMessage[]>([]);
const total = ref(0);
@@ -173,10 +178,8 @@ const rules = {
const displayTime = (value?: string) => (value ? value.replace('T', ' ').slice(0, 16) : '—');
const createRequestId = () => globalThis.crypto?.randomUUID?.() ?? `broadcast-${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;
const pendingPublishStoragePrefix = 'aihr.broadcast.pending.v2';
type PendingPublish = { tenantId: string; requestId: string; title: string; content: string };
const isValidPendingPublish = (candidate: Partial<PendingPublish>): candidate is PendingPublish =>
const isValidPendingPublish = (candidate: Partial<PendingBroadcastPublish>): candidate is PendingBroadcastPublish =>
typeof candidate.tenantId === 'string' && candidate.tenantId === scopeTenantId.value &&
candidate.tenantId === candidate.tenantId.trim() && candidate.tenantId.length <= 64 &&
typeof candidate.requestId === 'string' && candidate.requestId === candidate.requestId.trim() &&
@@ -184,42 +187,35 @@ const isValidPendingPublish = (candidate: Partial<PendingPublish>): candidate is
typeof candidate.title === 'string' && !!candidate.title.trim() && candidate.title.length <= 200 &&
typeof candidate.content === 'string' && !!candidate.content.trim() && candidate.content.length <= 10_000;
const pendingPublishUserId = () => String(userStore.userId || '').trim();
const pendingPublishStorageKey = (tenantId = pendingTenantId.value || scopeTenantId.value) =>
tenantId ? `${pendingPublishStoragePrefix}:${tenantId}:${userStore.userId || 'unknown'}` : '';
broadcastPendingStorageKey(tenantId, pendingPublishUserId());
const safelyReadStorage = (storageKey: string) => {
const pendingStorage = () => {
if (typeof window === 'undefined') throw new Error('browser storage unavailable');
let legacyLocalStorage: Storage | undefined;
try {
return storageKey ? localStorage.getItem(storageKey) : null;
legacyLocalStorage = window.localStorage;
} catch {
storageAvailable.value = false;
ElMessage.warning('浏览器暂时无法读取待确认发布记录,请保持本页面打开后重试。');
return null;
}
};
const safelyRemoveStorage = (storageKey: string) => {
try {
if (storageKey) localStorage.removeItem(storageKey);
return true;
} catch {
storageAvailable.value = false;
ElMessage.warning('浏览器暂时无法清理待确认发布记录,请勿在其他窗口重复发布。');
return false;
// A browser may deny persistent storage while still allowing session storage.
}
return createPendingBroadcastStorage({
sessionStorage: window.sessionStorage,
legacyLocalStorage
});
};
const persistPendingPublish = () => {
pendingTenantId.value = draftTenantId.value;
const storageKey = pendingPublishStorageKey();
const pending: PendingPublish = {
const pending: PendingBroadcastPublish = {
tenantId: pendingTenantId.value,
requestId: form.requestId,
title: form.title,
content: form.content
};
try {
if (!storageKey) throw new Error('missing tenant scope');
localStorage.setItem(storageKey, JSON.stringify(pending));
if (!pendingPublishStorageKey()) throw new Error('missing tenant scope');
pendingStorage().write(pendingPublishUserId(), pending);
return true;
} catch {
storageAvailable.value = false;
@@ -230,33 +226,38 @@ const persistPendingPublish = () => {
};
const clearPendingPublish = () => {
const cleared = safelyRemoveStorage(pendingPublishStorageKey());
if (cleared) pendingTenantId.value = '';
return cleared;
try {
const tenantId = pendingTenantId.value || scopeTenantId.value;
if (!pendingPublishStorageKey(tenantId)) throw new Error('missing tenant scope');
pendingStorage().remove(pendingPublishUserId(), tenantId);
pendingTenantId.value = '';
return true;
} catch {
storageAvailable.value = false;
ElMessage.warning('浏览器暂时无法清理待确认发布记录,请勿在其他窗口重复发布。');
return false;
}
};
const readPendingPublish = (): PendingPublish | undefined => {
const storageKey = pendingPublishStorageKey(scopeTenantId.value);
const stored = safelyReadStorage(storageKey);
if (!stored) return undefined;
const readPendingPublish = (): PendingBroadcastPublish | undefined => {
const tenantId = scopeTenantId.value;
try {
const candidate = JSON.parse(stored) as Partial<PendingPublish>;
if (!pendingPublishStorageKey(tenantId)) return undefined;
const candidate = pendingStorage().read(pendingPublishUserId(), tenantId);
if (!candidate) return undefined;
if (!isValidPendingPublish(candidate)) {
throw new Error('invalid pending publish');
pendingStorage().remove(pendingPublishUserId(), tenantId);
return undefined;
}
return {
tenantId: candidate.tenantId,
requestId: candidate.requestId,
title: candidate.title,
content: candidate.content
};
return candidate;
} catch {
safelyRemoveStorage(storageKey);
storageAvailable.value = false;
ElMessage.warning('浏览器暂时无法读取待确认发布记录,请保持本页面打开后重试。');
return undefined;
}
};
const applyPendingPublish = (candidate: PendingPublish) => {
const applyPendingPublish = (candidate: PendingBroadcastPublish) => {
draftTenantId.value = candidate.tenantId;
pendingTenantId.value = candidate.tenantId;
form.requestId = candidate.requestId;