import type { WechatLoginPlatform, WechatLoginResult } from '@dukang/shared-types'; import { getRuntimePlatform, isWechatBrowser } from './env'; import type { WeixinSdkConfig } from './types'; /** 非微信内置浏览器时提示(与 C 端 LoginPage 文案一致) */ export const WECHAT_INAPP_REQUIRED_MSG = '请在微信内打开以使用微信一键授权'; const OAUTH_STATE_KEY = 'dukang_wx_oauth_state'; function randomState() { return `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; } async function apiRequest( config: WeixinSdkConfig, path: string, options: RequestInit = {}, ): Promise { const headers: Record = { 'Content-Type': 'application/json', 'X-Client-App': config.clientApp, ...(options.headers as Record), }; 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 { 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 { if (!isWechatBrowser()) { throw new Error(WECHAT_INAPP_REQUIRED_MSG); } 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 { 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 { const resolvedPlatform = platform ?? (getRuntimePlatform() === 'mini' ? 'mini' : 'h5'); const loginPath = config.wechatLoginPath ?? '/auth/login/wechat'; return apiRequest(config, loginPath, { method: 'POST', body: JSON.stringify({ code, platform: resolvedPlatform }), }); } /** 处理 OAuth 回调 URL 中的 code 参数并登录 */ export async function handleWechatOAuthCallback( config: WeixinSdkConfig, searchParams?: URLSearchParams, ): Promise { 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 { return apiRequest(config, '/auth/wechat/bind-phone', { method: 'POST', body: JSON.stringify(payload), }); } /** 获取微信手机号(小程序 getPhoneNumber 返回的 code,需后端解密) */ export async function getWechatPhoneNumber(config: WeixinSdkConfig): Promise { const platform = getRuntimePlatform(); if (platform !== 'mini') { throw new Error('H5 环境请使用短信验证码绑定手机号'); } const phoneCode = await new Promise((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 { 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(WECHAT_INAPP_REQUIRED_MSG); }