From 23587418c123443cc58f547a59c7c76c9bcc0304 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Mon, 3 Aug 2026 12:37:01 +0800 Subject: [PATCH] =?UTF-8?q?=E9=97=A8=E5=BA=97=E7=AB=AF=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E7=9B=B8=E6=9C=BA=E5=8A=9F=E8=83=BD=E6=8F=90=E7=A4=BA=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-shop/src/pages/HomePage.tsx | 16 +++-- packages/weixin-sdk/src/index.ts | 2 +- packages/weixin-sdk/src/scan.ts | 92 ++++++++++++++++++++++++----- 3 files changed, 91 insertions(+), 19 deletions(-) diff --git a/apps/h5-shop/src/pages/HomePage.tsx b/apps/h5-shop/src/pages/HomePage.tsx index a09ca35..66a2fab 100644 --- a/apps/h5-shop/src/pages/HomePage.tsx +++ b/apps/h5-shop/src/pages/HomePage.tsx @@ -27,7 +27,7 @@ function formatScanError(e: unknown): string { return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名,并刷新页面后重试'; } if (/offline verifying|权限验证中|接口未就绪/i.test(msg)) { - return `${msg}(可在 URL 后加 ?wxdebug=1 开启 JSSDK 调试查看详情)`; + return '微信权限校验尚未完成,请等待 1~2 秒后再次点击扫码(首次绑定微信时较常见)'; } return msg; } @@ -73,7 +73,7 @@ export default function HomePage() { }; }, [loadDashboard]); - async function runScan() { + async function runScan(opts?: { postAuthWarmup?: boolean }) { if (!isWechatEnv()) { setScanMsg('请在微信内打开门店端进行扫码核销'); return; @@ -81,8 +81,14 @@ export default function HomePage() { setScanning(true); setScanMsg(''); try { + // 授权回跳后强制重签,避免沿用带 code 的旧签名态 + if (opts?.postAuthWarmup) { + weixinSdk.reset(); + } await weixinSdk.init(); - const raw = await weixinSdk.scanQrCode(); + const raw = await weixinSdk.scanQrCode( + opts?.postAuthWarmup ? { postAuthWarmup: true } : undefined, + ); if (!raw) { void loadDashboard(); return; @@ -111,12 +117,14 @@ export default function HomePage() { } setAuthModalOpen(false); setAuthError(''); + // 先清 OAuth 参数再扫,保证 JSSDK 签名 URL 与当前页一致 stripOAuthParamsFromLocation(); setSearchParams({}, { replace: true }); const shouldScan = sessionStorage.getItem(PENDING_SCAN_KEY) === '1'; sessionStorage.removeItem(PENDING_SCAN_KEY); if (shouldScan) { - window.setTimeout(() => void runScan(), 0); + // OAuth 回跳后微信权限离线校验未完成;稍候再扫,失败可再点一次 + window.setTimeout(() => void runScan({ postAuthWarmup: true }), 400); } }) .catch((e) => { diff --git a/packages/weixin-sdk/src/index.ts b/packages/weixin-sdk/src/index.ts index 0dcbd2d..eeffa6a 100644 --- a/packages/weixin-sdk/src/index.ts +++ b/packages/weixin-sdk/src/index.ts @@ -70,7 +70,7 @@ export function createWeixinSdk(config: WeixinSdkConfig) { getPhoneNumber: () => getWechatPhoneNumber(config), getLocation: () => getWechatLocation(config), getLocationDetailed: () => getWechatLocationDetailed(config), - scanQrCode: () => scanQrCode(config), + scanQrCode: (options?: Parameters[1]) => scanQrCode(config, options), chooseImages: (options?: Parameters[1]) => chooseWechatImages(config, options), pay: (prepay: Parameters[0]) => diff --git a/packages/weixin-sdk/src/scan.ts b/packages/weixin-sdk/src/scan.ts index e14c6c3..026c0ad 100644 --- a/packages/weixin-sdk/src/scan.ts +++ b/packages/weixin-sdk/src/scan.ts @@ -1,5 +1,10 @@ import { getRuntimePlatform, isIosDevice, isWechatDevTools } from './env'; -import { ensureJssdkReady } from './jssdk'; +import { + ensureJssdkReady, + getJssdkSignUrl, + initWechatJssdk, + resetJssdkConfig, +} from './jssdk'; import type { WeixinSdkConfig } from './types'; function delay(ms: number): Promise { @@ -11,7 +16,7 @@ export function formatScanFailMessage(errMsg: string): string { const msg = errMsg.trim() || '扫码失败'; if (/cancel/i.test(msg)) return ''; - // iOS 常见:JSSDK 权限离线校验尚未完成,并非系统相机权限 + // iOS / 授权回调后常见:JSSDK 权限离线校验尚未完成,并非系统相机权限 if (/offline verifying|permission value is offline/i.test(msg)) { return '微信权限验证中,请稍候再试或刷新页面后重新扫码'; } @@ -33,6 +38,47 @@ export function formatScanFailMessage(errMsg: string): string { return msg; } +function isScanPermissionWarmupError(msg: string): boolean { + return /offline verifying|permission value is offline|权限验证中|接口未就绪|invalid signature|config:fail|signature/i.test( + msg, + ); +} + +function checkJsApi(apiName: string): Promise { + return new Promise((resolve) => { + if (!window.wx?.checkJsApi) { + resolve(false); + return; + } + window.wx.checkJsApi({ + jsApiList: [apiName], + success: (res) => resolve(!!res.checkResult?.[apiName]), + fail: () => resolve(false), + }); + }); +} + +async function ensureScanJssdk(config: WeixinSdkConfig): Promise { + const jssdkOptions = { + apiBase: config.apiBase ?? '/api/v1', + clientApp: config.clientApp, + getAccessToken: config.getAccessToken, + jsApiList: ['scanQRCode', 'checkJsApi'], + }; + + await ensureJssdkReady(jssdkOptions); + + let ready = await checkJsApi('scanQRCode'); + if (!ready) { + resetJssdkConfig(); + await initWechatJssdk({ ...jssdkOptions, url: getJssdkSignUrl() }); + ready = await checkJsApi('scanQRCode'); + } + if (!ready) { + throw new Error('微信扫码接口未授权,请刷新页面后重试'); + } +} + function invokeScanQrCode(): Promise { return new Promise((resolve, reject) => { window.wx!.scanQRCode!({ @@ -52,8 +98,16 @@ function invokeScanQrCode(): Promise { }); } -/** 调起扫码(返回二维码/条码内容,失败返回 null) */ -export async function scanQrCode(config: WeixinSdkConfig): Promise { +export type ScanQrCodeOptions = { + /** OAuth / 首次绑定后权限离线校验更慢,加长预热 */ + postAuthWarmup?: boolean; +}; + +/** 调起扫码(返回二维码/条码内容,取消返回 null) */ +export async function scanQrCode( + config: WeixinSdkConfig, + options: ScanQrCodeOptions = {}, +): Promise { const platform = getRuntimePlatform(); if (platform === 'mini' && window.wx?.scanCode) { @@ -68,32 +122,42 @@ export async function scanQrCode(config: WeixinSdkConfig): Promise 0) { - await delay(800); + if (lastError && isScanPermissionWarmupError(lastError.message)) { + resetJssdkConfig(); + await initWechatJssdk({ ...jssdkOptions, url: getJssdkSignUrl() }); + await checkJsApi('scanQRCode'); + } + await delay(700 + attempt * 500); } try { return await invokeScanQrCode(); } catch (e) { lastError = e instanceof Error ? e : new Error('扫码失败'); - const raw = lastError.message; - const retryable = /offline verifying|权限验证中|接口未就绪/i.test(raw); + const retryable = isScanPermissionWarmupError(lastError.message); if (attempt < maxAttempts - 1 && retryable) continue; throw lastError; }