fix: H5 微信 OAuth 从 wechat_mp 读取 AppId 与回调根
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>
This commit is contained in:
zzh 2026-09-09 09:17:14 +08:00
parent de4fee4349
commit 99088ad461
8 changed files with 106 additions and 23 deletions

View File

@ -9,6 +9,7 @@ import type {
WxFollowQrcodeResult, WxFollowQrcodeResult,
WxLoginQrcodeResult, WxLoginQrcodeResult,
WxLoginResult, WxLoginResult,
WxOAuthConfigResult,
} from './types'; } from './types';
export * from './types'; export * from './types';
@ -22,7 +23,9 @@ function unwrapOrSelf<T>(body: unknown): T {
'qrCodeUrl' in record || 'qrCodeUrl' in record ||
'jumpUrl' in record || 'jumpUrl' in record ||
'accessToken' in record || 'accessToken' in record ||
'openid' in record 'openid' in record ||
'appId' in record ||
'AppId' in record
) { ) {
return body as T; return body as T;
} }
@ -43,6 +46,20 @@ export async function getFollowQrcode(): Promise<WxFollowQrcodeResult> {
return unwrapOrSelf<WxFollowQrcodeResult>(res.data); return unwrapOrSelf<WxFollowQrcodeResult>(res.data);
} }
/** H5 网页授权:从其他配置 wechat_mp 读取 AppId / OAuth 回调根 */
export async function getWeChatOAuthConfig(): Promise<WxOAuthConfigResult> {
const res = await axiosInstance.get(omsUrl('/wechat/mp/oauth-config'));
const data = unwrapOrSelf<WxOAuthConfigResult & Record<string, unknown>>(res.data);
const appId = String(data.appId ?? data.AppId ?? '').trim();
const oauthRedirectBase = String(
data.oauthRedirectBase ?? data.OAuthRedirectBase ?? '',
).trim();
return {
appId,
oauthRedirectBase: oauthRedirectBase || null,
};
}
/** 查询扫码 key 是否已关联账号(轮询用,无值时不抛错) */ /** 查询扫码 key 是否已关联账号(轮询用,无值时不抛错) */
export async function getWxLoginStatus(loginUuid: string): Promise<WxLoginStatusResult> { export async function getWxLoginStatus(loginUuid: string): Promise<WxLoginStatusResult> {
const res = await axiosInstance.get('/rbac/auth/wx-login/status', { const res = await axiosInstance.get('/rbac/auth/wx-login/status', {

View File

@ -15,6 +15,13 @@ export interface WxFollowQrcodeResult {
scene?: string; scene?: string;
} }
/** H5 网页授权公开配置(wechat_mp) */
export interface WxOAuthConfigResult {
appId: string;
/** OAuth 回调根,如 https://ideship.com;空则用当前页 origin */
oauthRedirectBase?: string | null;
}
export interface WxLoginStatusResult { export interface WxLoginStatusResult {
ready: boolean; ready: boolean;
account?: string; account?: string;

View File

@ -1,5 +1,6 @@
/** 微信公众号 H5 网页授权(注册 / 绑定页无 code 时跳转) */ /** 微信公众号 H5 网页授权(注册 / 绑定页无 code 时跳转) */
import { getWeChatOAuthConfig } from '@/api/wechat';
import { import {
clearStashedInviteRef, clearStashedInviteRef,
readStashedInviteRef, readStashedInviteRef,
@ -8,11 +9,45 @@ import {
export type WeChatH5Page = 'register' | 'bind' | 'login'; export type WeChatH5Page = 'register' | 'bind' | 'login';
const DEFAULT_MP_APP_ID = 'wxd5141ba46c856e1f'; type WeChatOAuthRuntimeConfig = {
appId: string;
/** 规范化后的 origin,如 https://ideship.com;空则用当前页 */
redirectOrigin: string;
};
export function resolveWeChatMpAppId(): string { let oauthConfigPromise: Promise<WeChatOAuthRuntimeConfig> | null = null;
const fromEnv = import.meta.env.VITE_WECHAT_MP_APPID as string | undefined;
return fromEnv?.trim() || DEFAULT_MP_APP_ID; 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 */ /** @deprecated 使用 stashInviteRef */
@ -59,25 +94,27 @@ export function resolveWeChatInviteRef(query: Record<string, unknown>): string {
return readStashedInviteRef(); return readStashedInviteRef();
} }
function resolveOAuthRedirectPath(page: WeChatH5Page): string { function resolveOAuthRedirectPath(page: WeChatH5Page, redirectOrigin: string): string {
const origin = redirectOrigin || window.location.origin;
if (page === 'login') { if (page === 'login') {
return `${window.location.origin}/h5/login`; return `${origin}/h5/login`;
} }
return `${window.location.origin}/wechat/${page}`; return `${origin}/wechat/${page}`;
} }
/** /**
* 跳转微信 OAuth。 * 跳转微信 OAuth。
* - AppId / 回调根来自其他配置 wechat_mp(AppKey、BaseUrl)
* - register/bind:邀请码写入 state + 7 天暂存 * - register/bind:邀请码写入 state + 7 天暂存
* - login:可选把登录后回跳路径写入 state(login:/h5/orders) * - login:可选把登录后回跳路径写入 state(login:/h5/orders)
*/ */
export function redirectToWeChatOAuth( export async function redirectToWeChatOAuth(
page: WeChatH5Page, page: WeChatH5Page,
inviteCode?: string | null, inviteCode?: string | null,
options?: { afterLoginPath?: string | null }, options?: { afterLoginPath?: string | null },
): void { ): Promise<void> {
const appId = resolveWeChatMpAppId(); const { appId, redirectOrigin } = await resolveWeChatMpOAuthConfig();
const redirectUri = encodeURIComponent(resolveOAuthRedirectPath(page)); const redirectUri = encodeURIComponent(resolveOAuthRedirectPath(page, redirectOrigin));
let state = page; let state = page;
if (page === 'login') { if (page === 'login') {

View File

@ -31,14 +31,19 @@ const label = computed(() =>
jumping.value ? t('h5.login.oauthRedirecting') : t('h5.login.oauthLogin'), jumping.value ? t('h5.login.oauthRedirecting') : t('h5.login.oauthLogin'),
); );
function handleClick() { async function handleClick() {
if (jumping.value || props.disabled) return; if (jumping.value || props.disabled) return;
if (!inWeChat.value) { if (!inWeChat.value) {
Message.warning(t('h5.login.openInWechat')); Message.warning(t('h5.login.openInWechat'));
return; return;
} }
jumping.value = true; jumping.value = true;
redirectToWeChatOAuth('login', null, { afterLoginPath: props.afterLoginPath }); try {
await redirectToWeChatOAuth('login', null, { afterLoginPath: props.afterLoginPath });
} catch (e: any) {
jumping.value = false;
Message.error(e?.message || t('h5.login.oauthFailed'));
}
} }
</script> </script>

View File

@ -176,7 +176,9 @@ function goRegister() {
Message.warning(t('h5.login.openInWechat')); Message.warning(t('h5.login.openInWechat'));
return; return;
} }
redirectToWeChatOAuth('register'); void redirectToWeChatOAuth('register').catch((e: any) => {
Message.error(e?.message || t('h5.login.oauthFailed'));
});
} }
function openAgreement(type: 'privacy' | 'terms') { function openAgreement(type: 'privacy' | 'terms') {
@ -304,7 +306,12 @@ onMounted(async () => {
if (isWeChatBrowser() && route.query.auto === '1') { if (isWeChatBrowser() && route.query.auto === '1') {
oauthBusy.value = true; oauthBusy.value = true;
redirectToWeChatOAuth('login', null, { afterLoginPath: resolveRedirectPath() }); try {
await redirectToWeChatOAuth('login', null, { afterLoginPath: resolveRedirectPath() });
} catch (e: any) {
oauthBusy.value = false;
Message.error(e?.message || t('h5.login.oauthFailed'));
}
} }
}); });
</script> </script>

View File

@ -549,7 +549,9 @@ function bindWechat() {
Message.warning(t('h5.me.bindWechatOpenInWechat')); Message.warning(t('h5.me.bindWechatOpenInWechat'));
return; return;
} }
redirectToWeChatOAuth('bind'); void redirectToWeChatOAuth('bind').catch((e: any) => {
Message.error(e?.message || t('h5.me.bindWechatOpenInWechat'));
});
} }
function openOnlineKf() { function openOnlineKf() {

View File

@ -181,7 +181,9 @@ function goCustomerCenter() {
function goLogin() { function goLogin() {
if (isWeChatBrowser()) { if (isWeChatBrowser()) {
redirectToWeChatOAuth('login', null, { afterLoginPath: '/h5/me' }); void redirectToWeChatOAuth('login', null, { afterLoginPath: '/h5/me' }).catch(() => {
router.replace('/h5/login');
});
return; return;
} }
router.replace('/h5/login'); router.replace('/h5/login');
@ -189,7 +191,9 @@ function goLogin() {
function goRegister() { function goRegister() {
if (isWeChatBrowser()) { if (isWeChatBrowser()) {
redirectToWeChatOAuth('register'); void redirectToWeChatOAuth('register').catch(() => {
router.replace('/wechat/register');
});
return; return;
} }
router.replace('/wechat/register'); router.replace('/wechat/register');
@ -197,7 +201,7 @@ function goRegister() {
function startOAuth() { function startOAuth() {
authModalVisible.value = false; authModalVisible.value = false;
redirectToWeChatOAuth('bind'); void redirectToWeChatOAuth('bind');
} }
function retryOAuth() { function retryOAuth() {

View File

@ -247,7 +247,9 @@ function goCustomerCenter() {
function goLogin() { function goLogin() {
if (isWeChatBrowser()) { if (isWeChatBrowser()) {
redirectToWeChatOAuth('login', null, { afterLoginPath: '/h5/me' }); void redirectToWeChatOAuth('login', null, { afterLoginPath: '/h5/me' }).catch(() => {
router.replace('/h5/login');
});
return; return;
} }
router.replace('/h5/login'); router.replace('/h5/login');
@ -259,7 +261,9 @@ function goOAuthLogin() {
function goBind() { function goBind() {
if (isWeChatBrowser()) { if (isWeChatBrowser()) {
redirectToWeChatOAuth('bind'); void redirectToWeChatOAuth('bind').catch(() => {
router.replace('/wechat/bind');
});
return; return;
} }
router.replace('/wechat/bind'); router.replace('/wechat/bind');
@ -267,7 +271,7 @@ function goBind() {
function startOAuth() { function startOAuth() {
authModalVisible.value = false; authModalVisible.value = false;
redirectToWeChatOAuth('register', inviteCode.value); void redirectToWeChatOAuth('register', inviteCode.value);
} }
function retryOAuth() { function retryOAuth() {