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,
WxLoginQrcodeResult,
WxLoginResult,
WxOAuthConfigResult,
} from './types';
export * from './types';
@ -22,7 +23,9 @@ function unwrapOrSelf<T>(body: unknown): T {
'qrCodeUrl' in record ||
'jumpUrl' in record ||
'accessToken' in record ||
'openid' in record
'openid' in record ||
'appId' in record ||
'AppId' in record
) {
return body as T;
}
@ -43,6 +46,20 @@ export async function getFollowQrcode(): Promise<WxFollowQrcodeResult> {
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 是否已关联账号(轮询用,无值时不抛错) */
export async function getWxLoginStatus(loginUuid: string): Promise<WxLoginStatusResult> {
const res = await axiosInstance.get('/rbac/auth/wx-login/status', {

View File

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

View File

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

View File

@ -31,14 +31,19 @@ const label = computed(() =>
jumping.value ? t('h5.login.oauthRedirecting') : t('h5.login.oauthLogin'),
);
function handleClick() {
async function handleClick() {
if (jumping.value || props.disabled) return;
if (!inWeChat.value) {
Message.warning(t('h5.login.openInWechat'));
return;
}
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>

View File

@ -176,7 +176,9 @@ function goRegister() {
Message.warning(t('h5.login.openInWechat'));
return;
}
redirectToWeChatOAuth('register');
void redirectToWeChatOAuth('register').catch((e: any) => {
Message.error(e?.message || t('h5.login.oauthFailed'));
});
}
function openAgreement(type: 'privacy' | 'terms') {
@ -304,7 +306,12 @@ onMounted(async () => {
if (isWeChatBrowser() && route.query.auto === '1') {
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>

View File

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

View File

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

View File

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