All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m27s
去掉写死 AppId,跳转前拉取 /wechat/mp/oauth-config,与公众号网页授权域名对齐。 Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
/** 微信公众号 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 : '';
|
||
}
|
||
}
|