From ed6556e008eed2a0764c2b0b912f95018fa253d4 Mon Sep 17 00:00:00 2001 From: jingyang <72259332+zjy365@users.noreply.github.com> Date: Thu, 21 May 2026 11:29:33 +0800 Subject: [PATCH] fix(costcenter): ensure PAYG subscribe button opens plan dialog (#6973) * fix(costcenter): ensure PAYG subscribe button opens plan dialog * fix(desktop): update running apps for explicit deep links * update --- .../src/services/backend/cronjob/index.ts | 141 ++++++++++++------ frontend/desktop/src/stores/app.ts | 75 ++++++++-- .../src/components/plan/PlanHeader.tsx | 32 ++-- 3 files changed, 171 insertions(+), 77 deletions(-) diff --git a/frontend/desktop/src/services/backend/cronjob/index.ts b/frontend/desktop/src/services/backend/cronjob/index.ts index b18d9d494..db1989046 100644 --- a/frontend/desktop/src/services/backend/cronjob/index.ts +++ b/frontend/desktop/src/services/backend/cronjob/index.ts @@ -18,6 +18,52 @@ export type CronJobStatus = { commit: () => Promise; }; const TIMEOUT = 5000; +const RUNNABLE_TRANSACTION_STATUSES = [TransactionStatus.READY, TransactionStatus.RUNNING]; + +const getRunnableTransactionUidList = async () => { + const transactions = await globalPrisma.precommitTransaction.findMany({ + where: { + status: { + in: RUNNABLE_TRANSACTION_STATUSES + } + }, + select: { + uid: true + } + }); + return transactions.map(({ uid }) => uid); +}; + +const findRunnableTransactionDetail = async ({ + regionUid, + status, + transactionUidList, + updatedAt +}: { + regionUid: string; + status: TransactionStatus; + transactionUidList: string[]; + updatedAt?: { + lte: Date; + }; +}) => { + if (transactionUidList.length === 0) return null; + + return globalPrisma.transactionDetail.findFirst({ + where: { + regionUid, + status, + transactionUid: { + in: transactionUidList + }, + ...(updatedAt ? { updatedAt } : {}) + }, + orderBy: { + updatedAt: 'asc' + } + }); +}; + const getJob = ( transaction: Prisma.Result< typeof globalPrisma.precommitTransaction, @@ -40,49 +86,22 @@ export const runTransactionjob = async () => { // console.log('run transactionjob', new Date()); const regionUid = getRegionUid(); let isTimeoutTransactionDetail = false; + const runnableTransactionUidList = await getRunnableTransactionUidList(); // find task - let transactionDetail = await globalPrisma.transactionDetail.findFirst({ - where: { - regionUid, - status: TransactionStatus.READY, - precommitTransaction: { - is: { - status: { - in: [TransactionStatus.READY, TransactionStatus.RUNNING] - } - } - } - }, - include: { - precommitTransaction: true - }, - orderBy: { - updatedAt: 'asc' - } + let transactionDetail = await findRunnableTransactionDetail({ + regionUid, + status: TransactionStatus.READY, + transactionUidList: runnableTransactionUidList }); if (!transactionDetail) { - transactionDetail = await globalPrisma.transactionDetail.findFirst({ - where: { - regionUid, - // death lock - status: TransactionStatus.RUNNING, - updatedAt: { - lte: dayjs().subtract(TIMEOUT, 'ms').toDate() - }, - precommitTransaction: { - is: { - status: { - in: [TransactionStatus.READY, TransactionStatus.RUNNING] - } - } - } + transactionDetail = await findRunnableTransactionDetail({ + regionUid, + // death lock + status: TransactionStatus.RUNNING, + updatedAt: { + lte: dayjs().subtract(TIMEOUT, 'ms').toDate() }, - orderBy: { - updatedAt: 'asc' - }, - include: { - precommitTransaction: true - } + transactionUidList: runnableTransactionUidList }); isTimeoutTransactionDetail = true; } @@ -90,7 +109,12 @@ export const runTransactionjob = async () => { if (!transactionDetail) { return; } - const transaction = transactionDetail.precommitTransaction; + const transaction = await globalPrisma.precommitTransaction.findUnique({ + where: { + uid: transactionDetail.transactionUid + } + }); + if (!transaction) return; const job = getJob(transaction); if (!job) return; @@ -173,21 +197,40 @@ export const finishTransactionJob = async () => { const transactionList = await globalPrisma.precommitTransaction.findMany({ where: { status: { - in: [TransactionStatus.RUNNING, TransactionStatus.READY] + in: RUNNABLE_TRANSACTION_STATUSES } }, - include: { - transactionDetail: { - select: { - status: true, - regionUid: true - } - } + select: { + uid: true } }); + const transactionUidList = transactionList.map(({ uid }) => uid); + if (transactionUidList.length === 0) return; + + const transactionDetailList = await globalPrisma.transactionDetail.findMany({ + where: { + transactionUid: { + in: transactionUidList + } + }, + select: { + status: true, + regionUid: true, + transactionUid: true + } + }); + const transactionDetailMap = transactionDetailList.reduce< + Record + >((acc, detail) => { + acc[detail.transactionUid] = acc[detail.transactionUid] ?? []; + acc[detail.transactionUid].push(detail); + return acc; + }, {}); const needFinishTransactionList = transactionList .filter((tx) => { - const finishList = tx.transactionDetail.filter((d) => d.status === TransactionStatus.FINISH); + const finishList = (transactionDetailMap[tx.uid] ?? []).filter( + (d) => d.status === TransactionStatus.FINISH + ); return regionList.every(({ uid }) => finishList.findIndex((f) => f.regionUid === uid) >= 0); }) .map((tx) => tx.uid); diff --git a/frontend/desktop/src/stores/app.ts b/frontend/desktop/src/stores/app.ts index 3eeeb29a8..9faf187a4 100644 --- a/frontend/desktop/src/stores/app.ts +++ b/frontend/desktop/src/stores/app.ts @@ -13,6 +13,45 @@ import useSessionStore from './session'; export const BRAIN_APP_KEY = 'system-brain'; export const SESSION_RESTORE_APP_KEY = 'sealos_desktop_restore_app_key'; +const buildAppUrl = ({ + baseUrl, + pathname = '/', + raw, + query +}: { + baseUrl: string; + pathname?: string; + raw?: string; + query?: Record; +}) => { + let finalUrl = baseUrl; + + if (pathname && pathname !== '/') { + const normalizedBaseUrl = finalUrl.endsWith('/') ? finalUrl.slice(0, -1) : finalUrl; + const normalizedPath = pathname.startsWith('/') ? pathname : '/' + pathname; + finalUrl = normalizedBaseUrl + normalizedPath; + } + + if (raw) { + finalUrl += finalUrl.includes('?') ? '&' : '?'; + finalUrl += raw; + } else if (query) { + finalUrl = formatUrl(finalUrl, query); + } + + return finalUrl; +}; + +const hasExplicitNavigationTarget = ({ + pathname, + raw, + query +}: { + pathname?: string; + raw?: string; + query?: Record; +}) => !!((pathname && pathname !== '/') || raw || (query && Object.keys(query).length > 0)); + export class AppInfo { pid: number; isShow: boolean; @@ -167,6 +206,21 @@ const useAppStore = create()( // 未支持多实例 let alreadyApp = get().runningInfo.find((x) => x.key === app.key); if (alreadyApp) { + const shouldNavigateRunningApp = hasExplicitNavigationTarget({ pathname, raw, query }); + + if (shouldNavigateRunningApp) { + const baseUrl = app.data?.url || alreadyApp.data?.url; + alreadyApp = { + ...alreadyApp, + data: { + ...alreadyApp.data, + url: baseUrl + ? buildAppUrl({ baseUrl, pathname, raw, query }) + : alreadyApp.data.url + } + }; + } + if (appSize) { get().updateOpenedAppInfo({ ...alreadyApp, @@ -175,8 +229,12 @@ const useAppStore = create()( cacheSize: appSize }); } else { + if (shouldNavigateRunningApp) { + get().updateOpenedAppInfo(alreadyApp); + } get().switchAppById(alreadyApp.pid); } + get().setToHighestLayerById(alreadyApp.pid); return; } @@ -198,22 +256,7 @@ const useAppStore = create()( _app.isShow = true; if (_app.data?.url) { - let finalUrl = _app.data.url; - - if (pathname && pathname !== '/') { - const baseUrl = finalUrl.endsWith('/') ? finalUrl.slice(0, -1) : finalUrl; - const normalizedPath = pathname.startsWith('/') ? pathname : '/' + pathname; - finalUrl = baseUrl + normalizedPath; - } - - if (raw) { - finalUrl += finalUrl.includes('?') ? '&' : '?'; - finalUrl += raw; - } else if (query) { - finalUrl = formatUrl(finalUrl, query); - } - - _app.data.url = finalUrl; + _app.data.url = buildAppUrl({ baseUrl: _app.data.url, pathname, raw, query }); } set((state) => { diff --git a/frontend/providers/costcenter/src/components/plan/PlanHeader.tsx b/frontend/providers/costcenter/src/components/plan/PlanHeader.tsx index 9ce73fc94..259d38c5b 100644 --- a/frontend/providers/costcenter/src/components/plan/PlanHeader.tsx +++ b/frontend/providers/costcenter/src/components/plan/PlanHeader.tsx @@ -189,20 +189,28 @@ export function PlanHeader({ children, onRenewSuccess }: PlanHeaderProps) { - {children?.({ - trigger: ( - - - + ) + }) + ) : ( + + + + - - -

{t('common:can_not_manage_payments')}

-
-
- ) - })} + +
+ +

{t('common:can_not_manage_payments')}

+
+
+ )} );