fix(partner): 修正微信选图 JSSDK 签名 URL 含 query 参数

录店页 ?step= 变化时重新 wx.config,微信内上传仅走 chooseImage 接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 16:24:42 +08:00
parent 656e906254
commit 076455944e
4 changed files with 54 additions and 68 deletions
@@ -39,10 +39,6 @@ function formatWechatUploadError(e: unknown): string {
return msg; return msg;
} }
function shouldFallbackToNativePicker(msg: string): boolean {
return /permission|denied|接口未就绪|signature|JSSDK|签名校验|invalid signature|config:fail|未授权/i.test(msg);
}
function acceptsImages(accept: string) { function acceptsImages(accept: string) {
return accept.includes('image'); return accept.includes('image');
} }
@@ -93,6 +89,7 @@ export default function OssUploadField({
useEffect(() => { useEffect(() => {
if (!useWechatPicker || needsAuth) return; if (!useWechatPicker || needsAuth) return;
weixinSdk.reset();
void weixinSdk.init().catch(() => { void weixinSdk.init().catch(() => {
/* 点击上传时会再次初始化 */ /* 点击上传时会再次初始化 */
}); });
@@ -135,10 +132,6 @@ export default function OssUploadField({
} }
} }
function openNativeFilePicker() {
inputRef.current?.click();
}
async function pickWechatImage() { async function pickWechatImage() {
setUploading(true); setUploading(true);
setError(''); setError('');
@@ -150,21 +143,9 @@ export default function OssUploadField({
count: 1, count: 1,
sourceType: ['album', 'camera'], sourceType: ['album', 'camera'],
}); });
if (!files) { if (!files?.[0]) return;
openNativeFilePicker();
return;
}
if (!files[0]) return;
await uploadSelectedFile(files[0]); await uploadSelectedFile(files[0]);
}); });
} catch (e) {
const msg = e instanceof Error ? e.message : '无法打开相册';
if (/cancel/i.test(msg)) return;
if (shouldFallbackToNativePicker(msg)) {
openNativeFilePicker();
return;
}
throw e;
} finally { } finally {
setUploading(false); setUploading(false);
} }
@@ -186,16 +167,12 @@ export default function OssUploadField({
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : '无法打开相册'; const msg = e instanceof Error ? e.message : '无法打开相册';
if (/cancel/i.test(msg)) return; if (/cancel/i.test(msg)) return;
if (shouldFallbackToNativePicker(msg)) {
openNativeFilePicker();
return;
}
showUploadError(formatWechatUploadError(e)); showUploadError(formatWechatUploadError(e));
} }
return; return;
} }
openNativeFilePicker(); inputRef.current?.click();
} }
const isImage = mediaType === 'IMAGE' && value; const isImage = mediaType === 'IMAGE' && value;
@@ -225,6 +202,7 @@ export default function OssUploadField({
</button> </button>
</div> </div>
)} )}
{!useWechatPicker && (
<input <input
ref={inputRef} ref={inputRef}
type="file" type="file"
@@ -236,6 +214,7 @@ export default function OssUploadField({
if (file) void uploadSelectedFile(file); if (file) void uploadSelectedFile(file);
}} }}
/> />
)}
{isImage ? ( {isImage ? (
<button {...triggerProps} className={`partner-upload-preview${wide ? ' partner-upload-preview--wide' : ''}`}> <button {...triggerProps} className={`partner-upload-preview${wide ? ' partner-upload-preview--wide' : ''}`}>
<img src={value} alt={label ?? '已上传'} /> <img src={value} alt={label ?? '已上传'} />
+1
View File
@@ -71,6 +71,7 @@ async function reportChooseImageEvent(
sourceType: payload.sourceType, sourceType: payload.sourceType,
stage: payload.stage, stage: payload.stage,
pageUrl: typeof window !== 'undefined' ? normalizeJssdkPageUrl(window.location.href) : undefined, pageUrl: typeof window !== 'undefined' ? normalizeJssdkPageUrl(window.location.href) : undefined,
signUrl: typeof window !== 'undefined' ? getJssdkSignUrl() : undefined,
}), }),
}); });
} catch { } catch {
+31 -29
View File
@@ -1,64 +1,66 @@
import type { WechatJssdkConfig } from '@dukang/shared-types'; import type { WechatJssdkConfig } from '@dukang/shared-types';
import { isIosDevice, isWechatBrowser, isWechatDevTools } from './env'; import { isWechatBrowser, isWechatDevTools } from './env';
import { DEFAULT_JS_API_LIST } from './types'; import { DEFAULT_JS_API_LIST } from './types';
const JSSDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js'; const JSSDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
const IOS_ENTRY_URL_KEY = 'dukang_wx_ios_entry_url'; const SIGN_URL_CACHE_KEY = 'dukang_wx_sign_url_v2';
let scriptPromise: Promise<void> | null = null; let scriptPromise: Promise<void> | null = null;
let configured = false; let configured = false;
let configuredUrl: string | null = null; let configuredUrl: string | null = null;
function pathnameOf(url: string): string {
try {
return new URL(url).pathname;
} catch {
return url.split('?')[0];
}
}
/** 清除 JSSDK 配置缓存(路由切换后须重新 wx.config) */ /** 清除 JSSDK 配置缓存(路由切换后须重新 wx.config) */
export function resetJssdkConfig(): void { export function resetJssdkConfig(): void {
configured = false; configured = false;
configuredUrl = null; configuredUrl = null;
} }
/** 参与 JSSDK 签名的页面 URL(去掉 hash、query;SPA 步骤参数不参与签名) */ /** 参与 JSSDK 签名的页面 URL:与微信文档一致,取 location.href 去掉 # 后的部分;剔除 OAuth 回调参数 */
export function normalizeJssdkPageUrl(rawUrl: string): string { export function normalizeJssdkPageUrl(rawUrl: string): string {
try { try {
const url = new URL(rawUrl); const url = new URL(rawUrl);
return `${url.origin}${url.pathname}`; url.hash = '';
url.searchParams.delete('code');
url.searchParams.delete('state');
const query = url.searchParams.toString();
return `${url.origin}${url.pathname}${query ? `?${query}` : ''}`;
} catch { } catch {
return rawUrl.split('#')[0].split('?')[0]; const noHash = rawUrl.split('#')[0];
try {
const url = new URL(noHash, typeof window !== 'undefined' ? window.location.origin : 'https://localhost');
url.searchParams.delete('code');
url.searchParams.delete('state');
const query = url.searchParams.toString();
return `${url.origin}${url.pathname}${query ? `?${query}` : ''}`;
} catch {
return noHash;
}
} }
} }
/** iOS 微信内 SPA:同 pathname 内复用入口 URL;跨路由时更新入口并失效旧签名 */ function signUrlChanged(prev: string | null, current: string): boolean {
return prev !== current;
}
/** 记录最近一次签名 URL;SPA 路由或 ?step= 变化时须重新 wx.config */
export function captureIosJssdkEntryUrl(): void { export function captureIosJssdkEntryUrl(): void {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
if (!isIosDevice() || !isWechatBrowser() || isWechatDevTools()) return; if (!isWechatBrowser() || isWechatDevTools()) return;
const current = normalizeJssdkPageUrl(window.location.href); const current = normalizeJssdkPageUrl(window.location.href);
const existing = sessionStorage.getItem(IOS_ENTRY_URL_KEY); const existing = sessionStorage.getItem(SIGN_URL_CACHE_KEY);
if (!existing) { if (!existing) {
sessionStorage.setItem(IOS_ENTRY_URL_KEY, current); sessionStorage.setItem(SIGN_URL_CACHE_KEY, current);
return; return;
} }
if (pathnameOf(existing) !== pathnameOf(current)) { if (signUrlChanged(existing, current)) {
sessionStorage.setItem(IOS_ENTRY_URL_KEY, current); sessionStorage.setItem(SIGN_URL_CACHE_KEY, current);
resetJssdkConfig(); resetJssdkConfig();
} }
} }
/** 获取参与 JSSDK 签名的 URL */ /** 获取参与 JSSDK 签名的 URL(始终为当前页完整 URL,含 query) */
export function getJssdkSignUrl(rawUrl?: string): string { export function getJssdkSignUrl(rawUrl?: string): string {
const current = normalizeJssdkPageUrl(rawUrl ?? (typeof window !== 'undefined' ? window.location.href : '')); return normalizeJssdkPageUrl(rawUrl ?? (typeof window !== 'undefined' ? window.location.href : ''));
if (typeof window !== 'undefined' && isIosDevice() && isWechatBrowser() && !isWechatDevTools()) {
const entry = sessionStorage.getItem(IOS_ENTRY_URL_KEY);
if (entry && pathnameOf(entry) === pathnameOf(current)) {
return entry;
}
}
return current;
} }
function isJssdkDebugEnabled(): boolean { function isJssdkDebugEnabled(): boolean {
@@ -163,7 +165,7 @@ export async function ensureJssdkReady(options: {
}): Promise<void> { }): Promise<void> {
captureIosJssdkEntryUrl(); captureIosJssdkEntryUrl();
const pageUrl = options.url ?? getJssdkSignUrl(); const pageUrl = options.url ?? getJssdkSignUrl();
if (configuredUrl && pathnameOf(configuredUrl) !== pathnameOf(pageUrl)) { if (configuredUrl && signUrlChanged(configuredUrl, pageUrl)) {
resetJssdkConfig(); resetJssdkConfig();
} }
if (!isJssdkReady()) { if (!isJssdkReady()) {
@@ -86,9 +86,13 @@ export class WechatController {
private normalizeJssdkUrl(rawUrl: string): string { private normalizeJssdkUrl(rawUrl: string): string {
try { try {
const parsed = new URL(rawUrl); const parsed = new URL(rawUrl);
return `${parsed.origin}${parsed.pathname}`; parsed.hash = '';
parsed.searchParams.delete('code');
parsed.searchParams.delete('state');
const query = parsed.searchParams.toString();
return `${parsed.origin}${parsed.pathname}${query ? `?${query}` : ''}`;
} catch { } catch {
return rawUrl.split('#')[0].split('?')[0]; return rawUrl.split('#')[0];
} }
} }