227 lines
6.6 KiB
TypeScript
227 lines
6.6 KiB
TypeScript
import CryptoJS from 'crypto-js';
|
||
import { encryptedStorage } from '@/utils/storage';
|
||
|
||
export const ACTIVE_QR_URL_CACHE_KEY = 'cashier:property:active-url';
|
||
export const ACTIVE_QR_HASH_CACHE_KEY = 'cashier:property:active-url-hash';
|
||
export const PENDING_MERCHANT_URL_KEY = 'cashier:pending-merchant-url';
|
||
|
||
const decodeOption = (value?: string) => {
|
||
if (!value) return '';
|
||
try {
|
||
return decodeURIComponent(value);
|
||
} catch (error) {
|
||
return value;
|
||
}
|
||
};
|
||
|
||
export const resolveMerchantUrlFromOptions = (options: Record<string, string>) => {
|
||
return decodeOption(options.url || options.qr_url || '');
|
||
};
|
||
|
||
const readQueryParam = (raw: string, key: string) => {
|
||
const match = raw.match(new RegExp(`[?&]${key}=([^&]+)`));
|
||
return match ? decodeOption(match[1]) : '';
|
||
};
|
||
|
||
export const resolveMerchantUrlFromScanResult = (raw: string) => {
|
||
const text = (raw || '').trim();
|
||
if (!text) return '';
|
||
|
||
if (/^https?:\/\//i.test(text)) {
|
||
return text;
|
||
}
|
||
|
||
const fromUrl = readQueryParam(text, 'url') || readQueryParam(text, 'qr_url');
|
||
if (fromUrl) return fromUrl;
|
||
|
||
const fromQ = readQueryParam(text, 'q');
|
||
if (fromQ) return resolveMerchantUrlFromScanResult(fromQ);
|
||
|
||
return '';
|
||
};
|
||
|
||
export const setPendingMerchantUrl = (merchantUrl: string) => {
|
||
if (!merchantUrl) return;
|
||
encryptedStorage.set(PENDING_MERCHANT_URL_KEY, merchantUrl);
|
||
// @ts-ignore
|
||
if (uni.$globalData) {
|
||
// @ts-ignore
|
||
uni.$globalData.pendingMerchantUrl = merchantUrl;
|
||
}
|
||
};
|
||
|
||
export const getPendingMerchantUrl = () => {
|
||
// @ts-ignore
|
||
const fromMemory = uni.$globalData?.pendingMerchantUrl;
|
||
if (fromMemory) return String(fromMemory);
|
||
|
||
return encryptedStorage.get<string>(PENDING_MERCHANT_URL_KEY) || '';
|
||
};
|
||
|
||
export const clearPendingMerchantUrl = () => {
|
||
encryptedStorage.remove(PENDING_MERCHANT_URL_KEY);
|
||
// @ts-ignore
|
||
if (uni.$globalData) {
|
||
// @ts-ignore
|
||
uni.$globalData.pendingMerchantUrl = '';
|
||
}
|
||
};
|
||
|
||
const resolveMerchantUrlFromRawEntry = (raw?: string) => {
|
||
if (!raw) return '';
|
||
return resolveMerchantUrlFromScanResult(decodeOption(raw));
|
||
};
|
||
|
||
const getLaunchQuery = () => {
|
||
try {
|
||
const launchOptions = uni.getLaunchOptionsSync?.();
|
||
return (launchOptions?.query || {}) as Record<string, string>;
|
||
} catch (error) {
|
||
return {};
|
||
}
|
||
};
|
||
|
||
/**
|
||
* App.onLaunch / App.onShow:从启动参数捕获商家码。
|
||
* 支持支付宝 qrCode、微信 q、以及 url / qr_url。
|
||
*/
|
||
export const captureMerchantUrlFromLaunchOptions = (options: Record<string, any> = {}) => {
|
||
const query = (options.query || {}) as Record<string, string>;
|
||
|
||
const fromDirect = resolveMerchantUrlFromOptions(query);
|
||
if (fromDirect) {
|
||
const merchantUrl = resolveMerchantUrlFromScanResult(fromDirect) || fromDirect;
|
||
if (merchantUrl) {
|
||
setPendingMerchantUrl(merchantUrl);
|
||
return merchantUrl;
|
||
}
|
||
}
|
||
|
||
if (query.q) {
|
||
const fromQ = resolveMerchantUrlFromRawEntry(String(query.q));
|
||
if (fromQ) {
|
||
setPendingMerchantUrl(fromQ);
|
||
return fromQ;
|
||
}
|
||
}
|
||
|
||
const qrCode = query.qrCode || query.qr_code || '';
|
||
if (qrCode) {
|
||
const merchantUrl = resolveMerchantUrlFromRawEntry(String(qrCode));
|
||
if (merchantUrl) {
|
||
setPendingMerchantUrl(merchantUrl);
|
||
return merchantUrl;
|
||
}
|
||
}
|
||
|
||
return '';
|
||
};
|
||
|
||
/** 启动 path 已是收款/半屏支付页时,不必再 reLaunch */
|
||
export const isPayEntryPath = (path?: string) => {
|
||
const normalized = String(path || '')
|
||
.replace(/^\//, '')
|
||
.split('?')[0];
|
||
return normalized === 'pages/pay/pay' || normalized === 'pages/pay/checkout';
|
||
};
|
||
|
||
/** 当前栈顶已是收款相关页时,避免重复跳转 */
|
||
export const isCurrentRoutePayPage = () => {
|
||
try {
|
||
const pages = getCurrentPages();
|
||
const current = pages[pages.length - 1] as { route?: string } | undefined;
|
||
const route = String(current?.route || '');
|
||
return route === 'pages/pay/pay' || route === 'pages/pay/checkout';
|
||
} catch {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
/** 有扫码商家码且当前不是收款入口时,跳到收银台收款页 */
|
||
export const redirectToPayIfMerchantUrl = (merchantUrl: string, options: Record<string, any> = {}) => {
|
||
if (!merchantUrl) return false;
|
||
if (isPayEntryPath(options.path)) return false;
|
||
if (isCurrentRoutePayPage()) return false;
|
||
|
||
uni.reLaunch({
|
||
url: `/pages/pay/pay?url=${encodeURIComponent(merchantUrl)}`
|
||
});
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* 统一解析进页 merchantUrl:
|
||
* 1. ?url= / ?qr_url=(开发调试、内部跳转)
|
||
* 2. 微信普通二维码 options.q
|
||
* 3. 支付宝 query.qrCode(页面参数 / getLaunchOptionsSync)
|
||
* 4. App 缓存 pendingMerchantUrl
|
||
*/
|
||
export const resolveMerchantUrlFromEntry = (options: Record<string, any> = {}) => {
|
||
const direct = resolveMerchantUrlFromOptions(options as Record<string, string>);
|
||
if (direct) {
|
||
const merchantUrl = resolveMerchantUrlFromScanResult(direct);
|
||
if (merchantUrl) return merchantUrl;
|
||
}
|
||
|
||
if (options.q) {
|
||
const fromQ = resolveMerchantUrlFromRawEntry(String(options.q));
|
||
if (fromQ) return fromQ;
|
||
}
|
||
|
||
const pageQrCode = options.qrCode || options.qr_code;
|
||
if (pageQrCode) {
|
||
const fromPageQr = resolveMerchantUrlFromRawEntry(String(pageQrCode));
|
||
if (fromPageQr) return fromPageQr;
|
||
}
|
||
|
||
const launchQuery = getLaunchQuery();
|
||
const launchQrCode = launchQuery.qrCode || launchQuery.qr_code;
|
||
if (launchQrCode) {
|
||
const fromLaunch = resolveMerchantUrlFromRawEntry(String(launchQrCode));
|
||
if (fromLaunch) return fromLaunch;
|
||
}
|
||
|
||
return getPendingMerchantUrl();
|
||
};
|
||
|
||
export const getMerchantUrlHash = (merchantUrl: string) => {
|
||
return CryptoJS.SHA256(merchantUrl).toString();
|
||
};
|
||
|
||
export const getPropertyBindingCacheKeyByHash = (hash: string) => {
|
||
// Hash key keeps cache key short/stable and avoids raw URL special characters.
|
||
return `cashier:property:binding:${hash}`;
|
||
};
|
||
|
||
export const getSupplierCacheKeyByHash = (hash: string) => {
|
||
return `cashier:supplier:detail:${hash}`;
|
||
};
|
||
|
||
export const getPropertyBindingCacheKey = (merchantUrl: string) => {
|
||
return getPropertyBindingCacheKeyByHash(getMerchantUrlHash(merchantUrl));
|
||
};
|
||
|
||
export const getSupplierCacheKey = (merchantUrl: string) => {
|
||
return getSupplierCacheKeyByHash(getMerchantUrlHash(merchantUrl));
|
||
};
|
||
|
||
export const syncActiveMerchantContext = (merchantUrl: string) => {
|
||
const currentHash = getMerchantUrlHash(merchantUrl);
|
||
const previousHash = encryptedStorage.get<string>(ACTIVE_QR_HASH_CACHE_KEY) || '';
|
||
encryptedStorage.set(ACTIVE_QR_URL_CACHE_KEY, merchantUrl);
|
||
encryptedStorage.set(ACTIVE_QR_HASH_CACHE_KEY, currentHash);
|
||
|
||
return {
|
||
currentHash,
|
||
previousHash,
|
||
switched: Boolean(previousHash && previousHash !== currentHash)
|
||
};
|
||
};
|
||
|
||
export const getActiveMerchantUrl = () => encryptedStorage.get<string>(ACTIVE_QR_URL_CACHE_KEY) || '';
|
||
|
||
export const clearActiveMerchantContext = () => {
|
||
encryptedStorage.remove(ACTIVE_QR_URL_CACHE_KEY);
|
||
encryptedStorage.remove(ACTIVE_QR_HASH_CACHE_KEY);
|
||
};
|