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) => { 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(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; } catch (error) { return {}; } }; /** * App.onLaunch / App.onShow:从启动参数捕获商家码。 * 支持支付宝 qrCode、微信 q、以及 url / qr_url。 */ export const captureMerchantUrlFromLaunchOptions = (options: Record = {}) => { const query = (options.query || {}) as Record; 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 = {}) => { 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 = {}) => { const direct = resolveMerchantUrlFromOptions(options as Record); 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(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(ACTIVE_QR_URL_CACHE_KEY) || ''; export const clearActiveMerchantContext = () => { encryptedStorage.remove(ACTIVE_QR_URL_CACHE_KEY); encryptedStorage.remove(ACTIVE_QR_HASH_CACHE_KEY); };