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
This commit is contained in:
jingyang
2026-05-21 11:29:33 +08:00
committed by GitHub
parent 509f6b7848
commit ed6556e008
3 changed files with 171 additions and 77 deletions
@@ -18,6 +18,52 @@ export type CronJobStatus = {
commit: () => Promise<void>;
};
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<string, { status: TransactionStatus; regionUid: string }[]>
>((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);
+59 -16
View File
@@ -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<string, string>;
}) => {
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<string, string>;
}) => !!((pathname && pathname !== '/') || raw || (query && Object.keys(query).length > 0));
export class AppInfo {
pid: number;
isShow: boolean;
@@ -167,6 +206,21 @@ const useAppStore = create<TOSState>()(
// 未支持多实例
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<TOSState>()(
cacheSize: appSize
});
} else {
if (shouldNavigateRunningApp) {
get().updateOpenedAppInfo(alreadyApp);
}
get().switchAppById(alreadyApp.pid);
}
get().setToHighestLayerById(alreadyApp.pid);
return;
}
@@ -198,22 +256,7 @@ const useAppStore = create<TOSState>()(
_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) => {
@@ -189,20 +189,28 @@ export function PlanHeader({ children, onRenewSuccess }: PlanHeaderProps) {
</div>
</div>
{children?.({
trigger: (
<Tooltip open={canManagePayment ? false : undefined}>
<TooltipTrigger asChild>
<Button size="lg" variant="outline" disabled={!canManagePayment}>
{canManagePayment ? (
children?.({
trigger: (
<Button size="lg" variant="outline">
<span>{inDebt ? t('common:renew') : t('common:subscribe_plan')}</span>
</Button>
)
})
) : (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<Button size="lg" variant="outline" disabled>
<span>{inDebt ? t('common:renew') : t('common:subscribe_plan')}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t('common:can_not_manage_payments')}</p>
</TooltipContent>
</Tooltip>
)
})}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{t('common:can_not_manage_payments')}</p>
</TooltipContent>
</Tooltip>
)}
</div>
</div>
);