微信SDK接通
This commit is contained in:
@@ -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('请在微信内打开以使用微信登录');
|
||||
}
|
||||
Reference in New Issue
Block a user