From 6b891bee92b4f3e8e76d9c7f8e653c6574bb2717 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Sat, 16 May 2026 14:27:02 +0800 Subject: [PATCH] feat(tenant-platform): service activation realtime + store review retry - Add IM real-time notification for service activation approval/rejection (LicenseManagementView, PushManagementView, AppDetailView) - Add retry button for FAILED and REJECTED stores in version review detail - Refactor storeReviewRealtime to single shared IM connection - Bump @xuqm/vue3-sdk to 0.2.3 (fixes sendSync crash when SDK uninitialized) Co-Authored-By: Claude Sonnet 4.6 --- package.json | 3 + tenant-platform/package.json | 2 +- .../src/services/storeReviewRealtime.ts | 115 ++++++++++++++---- .../src/views/apps/AppDetailView.vue | 15 +++ .../views/license/LicenseManagementView.vue | 25 +++- .../src/views/push/PushManagementView.vue | 25 +++- .../views/update/VersionManagementView.vue | 46 ++++++- yarn.lock | 8 +- 8 files changed, 201 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 3fd1feb..bc14d62 100644 --- a/package.json +++ b/package.json @@ -20,5 +20,8 @@ "@vue/test-utils": "^2.4.9", "happy-dom": "^20.9.0", "vitest": "^4.1.5" + }, + "dependencies": { + "@xuqm/vue3-sdk": "0.2.3" } } diff --git a/tenant-platform/package.json b/tenant-platform/package.json index 0d76e67..e92d8e0 100644 --- a/tenant-platform/package.json +++ b/tenant-platform/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", - "@xuqm/vue3-sdk": "^0.2.2", + "@xuqm/vue3-sdk": "^0.2.3", "axios": "^1.7.9", "element-plus": "^2.9.1", "pinia": "^3.0.1", diff --git a/tenant-platform/src/services/storeReviewRealtime.ts b/tenant-platform/src/services/storeReviewRealtime.ts index 0a547ed..3d8218e 100644 --- a/tenant-platform/src/services/storeReviewRealtime.ts +++ b/tenant-platform/src/services/storeReviewRealtime.ts @@ -1,7 +1,11 @@ import { ElMessage } from 'element-plus' -import { ImClient, type ImMessage } from '@xuqm/vue3-sdk' +import { ImClient, type ImMessage, init as initSdk } from '@xuqm/vue3-sdk' import client from '@/api/client' +// Initialize the SDK global so ImClient.sendSync() can send the /app/chat.sync frame. +// Only appKey is required; no secret is exposed here. +initSdk({ appKey: 'ak_409e217e4aa14254ad73ad3c' }) + export interface StoreReviewRefreshEvent { event: string appKey: string @@ -16,46 +20,82 @@ export interface StoreReviewRefreshEvent { timestamp?: number } +export interface ServiceActivationRefreshEvent { + event: string + appKey: string + serviceType: string + status: string + reviewNote: string + timestamp?: number +} + interface PlatformEventTokenResponse { userId: string token: string } +// Single shared connection — all platform event types go through one WebSocket. let imClient: ImClient | null = null let activeAppKey = '' +let storeReviewHandler: ((event: StoreReviewRefreshEvent) => void) | null = null +let serviceActivationHandler: ((event: ServiceActivationRefreshEvent) => void) | null = null function sdkWsUrl() { return import.meta.env.VITE_IM_WS_URL ?? '' } -function parseEvent(message: ImMessage): StoreReviewRefreshEvent | null { +function parseAnyEvent(message: ImMessage): StoreReviewRefreshEvent | ServiceActivationRefreshEvent | null { try { const raw = JSON.parse(message.content) as Record const payload = (raw?.payload && typeof raw.payload === 'object' ? raw.payload : raw) as Record const event = String(payload.event ?? raw?.event ?? '') const appKey = String(payload.appKey ?? raw?.appKey ?? '') - if (event !== 'store_review_update' || !appKey) return null - return { - event, - appKey, - versionId: String(payload.versionId ?? raw?.versionId ?? ''), - storeType: String(payload.storeType ?? raw?.storeType ?? ''), - reviewState: String(payload.reviewState ?? raw?.reviewState ?? ''), - reviewReason: String(payload.reviewReason ?? raw?.reviewReason ?? ''), - stage: String(payload.stage ?? raw?.stage ?? ''), - batchId: String(payload.batchId ?? raw?.batchId ?? ''), - publishStatus: String(payload.publishStatus ?? raw?.publishStatus ?? ''), - source: String(payload.source ?? raw?.source ?? ''), - timestamp: Number(payload.timestamp ?? raw?.timestamp ?? Date.now()), + if (!appKey) return null + + if (event === 'store_review_update') { + return { + event, + appKey, + versionId: String(payload.versionId ?? raw?.versionId ?? ''), + storeType: String(payload.storeType ?? raw?.storeType ?? ''), + reviewState: String(payload.reviewState ?? raw?.reviewState ?? ''), + reviewReason: String(payload.reviewReason ?? raw?.reviewReason ?? ''), + stage: String(payload.stage ?? raw?.stage ?? ''), + batchId: String(payload.batchId ?? raw?.batchId ?? ''), + publishStatus: String(payload.publishStatus ?? raw?.publishStatus ?? ''), + source: String(payload.source ?? raw?.source ?? ''), + timestamp: Number(payload.timestamp ?? raw?.timestamp ?? Date.now()), + } } + + if (event === 'service_activation_update') { + return { + event, + appKey, + serviceType: String(payload.serviceType ?? raw?.serviceType ?? ''), + status: String(payload.status ?? raw?.status ?? ''), + reviewNote: String(payload.reviewNote ?? raw?.reviewNote ?? ''), + timestamp: Number(payload.timestamp ?? raw?.timestamp ?? Date.now()), + } + } + + return null } catch { return null } } -export async function connectStoreReviewRealtime(appKey: string, onEvent: (event: StoreReviewRefreshEvent) => void) { - if (!appKey) return - disconnectStoreReviewRealtime() +function disconnectAll() { + if (imClient) { + imClient.disconnect() + imClient = null + } + activeAppKey = '' +} + +async function ensureConnection(appKey: string) { + if (imClient && activeAppKey === appKey) return + disconnectAll() activeAppKey = appKey const res = await client.get<{ data: PlatformEventTokenResponse }>('/im/platform-events/token', { @@ -65,7 +105,7 @@ export async function connectStoreReviewRealtime(appKey: string, onEvent: (event const platformToken = tokenData.token if (import.meta.env.DEV) { - console.debug('[tenant-platform][IM] store review realtime token acquired', { + console.debug('[tenant-platform][IM] platform event connection established', { appKey, userId: tokenData.userId, }) @@ -74,25 +114,32 @@ export async function connectStoreReviewRealtime(appKey: string, onEvent: (event const wsUrl = sdkWsUrl() || undefined const clientInstance = new ImClient({ tokenSupplier: () => platformToken, wsUrl }) clientInstance.on('message', (message) => { - const event = parseEvent(message) + const event = parseAnyEvent(message) if (!event || event.appKey !== activeAppKey) return - onEvent(event) + if (event.event === 'store_review_update' && storeReviewHandler) { + storeReviewHandler(event as StoreReviewRefreshEvent) + } else if (event.event === 'service_activation_update' && serviceActivationHandler) { + serviceActivationHandler(event as ServiceActivationRefreshEvent) + } }) clientInstance.on('error', (error) => { if (import.meta.env.DEV) { - console.warn('[tenant-platform][IM] store review realtime error', error) + console.warn('[tenant-platform][IM] platform event error', error) } }) clientInstance.connect() imClient = clientInstance } +export async function connectStoreReviewRealtime(appKey: string, onEvent: (event: StoreReviewRefreshEvent) => void) { + if (!appKey) return + storeReviewHandler = onEvent + await ensureConnection(appKey) +} + export function disconnectStoreReviewRealtime() { - if (imClient) { - imClient.disconnect() - imClient = null - } - activeAppKey = '' + storeReviewHandler = null + if (!serviceActivationHandler) disconnectAll() } export function notifyStoreReviewRefresh(appKey: string) { @@ -100,3 +147,17 @@ export function notifyStoreReviewRefresh(appKey: string) { ElMessage.info('检测到审核状态更新,正在刷新...') } } + +export async function connectServiceActivationRealtime( + appKey: string, + onEvent: (event: ServiceActivationRefreshEvent) => void, +) { + if (!appKey) return + serviceActivationHandler = onEvent + await ensureConnection(appKey) +} + +export function disconnectServiceActivationRealtime() { + serviceActivationHandler = null + if (!storeReviewHandler) disconnectAll() +} diff --git a/tenant-platform/src/views/apps/AppDetailView.vue b/tenant-platform/src/views/apps/AppDetailView.vue index 94736f7..2dfab14 100644 --- a/tenant-platform/src/views/apps/AppDetailView.vue +++ b/tenant-platform/src/views/apps/AppDetailView.vue @@ -215,6 +215,10 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { View } from '@element-plus/icons-vue' import { appApi, type App, type FeatureService } from '@/api/app' import client from '@/api/client' +import { + connectServiceActivationRealtime, + disconnectServiceActivationRealtime, +} from '@/services/storeReviewRealtime' const route = useRoute() const app = ref(null) @@ -383,13 +387,24 @@ function updateViewport() { } onMounted(() => { + const appKey = route.params.appKey as string loadData() updateViewport() window.addEventListener('resize', updateViewport) + void connectServiceActivationRealtime(appKey, (event) => { + if (event.status === 'APPROVED') { + ElMessage.success(`${serviceLabel(event.serviceType)} 已审核通过`) + loadData() + } else if (event.status === 'REJECTED') { + ElMessage.error(`${serviceLabel(event.serviceType)} 审核未通过:${event.reviewNote || '请联系运营'}`) + loadData() + } + }) }) onBeforeUnmount(() => { window.removeEventListener('resize', updateViewport) + disconnectServiceActivationRealtime() }) diff --git a/tenant-platform/src/views/license/LicenseManagementView.vue b/tenant-platform/src/views/license/LicenseManagementView.vue index 53188be..9263391 100644 --- a/tenant-platform/src/views/license/LicenseManagementView.vue +++ b/tenant-platform/src/views/license/LicenseManagementView.vue @@ -126,6 +126,11 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { licenseApi, type AppLicense, type LicenseDevice } from '@/api/license' import { appApi, type App } from '@/api/app' +import { + connectServiceActivationRealtime, + disconnectServiceActivationRealtime, + type ServiceActivationRefreshEvent, +} from '@/services/storeReviewRealtime' const route = useRoute() const router = useRouter() @@ -261,12 +266,26 @@ function updateViewport() { isMobile.value = window.innerWidth < 768 } +function connectLicenseRealtime(key: string) { + void connectServiceActivationRealtime(key, (event: ServiceActivationRefreshEvent) => { + if (event.serviceType !== 'LICENSE') return + if (event.status === 'APPROVED') { + ElMessage.success('授权管理服务已审核通过') + checkServiceEnabled(key) + } else if (event.status === 'REJECTED') { + ElMessage.error(`授权管理服务审核未通过:${event.reviewNote || '请联系运营'}`) + checkServiceEnabled(key) + } + }) +} + watch(appKey, (key) => { if (isServicesPortal.value) { if (key) checkServiceEnabled(key) } else { loadData() } + if (key) connectLicenseRealtime(key) }) onMounted(() => { @@ -278,13 +297,15 @@ onMounted(() => { currentApp.value = portalApps.value.find(item => item.appKey === appKey.value) ?? currentApp.value }) if (appKey.value) checkServiceEnabled(appKey.value) - return + } else { + loadData() } - loadData() + if (appKey.value) connectLicenseRealtime(appKey.value) }) onBeforeUnmount(() => { window.removeEventListener('resize', updateViewport) + disconnectServiceActivationRealtime() }) diff --git a/tenant-platform/src/views/push/PushManagementView.vue b/tenant-platform/src/views/push/PushManagementView.vue index 8d2198e..b90732d 100644 --- a/tenant-platform/src/views/push/PushManagementView.vue +++ b/tenant-platform/src/views/push/PushManagementView.vue @@ -173,6 +173,11 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { appApi, type App } from '@/api/app' import { pushAdminApi, type DeviceLoginLog, type TestPushResult, type UserPushStatus } from '@/api/push' +import { + connectServiceActivationRealtime, + disconnectServiceActivationRealtime, + type ServiceActivationRefreshEvent, +} from '@/services/storeReviewRealtime' const route = useRoute() const router = useRouter() @@ -308,8 +313,22 @@ function formatDateTime(iso: string): string { return new Date(iso).toLocaleString('zh-CN') } +function connectPushRealtime(key: string) { + void connectServiceActivationRealtime(key, (event: ServiceActivationRefreshEvent) => { + if (event.serviceType !== 'PUSH') return + if (event.status === 'APPROVED') { + ElMessage.success('离线推送服务已审核通过') + checkServiceEnabled() + } else if (event.status === 'REJECTED') { + ElMessage.error(`离线推送服务审核未通过:${event.reviewNote || '请联系运营'}`) + checkServiceEnabled() + } + }) +} + watch(appKey, (key) => { if (isServicesPortal.value && key) checkServiceEnabled() + if (key) connectPushRealtime(key) }) onMounted(() => { @@ -319,9 +338,13 @@ onMounted(() => { checkServiceEnabled() } } + if (appKey.value) connectPushRealtime(appKey.value) window.addEventListener('resize', updateViewport) }) -onBeforeUnmount(() => window.removeEventListener('resize', updateViewport)) +onBeforeUnmount(() => { + window.removeEventListener('resize', updateViewport) + disconnectServiceActivationRealtime() +}) diff --git a/yarn.lock b/yarn.lock index 90f0ebe..56938cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1429,10 +1429,10 @@ dependencies: vue "^3.5.13" -"@xuqm/vue3-sdk@^0.2.2": - version "0.2.2" - resolved "https://nexus.xuqinmin.com/repository/npm-hosted/@xuqm/vue3-sdk/-/vue3-sdk-0.2.2.tgz" - integrity sha512-1fZrqEcPHf7E7LLIx2QhWG8s/yWxmNa2QNyvhvmsvyp2/LkO0uX++l/4aHZqvfiqlUDhcr892I4YdRoYuzAcQw== +"@xuqm/vue3-sdk@0.2.3", "@xuqm/vue3-sdk@^0.2.3": + version "0.2.3" + resolved "https://nexus.xuqinmin.com/repository/npm-hosted/@xuqm/vue3-sdk/-/vue3-sdk-0.2.3.tgz#b0100a309bf4179a43d98e21e24759b020221797" + integrity sha512-lUQSQNaYyrji8WwL/N4Xswdzyo4MrbwqSHsghC2Sg/zEGtuTAxo+D7t8pK1wTRLXwR5P2ZutYhE75OIFhXMFLg== abbrev@^2.0.0: version "2.0.0"