front/src/utils/wechatOAuth.ts
zzh 99088ad461
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m27s
fix: H5 微信 OAuth 从 wechat_mp 读取 AppId 与回调根
去掉写死 AppId,跳转前拉取 /wechat/mp/oauth-config,与公众号网页授权域名对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 09:17:14 +08:00

151 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 微信公众号 H5 网页授权(注册 / 绑定页无 code 时跳转) */
import { getWeChatOAuthConfig } from '@/api/wechat';
import {
clearStashedInviteRef,
readStashedInviteRef,
stashInviteRef,
} from '@/utils/inviteRefStorage';
export type WeChatH5Page = 'register' | 'bind' | 'login';
type WeChatOAuthRuntimeConfig = {
appId: string;
/** 规范化后的 origin,如 https://ideship.com;空则用当前页 */
redirectOrigin: string;
};
let oauthConfigPromise: Promise<WeChatOAuthRuntimeConfig> | null = null;
function normalizeRedirectOrigin(base: string | null | undefined): string {
const raw = String(base ?? '').trim();
if (!raw) return '';
try {
const url = new URL(raw.includes('://') ? raw : `https://${raw}`);
return url.origin;
} catch {
return '';
}
}
/** 从其他配置 wechat_mp 取 AppId / OAuth 回调根(进程内缓存) */
export async function resolveWeChatMpOAuthConfig(): Promise<WeChatOAuthRuntimeConfig> {
if (!oauthConfigPromise) {
oauthConfigPromise = getWeChatOAuthConfig()
.then((cfg) => {
const appId = String(cfg.appId ?? '').trim();
if (!appId) {
throw new Error('未配置微信公众号 AppId(其他配置 wechat_mp.AppKey)');
}
return {
appId,
redirectOrigin: normalizeRedirectOrigin(cfg.oauthRedirectBase),
};
})
.catch((err) => {
oauthConfigPromise = null;
throw err;
});
}
return oauthConfigPromise;
}
/** @deprecated 使用 stashInviteRef */
export function stashWeChatInviteRef(inviteCode?: string | null): void {
stashInviteRef(inviteCode);
}
/** @deprecated 使用 readStashedInviteRef */
export function readStashedWeChatInviteRef(): string {
return readStashedInviteRef();
}
/** @deprecated 使用 clearStashedInviteRef */
export function clearStashedWeChatInviteRef(): void {
clearStashedInviteRef();
}
/** 从 OAuth state 解析邀请码(格式 register:CODE) */
export function parseInviteRefFromOAuthState(state: unknown): string {
const raw = String(state ?? '').trim();
const match = raw.match(/^(?:register|bind):(.+)$/i);
if (!match?.[1]) return '';
try {
return decodeURIComponent(match[1]).trim();
} catch {
return match[1].trim();
}
}
/**
* 解析当前页邀请码:query.ref / query.invite → OAuth state → 7 天暂存
*/
export function resolveWeChatInviteRef(query: Record<string, unknown>): string {
const fromQuery = String(query.ref ?? query.invite ?? '').trim();
if (fromQuery) {
stashInviteRef(fromQuery);
return fromQuery;
}
const fromState = parseInviteRefFromOAuthState(query.state);
if (fromState) {
stashInviteRef(fromState);
return fromState;
}
return readStashedInviteRef();
}
function resolveOAuthRedirectPath(page: WeChatH5Page, redirectOrigin: string): string {
const origin = redirectOrigin || window.location.origin;
if (page === 'login') {
return `${origin}/h5/login`;
}
return `${origin}/wechat/${page}`;
}
/**
* 跳转微信 OAuth。
* - AppId / 回调根来自其他配置 wechat_mp(AppKey、BaseUrl)
* - register/bind:邀请码写入 state + 7 天暂存
* - login:可选把登录后回跳路径写入 state(login:/h5/orders)
*/
export async function redirectToWeChatOAuth(
page: WeChatH5Page,
inviteCode?: string | null,
options?: { afterLoginPath?: string | null },
): Promise<void> {
const { appId, redirectOrigin } = await resolveWeChatMpOAuthConfig();
const redirectUri = encodeURIComponent(resolveOAuthRedirectPath(page, redirectOrigin));
let state = page;
if (page === 'login') {
const after = String(options?.afterLoginPath ?? '').trim();
if (after.startsWith('/h5')) {
state = `login:${encodeURIComponent(after)}`;
}
} else {
const code = String(inviteCode ?? '').trim() || readStashedInviteRef();
if (code) stashInviteRef(code);
state = code ? `${page}:${encodeURIComponent(code)}` : page;
}
window.location.replace(
`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}` +
`&redirect_uri=${redirectUri}` +
`&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`,
);
}
/** 从 OAuth state 解析登录后回跳路径 */
export function parseAfterLoginPathFromOAuthState(state: unknown): string {
const raw = String(state ?? '').trim();
const match = raw.match(/^login:(.+)$/i);
if (!match?.[1]) return '';
try {
const path = decodeURIComponent(match[1]).trim();
return path.startsWith('/h5') ? path : '';
} catch {
const path = match[1].trim();
return path.startsWith('/h5') ? path : '';
}
}