微信SDK接通

This commit is contained in:
2026-07-01 19:56:44 +08:00
parent a1cbfc7241
commit aea1513836
38 changed files with 1610 additions and 92 deletions
+145
View File
@@ -0,0 +1,145 @@
import type { WechatLoginPlatform, WechatLoginResult } from '@dukang/shared-types';
import { getRuntimePlatform, isWechatBrowser } from './env';
import type { WeixinSdkConfig } from './types';
const OAUTH_STATE_KEY = 'dukang_wx_oauth_state';
function randomState() {
return `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
}
async function apiRequest<T>(
config: WeixinSdkConfig,
path: string,
options: RequestInit = {},
): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Client-App': config.clientApp,
...(options.headers as Record<string, string>),
};
const token = config.getAccessToken?.();
if (token) headers.Authorization = `Bearer ${token}`;
const apiBase = config.apiBase ?? '/api/v1';
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
const json = await res.json();
if (json.code !== 0) throw new Error(json.message || '请求失败');
return json.data as T;
}
/** 获取公众号 OAuth 授权跳转 URL */
export async function getWechatOAuthUrl(
config: WeixinSdkConfig,
redirectUri: string,
scope: 'snsapi_base' | 'snsapi_userinfo' = 'snsapi_userinfo',
): Promise<string> {
const state = randomState();
sessionStorage.setItem(OAUTH_STATE_KEY, state);
const qs = new URLSearchParams({ redirectUri, scope, state });
const data = await apiRequest<{ url: string }>(config, `/common/wechat/oauth-url?${qs}`);
return data.url;
}
/** 发起微信 OAuth 登录(H5 公众号内跳转授权) */
export async function startWechatOAuthLogin(config: WeixinSdkConfig, redirectUri?: string): Promise<void> {
if (!isWechatBrowser()) {
throw new Error('请在微信内打开');
}
const uri = redirectUri ?? window.location.href.split('#')[0];
const url = await getWechatOAuthUrl(config, uri);
window.location.href = url;
}
/** 小程序 wx.login 获取 code */
export function getMiniProgramLoginCode(): Promise<string> {
return new Promise((resolve, reject) => {
if (!window.wx?.login) {
reject(new Error('wx.login 不可用'));
return;
}
window.wx.login({
success: (res) => (res.code ? resolve(res.code) : reject(new Error('未获取到 code'))),
fail: (err) => reject(new Error(err.errMsg || 'wx.login 失败')),
});
});
}
/** 用 code 完成微信登录(自动识别 H5 / 小程序) */
export async function loginWithWechatCode(
config: WeixinSdkConfig,
code: string,
platform?: WechatLoginPlatform,
): Promise<WechatLoginResult> {
const resolvedPlatform = platform ?? (getRuntimePlatform() === 'mini' ? 'mini' : 'h5');
return apiRequest<WechatLoginResult>(config, '/auth/login/wechat', {
method: 'POST',
body: JSON.stringify({ code, platform: resolvedPlatform }),
});
}
/** 处理 OAuth 回调 URL 中的 code 参数并登录 */
export async function handleWechatOAuthCallback(
config: WeixinSdkConfig,
searchParams?: URLSearchParams,
): Promise<WechatLoginResult | null> {
const params = searchParams ?? new URLSearchParams(window.location.search);
const code = params.get('code');
if (!code) return null;
const state = params.get('state');
const saved = sessionStorage.getItem(OAUTH_STATE_KEY);
if (saved && state && saved !== state) {
throw new Error('OAuth state 校验失败');
}
sessionStorage.removeItem(OAUTH_STATE_KEY);
return loginWithWechatCode(config, code, 'h5');
}
/** 微信首登绑定手机号 */
export async function bindWechatPhone(
config: WeixinSdkConfig,
payload: { wxSessionKey: string; phone: string; code: string },
): Promise<WechatLoginResult> {
return apiRequest<WechatLoginResult>(config, '/auth/wechat/bind-phone', {
method: 'POST',
body: JSON.stringify(payload),
});
}
/** 获取微信手机号(小程序 getPhoneNumber 返回的 code,需后端解密) */
export async function getWechatPhoneNumber(config: WeixinSdkConfig): Promise<string> {
const platform = getRuntimePlatform();
if (platform !== 'mini') {
throw new Error('H5 环境请使用短信验证码绑定手机号');
}
const phoneCode = await new Promise<string>((resolve, reject) => {
if (!window.wx?.getPhoneNumber) {
reject(new Error('getPhoneNumber 不可用'));
return;
}
window.wx.getPhoneNumber({
success: (res) => (res.code ? resolve(res.code) : reject(new Error('未获取到手机号 code'))),
fail: (err) => reject(new Error(err.errMsg || '获取手机号失败')),
});
});
const data = await apiRequest<{ phone: string }>(config, '/common/wechat/phone-number', {
method: 'POST',
body: JSON.stringify({ code: phoneCode, platform: 'mini' }),
});
return data.phone;
}
/** 一键微信登录(自动选择 OAuth 或 wx.login */
export async function wechatLogin(config: WeixinSdkConfig): Promise<WechatLoginResult | void> {
const platform = getRuntimePlatform();
if (platform === 'mini') {
const code = await getMiniProgramLoginCode();
return loginWithWechatCode(config, code, 'mini');
}
if (platform === 'wechat-h5') {
await startWechatOAuthLogin(config);
return;
}
throw new Error('请在微信内打开以使用微信登录');
}